Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 31 additions & 47 deletions apps/webapp/app/components/BlankStatePanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import {
v3NewProjectAlertPath,
v3NewSchedulePath,
} from "~/utils/pathBuilder";
import { AskAI } from "./AskAI";
import { CodeBlock } from "./code/CodeBlock";
import { InlineCode } from "./code/InlineCode";
import { environmentFullTitle, EnvironmentIcon } from "./environments/EnvironmentLabel";
Expand Down Expand Up @@ -62,6 +61,35 @@ import {
import { StepContentContainer } from "./StepContentContainer";
import { V4Badge } from "./V4Badge";

function DeployDocsLinks() {
return (
<>
<SimpleTooltip
button={
<LinkButton
variant="small-menu-item"
LeadingIcon={BookOpenIcon}
leadingIconClassName="text-blue-500"
to={docsPath("deployment/overview")}
/>
}
content="Deploy docs"
/>
<SimpleTooltip
button={
<LinkButton
variant="small-menu-item"
LeadingIcon={QuestionMarkCircleIcon}
leadingIconClassName="text-blue-500"
to={docsPath("troubleshooting#deployment")}
/>
}
content="Troubleshooting docs"
/>
</>
);
}

export function HasNoTasksDev() {
return (
<PackageManagerProvider>
Expand Down Expand Up @@ -270,29 +298,7 @@ export function DeploymentsNoneDev() {
<Header1>Deploy your tasks</Header1>
</div>
<div className="flex items-center">
<SimpleTooltip
button={
<LinkButton
variant="small-menu-item"
LeadingIcon={BookOpenIcon}
leadingIconClassName="text-blue-500"
to={docsPath("deployment/overview")}
/>
}
content="Deploy docs"
/>
<SimpleTooltip
button={
<LinkButton
variant="small-menu-item"
LeadingIcon={QuestionMarkCircleIcon}
leadingIconClassName="text-blue-500"
to={docsPath("troubleshooting#deployment")}
/>
}
content="Troubleshooting docs"
/>
<AskAI />
<DeployDocsLinks />
</div>
</div>
<StepNumber stepNumber="→" title="Switch to a deployed environment" />
Expand Down Expand Up @@ -658,29 +664,7 @@ function DeploymentOnboardingSteps() {
</Header1>
</div>
<div className="flex items-center">
<SimpleTooltip
button={
<LinkButton
variant="small-menu-item"
LeadingIcon={BookOpenIcon}
leadingIconClassName="text-blue-500"
to={docsPath("deployment/overview")}
/>
}
content="Deploy docs"
/>
<SimpleTooltip
button={
<LinkButton
variant="small-menu-item"
LeadingIcon={QuestionMarkCircleIcon}
leadingIconClassName="text-blue-500"
to={docsPath("troubleshooting#deployment")}
/>
}
content="Troubleshooting docs"
/>
<AskAI />
<DeployDocsLinks />
</div>
</div>
<ClientTabs defaultValue="github">
Expand Down
20 changes: 18 additions & 2 deletions apps/webapp/app/components/Shortcuts.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { KeyboardIcon } from "~/assets/icons/KeyboardIcon";
import { useState } from "react";
import { ASK_AGENT_LABEL } from "~/components/dashboard-agent/agent-identity";
import { NEW_CHAT_SHORTCUT } from "~/components/dashboard-agent/DashboardAgentHeader";
import { TOGGLE_PANEL_SHORTCUT } from "~/components/dashboard-agent/dashboardAgentLauncher";
import { useShortcutKeys } from "~/hooks/useShortcutKeys";
import { Header3 } from "./primitives/Headers";
import { SideMenuItemButton } from "./navigation/SideMenuItem";
Expand Down Expand Up @@ -62,9 +65,9 @@ function ShortcutContent() {
<ShortcutKey shortcut={{ modifiers: ["mod"] }} variant="medium/bright" />
<ShortcutKey shortcut={{ key: "enter" }} variant="medium/bright" />
</Shortcut>
<Shortcut name="Ask AI">
<Shortcut name={ASK_AGENT_LABEL}>
<ShortcutKey shortcut={{ modifiers: ["mod"] }} variant="medium/bright" />
<ShortcutKey shortcut={{ key: "i" }} variant="medium/bright" />
<ShortcutKey shortcut={{ key: TOGGLE_PANEL_SHORTCUT.key }} variant="medium/bright" />
</Shortcut>
<Shortcut name="Filter">
<ShortcutKey shortcut={{ key: "f" }} variant="medium/bright" />
Expand Down Expand Up @@ -94,6 +97,19 @@ function ShortcutContent() {
<ShortcutKey shortcut={{ key: "h" }} variant="medium/bright" />
</Shortcut>
</div>
<div className="space-y-3">
<Header3>Chat</Header3>
<Shortcut name="New chat">
<ShortcutKey
shortcut={{ modifiers: NEW_CHAT_SHORTCUT.modifiers }}
variant="medium/bright"
/>
<ShortcutKey shortcut={{ key: NEW_CHAT_SHORTCUT.key }} variant="medium/bright" />
</Shortcut>
<Shortcut name="Close chat">
<ShortcutKey shortcut={{ key: "esc" }} variant="medium/bright" />
</Shortcut>
</div>
<div className="space-y-3">
<Header3>Runs page</Header3>
<Shortcut name="Bulk action: Cancel runs">
Expand Down
31 changes: 31 additions & 0 deletions apps/webapp/app/components/dashboard-agent/ActionsBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type {
ActionsBlock as ActionsBlockPayload,
AgentIntent,
} from "@internal/dashboard-agent-contracts";
import { Button } from "~/components/primitives/Buttons";
import { ChatActionsRow } from "./chat-layout";
import { renderableActions } from "./view-actions";

export function ActionsBlock({
block,
onIntent,
}: {
block: ActionsBlockPayload;
onIntent?: (intent: AgentIntent) => void;
}) {
const renderable = renderableActions(block.actions);
if (!onIntent || renderable.length === 0) return null;
return (
<ChatActionsRow>
{renderable.map((action, i) => (
<Button
key={i}
variant={i === 0 ? "primary/small" : "secondary/small"}
onClick={() => onIntent(action.intent as AgentIntent)}
>
{action.label}
</Button>
))}
</ChatActionsRow>
);
}
88 changes: 67 additions & 21 deletions apps/webapp/app/components/dashboard-agent/AgentChart.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import type { OutputColumnMetadata } from "@internal/clickhouse";
import type { ChartBlock } from "@internal/dashboard-agent";
import type { AgentIntent, ChartAction } from "@internal/dashboard-agent-contracts";
import { useEffect, useState } from "react";
import { QueryResultsChart } from "~/components/code/QueryResultsChart";
import type { ChartConfiguration } from "~/components/metrics/QueryWidget";
import { Spinner } from "~/components/primitives/Spinner";
import { Button } from "~/components/primitives/Buttons";
import { AgentSpinner } from "~/components/primitives/Spinner";
import { useOptionalEnvironment } from "~/hooks/useEnvironment";
import { useOptionalOrganization } from "~/hooks/useOrganizations";
import { useOptionalProject } from "~/hooks/useProject";

// Render an agent "chart" block by running its TRQL query through the dashboard's
// own /resources/metric endpoint (session-authed, returns rows + real column
// metadata) and feeding the result into QueryResultsChart. So the chart is live
// and matches the Query page exactly: the agent only emits the query + chart
// config, never the rows. Runs against the project/env the panel is open in.
import { cn } from "~/utils/cn";
import { AgentCard, AgentCardHeader } from "./agent-card";
import { ChatActionsRow } from "./chat-layout";
import { renderableActions } from "./view-actions";

type MetricResponse =
| { success: false; error: string }
Expand All @@ -25,6 +25,18 @@ type MetricResponse =
};
};

// `chartBlockBodySchema` carries only `period`, so scope and from/to are fixed here.
const CHART_SCOPE = "environment";
const CHART_FROM = null;
const CHART_TO = null;
// `min-h` as well: the chart draws nothing at zero height if a flex parent collapses it.
const CHART_HEIGHT_CLASS = "h-64 min-h-64";
const CHART_PADDING_CLASS = "px-2 pb-2 pt-4";
export const AGENT_CHART_PLOT_CLASS = `w-full ${CHART_PADDING_CLASS} ${CHART_HEIGHT_CLASS}`;

// Query errors can carry SQL and schema detail, so the real one only goes to the console.
const CHART_ERROR_MESSAGE = "This chart's query couldn't run.";

type ChartState =
| { status: "loading" }
| { status: "error"; error: string }
Expand All @@ -35,7 +47,39 @@ type ChartState =
timeRange?: { from: string; to: string };
};

export function AgentChart({ block }: { block: ChartBlock }) {
export function ChartActions({
actions,
onIntent,
}: {
actions: ChartAction[];
onIntent?: (intent: AgentIntent) => void;
}) {
const renderable = renderableActions(actions);
if (!onIntent || renderable.length === 0) return null;
return (
<div className="border-t border-grid-bright px-2 pb-2 pt-2">
<ChatActionsRow>
{renderable.map((action, i) => (
<Button
key={i}
variant={i === 0 ? "primary/small" : "secondary/small"}
onClick={() => onIntent(action.intent as AgentIntent)}
>
{action.label}
</Button>
))}
</ChatActionsRow>
</div>
);
}

export function AgentChart({
block,
onIntent,
}: {
block: ChartBlock;
onIntent?: (intent: AgentIntent) => void;
}) {
const organization = useOptionalOrganization();
const project = useOptionalProject();
const environment = useOptionalEnvironment();
Expand All @@ -46,8 +90,7 @@ export function AgentChart({ block }: { block: ChartBlock }) {
const environmentId = environment?.id;

useEffect(() => {
// The block can render before its `query` has finished streaming in; wait
// for it rather than POST an empty query (which 400s).
// The block can render before `query` has streamed in; an empty query 400s.
if (!block.query) return;
if (!organizationId || !projectId || !environmentId) {
setState({ status: "error", error: "No environment context to run the query." });
Expand All @@ -63,10 +106,10 @@ export function AgentChart({ block }: { block: ChartBlock }) {
organizationId,
projectId,
environmentId,
scope: "environment",
scope: CHART_SCOPE,
period: block.period ?? null,
from: null,
to: null,
from: CHART_FROM,
to: CHART_TO,
userAuthoredQuery: true,
}),
signal: controller.signal,
Expand All @@ -75,7 +118,8 @@ export function AgentChart({ block }: { block: ChartBlock }) {
.then((data) => {
if (controller.signal.aborted) return;
if (!data.success) {
setState({ status: "error", error: data.error });
console.error("Dashboard agent chart query failed:", data.error);
setState({ status: "error", error: CHART_ERROR_MESSAGE });
} else {
setState({
status: "ready",
Expand All @@ -87,7 +131,8 @@ export function AgentChart({ block }: { block: ChartBlock }) {
})
.catch((err) => {
if (controller.signal.aborted) return;
setState({ status: "error", error: err?.message ?? "The query failed to run." });
console.error("Dashboard agent chart request failed:", err);
setState({ status: "error", error: CHART_ERROR_MESSAGE });
});
return () => controller.abort();
}, [block.query, block.period, organizationId, projectId, environmentId]);
Expand All @@ -104,16 +149,16 @@ export function AgentChart({ block }: { block: ChartBlock }) {
};

return (
<div className="overflow-hidden rounded-lg border border-border-bright bg-background-dimmed">
<AgentCard>
{block.title ? (
<div className="border-b border-grid-bright bg-background-bright px-3 py-2 text-xs font-medium text-text-dimmed">
<AgentCardHeader className="text-xs font-medium text-text-dimmed">
{block.title}
</div>
</AgentCardHeader>
) : null}
<div className="h-64 w-full p-2">
<div className={cn(AGENT_CHART_PLOT_CLASS)}>
{state.status === "loading" ? (
<div className="flex h-full items-center justify-center gap-2 text-xs text-text-dimmed">
<Spinner className="size-3" />
<AgentSpinner size={12} />
Running query…
</div>
) : state.status === "error" ? (
Expand All @@ -129,6 +174,7 @@ export function AgentChart({ block }: { block: ChartBlock }) {
/>
)}
</div>
</div>
<ChartActions actions={block.actions ?? []} onIntent={onIntent} />
</AgentCard>
);
}
58 changes: 58 additions & 0 deletions apps/webapp/app/components/dashboard-agent/AgentUpgradeGate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Link } from "@remix-run/react";
import { LinkButton } from "~/components/primitives/Buttons";
import { useOrganization } from "~/hooks/useOrganizations";
import { cn } from "~/utils/cn";
import { v3BillingPath } from "~/utils/pathBuilder";
import { AgentIcon, AGENT_ICON_ACCENT_CLASS, ASK_AGENT_LABEL } from "./agent-identity";

// Matches the composer's outer geometry so the replacement lands in the same place.
const SLOT = "flex shrink-0 flex-col bg-background-bright px-3 pb-3 pt-1";

export function AgentUpgradeBlock({
limit,
context,
}: {
limit: number;
context?: React.ReactNode;
}) {
const organization = useOrganization();

return (
<div className={SLOT}>
{context}
<div className="mt-1.5 flex flex-col gap-2 rounded-md border border-border-bright bg-background-dimmed p-3">
<div className="flex items-center gap-1.5">
<AgentIcon className={cn("size-4 shrink-0", AGENT_ICON_ACCENT_CLASS)} />
<span className="text-sm font-medium text-text-bright">
Upgrade to unlock {ASK_AGENT_LABEL}
</span>
</div>
<p className="text-xs text-text-dimmed">
You've used all {limit} messages included on the Free plan. Your chats stay here to read.
</p>
<LinkButton variant="primary/small" to={v3BillingPath(organization)} fullWidth>
Upgrade
</LinkButton>
</div>
</div>
);
}

export function AgentQuotaNotice({ remaining, limit }: { remaining: number; limit: number }) {
const organization = useOrganization();

return (
<div className="flex shrink-0 items-center gap-1 bg-background-bright px-3 pb-2 text-xs text-text-dimmed">
<span>
{remaining} of {limit} free messages left
</span>
<span aria-hidden>·</span>
<Link
to={v3BillingPath(organization)}
className="text-text-link underline-offset-2 hover:underline"
>
Upgrade
</Link>
</div>
);
}
Loading
Loading