Skip to content

Commit a9f05ce

Browse files
committed
feat(webapp): the dashboard agent's UI
The panel, the page-context marks on the pages the agent reads, and the entry points.
1 parent 798fdf9 commit a9f05ce

139 files changed

Lines changed: 11402 additions & 981 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.

apps/webapp/app/components/BlankStatePanels.tsx

Lines changed: 31 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import {
3434
v3NewProjectAlertPath,
3535
v3NewSchedulePath,
3636
} from "~/utils/pathBuilder";
37-
import { AskAI } from "./AskAI";
3837
import { CodeBlock } from "./code/CodeBlock";
3938
import { InlineCode } from "./code/InlineCode";
4039
import { environmentFullTitle, EnvironmentIcon } from "./environments/EnvironmentLabel";
@@ -62,6 +61,35 @@ import {
6261
import { StepContentContainer } from "./StepContentContainer";
6362
import { V4Badge } from "./V4Badge";
6463

64+
function DeployDocsLinks() {
65+
return (
66+
<>
67+
<SimpleTooltip
68+
button={
69+
<LinkButton
70+
variant="small-menu-item"
71+
LeadingIcon={BookOpenIcon}
72+
leadingIconClassName="text-blue-500"
73+
to={docsPath("deployment/overview")}
74+
/>
75+
}
76+
content="Deploy docs"
77+
/>
78+
<SimpleTooltip
79+
button={
80+
<LinkButton
81+
variant="small-menu-item"
82+
LeadingIcon={QuestionMarkCircleIcon}
83+
leadingIconClassName="text-blue-500"
84+
to={docsPath("troubleshooting#deployment")}
85+
/>
86+
}
87+
content="Troubleshooting docs"
88+
/>
89+
</>
90+
);
91+
}
92+
6593
export function HasNoTasksDev() {
6694
return (
6795
<PackageManagerProvider>
@@ -270,29 +298,7 @@ export function DeploymentsNoneDev() {
270298
<Header1>Deploy your tasks</Header1>
271299
</div>
272300
<div className="flex items-center">
273-
<SimpleTooltip
274-
button={
275-
<LinkButton
276-
variant="small-menu-item"
277-
LeadingIcon={BookOpenIcon}
278-
leadingIconClassName="text-blue-500"
279-
to={docsPath("deployment/overview")}
280-
/>
281-
}
282-
content="Deploy docs"
283-
/>
284-
<SimpleTooltip
285-
button={
286-
<LinkButton
287-
variant="small-menu-item"
288-
LeadingIcon={QuestionMarkCircleIcon}
289-
leadingIconClassName="text-blue-500"
290-
to={docsPath("troubleshooting#deployment")}
291-
/>
292-
}
293-
content="Troubleshooting docs"
294-
/>
295-
<AskAI />
301+
<DeployDocsLinks />
296302
</div>
297303
</div>
298304
<StepNumber stepNumber="→" title="Switch to a deployed environment" />
@@ -658,29 +664,7 @@ function DeploymentOnboardingSteps() {
658664
</Header1>
659665
</div>
660666
<div className="flex items-center">
661-
<SimpleTooltip
662-
button={
663-
<LinkButton
664-
variant="small-menu-item"
665-
LeadingIcon={BookOpenIcon}
666-
leadingIconClassName="text-blue-500"
667-
to={docsPath("deployment/overview")}
668-
/>
669-
}
670-
content="Deploy docs"
671-
/>
672-
<SimpleTooltip
673-
button={
674-
<LinkButton
675-
variant="small-menu-item"
676-
LeadingIcon={QuestionMarkCircleIcon}
677-
leadingIconClassName="text-blue-500"
678-
to={docsPath("troubleshooting#deployment")}
679-
/>
680-
}
681-
content="Troubleshooting docs"
682-
/>
683-
<AskAI />
667+
<DeployDocsLinks />
684668
</div>
685669
</div>
686670
<ClientTabs defaultValue="github">

apps/webapp/app/components/Shortcuts.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { KeyboardIcon } from "~/assets/icons/KeyboardIcon";
22
import { useState } from "react";
3+
import { ASK_AGENT_LABEL } from "~/components/dashboard-agent/agent-identity";
4+
import { NEW_CHAT_SHORTCUT } from "~/components/dashboard-agent/DashboardAgentHeader";
5+
import { TOGGLE_PANEL_SHORTCUT } from "~/components/dashboard-agent/dashboardAgentLauncher";
36
import { useShortcutKeys } from "~/hooks/useShortcutKeys";
47
import { Header3 } from "./primitives/Headers";
58
import { SideMenuItemButton } from "./navigation/SideMenuItem";
@@ -62,9 +65,9 @@ function ShortcutContent() {
6265
<ShortcutKey shortcut={{ modifiers: ["mod"] }} variant="medium/bright" />
6366
<ShortcutKey shortcut={{ key: "enter" }} variant="medium/bright" />
6467
</Shortcut>
65-
<Shortcut name="Ask AI">
68+
<Shortcut name={ASK_AGENT_LABEL}>
6669
<ShortcutKey shortcut={{ modifiers: ["mod"] }} variant="medium/bright" />
67-
<ShortcutKey shortcut={{ key: "i" }} variant="medium/bright" />
70+
<ShortcutKey shortcut={{ key: TOGGLE_PANEL_SHORTCUT.key }} variant="medium/bright" />
6871
</Shortcut>
6972
<Shortcut name="Filter">
7073
<ShortcutKey shortcut={{ key: "f" }} variant="medium/bright" />
@@ -94,6 +97,19 @@ function ShortcutContent() {
9497
<ShortcutKey shortcut={{ key: "h" }} variant="medium/bright" />
9598
</Shortcut>
9699
</div>
100+
<div className="space-y-3">
101+
<Header3>Chat</Header3>
102+
<Shortcut name="New chat">
103+
<ShortcutKey
104+
shortcut={{ modifiers: NEW_CHAT_SHORTCUT.modifiers }}
105+
variant="medium/bright"
106+
/>
107+
<ShortcutKey shortcut={{ key: NEW_CHAT_SHORTCUT.key }} variant="medium/bright" />
108+
</Shortcut>
109+
<Shortcut name="Close chat">
110+
<ShortcutKey shortcut={{ key: "esc" }} variant="medium/bright" />
111+
</Shortcut>
112+
</div>
97113
<div className="space-y-3">
98114
<Header3>Runs page</Header3>
99115
<Shortcut name="Bulk action: Cancel runs">
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type {
2+
ActionsBlock as ActionsBlockPayload,
3+
AgentIntent,
4+
} from "@internal/dashboard-agent-contracts";
5+
import { Button } from "~/components/primitives/Buttons";
6+
import { ChatActionsRow } from "./chat-layout";
7+
import { renderableActions } from "./view-actions";
8+
9+
export function ActionsBlock({
10+
block,
11+
onIntent,
12+
}: {
13+
block: ActionsBlockPayload;
14+
onIntent?: (intent: AgentIntent) => void;
15+
}) {
16+
const renderable = renderableActions(block.actions);
17+
if (!onIntent || renderable.length === 0) return null;
18+
return (
19+
<ChatActionsRow>
20+
{renderable.map((action, i) => (
21+
<Button
22+
key={i}
23+
variant={i === 0 ? "primary/small" : "secondary/small"}
24+
onClick={() => onIntent(action.intent as AgentIntent)}
25+
>
26+
{action.label}
27+
</Button>
28+
))}
29+
</ChatActionsRow>
30+
);
31+
}

apps/webapp/app/components/dashboard-agent/AgentChart.tsx

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import type { OutputColumnMetadata } from "@internal/clickhouse";
22
import type { ChartBlock } from "@internal/dashboard-agent";
3+
import type { AgentIntent, ChartAction } from "@internal/dashboard-agent-contracts";
34
import { useEffect, useState } from "react";
45
import { QueryResultsChart } from "~/components/code/QueryResultsChart";
56
import type { ChartConfiguration } from "~/components/metrics/QueryWidget";
6-
import { Spinner } from "~/components/primitives/Spinner";
7+
import { Button } from "~/components/primitives/Buttons";
8+
import { AgentSpinner } from "~/components/primitives/Spinner";
79
import { useOptionalEnvironment } from "~/hooks/useEnvironment";
810
import { useOptionalOrganization } from "~/hooks/useOrganizations";
911
import { useOptionalProject } from "~/hooks/useProject";
10-
11-
// Render an agent "chart" block by running its TRQL query through the dashboard's
12-
// own /resources/metric endpoint (session-authed, returns rows + real column
13-
// metadata) and feeding the result into QueryResultsChart. So the chart is live
14-
// and matches the Query page exactly: the agent only emits the query + chart
15-
// config, never the rows. Runs against the project/env the panel is open in.
12+
import { cn } from "~/utils/cn";
13+
import { AgentCard, AgentCardHeader } from "./agent-card";
14+
import { ChatActionsRow } from "./chat-layout";
15+
import { renderableActions } from "./view-actions";
1616

1717
type MetricResponse =
1818
| { success: false; error: string }
@@ -25,6 +25,18 @@ type MetricResponse =
2525
};
2626
};
2727

28+
// `chartBlockBodySchema` carries only `period`, so scope and from/to are fixed here.
29+
const CHART_SCOPE = "environment";
30+
const CHART_FROM = null;
31+
const CHART_TO = null;
32+
// `min-h` as well: the chart draws nothing at zero height if a flex parent collapses it.
33+
const CHART_HEIGHT_CLASS = "h-64 min-h-64";
34+
const CHART_PADDING_CLASS = "px-2 pb-2 pt-4";
35+
export const AGENT_CHART_PLOT_CLASS = `w-full ${CHART_PADDING_CLASS} ${CHART_HEIGHT_CLASS}`;
36+
37+
// Query errors can carry SQL and schema detail, so the real one only goes to the console.
38+
const CHART_ERROR_MESSAGE = "This chart's query couldn't run.";
39+
2840
type ChartState =
2941
| { status: "loading" }
3042
| { status: "error"; error: string }
@@ -35,7 +47,39 @@ type ChartState =
3547
timeRange?: { from: string; to: string };
3648
};
3749

38-
export function AgentChart({ block }: { block: ChartBlock }) {
50+
export function ChartActions({
51+
actions,
52+
onIntent,
53+
}: {
54+
actions: ChartAction[];
55+
onIntent?: (intent: AgentIntent) => void;
56+
}) {
57+
const renderable = renderableActions(actions);
58+
if (!onIntent || renderable.length === 0) return null;
59+
return (
60+
<div className="border-t border-grid-bright px-2 pb-2 pt-2">
61+
<ChatActionsRow>
62+
{renderable.map((action, i) => (
63+
<Button
64+
key={i}
65+
variant={i === 0 ? "primary/small" : "secondary/small"}
66+
onClick={() => onIntent(action.intent as AgentIntent)}
67+
>
68+
{action.label}
69+
</Button>
70+
))}
71+
</ChatActionsRow>
72+
</div>
73+
);
74+
}
75+
76+
export function AgentChart({
77+
block,
78+
onIntent,
79+
}: {
80+
block: ChartBlock;
81+
onIntent?: (intent: AgentIntent) => void;
82+
}) {
3983
const organization = useOptionalOrganization();
4084
const project = useOptionalProject();
4185
const environment = useOptionalEnvironment();
@@ -46,8 +90,7 @@ export function AgentChart({ block }: { block: ChartBlock }) {
4690
const environmentId = environment?.id;
4791

4892
useEffect(() => {
49-
// The block can render before its `query` has finished streaming in; wait
50-
// for it rather than POST an empty query (which 400s).
93+
// The block can render before `query` has streamed in; an empty query 400s.
5194
if (!block.query) return;
5295
if (!organizationId || !projectId || !environmentId) {
5396
setState({ status: "error", error: "No environment context to run the query." });
@@ -63,10 +106,10 @@ export function AgentChart({ block }: { block: ChartBlock }) {
63106
organizationId,
64107
projectId,
65108
environmentId,
66-
scope: "environment",
109+
scope: CHART_SCOPE,
67110
period: block.period ?? null,
68-
from: null,
69-
to: null,
111+
from: CHART_FROM,
112+
to: CHART_TO,
70113
userAuthoredQuery: true,
71114
}),
72115
signal: controller.signal,
@@ -75,7 +118,8 @@ export function AgentChart({ block }: { block: ChartBlock }) {
75118
.then((data) => {
76119
if (controller.signal.aborted) return;
77120
if (!data.success) {
78-
setState({ status: "error", error: data.error });
121+
console.error("Dashboard agent chart query failed:", data.error);
122+
setState({ status: "error", error: CHART_ERROR_MESSAGE });
79123
} else {
80124
setState({
81125
status: "ready",
@@ -87,7 +131,8 @@ export function AgentChart({ block }: { block: ChartBlock }) {
87131
})
88132
.catch((err) => {
89133
if (controller.signal.aborted) return;
90-
setState({ status: "error", error: err?.message ?? "The query failed to run." });
134+
console.error("Dashboard agent chart request failed:", err);
135+
setState({ status: "error", error: CHART_ERROR_MESSAGE });
91136
});
92137
return () => controller.abort();
93138
}, [block.query, block.period, organizationId, projectId, environmentId]);
@@ -104,16 +149,16 @@ export function AgentChart({ block }: { block: ChartBlock }) {
104149
};
105150

106151
return (
107-
<div className="overflow-hidden rounded-lg border border-border-bright bg-background-dimmed">
152+
<AgentCard>
108153
{block.title ? (
109-
<div className="border-b border-grid-bright bg-background-bright px-3 py-2 text-xs font-medium text-text-dimmed">
154+
<AgentCardHeader className="text-xs font-medium text-text-dimmed">
110155
{block.title}
111-
</div>
156+
</AgentCardHeader>
112157
) : null}
113-
<div className="h-64 w-full p-2">
158+
<div className={cn(AGENT_CHART_PLOT_CLASS)}>
114159
{state.status === "loading" ? (
115160
<div className="flex h-full items-center justify-center gap-2 text-xs text-text-dimmed">
116-
<Spinner className="size-3" />
161+
<AgentSpinner size={12} />
117162
Running query…
118163
</div>
119164
) : state.status === "error" ? (
@@ -129,6 +174,7 @@ export function AgentChart({ block }: { block: ChartBlock }) {
129174
/>
130175
)}
131176
</div>
132-
</div>
177+
<ChartActions actions={block.actions ?? []} onIntent={onIntent} />
178+
</AgentCard>
133179
);
134180
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Link } from "@remix-run/react";
2+
import { LinkButton } from "~/components/primitives/Buttons";
3+
import { useOrganization } from "~/hooks/useOrganizations";
4+
import { cn } from "~/utils/cn";
5+
import { v3BillingPath } from "~/utils/pathBuilder";
6+
import { AgentIcon, AGENT_ICON_ACCENT_CLASS, ASK_AGENT_LABEL } from "./agent-identity";
7+
8+
// Matches the composer's outer geometry so the replacement lands in the same place.
9+
const SLOT = "flex shrink-0 flex-col bg-background-bright px-3 pb-3 pt-1";
10+
11+
export function AgentUpgradeBlock({
12+
limit,
13+
context,
14+
}: {
15+
limit: number;
16+
context?: React.ReactNode;
17+
}) {
18+
const organization = useOrganization();
19+
20+
return (
21+
<div className={SLOT}>
22+
{context}
23+
<div className="mt-1.5 flex flex-col gap-2 rounded-md border border-border-bright bg-background-dimmed p-3">
24+
<div className="flex items-center gap-1.5">
25+
<AgentIcon className={cn("size-4 shrink-0", AGENT_ICON_ACCENT_CLASS)} />
26+
<span className="text-sm font-medium text-text-bright">
27+
Upgrade to unlock {ASK_AGENT_LABEL}
28+
</span>
29+
</div>
30+
<p className="text-xs text-text-dimmed">
31+
You've used all {limit} messages included on the Free plan. Your chats stay here to read.
32+
</p>
33+
<LinkButton variant="primary/small" to={v3BillingPath(organization)} fullWidth>
34+
Upgrade
35+
</LinkButton>
36+
</div>
37+
</div>
38+
);
39+
}
40+
41+
export function AgentQuotaNotice({ remaining, limit }: { remaining: number; limit: number }) {
42+
const organization = useOrganization();
43+
44+
return (
45+
<div className="flex shrink-0 items-center gap-1 bg-background-bright px-3 pb-2 text-xs text-text-dimmed">
46+
<span>
47+
{remaining} of {limit} free messages left
48+
</span>
49+
<span aria-hidden>·</span>
50+
<Link
51+
to={v3BillingPath(organization)}
52+
className="text-text-link underline-offset-2 hover:underline"
53+
>
54+
Upgrade
55+
</Link>
56+
</div>
57+
);
58+
}

0 commit comments

Comments
 (0)