-
Notifications
You must be signed in to change notification settings - Fork 14
feat(appkit): reference agent-app, dev-playground chat UI, docs, and template #306
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0213e14
0adf359
3ab0ce6
88c051f
f1097f9
12b49b6
e4f40c2
f682e1d
b85688d
8924883
eeb5060
6f21dbc
b3422df
bb3796b
acade71
9daf107
42d658e
ce326d3
a934113
c819b40
929b0e6
1c86186
6800f3a
62a687f
7b3d48b
60ba6f1
19e572f
62599fc
caf04c8
cbd35ab
00e5702
fed3cc8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a lot of the code for the dev playground. Honestly, I'm not sure if that fits in the dev playground - maybe we should move it somewhere? E.g. Move it as a separate template in IMO this is too good to keep it in the dev playground.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, as a second thought - the "Smart dashboard" example is kind of reimplementing Metric Views? Correct me if I'm wrong 🤔
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agentic reviews (aggregated report): Code Review — agent/v2/6-apps-docsContextBranch What works well
P0 — Must fix1. Doc/behavior contradiction on auto-inheritFile: Level 1 documentation says:
But the Configuration Reference says:
These directly contradict each other. A developer following Level 1 will expect their markdown agent to see all plugin tools out of the box, but it won't — they get zero tools. This is the single most confusing thing in the agents DX. Fix: Either (a) add P1 — Should fix before merge2. MCP
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { CheckCircle2Icon } from "lucide-react"; | ||
| import { useEffect, useState } from "react"; | ||
|
|
||
| interface ActionToastProps { | ||
| /** | ||
| * Latest dispatcher-surfaced action summary. Each new value bumps a | ||
| * render key so the toast re-animates even if the same message arrives | ||
| * twice (e.g. two identical filter calls in a row). | ||
| */ | ||
| message: string | null; | ||
| durationMs?: number; | ||
| } | ||
|
|
||
| /** | ||
| * Non-intrusive bottom-left toast that confirms every agent-driven UI | ||
| * action. Silent success was the worst failure mode before: an action | ||
| * silently not-applied looked identical to one that worked but didn't | ||
| * show its effect. | ||
| */ | ||
| export function ActionToast({ message, durationMs = 2800 }: ActionToastProps) { | ||
| const [visible, setVisible] = useState<{ key: number; text: string } | null>( | ||
| null, | ||
| ); | ||
|
|
||
| useEffect(() => { | ||
| if (!message) return; | ||
| const key = Date.now(); | ||
| setVisible({ key, text: message }); | ||
| const t = setTimeout(() => { | ||
| setVisible((v) => (v?.key === key ? null : v)); | ||
| }, durationMs); | ||
| return () => { | ||
| clearTimeout(t); | ||
| }; | ||
| }, [message, durationMs]); | ||
|
|
||
| if (!visible) return null; | ||
|
|
||
| return ( | ||
| <div | ||
| key={visible.key} | ||
| className="fixed bottom-20 left-4 z-30 rounded-full bg-card border border-border shadow-lg px-3 py-1.5 flex items-center gap-2 animate-in fade-in slide-in-from-bottom-2 duration-200" | ||
| > | ||
| <CheckCircle2Icon className="h-3.5 w-3.5 text-green-500 shrink-0" /> | ||
| <span className="text-xs text-foreground">{visible.text}</span> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| import { | ||
| AlertTriangleIcon, | ||
| ArrowRightIcon, | ||
| CalendarIcon, | ||
| CrosshairIcon, | ||
| DollarSignIcon, | ||
| HighlighterIcon, | ||
| LightbulbIcon, | ||
| MapPinIcon, | ||
| MessageSquareIcon, | ||
| } from "lucide-react"; | ||
| import type { FeedAction } from "../lib/feed-actions"; | ||
|
|
||
| type Variant = "insight" | "anomaly"; | ||
| type Severity = "low" | "medium" | "high"; | ||
|
|
||
| interface ActionableCardProps { | ||
| variant: Variant; | ||
| severity?: Severity; | ||
| title: string; | ||
| description: string; | ||
| actions: FeedAction[]; | ||
| /** Fired for non-ask actions. Route applies them to dashboard state. */ | ||
| onAction: (action: FeedAction) => void; | ||
| /** Fired for `ask` actions. Route forwards the prompt to the chat drawer. */ | ||
| onAsk: (prompt: string) => void; | ||
| } | ||
|
|
||
| // Backgrounds are written as arbitrary 8-digit hex (e.g. `bg-[#eff6ff80]`) | ||
| // instead of Tailwind's `/N` alpha shorthand. Rationale: `bg-blue-50/50` | ||
| // compiles in Tailwind v4 to a pair — an sRGB hex fallback and a | ||
| // `@supports (color-mix)` override that re-mixes in oklab over the oklch | ||
| // palette token. Browsers that support `color-mix` (recent Chrome/Arc) take | ||
| // the oklab path; older embedded Chromiums (e.g. Cursor's built-in browser | ||
| // at the time of writing) fall through to the sRGB hex. Because oklab and | ||
| // sRGB interpolation produce visibly different tints — especially against | ||
| // the dark `--card` token — the same card ends up looking different in each | ||
| // browser. Pinning the colour to a literal hex (no `/N`, no @supports | ||
| // override) keeps all browsers on the same sRGB path and therefore the same | ||
| // visual result. | ||
| const INSIGHT_STYLES = { | ||
| border: "border-blue-200 dark:border-blue-900", | ||
| bg: "bg-[#eff6ff80] dark:bg-[#1624564d]", | ||
| icon: "text-blue-500", | ||
| }; | ||
|
|
||
| const ANOMALY_STYLES: Record< | ||
| Severity, | ||
| { border: string; bg: string; icon: string; badge: string } | ||
| > = { | ||
| low: { | ||
| border: "border-yellow-200 dark:border-yellow-900", | ||
| bg: "bg-[#fefce880] dark:bg-[#4320044d]", | ||
| icon: "text-yellow-500", | ||
| badge: | ||
| "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/50 dark:text-yellow-400", | ||
| }, | ||
| medium: { | ||
| border: "border-orange-200 dark:border-orange-900", | ||
| bg: "bg-[#fff7ed80] dark:bg-[#4413064d]", | ||
| icon: "text-orange-500", | ||
| badge: | ||
| "bg-orange-100 text-orange-700 dark:bg-orange-900/50 dark:text-orange-400", | ||
| }, | ||
| high: { | ||
| border: "border-red-200 dark:border-red-900", | ||
| bg: "bg-[#fef2f280] dark:bg-[#4608094d]", | ||
| icon: "text-red-500", | ||
| badge: "bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-400", | ||
| }, | ||
| }; | ||
|
|
||
| function iconForAction(kind: FeedAction["kind"]): React.ReactNode { | ||
| const cls = "h-3 w-3"; | ||
| switch (kind) { | ||
| case "filter_date": | ||
| return <CalendarIcon className={cls} />; | ||
| case "filter_zip": | ||
| return <MapPinIcon className={cls} />; | ||
| case "filter_fare": | ||
| return <DollarSignIcon className={cls} />; | ||
| case "highlight_period": | ||
| return <HighlighterIcon className={cls} />; | ||
| case "highlight_zone": | ||
| return <MapPinIcon className={cls} />; | ||
| case "focus_chart": | ||
| return <CrosshairIcon className={cls} />; | ||
| case "ask": | ||
| return <MessageSquareIcon className={cls} />; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Action chip for a single feed suggestion. The chip's visual weight depends | ||
| * on its kind: structural mutations (filter/highlight/focus) use the primary | ||
| * tint, `ask` uses a neutral outline so the user can tell "this opens the | ||
| * chat" from "this changes the dashboard" without reading the label. | ||
| */ | ||
| function ActionChip({ | ||
| action, | ||
| onAction, | ||
| onAsk, | ||
| }: { | ||
| action: FeedAction; | ||
| onAction: (a: FeedAction) => void; | ||
| onAsk: (prompt: string) => void; | ||
| }) { | ||
| const isAsk = action.kind === "ask"; | ||
| const isHighlight = | ||
| action.kind === "highlight_period" || action.kind === "highlight_zone"; | ||
|
|
||
| return ( | ||
| <button | ||
| type="button" | ||
| onClick={() => { | ||
| if (isAsk) onAsk(action.prompt); | ||
| else onAction(action); | ||
| }} | ||
| className={`inline-flex items-center gap-1 text-[11px] font-medium px-2 py-1 rounded-md transition-colors ${ | ||
| isAsk | ||
| ? "border border-border bg-background text-foreground/80 hover:bg-muted hover:text-foreground" | ||
| : isHighlight | ||
| ? "bg-amber-100 text-amber-800 hover:bg-amber-200 dark:bg-amber-900/40 dark:text-amber-200 dark:hover:bg-amber-900/60" | ||
| : "bg-primary/10 text-primary hover:bg-primary/20" | ||
| }`} | ||
| > | ||
| {iconForAction(action.kind)} | ||
| <span>{action.label}</span> | ||
| {isAsk && <ArrowRightIcon className="h-3 w-3 opacity-70" />} | ||
| </button> | ||
| ); | ||
| } | ||
|
|
||
| export function ActionableCard({ | ||
| variant, | ||
| severity, | ||
| title, | ||
| description, | ||
| actions, | ||
| onAction, | ||
| onAsk, | ||
| }: ActionableCardProps) { | ||
| const isAnomaly = variant === "anomaly"; | ||
| const styles = isAnomaly | ||
| ? ANOMALY_STYLES[severity ?? "low"] | ||
| : { ...INSIGHT_STYLES, badge: "" }; | ||
|
|
||
| return ( | ||
| <div className={`rounded-lg border ${styles.border} ${styles.bg} p-3`}> | ||
| <div className="flex items-start gap-2 mb-2"> | ||
| {isAnomaly ? ( | ||
| <AlertTriangleIcon | ||
| className={`h-4 w-4 ${styles.icon} mt-0.5 shrink-0`} | ||
| /> | ||
| ) : ( | ||
| <LightbulbIcon className={`h-4 w-4 ${styles.icon} mt-0.5 shrink-0`} /> | ||
| )} | ||
| <div className="min-w-0 flex-1"> | ||
| <div className="flex items-start gap-2"> | ||
| <p className="text-sm font-medium text-foreground leading-tight flex-1"> | ||
| {title} | ||
| </p> | ||
| {isAnomaly && severity && ( | ||
| <span | ||
| className={`text-[10px] font-medium px-1.5 py-0.5 rounded shrink-0 ${styles.badge}`} | ||
| > | ||
| {severity} | ||
| </span> | ||
| )} | ||
| </div> | ||
| <p className="text-xs text-muted-foreground mt-1 leading-relaxed"> | ||
| {description} | ||
| </p> | ||
| </div> | ||
| </div> | ||
|
|
||
| {actions.length > 0 && ( | ||
| <div className="flex flex-wrap gap-1.5 pl-6"> | ||
| {actions.map((action, i) => ( | ||
| <ActionChip | ||
| key={`${action.kind}-${i}-${action.label}`} | ||
| action={action} | ||
| onAction={onAction} | ||
| onAsk={onAsk} | ||
| /> | ||
| ))} | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait, this file shouldn't be modified, right? Because there's no breaking change if we haven't released agent plugin yet?
(BTW, why do we have so many "Changelog" headers? 😄 something is wrong with our changelog generation, I guess)