Files
stackpilot/frontend/src/components/dashboard/AiPromptBar.tsx
T
menzeljandClaude Fable 5 34cb215266 Phase 24: Design System v2 — analytics-style UI (0.30.0)
- New /api/dashboard/funnel (5-stage stack health, 30s TTL cache) and
  /api/dashboard/summary (containers, daily uptime jsonl, ops activity)
- Token system (tokens.css + Tailwind sp-* aliases); legacy bg/card/accent
  remapped onto the tokens; Schibsted Grotesk bundled via fontsource
- TopNav pill navigation + AppShell replace the sidebar layout (off-canvas
  drawer below 1024px); central display-weight page titles
- Dashboard redesign: FunnelChart (gradient/hatch SVG waterfall), container
  count card with per-host bars + Insights chip, UptimeChart, OpsGrid,
  AiPromptBar; 30/7-day range selector; host sections retained below
- Stacks page honours ?q= / ?filter= deep links + new status-filter select

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 10:54:06 +00:00

98 lines
3.3 KiB
TypeScript

import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { ChevronDown, CornerDownLeft } from "lucide-react";
import { cn } from "@/lib/utils";
const CONTEXT_TAGS = [
{ tag: "/running", filter: "running" },
{ tag: "/stopped", filter: "stopped" },
{ tag: "/attention", filter: "attention" },
];
function SparkleIcon() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="M8 1.5 9.4 6 14 7.5 9.4 9 8 13.5 6.6 9 2 7.5 6.6 6 8 1.5Z"
fill="var(--sp-amber)"
/>
<path d="M13 11l.6 1.7L15.3 13l-1.7.6L13 15.3l-.6-1.7-1.7-.6 1.7-.6.6-1.7Z" fill="var(--sp-amber)" fillOpacity="0.7" />
</svg>
);
}
/** Inline explore-prompt: typed (or tag-clicked) queries jump to the Stacks
* page with a matching filter — no LLM behind it (yet). */
export function AiPromptBar({ onSubmit }: { onSubmit?: (query: string) => void }) {
const [open, setOpen] = useState(true);
const [value, setValue] = useState("");
const navigate = useNavigate();
const submit = (raw: string) => {
const query = raw.trim();
if (!query) return;
if (onSubmit) {
onSubmit(query);
return;
}
const tag = CONTEXT_TAGS.find((t) => query.startsWith(t.tag));
if (tag) navigate(`/stacks?filter=${tag.filter}`);
else navigate(`/stacks?q=${encodeURIComponent(query)}`);
};
return (
<div className="rounded-2xl border border-sp-border bg-sp-surface-2">
<button
onClick={() => setOpen((v) => !v)}
className="flex w-full items-center gap-2 px-4 py-3 text-left"
aria-expanded={open}
>
<SparkleIcon />
<span className="text-sm font-medium text-sp-text-1">
What would you like to explore next?
</span>
<ChevronDown
className={cn("ml-auto h-4 w-4 text-sp-text-3 transition-transform", open && "rotate-180")}
/>
</button>
{open && (
<div className="px-4 pb-3.5">
<form
onSubmit={(e) => {
e.preventDefault();
submit(value);
}}
className="flex items-center gap-2 rounded-xl border border-sp-border bg-sp-surface px-3 py-2"
>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Show me stacks that are…"
className="min-w-0 flex-1 bg-transparent text-sm text-sp-text-1 placeholder:text-sp-text-3 focus:outline-none"
/>
<button
type="submit"
className="rounded-lg p-1.5 text-sp-text-3 hover:bg-sp-surface-2 hover:text-sp-text-1"
title="Go"
aria-label="Submit"
>
<CornerDownLeft className="h-4 w-4" />
</button>
</form>
<div className="mt-2 flex flex-wrap gap-1.5">
{CONTEXT_TAGS.map(({ tag }) => (
<button
key={tag}
onClick={() => submit(tag)}
className="rounded-chip bg-sp-amber/15 px-2 py-0.5 font-mono text-xs font-semibold text-sp-amber hover:bg-sp-amber/25"
>
{tag}
</button>
))}
</div>
</div>
)}
</div>
);
}