Skip to content

Commit 6bd6dab

Browse files
authored
feat(appkit): reference agent-app, dev-playground chat UI, docs, and template (#306)
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com> Co-authored-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
1 parent ed9f163 commit 6bd6dab

156 files changed

Lines changed: 12270 additions & 635 deletions

File tree

Some content is hidden

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

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ jobs:
163163
APPKIT_E2E_TEST: 'true'
164164
DATABRICKS_WAREHOUSE_ID: e2e-mock
165165
DATABRICKS_WORKSPACE_ID: e2e-mock
166+
DATABRICKS_SERVING_ENDPOINT_NAME: e2e-mock
166167

167168
pr-template-artifact:
168169
name: PR Template Artifact

apps/dev-playground/client/package-lock.json

Lines changed: 64 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/dev-playground/client/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
"@tanstack/router-plugin": "1.133.22",
2121
"class-variance-authority": "0.7.1",
2222
"clsx": "2.1.1",
23+
"html2canvas": "1.4.1",
24+
"html2canvas-pro": "2.0.2",
2325
"lucide-react": "0.546.0",
2426
"react": "19.2.0",
2527
"react-dom": "19.2.0",
@@ -30,6 +32,7 @@
3032
},
3133
"devDependencies": {
3234
"@eslint/js": "9.36.0",
35+
"@tailwindcss/postcss": "4.1.17",
3336
"@tanstack/router-cli": "1.133.20",
3437
"@types/node": "24.6.0",
3538
"@types/react": "19.2.2",
@@ -43,7 +46,6 @@
4346
"postcss": "8.5.6",
4447
"shiki": "3.15.0",
4548
"tailwindcss": "4.1.17",
46-
"@tailwindcss/postcss": "4.1.17",
4749
"typescript": "5.9.3",
4850
"typescript-eslint": "8.45.0",
4951
"vite": "npm:rolldown-vite@7.1.14"
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { CheckCircle2Icon } from "lucide-react";
2+
import { useEffect, useState } from "react";
3+
4+
interface ActionToastProps {
5+
/**
6+
* Latest dispatcher-surfaced action summary. Each new value bumps a
7+
* render key so the toast re-animates even if the same message arrives
8+
* twice (e.g. two identical filter calls in a row).
9+
*/
10+
message: string | null;
11+
durationMs?: number;
12+
}
13+
14+
/**
15+
* Non-intrusive bottom-left toast that confirms every agent-driven UI
16+
* action. Silent success was the worst failure mode before: an action
17+
* silently not-applied looked identical to one that worked but didn't
18+
* show its effect.
19+
*/
20+
export function ActionToast({ message, durationMs = 2800 }: ActionToastProps) {
21+
const [visible, setVisible] = useState<{ key: number; text: string } | null>(
22+
null,
23+
);
24+
25+
useEffect(() => {
26+
if (!message) return;
27+
const key = Date.now();
28+
setVisible({ key, text: message });
29+
const t = setTimeout(() => {
30+
setVisible((v) => (v?.key === key ? null : v));
31+
}, durationMs);
32+
return () => {
33+
clearTimeout(t);
34+
};
35+
}, [message, durationMs]);
36+
37+
if (!visible) return null;
38+
39+
return (
40+
<div
41+
key={visible.key}
42+
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"
43+
>
44+
<CheckCircle2Icon className="h-3.5 w-3.5 text-green-500 shrink-0" />
45+
<span className="text-xs text-foreground">{visible.text}</span>
46+
</div>
47+
);
48+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import {
2+
AlertTriangleIcon,
3+
ArrowRightIcon,
4+
CalendarIcon,
5+
CrosshairIcon,
6+
DollarSignIcon,
7+
HighlighterIcon,
8+
LightbulbIcon,
9+
MapPinIcon,
10+
MessageSquareIcon,
11+
} from "lucide-react";
12+
import type { FeedAction } from "../lib/feed-actions";
13+
14+
type Variant = "insight" | "anomaly";
15+
type Severity = "low" | "medium" | "high";
16+
17+
interface ActionableCardProps {
18+
variant: Variant;
19+
severity?: Severity;
20+
title: string;
21+
description: string;
22+
actions: FeedAction[];
23+
/** Fired for non-ask actions. Route applies them to dashboard state. */
24+
onAction: (action: FeedAction) => void;
25+
/** Fired for `ask` actions. Route forwards the prompt to the chat drawer. */
26+
onAsk: (prompt: string) => void;
27+
}
28+
29+
// Backgrounds are written as arbitrary 8-digit hex (e.g. `bg-[#eff6ff80]`)
30+
// instead of Tailwind's `/N` alpha shorthand. Rationale: `bg-blue-50/50`
31+
// compiles in Tailwind v4 to a pair — an sRGB hex fallback and a
32+
// `@supports (color-mix)` override that re-mixes in oklab over the oklch
33+
// palette token. Browsers that support `color-mix` (recent Chrome/Arc) take
34+
// the oklab path; older embedded Chromiums (e.g. Cursor's built-in browser
35+
// at the time of writing) fall through to the sRGB hex. Because oklab and
36+
// sRGB interpolation produce visibly different tints — especially against
37+
// the dark `--card` token — the same card ends up looking different in each
38+
// browser. Pinning the colour to a literal hex (no `/N`, no @supports
39+
// override) keeps all browsers on the same sRGB path and therefore the same
40+
// visual result.
41+
const INSIGHT_STYLES = {
42+
border: "border-blue-200 dark:border-blue-900",
43+
bg: "bg-[#eff6ff80] dark:bg-[#1624564d]",
44+
icon: "text-blue-500",
45+
};
46+
47+
const ANOMALY_STYLES: Record<
48+
Severity,
49+
{ border: string; bg: string; icon: string; badge: string }
50+
> = {
51+
low: {
52+
border: "border-yellow-200 dark:border-yellow-900",
53+
bg: "bg-[#fefce880] dark:bg-[#4320044d]",
54+
icon: "text-yellow-500",
55+
badge:
56+
"bg-yellow-100 text-yellow-700 dark:bg-yellow-900/50 dark:text-yellow-400",
57+
},
58+
medium: {
59+
border: "border-orange-200 dark:border-orange-900",
60+
bg: "bg-[#fff7ed80] dark:bg-[#4413064d]",
61+
icon: "text-orange-500",
62+
badge:
63+
"bg-orange-100 text-orange-700 dark:bg-orange-900/50 dark:text-orange-400",
64+
},
65+
high: {
66+
border: "border-red-200 dark:border-red-900",
67+
bg: "bg-[#fef2f280] dark:bg-[#4608094d]",
68+
icon: "text-red-500",
69+
badge: "bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-400",
70+
},
71+
};
72+
73+
function iconForAction(kind: FeedAction["kind"]): React.ReactNode {
74+
const cls = "h-3 w-3";
75+
switch (kind) {
76+
case "filter_date":
77+
return <CalendarIcon className={cls} />;
78+
case "filter_zip":
79+
return <MapPinIcon className={cls} />;
80+
case "filter_fare":
81+
return <DollarSignIcon className={cls} />;
82+
case "highlight_period":
83+
return <HighlighterIcon className={cls} />;
84+
case "highlight_zone":
85+
return <MapPinIcon className={cls} />;
86+
case "focus_chart":
87+
return <CrosshairIcon className={cls} />;
88+
case "ask":
89+
return <MessageSquareIcon className={cls} />;
90+
}
91+
}
92+
93+
/**
94+
* Action chip for a single feed suggestion. The chip's visual weight depends
95+
* on its kind: structural mutations (filter/highlight/focus) use the primary
96+
* tint, `ask` uses a neutral outline so the user can tell "this opens the
97+
* chat" from "this changes the dashboard" without reading the label.
98+
*/
99+
function ActionChip({
100+
action,
101+
onAction,
102+
onAsk,
103+
}: {
104+
action: FeedAction;
105+
onAction: (a: FeedAction) => void;
106+
onAsk: (prompt: string) => void;
107+
}) {
108+
const isAsk = action.kind === "ask";
109+
const isHighlight =
110+
action.kind === "highlight_period" || action.kind === "highlight_zone";
111+
112+
return (
113+
<button
114+
type="button"
115+
onClick={() => {
116+
if (isAsk) onAsk(action.prompt);
117+
else onAction(action);
118+
}}
119+
className={`inline-flex items-center gap-1 text-[11px] font-medium px-2 py-1 rounded-md transition-colors ${
120+
isAsk
121+
? "border border-border bg-background text-foreground/80 hover:bg-muted hover:text-foreground"
122+
: isHighlight
123+
? "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"
124+
: "bg-primary/10 text-primary hover:bg-primary/20"
125+
}`}
126+
>
127+
{iconForAction(action.kind)}
128+
<span>{action.label}</span>
129+
{isAsk && <ArrowRightIcon className="h-3 w-3 opacity-70" />}
130+
</button>
131+
);
132+
}
133+
134+
export function ActionableCard({
135+
variant,
136+
severity,
137+
title,
138+
description,
139+
actions,
140+
onAction,
141+
onAsk,
142+
}: ActionableCardProps) {
143+
const isAnomaly = variant === "anomaly";
144+
const styles = isAnomaly
145+
? ANOMALY_STYLES[severity ?? "low"]
146+
: { ...INSIGHT_STYLES, badge: "" };
147+
148+
return (
149+
<div className={`rounded-lg border ${styles.border} ${styles.bg} p-3`}>
150+
<div className="flex items-start gap-2 mb-2">
151+
{isAnomaly ? (
152+
<AlertTriangleIcon
153+
className={`h-4 w-4 ${styles.icon} mt-0.5 shrink-0`}
154+
/>
155+
) : (
156+
<LightbulbIcon className={`h-4 w-4 ${styles.icon} mt-0.5 shrink-0`} />
157+
)}
158+
<div className="min-w-0 flex-1">
159+
<div className="flex items-start gap-2">
160+
<p className="text-sm font-medium text-foreground leading-tight flex-1">
161+
{title}
162+
</p>
163+
{isAnomaly && severity && (
164+
<span
165+
className={`text-[10px] font-medium px-1.5 py-0.5 rounded shrink-0 ${styles.badge}`}
166+
>
167+
{severity}
168+
</span>
169+
)}
170+
</div>
171+
<p className="text-xs text-muted-foreground mt-1 leading-relaxed">
172+
{description}
173+
</p>
174+
</div>
175+
</div>
176+
177+
{actions.length > 0 && (
178+
<div className="flex flex-wrap gap-1.5 pl-6">
179+
{actions.map((action, i) => (
180+
<ActionChip
181+
key={`${action.kind}-${i}-${action.label}`}
182+
action={action}
183+
onAction={onAction}
184+
onAsk={onAsk}
185+
/>
186+
))}
187+
</div>
188+
)}
189+
</div>
190+
);
191+
}

0 commit comments

Comments
 (0)