@burdenoff/microfe-vibecontrols 2026.531.2 → 2026.531.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ai/AIPlanPanel.js +1 -1
- package/dist/components/ai/AIPlanPanel.js.map +1 -1
- package/dist/components/scorecard/ScorecardEmptyState.js +1 -2
- package/dist/components/scorecard/ScorecardEmptyState.js.map +1 -1
- package/dist/components/scorecard/ScorecardPanel.js.map +1 -1
- package/dist/components/scorecard/ScorecardsOverviewPanel.js.map +1 -1
- package/dist/components/security/SecurityScanList.js.map +1 -1
- package/dist/components/security/agentClient.js.map +1 -1
- package/dist/generated/wspace-types.js +1 -1
- package/dist/generated/wspace-types.js.map +1 -1
- package/dist/hooks/useGitops.js.map +1 -1
- package/package.json +1 -1
|
@@ -266,7 +266,7 @@ function j({ agent: n, agents: r, tunnelUrl: i, runningSessions: a, onReconfigur
|
|
|
266
266
|
return O({ variables: {
|
|
267
267
|
agentId: n.id,
|
|
268
268
|
profile: n.profile ?? "default",
|
|
269
|
-
scope: t.
|
|
269
|
+
scope: t.PlanSession,
|
|
270
270
|
resourceId: D,
|
|
271
271
|
ttlSeconds: 60
|
|
272
272
|
} }).then((t) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AIPlanPanel.js","names":[],"sources":["../../../src/components/ai/AIPlanPanel.tsx"],"sourcesContent":["/**\n * AIPlanPanel — the body of /ai-tools/plan.\n *\n * Five UI states gated in order:\n * 1. installing-plugin → vibe-plugin-plan + vibe-plugin-plan-plannotator\n * not installed on the agent. Renders the PluginGate install card.\n * 2. installing-cli → plugins installed but plannotator binary\n * missing on disk. Renders an Install CLI CTA.\n * 3. wizard → CLI installed but no AI-agent hook configured\n * yet. Renders the Claude/OpenCode/Codex/Pi/Gemini wizard.\n * 4. ready → all prereqs satisfied. Renders the tunnel\n * iframe pointing at ${tunnelUrl}/plan/, with postMessage auth\n * handoff on iframe load (mirrors PluginUIPanel.tsx).\n * 5. error → unrecoverable load failure for the status\n * query. Renders an error banner with retry.\n *\n * Auth handoff (matches PluginUIPanel.tsx:194–210):\n * onLoad → iframe.contentWindow.postMessage({type:'vibe-auth', apiKey},\n * new URL(tunnelUrl).origin)\n *\n * Surface contracts:\n * GET /api/plan-plannotator/status → PlannotatorStatus\n * POST /api/plan-plannotator/install → install binary\n * GET /api/plan-plannotator/agents/supported → AgentDetectionResult[]\n * POST /api/plan-plannotator/agents/:agent/configure-hook\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport {\n AlertCircle,\n CheckCircle2,\n Inbox,\n Loader2,\n PlayCircle,\n RefreshCcw,\n Settings,\n Wifi,\n WifiOff,\n} from 'lucide-react';\n\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { PluginGate } from '@/components/shared';\nimport { PLAN_META_PACKAGE, PLAN_PLANNOTATOR_PACKAGE } from '@/constants/pluginCatalog';\nimport {\n configureAgentHook,\n getPlannotatorStatus,\n installPlannotatorBinary,\n listSupportedAgents,\n type AgentDetectionResult,\n type PlannotatorStatus,\n} from '@/services/planApi';\nimport type { AgentRef } from '@/services/aiApi';\nimport { useMintAgentCapabilityMutation } from '@/generated/wspace-operations';\nimport { AgentCapabilityScope } from '@/generated/wspace-types';\n\ninterface AIPlanPanelProps {\n agent: AgentRef;\n tunnelUrl: string | null;\n}\n\ntype ConnectionStatus = 'connecting' | 'connected' | 'error';\n\nexport function AIPlanPanel({ agent, tunnelUrl }: AIPlanPanelProps) {\n const { t } = useI18n();\n // PluginGate renders eyebrow/title/description verbatim (no i18n lookup of\n // its own), so resolve them here with English fallbacks until plan.* keys exist.\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n return (\n <PluginGate\n agentId={agent.id}\n required={[PLAN_META_PACKAGE, PLAN_PLANNOTATOR_PACKAGE]}\n mode=\"all\"\n eyebrow={tr('plan.pluginRequiredEyebrow', 'Plan plugin required')}\n title={tr('plan.installPlanProviderTitle', 'Install the plan provider')}\n description={tr(\n 'plan.installPlanProviderDesc',\n 'Plan mode routes plan sessions through a plan provider plugin on your agent. Install the orchestrator and a provider (Plannotator) below to continue.'\n )}\n >\n <PlanPanelInner agent={agent} tunnelUrl={tunnelUrl} />\n </PluginGate>\n );\n}\n\nfunction PlanPanelInner({ agent, tunnelUrl }: AIPlanPanelProps) {\n const { t } = useI18n();\n const tr = useCallback(\n (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n },\n [t]\n );\n\n const [status, setStatus] = useState<PlannotatorStatus | null>(null);\n const [statusError, setStatusError] = useState<string | null>(null);\n const [statusLoading, setStatusLoading] = useState(true);\n const [agents, setAgents] = useState<AgentDetectionResult[]>([]);\n const [installing, setInstalling] = useState(false);\n const [configuring, setConfiguring] = useState<string | null>(null);\n const [actionMessage, setActionMessage] = useState<string | null>(null);\n const [actionError, setActionError] = useState<string | null>(null);\n // \"Manage hooks\" forces the wizard back open even when a hook is already\n // configured — the only way to re-write a stale/broken hook (wrong path or\n // a key from a prior agent identity), which otherwise locks the panel into\n // the ready view forever.\n const [forceWizard, setForceWizard] = useState(false);\n\n const refresh = useCallback(\n // `silent` background refreshes (polling) only update session status: they\n // skip the loading/error toggles AND the agent re-detection, so a transient\n // listSupportedAgents failure can't demote the ready view to the wizard.\n async (silent = false) => {\n if (!silent) {\n setStatusLoading(true);\n setStatusError(null);\n }\n try {\n if (silent) {\n setStatus(await getPlannotatorStatus(agent));\n return;\n }\n const [statusResult, agentsResult] = await Promise.all([\n getPlannotatorStatus(agent),\n listSupportedAgents(agent).catch(() => ({ agents: [] })),\n ]);\n setStatus(statusResult);\n setAgents(agentsResult.agents ?? []);\n } catch (err) {\n if (!silent) {\n setStatusError(err instanceof Error ? err.message : 'Failed to load plan status');\n }\n } finally {\n if (!silent) setStatusLoading(false);\n }\n },\n [agent]\n );\n\n useEffect(() => {\n void refresh();\n }, [refresh]);\n\n const handleInstallCli = useCallback(async () => {\n setInstalling(true);\n setActionError(null);\n setActionMessage(null);\n try {\n const result = await installPlannotatorBinary(agent);\n if (result.ok) {\n setActionMessage(tr('plan.cliInstalled', 'Plannotator CLI installed.'));\n await refresh();\n } else {\n setActionError(result.error ?? 'Install failed');\n }\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Install failed');\n } finally {\n setInstalling(false);\n }\n }, [agent, refresh, tr]);\n\n const handleConfigureHook = useCallback(\n async (agentKind: AgentDetectionResult['agent']) => {\n setConfiguring(agentKind);\n setActionError(null);\n setActionMessage(null);\n try {\n const result = await configureAgentHook(agent, agentKind);\n if (result.ok) {\n setActionMessage(tr('plan.hookConfigured', `Hook configured for ${agentKind}.`));\n await refresh();\n } else {\n setActionError(result.error ?? 'Hook configure failed');\n }\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Hook configure failed');\n } finally {\n setConfiguring(null);\n }\n },\n [agent, refresh, tr]\n );\n\n // Derived render state — guarded ordering means we always render the\n // most prerequisite-bound CTA first.\n const view = useMemo<'loading' | 'status-error' | 'install-cli' | 'wizard' | 'ready'>(() => {\n if (statusLoading) return 'loading';\n if (statusError) return 'status-error';\n if (!status?.installed) return 'install-cli';\n const anyConfigured = agents.some((a) => a.hookConfigured);\n if (!anyConfigured || forceWizard) return 'wizard';\n return 'ready';\n }, [status, statusError, statusLoading, agents, forceWizard]);\n\n // While the ready panel is shown, poll status in the background so a plan\n // session created by a coding agent's hook (or one that ends) is reflected\n // without a manual reload — `runningSessions` gates the iframe vs the empty\n // state below.\n useEffect(() => {\n if (view !== 'ready') return;\n // Skip ticks while the tab is hidden — no point polling a panel nobody sees.\n const id = setInterval(() => {\n if (!document.hidden) void refresh(true);\n }, 5000);\n return () => clearInterval(id);\n }, [view, refresh]);\n\n if (view === 'loading') {\n return (\n <div className=\"flex items-center gap-2 p-6 text-text-secondary\">\n <Loader2 className=\"size-4 animate-spin\" />\n <span>{tr('plan.loadingStatus', 'Loading plan status…')}</span>\n </div>\n );\n }\n\n if (view === 'status-error') {\n return (\n <div className=\"rounded-md border border-status-error-border bg-status-error-bg p-4 text-sm text-status-error-text\">\n <div className=\"flex items-start gap-2\">\n <AlertCircle className=\"size-4 mt-0.5 shrink-0\" />\n <div className=\"flex-1\">\n <p className=\"font-medium\">\n {tr('plan.statusError', 'Could not load plan status from the agent.')}\n </p>\n <p className=\"text-xs opacity-80 mt-1\">{statusError}</p>\n <button\n type=\"button\"\n onClick={() => void refresh()}\n className=\"mt-3 inline-flex items-center gap-1 text-xs font-medium hover:underline\"\n >\n <RefreshCcw className=\"size-3\" /> {tr('plan.retry', 'Retry')}\n </button>\n </div>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-4\">\n {actionMessage && (\n <div\n role=\"status\"\n className=\"rounded-md border border-status-success-border bg-status-success-bg px-3 py-2 text-xs text-status-success-text\"\n >\n {actionMessage}\n </div>\n )}\n {actionError && (\n <div\n role=\"alert\"\n className=\"rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-xs text-status-error-text\"\n >\n {actionError}\n </div>\n )}\n\n {view === 'install-cli' && (\n <InstallCliCard installing={installing} onInstall={handleInstallCli} tr={tr} />\n )}\n\n {view === 'wizard' && (\n <WizardCard\n agents={agents}\n configuring={configuring}\n onConfigure={handleConfigureHook}\n // Offer \"Done\" only when a ready view exists to return to — i.e.\n // at least one hook is configured (we got here via Manage hooks),\n // not during the genuine first-run wizard.\n canExit={forceWizard && agents.some((a) => a.hookConfigured)}\n onExit={() => setForceWizard(false)}\n tr={tr}\n />\n )}\n\n {view === 'ready' && (\n <PlanReadyPanel\n agent={agent}\n agents={agents}\n tunnelUrl={tunnelUrl}\n runningSessions={status?.runningSessions ?? []}\n onReconfigure={() => {\n setActionMessage(null);\n setForceWizard(true);\n }}\n tr={tr}\n />\n )}\n </div>\n );\n}\n\ninterface InstallCliCardProps {\n installing: boolean;\n onInstall: () => void;\n tr: (key: string, fallback: string) => string;\n}\n\nfunction InstallCliCard({ installing, onInstall, tr }: InstallCliCardProps) {\n return (\n <div className=\"rounded-lg border border-border-default bg-bg-elevated p-6\">\n <div className=\"flex items-start gap-3\">\n <PlayCircle className=\"size-6 text-action-primary-bg shrink-0\" />\n <div className=\"flex-1\">\n <h3 className=\"text-base font-semibold text-text-primary\">\n {tr('plan.installCliTitle', 'Install the plannotator CLI')}\n </h3>\n <p className=\"text-sm text-text-secondary mt-1\">\n {tr(\n 'plan.installCliDesc',\n 'Plan mode runs a local plannotator binary on the agent and proxies its UI back here. The plugin installs the binary into ~/.local/bin/plannotator (sha256-pinned). Roughly 30 seconds.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={onInstall}\n disabled={installing}\n className=\"mt-4 inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bgHover disabled:opacity-50\"\n >\n {installing ? (\n <>\n <Loader2 className=\"size-4 animate-spin\" />\n {tr('plan.installing', 'Installing…')}\n </>\n ) : (\n tr('plan.installCliCta', 'Install plannotator CLI')\n )}\n </button>\n </div>\n </div>\n </div>\n );\n}\n\ninterface WizardCardProps {\n agents: AgentDetectionResult[];\n configuring: string | null;\n onConfigure: (agentKind: AgentDetectionResult['agent']) => void;\n canExit?: boolean;\n onExit?: () => void;\n tr: (key: string, fallback: string) => string;\n}\n\nfunction WizardCard({ agents, configuring, onConfigure, canExit, onExit, tr }: WizardCardProps) {\n const installed = agents.filter((a) => a.cliInstalled);\n return (\n <div className=\"rounded-lg border border-border-default bg-bg-elevated p-6\">\n <div className=\"flex items-start gap-3\">\n <Settings className=\"size-6 text-action-primary-bg shrink-0\" />\n <div className=\"flex-1\">\n <div className=\"flex items-center justify-between gap-3\">\n <h3 className=\"text-base font-semibold text-text-primary\">\n {tr('plan.wizardTitle', 'Connect an AI coding agent')}\n </h3>\n {canExit && onExit && (\n <button\n type=\"button\"\n onClick={onExit}\n className=\"shrink-0 inline-flex items-center gap-1 text-xs text-text-secondary hover:text-text-primary\"\n >\n {tr('plan.wizardDone', 'Done')}\n </button>\n )}\n </div>\n <p className=\"text-sm text-text-secondary mt-1\">\n {tr(\n 'plan.wizardDesc',\n 'Configure a plannotator hook for at least one installed AI coding agent. The hook forwards plan content from the agent to plannotator on every plan-finalisation step.'\n )}\n </p>\n {installed.length === 0 && (\n <p className=\"mt-4 text-sm text-status-warning-text\">\n {tr(\n 'plan.wizardNoAgents',\n 'No AI coding agents detected in this sandbox. Install one (Claude Code, OpenCode, Codex, Pi, or Gemini) and refresh.'\n )}\n </p>\n )}\n <ul className=\"mt-4 space-y-2\">\n {agents.map((entry) => (\n <li\n key={entry.agent}\n className=\"flex items-start gap-3 p-3 border border-border-default rounded-md bg-bg-surface\"\n >\n {entry.hookConfigured ? (\n <CheckCircle2 className=\"size-5 text-status-success-text shrink-0 mt-0.5\" />\n ) : (\n <div className=\"size-5 rounded-full border-2 border-border-default shrink-0 mt-0.5\" />\n )}\n <div className=\"flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">{entry.name}</p>\n <p className=\"text-xs text-text-secondary mt-0.5\">{entry.instructions}</p>\n {!entry.cliInstalled && (\n <p className=\"text-xs text-status-warning-text mt-1\">\n {tr('plan.agentCliMissing', 'CLI not detected on this sandbox.')}\n </p>\n )}\n </div>\n <button\n type=\"button\"\n // Re-enabled when already configured: re-writing is the only\n // way to repair a stale hook (wrong path / old agent key).\n disabled={!entry.cliInstalled || configuring === entry.agent}\n onClick={() => onConfigure(entry.agent)}\n className=\"px-3 py-1.5 text-xs rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover disabled:opacity-50\"\n >\n {configuring === entry.agent\n ? tr('plan.configuring', 'Configuring…')\n : entry.hookConfigured\n ? tr('plan.reconfigureHook', 'Reconfigure')\n : tr('plan.configureHook', 'Configure hook')}\n </button>\n </li>\n ))}\n </ul>\n </div>\n </div>\n </div>\n );\n}\n\ninterface PlanReadyPanelProps {\n agent: AgentRef;\n agents: AgentDetectionResult[];\n tunnelUrl: string | null;\n runningSessions: PlannotatorStatus['runningSessions'];\n onReconfigure: () => void;\n tr: (key: string, fallback: string) => string;\n}\n\nfunction PlanReadyPanel({\n agent,\n agents,\n tunnelUrl,\n runningSessions,\n onReconfigure,\n tr,\n}: PlanReadyPanelProps) {\n const iframeRef = useRef<HTMLIFrameElement | null>(null);\n const [connection, setConnection] = useState<ConnectionStatus>('connecting');\n\n // plannotator is session-scoped — it only serves a plan at /plan/<sessionId>/.\n // A session exists once a connected coding agent finalises a plan (its hook\n // POSTs to the agent). With none running there is nothing to embed (the bare\n // /plan/ root 404s), so we show an empty state instead. The status endpoint\n // doesn't guarantee ordering, so pick the most recently started session.\n const activeSession = useMemo(\n () =>\n runningSessions.length > 0\n ? // `|| 0` keeps the sort deterministic if a startedAt is malformed (Date.parse → NaN).\n [...runningSessions].sort(\n (a, b) => (Date.parse(b.startedAt) || 0) - (Date.parse(a.startedAt) || 0)\n )[0]\n : null,\n [runningSessions]\n );\n const activeSessionId = activeSession?.sessionId ?? null;\n\n // F4 Wave B — mint a short-lived IFRAME_PLUGIN capability instead of\n // embedding the deprecated long-lived `agentApiKey` in `?apiKey=`. The\n // agent's `/plan/<sessionId>` bootstrap exchanges the `#vt=` fragment\n // for a scoped HttpOnly cookie before proxying to plannotator.\n const [mintAgentCapability] = useMintAgentCapabilityMutation();\n const [iframeToken, setIframeToken] = useState<string | null>(null);\n\n useEffect(() => {\n let cancelled = false;\n if (!agent.id || !activeSessionId) {\n setIframeToken(null);\n return undefined;\n }\n mintAgentCapability({\n variables: {\n agentId: agent.id,\n profile: agent.profile ?? 'default',\n scope: AgentCapabilityScope.IframePlugin,\n resourceId: activeSessionId,\n ttlSeconds: 60,\n },\n })\n .then((res) => {\n if (cancelled) return;\n setIframeToken(res.data?.mintAgentCapability?.token ?? null);\n })\n .catch(() => {\n if (!cancelled) setIframeToken(null);\n });\n return () => {\n cancelled = true;\n };\n }, [agent.id, agent.profile, activeSessionId, mintAgentCapability]);\n\n const iframeSrc = useMemo(() => {\n if (!tunnelUrl || !iframeToken || !activeSessionId) return null;\n const base = tunnelUrl.replace(/\\/$/, '');\n return `${base}/plan/${activeSessionId}/#vt=${encodeURIComponent(iframeToken)}`;\n }, [tunnelUrl, iframeToken, activeSessionId]);\n\n // Reset the connection indicator when the embedded session changes so a\n // prior session's 'error'/'connected' state doesn't bleed into the next one.\n useEffect(() => {\n setConnection('connecting');\n }, [activeSessionId]);\n\n const sendAuth = useCallback(() => {\n // No-op post-F4: the iframe authenticates via the `#vt=` fragment +\n // agent-issued cookie. Kept as a stable reference for handleLoad.\n }, []);\n\n const handleLoad = useCallback(() => {\n setConnection('connected');\n sendAuth();\n }, [sendAuth]);\n\n const handleError = useCallback(() => {\n setConnection('error');\n }, []);\n\n const handleRetry = useCallback(() => {\n const iframe = iframeRef.current;\n if (!iframe) return;\n setConnection('connecting');\n const src = iframe.src;\n iframe.src = 'about:blank';\n requestAnimationFrame(() => {\n iframe.src = src;\n });\n }, []);\n\n // Used elsewhere for analytics / breadcrumbs.\n void agent;\n\n if (!tunnelUrl) {\n return (\n <div className=\"rounded-lg border border-status-warning-border bg-status-warning-bg p-4 text-sm text-status-warning-text\">\n {tr(\n 'plan.tunnelMissing',\n 'Agent tunnel URL is missing. Bring the agent online and refresh.'\n )}\n </div>\n );\n }\n\n const configuredAgents = agents.filter((a) => a.hookConfigured);\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center gap-2 text-xs text-text-secondary\">\n {activeSession && (\n <>\n <span className=\"inline-flex items-center gap-1\">\n {connection === 'connected' && <Wifi className=\"size-3.5 text-status-success-text\" />}\n {connection === 'connecting' && (\n <Loader2 className=\"size-3.5 animate-spin text-status-warning-text\" />\n )}\n {connection === 'error' && <WifiOff className=\"size-3.5 text-status-error-text\" />}\n {tr(`plan.connection.${connection}`, connection)}\n </span>\n <span aria-hidden=\"true\">·</span>\n </>\n )}\n <span>{tr('plan.hookCount', `${configuredAgents.length} hook(s) configured`)}</span>\n <button\n type=\"button\"\n onClick={onReconfigure}\n className=\"ml-auto inline-flex items-center gap-1 hover:text-text-primary\"\n >\n <Settings className=\"size-3.5\" />\n {tr('plan.reopenWizard', 'Manage hooks')}\n </button>\n </div>\n\n <div className=\"border border-border-default rounded-lg overflow-hidden bg-bg-sunken relative h-[calc(100vh-320px)] min-h-[500px]\">\n {!activeSession && (\n <div className=\"absolute inset-0 flex flex-col items-center justify-center gap-2 p-6 text-center\">\n <Inbox className=\"size-8 text-text-muted\" />\n <p className=\"text-sm font-medium text-text-primary\">\n {tr('plan.noSessionTitle', 'No active plan session')}\n </p>\n <p className=\"max-w-md text-xs text-text-secondary\">\n {tr(\n 'plan.noSessionDesc',\n 'Start a plan in a connected coding agent — when it finalises a plan, the configured hook forwards it here for review.'\n )}\n </p>\n </div>\n )}\n {iframeSrc && (\n /* The src carries ?apiKey= for the initial auth (an iframe can't set\n request headers). Drop popups/top-navigation and send no referrer\n so that key can't leak out of the frame via a navigation or popup. */\n <iframe\n ref={iframeRef}\n src={iframeSrc}\n title={tr('plan.iframeTitle', 'Plannotator plan workspace')}\n className=\"w-full h-full border-0\"\n allow=\"clipboard-read; clipboard-write\"\n referrerPolicy=\"no-referrer\"\n sandbox=\"allow-same-origin allow-scripts allow-forms\"\n onLoad={handleLoad}\n onError={handleError}\n data-testid=\"plan-iframe\"\n />\n )}\n {activeSession && connection === 'connecting' && (\n <div className=\"absolute inset-0 flex items-center justify-center bg-bg-sunken/80 pointer-events-none\">\n <Loader2 className=\"size-6 animate-spin text-action-primary-bg\" />\n </div>\n )}\n {activeSession && connection === 'error' && (\n <div className=\"absolute inset-0 flex items-center justify-center bg-bg-sunken p-6 text-center\">\n <div>\n <AlertCircle className=\"size-6 text-status-error-text mx-auto\" />\n <p className=\"text-sm font-medium text-text-primary mt-2\">\n {tr('plan.iframeErrorTitle', 'Could not load plannotator.')}\n </p>\n <p className=\"text-xs text-text-secondary mt-1\">\n {tr(\n 'plan.iframeErrorDesc',\n 'The agent reverse proxy returned an error. Check the agent status and try again.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={handleRetry}\n className=\"mt-3 inline-flex items-center gap-1 rounded-md bg-action-primary-bg px-3 py-1.5 text-sm text-action-primary-text hover:bg-action-primary-bgHover\"\n >\n <RefreshCcw className=\"size-3.5\" /> {tr('plan.retry', 'Retry')}\n </button>\n </div>\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;AA8DA,SAAgB,EAAY,EAAE,UAAO,gBAA+B;CAClE,IAAM,EAAE,SAAM,GAAS,EAGjB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAQ,EAAE,EAAI;AACpB,SAAO,MAAU,IAAM,IAAW;;AAGpC,QACE,kBAAC,GAAD;EACE,SAAS,EAAM;EACf,UAAU,CAAC,GAAmB,EAAyB;EACvD,MAAK;EACL,SAAS,EAAG,8BAA8B,uBAAuB;EACjE,OAAO,EAAG,iCAAiC,4BAA4B;EACvE,aAAa,EACX,gCACA,wJACD;YAED,kBAAC,GAAD;GAAuB;GAAkB;GAAa,CAAA;EAC3C,CAAA;;AAIjB,SAAS,EAAe,EAAE,UAAO,gBAA+B;CAC9D,IAAM,EAAE,SAAM,GAAS,EACjB,IAAK,GACR,GAAa,MAA6B;EACzC,IAAM,IAAQ,EAAE,EAAI;AACpB,SAAO,MAAU,IAAM,IAAW;IAEpC,CAAC,EAAE,CACJ,EAEK,CAAC,GAAQ,KAAa,EAAmC,KAAK,EAC9D,CAAC,GAAa,KAAkB,EAAwB,KAAK,EAC7D,CAAC,GAAe,KAAoB,EAAS,GAAK,EAClD,CAAC,GAAQ,KAAa,EAAiC,EAAE,CAAC,EAC1D,CAAC,GAAY,KAAiB,EAAS,GAAM,EAC7C,CAAC,GAAa,KAAkB,EAAwB,KAAK,EAC7D,CAAC,GAAe,KAAoB,EAAwB,KAAK,EACjE,CAAC,GAAa,KAAkB,EAAwB,KAAK,EAK7D,CAAC,GAAa,KAAkB,EAAS,GAAM,EAE/C,IAAU,EAId,OAAO,IAAS,OAAU;AACxB,EAAK,MACH,EAAiB,GAAK,EACtB,EAAe,KAAK;AAEtB,MAAI;AACF,OAAI,GAAQ;AACV,MAAU,MAAM,EAAqB,EAAM,CAAC;AAC5C;;GAEF,IAAM,CAAC,GAAc,KAAgB,MAAM,QAAQ,IAAI,CACrD,EAAqB,EAAM,EAC3B,EAAoB,EAAM,CAAC,aAAa,EAAE,QAAQ,EAAE,EAAE,EAAE,CACzD,CAAC;AAEF,GADA,EAAU,EAAa,EACvB,EAAU,EAAa,UAAU,EAAE,CAAC;WAC7B,GAAK;AACZ,GAAK,KACH,EAAe,aAAe,QAAQ,EAAI,UAAU,6BAA6B;YAE3E;AACR,GAAK,KAAQ,EAAiB,GAAM;;IAGxC,CAAC,EAAM,CACR;AAED,SAAgB;AACT,KAAS;IACb,CAAC,EAAQ,CAAC;CAEb,IAAM,IAAmB,EAAY,YAAY;AAG/C,EAFA,EAAc,GAAK,EACnB,EAAe,KAAK,EACpB,EAAiB,KAAK;AACtB,MAAI;GACF,IAAM,IAAS,MAAM,EAAyB,EAAM;AACpD,GAAI,EAAO,MACT,EAAiB,EAAG,qBAAqB,6BAA6B,CAAC,EACvE,MAAM,GAAS,IAEf,EAAe,EAAO,SAAS,iBAAiB;WAE3C,GAAK;AACZ,KAAe,aAAe,QAAQ,EAAI,UAAU,iBAAiB;YAC7D;AACR,KAAc,GAAM;;IAErB;EAAC;EAAO;EAAS;EAAG,CAAC,EAElB,IAAsB,EAC1B,OAAO,MAA6C;AAGlD,EAFA,EAAe,EAAU,EACzB,EAAe,KAAK,EACpB,EAAiB,KAAK;AACtB,MAAI;GACF,IAAM,IAAS,MAAM,EAAmB,GAAO,EAAU;AACzD,GAAI,EAAO,MACT,EAAiB,EAAG,uBAAuB,uBAAuB,EAAU,GAAG,CAAC,EAChF,MAAM,GAAS,IAEf,EAAe,EAAO,SAAS,wBAAwB;WAElD,GAAK;AACZ,KAAe,aAAe,QAAQ,EAAI,UAAU,wBAAwB;YACpE;AACR,KAAe,KAAK;;IAGxB;EAAC;EAAO;EAAS;EAAG,CACrB,EAIK,IAAO,QACP,IAAsB,YACtB,IAAoB,iBACnB,GAAQ,YAET,CADkB,EAAO,MAAM,MAAM,EAAE,eAAe,IACpC,IAAoB,WACnC,UAHwB,eAI9B;EAAC;EAAQ;EAAa;EAAe;EAAQ;EAAY,CAAC;AA+C7D,QAzCA,QAAgB;AACd,MAAI,MAAS,QAAS;EAEtB,IAAM,IAAK,kBAAkB;AAC3B,GAAK,SAAS,UAAa,EAAQ,GAAK;KACvC,IAAK;AACR,eAAa,cAAc,EAAG;IAC7B,CAAC,GAAM,EAAQ,CAAC,EAEf,MAAS,YAET,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,GAAD,EAAS,WAAU,uBAAwB,CAAA,EAC3C,kBAAC,QAAD,EAAA,UAAO,EAAG,sBAAsB,uBAAuB,EAAQ,CAAA,CAC3D;MAIN,MAAS,iBAET,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD,EAAa,WAAU,0BAA2B,CAAA,EAClD,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAG,oBAAoB,6CAA6C;MACnE,CAAA;KACJ,kBAAC,KAAD;MAAG,WAAU;gBAA2B;MAAgB,CAAA;KACxD,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,KAAK,GAAS;MAC7B,WAAU;gBAHZ;OAKE,kBAAC,GAAD,EAAY,WAAU,UAAW,CAAA;;OAAE,EAAG,cAAc,QAAQ;OACrD;;KACL;MACF;;EACF,CAAA,GAKR,kBAAC,OAAD;EAAK,WAAU;YAAf;GACG,KACC,kBAAC,OAAD;IACE,MAAK;IACL,WAAU;cAET;IACG,CAAA;GAEP,KACC,kBAAC,OAAD;IACE,MAAK;IACL,WAAU;cAET;IACG,CAAA;GAGP,MAAS,iBACR,kBAAC,GAAD;IAA4B;IAAY,WAAW;IAAsB;IAAM,CAAA;GAGhF,MAAS,YACR,kBAAC,GAAD;IACU;IACK;IACb,aAAa;IAIb,SAAS,KAAe,EAAO,MAAM,MAAM,EAAE,eAAe;IAC5D,cAAc,EAAe,GAAM;IAC/B;IACJ,CAAA;GAGH,MAAS,WACR,kBAAC,GAAD;IACS;IACC;IACG;IACX,iBAAiB,GAAQ,mBAAmB,EAAE;IAC9C,qBAAqB;AAEnB,KADA,EAAiB,KAAK,EACtB,EAAe,GAAK;;IAElB;IACJ,CAAA;GAEA;;;AAUV,SAAS,EAAe,EAAE,eAAY,cAAW,SAA2B;AAC1E,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD,EAAY,WAAU,0CAA2C,CAAA,EACjE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAG,wBAAwB,8BAA8B;MACvD,CAAA;KACL,kBAAC,KAAD;MAAG,WAAU;gBACV,EACC,uBACA,yLACD;MACC,CAAA;KACJ,kBAAC,UAAD;MACE,MAAK;MACL,SAAS;MACT,UAAU;MACV,WAAU;gBAET,IACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD,EAAS,WAAU,uBAAwB,CAAA,EAC1C,EAAG,mBAAmB,cAAc,CACpC,EAAA,CAAA,GAEH,EAAG,sBAAsB,0BAA0B;MAE9C,CAAA;KACL;MACF;;EACF,CAAA;;AAaV,SAAS,EAAW,EAAE,WAAQ,gBAAa,gBAAa,YAAS,WAAQ,SAAuB;CAC9F,IAAM,IAAY,EAAO,QAAQ,MAAM,EAAE,aAAa;AACtD,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD,EAAU,WAAU,0CAA2C,CAAA,EAC/D,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAG,oBAAoB,6BAA6B;OAClD,CAAA,EACJ,KAAW,KACV,kBAAC,UAAD;OACE,MAAK;OACL,SAAS;OACT,WAAU;iBAET,EAAG,mBAAmB,OAAO;OACvB,CAAA,CAEP;;KACN,kBAAC,KAAD;MAAG,WAAU;gBACV,EACC,mBACA,yKACD;MACC,CAAA;KACH,EAAU,WAAW,KACpB,kBAAC,KAAD;MAAG,WAAU;gBACV,EACC,uBACA,uHACD;MACC,CAAA;KAEN,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAO,KAAK,MACX,kBAAC,MAAD;OAEE,WAAU;iBAFZ;QAIG,EAAM,iBACL,kBAAC,GAAD,EAAc,WAAU,mDAAoD,CAAA,GAE5E,kBAAC,OAAD,EAAK,WAAU,sEAAuE,CAAA;QAExF,kBAAC,OAAD;SAAK,WAAU;mBAAf;UACE,kBAAC,KAAD;WAAG,WAAU;qBAAyC,EAAM;WAAS,CAAA;UACrE,kBAAC,KAAD;WAAG,WAAU;qBAAsC,EAAM;WAAiB,CAAA;UACzE,CAAC,EAAM,gBACN,kBAAC,KAAD;WAAG,WAAU;qBACV,EAAG,wBAAwB,oCAAoC;WAC9D,CAAA;UAEF;;QACN,kBAAC,UAAD;SACE,MAAK;SAGL,UAAU,CAAC,EAAM,gBAAgB,MAAgB,EAAM;SACvD,eAAe,EAAY,EAAM,MAAM;SACvC,WAAU;mBAET,MAAgB,EAAM,QACnB,EAAG,oBAAoB,eAAe,GACtC,EAAM,iBACJ,EAAG,wBAAwB,cAAc,GACzC,EAAG,sBAAsB,iBAAiB;SACzC,CAAA;QACN;SA/BE,EAAM,MA+BR,CACL;MACC,CAAA;KACD;MACF;;EACF,CAAA;;AAaV,SAAS,EAAe,EACtB,UACA,WACA,cACA,oBACA,kBACA,SACsB;CACtB,IAAM,IAAY,EAAiC,KAAK,EAClD,CAAC,GAAY,KAAiB,EAA2B,aAAa,EAOtE,IAAgB,QAElB,EAAgB,SAAS,IAErB,CAAC,GAAG,EAAgB,CAAC,MAClB,GAAG,OAAO,KAAK,MAAM,EAAE,UAAU,IAAI,MAAM,KAAK,MAAM,EAAE,UAAU,IAAI,GACxE,CAAC,KACF,MACN,CAAC,EAAgB,CAClB,EACK,IAAkB,GAAe,aAAa,MAM9C,CAAC,KAAuB,GAAgC,EACxD,CAAC,GAAa,KAAkB,EAAwB,KAAK;AAEnE,SAAgB;EACd,IAAI,IAAY;AAChB,MAAI,CAAC,EAAM,MAAM,CAAC,GAAiB;AACjC,KAAe,KAAK;AACpB;;AAkBF,SAhBA,EAAoB,EAClB,WAAW;GACT,SAAS,EAAM;GACf,SAAS,EAAM,WAAW;GAC1B,OAAO,EAAqB;GAC5B,YAAY;GACZ,YAAY;GACb,EACF,CAAC,CACC,MAAM,MAAQ;AACT,QACJ,EAAe,EAAI,MAAM,qBAAqB,SAAS,KAAK;IAC5D,CACD,YAAY;AACX,GAAK,KAAW,EAAe,KAAK;IACpC,QACS;AACX,OAAY;;IAEb;EAAC,EAAM;EAAI,EAAM;EAAS;EAAiB;EAAoB,CAAC;CAEnE,IAAM,IAAY,QACZ,CAAC,KAAa,CAAC,KAAe,CAAC,IAAwB,OAEpD,GADM,EAAU,QAAQ,OAAO,GAAG,CAC1B,QAAQ,EAAgB,OAAO,mBAAmB,EAAY,IAC5E;EAAC;EAAW;EAAa;EAAgB,CAAC;AAI7C,SAAgB;AACd,IAAc,aAAa;IAC1B,CAAC,EAAgB,CAAC;CAErB,IAAM,IAAW,QAAkB,IAGhC,EAAE,CAAC,EAEA,IAAa,QAAkB;AAEnC,EADA,EAAc,YAAY,EAC1B,GAAU;IACT,CAAC,EAAS,CAAC,EAER,IAAc,QAAkB;AACpC,IAAc,QAAQ;IACrB,EAAE,CAAC,EAEA,IAAc,QAAkB;EACpC,IAAM,IAAS,EAAU;AACzB,MAAI,CAAC,EAAQ;AACb,IAAc,aAAa;EAC3B,IAAM,IAAM,EAAO;AAEnB,EADA,EAAO,MAAM,eACb,4BAA4B;AAC1B,KAAO,MAAM;IACb;IACD,EAAE,CAAC;AAKN,KAAI,CAAC,EACH,QACE,kBAAC,OAAD;EAAK,WAAU;YACZ,EACC,sBACA,mEACD;EACG,CAAA;CAIV,IAAM,IAAmB,EAAO,QAAQ,MAAM,EAAE,eAAe;AAE/D,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACG,KACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,QAAD;KAAM,WAAU;eAAhB;MACG,MAAe,eAAe,kBAAC,GAAD,EAAM,WAAU,qCAAsC,CAAA;MACpF,MAAe,gBACd,kBAAC,GAAD,EAAS,WAAU,kDAAmD,CAAA;MAEvE,MAAe,WAAW,kBAAC,GAAD,EAAS,WAAU,mCAAoC,CAAA;MACjF,EAAG,mBAAmB,KAAc,EAAW;MAC3C;QACP,kBAAC,QAAD;KAAM,eAAY;eAAO;KAAQ,CAAA,CAChC,EAAA,CAAA;IAEL,kBAAC,QAAD,EAAA,UAAO,EAAG,kBAAkB,GAAG,EAAiB,OAAO,qBAAqB,EAAQ,CAAA;IACpF,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,WAAU;eAHZ,CAKE,kBAAC,GAAD,EAAU,WAAU,YAAa,CAAA,EAChC,EAAG,qBAAqB,eAAe,CACjC;;IACL;MAEN,kBAAC,OAAD;GAAK,WAAU;aAAf;IACG,CAAC,KACA,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,GAAD,EAAO,WAAU,0BAA2B,CAAA;MAC5C,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAG,uBAAuB,yBAAyB;OAClD,CAAA;MACJ,kBAAC,KAAD;OAAG,WAAU;iBACV,EACC,sBACA,wHACD;OACC,CAAA;MACA;;IAEP,KAIC,kBAAC,UAAD;KACE,KAAK;KACL,KAAK;KACL,OAAO,EAAG,oBAAoB,6BAA6B;KAC3D,WAAU;KACV,OAAM;KACN,gBAAe;KACf,SAAQ;KACR,QAAQ;KACR,SAAS;KACT,eAAY;KACZ,CAAA;IAEH,KAAiB,MAAe,gBAC/B,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD,EAAS,WAAU,8CAA+C,CAAA;KAC9D,CAAA;IAEP,KAAiB,MAAe,WAC/B,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,OAAD,EAAA,UAAA;MACE,kBAAC,GAAD,EAAa,WAAU,yCAA0C,CAAA;MACjE,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAG,yBAAyB,8BAA8B;OACzD,CAAA;MACJ,kBAAC,KAAD;OAAG,WAAU;iBACV,EACC,wBACA,mFACD;OACC,CAAA;MACJ,kBAAC,UAAD;OACE,MAAK;OACL,SAAS;OACT,WAAU;iBAHZ;QAKE,kBAAC,GAAD,EAAY,WAAU,YAAa,CAAA;;QAAE,EAAG,cAAc,QAAQ;QACvD;;MACL,EAAA,CAAA;KACF,CAAA;IAEJ;KACF"}
|
|
1
|
+
{"version":3,"file":"AIPlanPanel.js","names":[],"sources":["../../../src/components/ai/AIPlanPanel.tsx"],"sourcesContent":["/**\n * AIPlanPanel — the body of /ai-tools/plan.\n *\n * Five UI states gated in order:\n * 1. installing-plugin → vibe-plugin-plan + vibe-plugin-plan-plannotator\n * not installed on the agent. Renders the PluginGate install card.\n * 2. installing-cli → plugins installed but plannotator binary\n * missing on disk. Renders an Install CLI CTA.\n * 3. wizard → CLI installed but no AI-agent hook configured\n * yet. Renders the Claude/OpenCode/Codex/Pi/Gemini wizard.\n * 4. ready → all prereqs satisfied. Renders the tunnel\n * iframe pointing at ${tunnelUrl}/plan/, with postMessage auth\n * handoff on iframe load (mirrors PluginUIPanel.tsx).\n * 5. error → unrecoverable load failure for the status\n * query. Renders an error banner with retry.\n *\n * Auth handoff (matches PluginUIPanel.tsx:194–210):\n * onLoad → iframe.contentWindow.postMessage({type:'vibe-auth', apiKey},\n * new URL(tunnelUrl).origin)\n *\n * Surface contracts:\n * GET /api/plan-plannotator/status → PlannotatorStatus\n * POST /api/plan-plannotator/install → install binary\n * GET /api/plan-plannotator/agents/supported → AgentDetectionResult[]\n * POST /api/plan-plannotator/agents/:agent/configure-hook\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport {\n AlertCircle,\n CheckCircle2,\n Inbox,\n Loader2,\n PlayCircle,\n RefreshCcw,\n Settings,\n Wifi,\n WifiOff,\n} from 'lucide-react';\n\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { PluginGate } from '@/components/shared';\nimport { PLAN_META_PACKAGE, PLAN_PLANNOTATOR_PACKAGE } from '@/constants/pluginCatalog';\nimport {\n configureAgentHook,\n getPlannotatorStatus,\n installPlannotatorBinary,\n listSupportedAgents,\n type AgentDetectionResult,\n type PlannotatorStatus,\n} from '@/services/planApi';\nimport type { AgentRef } from '@/services/aiApi';\nimport { useMintAgentCapabilityMutation } from '@/generated/wspace-operations';\nimport { AgentCapabilityScope } from '@/generated/wspace-types';\n\ninterface AIPlanPanelProps {\n agent: AgentRef;\n tunnelUrl: string | null;\n}\n\ntype ConnectionStatus = 'connecting' | 'connected' | 'error';\n\nexport function AIPlanPanel({ agent, tunnelUrl }: AIPlanPanelProps) {\n const { t } = useI18n();\n // PluginGate renders eyebrow/title/description verbatim (no i18n lookup of\n // its own), so resolve them here with English fallbacks until plan.* keys exist.\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n return (\n <PluginGate\n agentId={agent.id}\n required={[PLAN_META_PACKAGE, PLAN_PLANNOTATOR_PACKAGE]}\n mode=\"all\"\n eyebrow={tr('plan.pluginRequiredEyebrow', 'Plan plugin required')}\n title={tr('plan.installPlanProviderTitle', 'Install the plan provider')}\n description={tr(\n 'plan.installPlanProviderDesc',\n 'Plan mode routes plan sessions through a plan provider plugin on your agent. Install the orchestrator and a provider (Plannotator) below to continue.'\n )}\n >\n <PlanPanelInner agent={agent} tunnelUrl={tunnelUrl} />\n </PluginGate>\n );\n}\n\nfunction PlanPanelInner({ agent, tunnelUrl }: AIPlanPanelProps) {\n const { t } = useI18n();\n const tr = useCallback(\n (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n },\n [t]\n );\n\n const [status, setStatus] = useState<PlannotatorStatus | null>(null);\n const [statusError, setStatusError] = useState<string | null>(null);\n const [statusLoading, setStatusLoading] = useState(true);\n const [agents, setAgents] = useState<AgentDetectionResult[]>([]);\n const [installing, setInstalling] = useState(false);\n const [configuring, setConfiguring] = useState<string | null>(null);\n const [actionMessage, setActionMessage] = useState<string | null>(null);\n const [actionError, setActionError] = useState<string | null>(null);\n // \"Manage hooks\" forces the wizard back open even when a hook is already\n // configured — the only way to re-write a stale/broken hook (wrong path or\n // a key from a prior agent identity), which otherwise locks the panel into\n // the ready view forever.\n const [forceWizard, setForceWizard] = useState(false);\n\n const refresh = useCallback(\n // `silent` background refreshes (polling) only update session status: they\n // skip the loading/error toggles AND the agent re-detection, so a transient\n // listSupportedAgents failure can't demote the ready view to the wizard.\n async (silent = false) => {\n if (!silent) {\n setStatusLoading(true);\n setStatusError(null);\n }\n try {\n if (silent) {\n setStatus(await getPlannotatorStatus(agent));\n return;\n }\n const [statusResult, agentsResult] = await Promise.all([\n getPlannotatorStatus(agent),\n listSupportedAgents(agent).catch(() => ({ agents: [] })),\n ]);\n setStatus(statusResult);\n setAgents(agentsResult.agents ?? []);\n } catch (err) {\n if (!silent) {\n setStatusError(err instanceof Error ? err.message : 'Failed to load plan status');\n }\n } finally {\n if (!silent) setStatusLoading(false);\n }\n },\n [agent]\n );\n\n useEffect(() => {\n void refresh();\n }, [refresh]);\n\n const handleInstallCli = useCallback(async () => {\n setInstalling(true);\n setActionError(null);\n setActionMessage(null);\n try {\n const result = await installPlannotatorBinary(agent);\n if (result.ok) {\n setActionMessage(tr('plan.cliInstalled', 'Plannotator CLI installed.'));\n await refresh();\n } else {\n setActionError(result.error ?? 'Install failed');\n }\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Install failed');\n } finally {\n setInstalling(false);\n }\n }, [agent, refresh, tr]);\n\n const handleConfigureHook = useCallback(\n async (agentKind: AgentDetectionResult['agent']) => {\n setConfiguring(agentKind);\n setActionError(null);\n setActionMessage(null);\n try {\n const result = await configureAgentHook(agent, agentKind);\n if (result.ok) {\n setActionMessage(tr('plan.hookConfigured', `Hook configured for ${agentKind}.`));\n await refresh();\n } else {\n setActionError(result.error ?? 'Hook configure failed');\n }\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Hook configure failed');\n } finally {\n setConfiguring(null);\n }\n },\n [agent, refresh, tr]\n );\n\n // Derived render state — guarded ordering means we always render the\n // most prerequisite-bound CTA first.\n const view = useMemo<'loading' | 'status-error' | 'install-cli' | 'wizard' | 'ready'>(() => {\n if (statusLoading) return 'loading';\n if (statusError) return 'status-error';\n if (!status?.installed) return 'install-cli';\n const anyConfigured = agents.some((a) => a.hookConfigured);\n if (!anyConfigured || forceWizard) return 'wizard';\n return 'ready';\n }, [status, statusError, statusLoading, agents, forceWizard]);\n\n // While the ready panel is shown, poll status in the background so a plan\n // session created by a coding agent's hook (or one that ends) is reflected\n // without a manual reload — `runningSessions` gates the iframe vs the empty\n // state below.\n useEffect(() => {\n if (view !== 'ready') return;\n // Skip ticks while the tab is hidden — no point polling a panel nobody sees.\n const id = setInterval(() => {\n if (!document.hidden) void refresh(true);\n }, 5000);\n return () => clearInterval(id);\n }, [view, refresh]);\n\n if (view === 'loading') {\n return (\n <div className=\"flex items-center gap-2 p-6 text-text-secondary\">\n <Loader2 className=\"size-4 animate-spin\" />\n <span>{tr('plan.loadingStatus', 'Loading plan status…')}</span>\n </div>\n );\n }\n\n if (view === 'status-error') {\n return (\n <div className=\"rounded-md border border-status-error-border bg-status-error-bg p-4 text-sm text-status-error-text\">\n <div className=\"flex items-start gap-2\">\n <AlertCircle className=\"size-4 mt-0.5 shrink-0\" />\n <div className=\"flex-1\">\n <p className=\"font-medium\">\n {tr('plan.statusError', 'Could not load plan status from the agent.')}\n </p>\n <p className=\"text-xs opacity-80 mt-1\">{statusError}</p>\n <button\n type=\"button\"\n onClick={() => void refresh()}\n className=\"mt-3 inline-flex items-center gap-1 text-xs font-medium hover:underline\"\n >\n <RefreshCcw className=\"size-3\" /> {tr('plan.retry', 'Retry')}\n </button>\n </div>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-4\">\n {actionMessage && (\n <div\n role=\"status\"\n className=\"rounded-md border border-status-success-border bg-status-success-bg px-3 py-2 text-xs text-status-success-text\"\n >\n {actionMessage}\n </div>\n )}\n {actionError && (\n <div\n role=\"alert\"\n className=\"rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-xs text-status-error-text\"\n >\n {actionError}\n </div>\n )}\n\n {view === 'install-cli' && (\n <InstallCliCard installing={installing} onInstall={handleInstallCli} tr={tr} />\n )}\n\n {view === 'wizard' && (\n <WizardCard\n agents={agents}\n configuring={configuring}\n onConfigure={handleConfigureHook}\n // Offer \"Done\" only when a ready view exists to return to — i.e.\n // at least one hook is configured (we got here via Manage hooks),\n // not during the genuine first-run wizard.\n canExit={forceWizard && agents.some((a) => a.hookConfigured)}\n onExit={() => setForceWizard(false)}\n tr={tr}\n />\n )}\n\n {view === 'ready' && (\n <PlanReadyPanel\n agent={agent}\n agents={agents}\n tunnelUrl={tunnelUrl}\n runningSessions={status?.runningSessions ?? []}\n onReconfigure={() => {\n setActionMessage(null);\n setForceWizard(true);\n }}\n tr={tr}\n />\n )}\n </div>\n );\n}\n\ninterface InstallCliCardProps {\n installing: boolean;\n onInstall: () => void;\n tr: (key: string, fallback: string) => string;\n}\n\nfunction InstallCliCard({ installing, onInstall, tr }: InstallCliCardProps) {\n return (\n <div className=\"rounded-lg border border-border-default bg-bg-elevated p-6\">\n <div className=\"flex items-start gap-3\">\n <PlayCircle className=\"size-6 text-action-primary-bg shrink-0\" />\n <div className=\"flex-1\">\n <h3 className=\"text-base font-semibold text-text-primary\">\n {tr('plan.installCliTitle', 'Install the plannotator CLI')}\n </h3>\n <p className=\"text-sm text-text-secondary mt-1\">\n {tr(\n 'plan.installCliDesc',\n 'Plan mode runs a local plannotator binary on the agent and proxies its UI back here. The plugin installs the binary into ~/.local/bin/plannotator (sha256-pinned). Roughly 30 seconds.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={onInstall}\n disabled={installing}\n className=\"mt-4 inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bgHover disabled:opacity-50\"\n >\n {installing ? (\n <>\n <Loader2 className=\"size-4 animate-spin\" />\n {tr('plan.installing', 'Installing…')}\n </>\n ) : (\n tr('plan.installCliCta', 'Install plannotator CLI')\n )}\n </button>\n </div>\n </div>\n </div>\n );\n}\n\ninterface WizardCardProps {\n agents: AgentDetectionResult[];\n configuring: string | null;\n onConfigure: (agentKind: AgentDetectionResult['agent']) => void;\n canExit?: boolean;\n onExit?: () => void;\n tr: (key: string, fallback: string) => string;\n}\n\nfunction WizardCard({ agents, configuring, onConfigure, canExit, onExit, tr }: WizardCardProps) {\n const installed = agents.filter((a) => a.cliInstalled);\n return (\n <div className=\"rounded-lg border border-border-default bg-bg-elevated p-6\">\n <div className=\"flex items-start gap-3\">\n <Settings className=\"size-6 text-action-primary-bg shrink-0\" />\n <div className=\"flex-1\">\n <div className=\"flex items-center justify-between gap-3\">\n <h3 className=\"text-base font-semibold text-text-primary\">\n {tr('plan.wizardTitle', 'Connect an AI coding agent')}\n </h3>\n {canExit && onExit && (\n <button\n type=\"button\"\n onClick={onExit}\n className=\"shrink-0 inline-flex items-center gap-1 text-xs text-text-secondary hover:text-text-primary\"\n >\n {tr('plan.wizardDone', 'Done')}\n </button>\n )}\n </div>\n <p className=\"text-sm text-text-secondary mt-1\">\n {tr(\n 'plan.wizardDesc',\n 'Configure a plannotator hook for at least one installed AI coding agent. The hook forwards plan content from the agent to plannotator on every plan-finalisation step.'\n )}\n </p>\n {installed.length === 0 && (\n <p className=\"mt-4 text-sm text-status-warning-text\">\n {tr(\n 'plan.wizardNoAgents',\n 'No AI coding agents detected in this sandbox. Install one (Claude Code, OpenCode, Codex, Pi, or Gemini) and refresh.'\n )}\n </p>\n )}\n <ul className=\"mt-4 space-y-2\">\n {agents.map((entry) => (\n <li\n key={entry.agent}\n className=\"flex items-start gap-3 p-3 border border-border-default rounded-md bg-bg-surface\"\n >\n {entry.hookConfigured ? (\n <CheckCircle2 className=\"size-5 text-status-success-text shrink-0 mt-0.5\" />\n ) : (\n <div className=\"size-5 rounded-full border-2 border-border-default shrink-0 mt-0.5\" />\n )}\n <div className=\"flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">{entry.name}</p>\n <p className=\"text-xs text-text-secondary mt-0.5\">{entry.instructions}</p>\n {!entry.cliInstalled && (\n <p className=\"text-xs text-status-warning-text mt-1\">\n {tr('plan.agentCliMissing', 'CLI not detected on this sandbox.')}\n </p>\n )}\n </div>\n <button\n type=\"button\"\n // Re-enabled when already configured: re-writing is the only\n // way to repair a stale hook (wrong path / old agent key).\n disabled={!entry.cliInstalled || configuring === entry.agent}\n onClick={() => onConfigure(entry.agent)}\n className=\"px-3 py-1.5 text-xs rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover disabled:opacity-50\"\n >\n {configuring === entry.agent\n ? tr('plan.configuring', 'Configuring…')\n : entry.hookConfigured\n ? tr('plan.reconfigureHook', 'Reconfigure')\n : tr('plan.configureHook', 'Configure hook')}\n </button>\n </li>\n ))}\n </ul>\n </div>\n </div>\n </div>\n );\n}\n\ninterface PlanReadyPanelProps {\n agent: AgentRef;\n agents: AgentDetectionResult[];\n tunnelUrl: string | null;\n runningSessions: PlannotatorStatus['runningSessions'];\n onReconfigure: () => void;\n tr: (key: string, fallback: string) => string;\n}\n\nfunction PlanReadyPanel({\n agent,\n agents,\n tunnelUrl,\n runningSessions,\n onReconfigure,\n tr,\n}: PlanReadyPanelProps) {\n const iframeRef = useRef<HTMLIFrameElement | null>(null);\n const [connection, setConnection] = useState<ConnectionStatus>('connecting');\n\n // plannotator is session-scoped — it only serves a plan at /plan/<sessionId>/.\n // A session exists once a connected coding agent finalises a plan (its hook\n // POSTs to the agent). With none running there is nothing to embed (the bare\n // /plan/ root 404s), so we show an empty state instead. The status endpoint\n // doesn't guarantee ordering, so pick the most recently started session.\n const activeSession = useMemo(\n () =>\n runningSessions.length > 0\n ? // `|| 0` keeps the sort deterministic if a startedAt is malformed (Date.parse → NaN).\n [...runningSessions].sort(\n (a, b) => (Date.parse(b.startedAt) || 0) - (Date.parse(a.startedAt) || 0)\n )[0]\n : null,\n [runningSessions]\n );\n const activeSessionId = activeSession?.sessionId ?? null;\n\n // F4 Wave B — mint a short-lived IFRAME_PLUGIN capability instead of\n // embedding the deprecated long-lived `agentApiKey` in `?apiKey=`. The\n // agent's `/plan/<sessionId>` bootstrap exchanges the `#vt=` fragment\n // for a scoped HttpOnly cookie before proxying to plannotator.\n const [mintAgentCapability] = useMintAgentCapabilityMutation();\n const [iframeToken, setIframeToken] = useState<string | null>(null);\n\n useEffect(() => {\n let cancelled = false;\n if (!agent.id || !activeSessionId) {\n setIframeToken(null);\n return undefined;\n }\n mintAgentCapability({\n variables: {\n agentId: agent.id,\n profile: agent.profile ?? 'default',\n // PLAN_SESSION binds the ticket to `/plan/<sessionId>` — the path the\n // iframe actually loads. IFRAME_PLUGIN would scope it to `/ui/<id>`,\n // which never matches the plan proxy URL (the bug that left the iframe\n // 401'd). The agent's `/plan/*` shim verifies this `/plan/<sid>` ticket.\n scope: AgentCapabilityScope.PlanSession,\n resourceId: activeSessionId,\n ttlSeconds: 60,\n },\n })\n .then((res) => {\n if (cancelled) return;\n setIframeToken(res.data?.mintAgentCapability?.token ?? null);\n })\n .catch(() => {\n if (!cancelled) setIframeToken(null);\n });\n return () => {\n cancelled = true;\n };\n }, [agent.id, agent.profile, activeSessionId, mintAgentCapability]);\n\n const iframeSrc = useMemo(() => {\n if (!tunnelUrl || !iframeToken || !activeSessionId) return null;\n const base = tunnelUrl.replace(/\\/$/, '');\n return `${base}/plan/${activeSessionId}/#vt=${encodeURIComponent(iframeToken)}`;\n }, [tunnelUrl, iframeToken, activeSessionId]);\n\n // Reset the connection indicator when the embedded session changes so a\n // prior session's 'error'/'connected' state doesn't bleed into the next one.\n useEffect(() => {\n setConnection('connecting');\n }, [activeSessionId]);\n\n const sendAuth = useCallback(() => {\n // No-op post-F4: the iframe authenticates via the `#vt=` fragment +\n // agent-issued cookie. Kept as a stable reference for handleLoad.\n }, []);\n\n const handleLoad = useCallback(() => {\n setConnection('connected');\n sendAuth();\n }, [sendAuth]);\n\n const handleError = useCallback(() => {\n setConnection('error');\n }, []);\n\n const handleRetry = useCallback(() => {\n const iframe = iframeRef.current;\n if (!iframe) return;\n setConnection('connecting');\n const src = iframe.src;\n iframe.src = 'about:blank';\n requestAnimationFrame(() => {\n iframe.src = src;\n });\n }, []);\n\n // Used elsewhere for analytics / breadcrumbs.\n void agent;\n\n if (!tunnelUrl) {\n return (\n <div className=\"rounded-lg border border-status-warning-border bg-status-warning-bg p-4 text-sm text-status-warning-text\">\n {tr(\n 'plan.tunnelMissing',\n 'Agent tunnel URL is missing. Bring the agent online and refresh.'\n )}\n </div>\n );\n }\n\n const configuredAgents = agents.filter((a) => a.hookConfigured);\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center gap-2 text-xs text-text-secondary\">\n {activeSession && (\n <>\n <span className=\"inline-flex items-center gap-1\">\n {connection === 'connected' && <Wifi className=\"size-3.5 text-status-success-text\" />}\n {connection === 'connecting' && (\n <Loader2 className=\"size-3.5 animate-spin text-status-warning-text\" />\n )}\n {connection === 'error' && <WifiOff className=\"size-3.5 text-status-error-text\" />}\n {tr(`plan.connection.${connection}`, connection)}\n </span>\n <span aria-hidden=\"true\">·</span>\n </>\n )}\n <span>{tr('plan.hookCount', `${configuredAgents.length} hook(s) configured`)}</span>\n <button\n type=\"button\"\n onClick={onReconfigure}\n className=\"ml-auto inline-flex items-center gap-1 hover:text-text-primary\"\n >\n <Settings className=\"size-3.5\" />\n {tr('plan.reopenWizard', 'Manage hooks')}\n </button>\n </div>\n\n <div className=\"border border-border-default rounded-lg overflow-hidden bg-bg-sunken relative h-[calc(100vh-320px)] min-h-[500px]\">\n {!activeSession && (\n <div className=\"absolute inset-0 flex flex-col items-center justify-center gap-2 p-6 text-center\">\n <Inbox className=\"size-8 text-text-muted\" />\n <p className=\"text-sm font-medium text-text-primary\">\n {tr('plan.noSessionTitle', 'No active plan session')}\n </p>\n <p className=\"max-w-md text-xs text-text-secondary\">\n {tr(\n 'plan.noSessionDesc',\n 'Start a plan in a connected coding agent — when it finalises a plan, the configured hook forwards it here for review.'\n )}\n </p>\n </div>\n )}\n {iframeSrc && (\n /* The src carries ?apiKey= for the initial auth (an iframe can't set\n request headers). Drop popups/top-navigation and send no referrer\n so that key can't leak out of the frame via a navigation or popup. */\n <iframe\n ref={iframeRef}\n src={iframeSrc}\n title={tr('plan.iframeTitle', 'Plannotator plan workspace')}\n className=\"w-full h-full border-0\"\n allow=\"clipboard-read; clipboard-write\"\n referrerPolicy=\"no-referrer\"\n sandbox=\"allow-same-origin allow-scripts allow-forms\"\n onLoad={handleLoad}\n onError={handleError}\n data-testid=\"plan-iframe\"\n />\n )}\n {activeSession && connection === 'connecting' && (\n <div className=\"absolute inset-0 flex items-center justify-center bg-bg-sunken/80 pointer-events-none\">\n <Loader2 className=\"size-6 animate-spin text-action-primary-bg\" />\n </div>\n )}\n {activeSession && connection === 'error' && (\n <div className=\"absolute inset-0 flex items-center justify-center bg-bg-sunken p-6 text-center\">\n <div>\n <AlertCircle className=\"size-6 text-status-error-text mx-auto\" />\n <p className=\"text-sm font-medium text-text-primary mt-2\">\n {tr('plan.iframeErrorTitle', 'Could not load plannotator.')}\n </p>\n <p className=\"text-xs text-text-secondary mt-1\">\n {tr(\n 'plan.iframeErrorDesc',\n 'The agent reverse proxy returned an error. Check the agent status and try again.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={handleRetry}\n className=\"mt-3 inline-flex items-center gap-1 rounded-md bg-action-primary-bg px-3 py-1.5 text-sm text-action-primary-text hover:bg-action-primary-bgHover\"\n >\n <RefreshCcw className=\"size-3.5\" /> {tr('plan.retry', 'Retry')}\n </button>\n </div>\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -216,8 +216,7 @@ var h = ({ ownerType: h, ownerId: g, workspaceId: _, applyingTemplateId: v, onAp
|
|
|
216
216
|
className: "text-text-primary",
|
|
217
217
|
children: "Score"
|
|
218
218
|
}),
|
|
219
|
-
" — each metric is scored 0–100. For",
|
|
220
|
-
" ",
|
|
219
|
+
" — each metric is scored 0–100. For ",
|
|
221
220
|
/* @__PURE__ */ p("em", { children: "higher-is-better" }),
|
|
222
221
|
" metrics the score is",
|
|
223
222
|
" ",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ScorecardEmptyState.js","names":[],"sources":["../../../src/components/scorecard/ScorecardEmptyState.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState } from 'react';\nimport {\n BarChart3,\n Plus,\n LayoutTemplate,\n TerminalSquare,\n Copy,\n Check,\n KeyRound,\n HelpCircle,\n} from 'lucide-react';\nimport { Card } from '@/components/shared';\nimport {\n useGetScorecardTemplatesQuery,\n useGetScorecardPushTokenInfoQuery,\n useRotateScorecardPushTokenMutation,\n type ScorecardTemplateFieldsFragment,\n} from '@/generated/wspace-operations';\n\ninterface ScorecardEmptyStateProps {\n ownerType: 'CATALOG' | 'COMPONENT' | 'VIBE';\n ownerId: string;\n workspaceId: string | null;\n applyingTemplateId: string | null;\n onApplyTemplate: (templateId: string) => void;\n onAddManual: () => void;\n}\n\n/**\n * Onboarding surface shown when a scorecard has no metrics. Explains what a\n * scorecard is and the three ways to populate it: apply a template, add a\n * metric by hand, or push from CI (with a copyable command + token control).\n */\nexport const ScorecardEmptyState: FC<ScorecardEmptyStateProps> = ({\n ownerType,\n ownerId,\n workspaceId,\n applyingTemplateId,\n onApplyTemplate,\n onAddManual,\n}) => {\n const { data: templatesData, loading: templatesLoading } = useGetScorecardTemplatesQuery({\n fetchPolicy: 'cache-and-network',\n });\n const { data: tokenData, refetch: refetchToken } = useGetScorecardPushTokenInfoQuery({\n fetchPolicy: 'cache-and-network',\n });\n const [rotateToken, { loading: rotating }] = useRotateScorecardPushTokenMutation();\n\n const [revealedToken, setRevealedToken] = useState<string | null>(null);\n const [copied, setCopied] = useState<string | null>(null);\n const [showHelp, setShowHelp] = useState(false);\n\n const templates: ScorecardTemplateFieldsFragment[] =\n templatesData?.vibecontrolsScorecardTemplates ?? [];\n const tokenInfo = tokenData?.vibecontrolsScorecardPushTokenInfo;\n\n const tokenForSnippet = revealedToken ?? tokenInfo?.tokenPrefix ?? '<SCORECARD_PUSH_TOKEN>';\n\n const curlSnippet = `curl -X POST \"$WORKSPACES_API_BASE/api/scorecard/push\" \\\\\n -H \"Authorization: Bearer ${tokenForSnippet}\" \\\\\n -H \"Content-Type: application/json\" \\\\\n -d '{\n \"workspaceId\": \"${workspaceId ?? '<WORKSPACE_ID>'}\",\n \"ownerType\": \"${ownerType}\",\n \"ownerId\": \"${ownerId}\",\n \"metrics\": [\n { \"name\": \"Test coverage\", \"type\": \"TEST_COVERAGE\", \"value\": 86, \"threshold\": 80, \"unit\": \"%\", \"direction\": \"HIGHER_IS_BETTER\" },\n { \"name\": \"Open incidents\", \"type\": \"INCIDENT_COUNT\", \"value\": 0, \"threshold\": 0, \"unit\": \"count\", \"direction\": \"LOWER_IS_BETTER\" }\n ]\n }'`;\n\n const copy = async (text: string, key: string): Promise<void> => {\n try {\n await navigator.clipboard.writeText(text);\n setCopied(key);\n window.setTimeout(() => setCopied((c) => (c === key ? null : c)), 1500);\n } catch {\n // Clipboard may be unavailable (insecure context) — ignore silently.\n }\n };\n\n const handleGenerateToken = async (): Promise<void> => {\n const result = await rotateToken();\n const token = result.data?.rotateVibecontrolsScorecardPushToken.token;\n if (token) {\n setRevealedToken(token);\n await refetchToken();\n }\n };\n\n return (\n <div className=\"space-y-6\">\n {/* Intro */}\n <Card padding=\"lg\">\n <div className=\"flex items-start gap-3\">\n <div className=\"flex size-10 flex-shrink-0 items-center justify-center rounded-full bg-bg-sunken\">\n <BarChart3 className=\"size-5 text-text-secondary\" />\n </div>\n <div>\n <h3 className=\"text-lg font-semibold text-text-primary\">Track this {ownerType.toLowerCase()}'s health</h3>\n <p className=\"mt-1 text-sm text-text-secondary\">\n A scorecard rolls up the quality and reliability signals for this entity into a single\n 0–100 score. Each metric has a value, a target threshold, and a direction; its status\n (passing / warning / failing) is derived automatically from how the value compares to\n the threshold. Populate it one of three ways:\n </p>\n </div>\n </div>\n </Card>\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-3\">\n {/* 1. Apply a template */}\n <Card padding=\"lg\">\n <div className=\"flex items-center gap-2\">\n <LayoutTemplate className=\"size-4 text-action-primary-text\" />\n <h4 className=\"font-medium text-text-primary\">Apply a template</h4>\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">\n Start from a standard set of checks defined for your workspace.\n </p>\n <div className=\"mt-3 space-y-2\">\n {templatesLoading && templates.length === 0 && (\n <p className=\"text-xs text-text-muted\">Loading templates…</p>\n )}\n {templates.map((tpl) => (\n <button\n key={tpl.id}\n type=\"button\"\n onClick={() => onApplyTemplate(tpl.id)}\n disabled={applyingTemplateId !== null}\n className=\"flex w-full items-center justify-between rounded-md border border-border-default px-3 py-2 text-left text-sm text-text-primary hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-60\"\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate font-medium\">{tpl.name}</span>\n <span className=\"block truncate text-xs text-text-muted\">\n {tpl.metrics.length} metric{tpl.metrics.length === 1 ? '' : 's'}\n {tpl.isBuiltIn ? ' · starter' : ''}\n </span>\n </span>\n <span className=\"ml-2 flex-shrink-0 text-xs text-action-primary-text\">\n {applyingTemplateId === tpl.id ? 'Applying…' : 'Apply'}\n </span>\n </button>\n ))}\n {!templatesLoading && templates.length === 0 && (\n <p className=\"text-xs text-text-muted\">No templates yet.</p>\n )}\n </div>\n </Card>\n\n {/* 2. Add a metric manually */}\n <Card padding=\"lg\">\n <div className=\"flex items-center gap-2\">\n <Plus className=\"size-4 text-action-primary-text\" />\n <h4 className=\"font-medium text-text-primary\">Add a metric</h4>\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">\n Define a single metric by hand — name, value, threshold, and direction.\n </p>\n <button\n type=\"button\"\n onClick={onAddManual}\n className=\"mt-3 flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm text-action-primary-text hover:bg-action-primary-bg-hover\"\n >\n <Plus className=\"size-4\" />\n Add metric\n </button>\n </Card>\n\n {/* 3. Push from CI */}\n <Card padding=\"lg\">\n <div className=\"flex items-center gap-2\">\n <TerminalSquare className=\"size-4 text-action-primary-text\" />\n <h4 className=\"font-medium text-text-primary\">Push from CI</h4>\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">\n Keep metrics fresh automatically from your pipeline via the push webhook.\n </p>\n <div className=\"mt-3 flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={handleGenerateToken}\n disabled={rotating}\n className=\"flex items-center gap-1.5 rounded-md border border-border-default px-3 py-1.5 text-xs text-text-primary hover:bg-bg-sunken disabled:opacity-60\"\n >\n <KeyRound className=\"size-3.5\" />\n {tokenInfo?.exists ? 'Regenerate token' : 'Generate token'}\n </button>\n {revealedToken && (\n <button\n type=\"button\"\n onClick={() => copy(revealedToken, 'token')}\n className=\"flex items-center gap-1 text-xs text-action-primary-text\"\n >\n {copied === 'token' ? <Check className=\"size-3.5\" /> : <Copy className=\"size-3.5\" />}\n Copy token\n </button>\n )}\n </div>\n {revealedToken && (\n <p className=\"mt-2 rounded-md border border-status-warning-border bg-status-warning-bg px-2 py-1.5 text-[11px] text-status-warning-text\">\n Copy this token now — it is shown only once. Store it in your CI secrets.\n </p>\n )}\n </Card>\n </div>\n\n {/* CI snippet */}\n <Card padding=\"lg\">\n <div className=\"flex items-center justify-between\">\n <h4 className=\"text-sm font-medium text-text-primary\">CI push example</h4>\n <button\n type=\"button\"\n onClick={() => copy(curlSnippet, 'curl')}\n className=\"flex items-center gap-1 text-xs text-action-primary-text\"\n >\n {copied === 'curl' ? <Check className=\"size-3.5\" /> : <Copy className=\"size-3.5\" />}\n {copied === 'curl' ? 'Copied' : 'Copy'}\n </button>\n </div>\n <pre className=\"mt-2 overflow-x-auto rounded-md bg-bg-sunken p-3 text-[11px] leading-relaxed text-text-primary\">\n <code>{curlSnippet}</code>\n </pre>\n <p className=\"mt-2 text-[11px] text-text-muted\">\n Set <code className=\"rounded bg-bg-sunken px-1\">WORKSPACES_API_BASE</code> to your\n workspaces API base URL. Status is derived from value vs threshold; pass an explicit{' '}\n <code className=\"rounded bg-bg-sunken px-1\">status</code> only to override it.\n </p>\n </Card>\n\n {/* How scoring works */}\n <Card padding=\"lg\">\n <button\n type=\"button\"\n onClick={() => setShowHelp((s) => !s)}\n className=\"flex w-full items-center gap-2 text-left text-sm font-medium text-text-primary\"\n >\n <HelpCircle className=\"size-4 text-text-secondary\" />\n How metrics are measured\n </button>\n {showHelp && (\n <div className=\"mt-3 space-y-2 text-xs text-text-secondary\">\n <p>\n <strong className=\"text-text-primary\">Score</strong> — each metric is scored 0–100. For{' '}\n <em>higher-is-better</em> metrics the score is{' '}\n <code className=\"rounded bg-bg-sunken px-1\">value / threshold × 100</code>; for{' '}\n <em>lower-is-better</em> metrics (incidents, latency) it is{' '}\n <code className=\"rounded bg-bg-sunken px-1\">threshold / value × 100</code>. Both are\n capped at 100.\n </p>\n <p>\n <strong className=\"text-text-primary\">Status</strong> — meeting the threshold passes;\n within 10% of it warns; below that fails. You can pin a status manually if you need to.\n </p>\n <p>\n <strong className=\"text-text-primary\">Overall score</strong> — the weighted average of\n all metric scores. Give critical metrics a higher weight to make them count for more.\n </p>\n </div>\n )}\n </Card>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ScorecardEmptyState.js","names":[],"sources":["../../../src/components/scorecard/ScorecardEmptyState.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState } from 'react';\nimport {\n BarChart3,\n Plus,\n LayoutTemplate,\n TerminalSquare,\n Copy,\n Check,\n KeyRound,\n HelpCircle,\n} from 'lucide-react';\nimport { Card } from '@/components/shared';\nimport {\n useGetScorecardTemplatesQuery,\n useGetScorecardPushTokenInfoQuery,\n useRotateScorecardPushTokenMutation,\n type ScorecardTemplateFieldsFragment,\n} from '@/generated/wspace-operations';\n\ninterface ScorecardEmptyStateProps {\n ownerType: 'CATALOG' | 'COMPONENT' | 'VIBE';\n ownerId: string;\n workspaceId: string | null;\n applyingTemplateId: string | null;\n onApplyTemplate: (templateId: string) => void;\n onAddManual: () => void;\n}\n\n/**\n * Onboarding surface shown when a scorecard has no metrics. Explains what a\n * scorecard is and the three ways to populate it: apply a template, add a\n * metric by hand, or push from CI (with a copyable command + token control).\n */\nexport const ScorecardEmptyState: FC<ScorecardEmptyStateProps> = ({\n ownerType,\n ownerId,\n workspaceId,\n applyingTemplateId,\n onApplyTemplate,\n onAddManual,\n}) => {\n const { data: templatesData, loading: templatesLoading } = useGetScorecardTemplatesQuery({\n fetchPolicy: 'cache-and-network',\n });\n const { data: tokenData, refetch: refetchToken } = useGetScorecardPushTokenInfoQuery({\n fetchPolicy: 'cache-and-network',\n });\n const [rotateToken, { loading: rotating }] = useRotateScorecardPushTokenMutation();\n\n const [revealedToken, setRevealedToken] = useState<string | null>(null);\n const [copied, setCopied] = useState<string | null>(null);\n const [showHelp, setShowHelp] = useState(false);\n\n const templates: ScorecardTemplateFieldsFragment[] =\n templatesData?.vibecontrolsScorecardTemplates ?? [];\n const tokenInfo = tokenData?.vibecontrolsScorecardPushTokenInfo;\n\n const tokenForSnippet = revealedToken ?? tokenInfo?.tokenPrefix ?? '<SCORECARD_PUSH_TOKEN>';\n\n const curlSnippet = `curl -X POST \"$WORKSPACES_API_BASE/api/scorecard/push\" \\\\\n -H \"Authorization: Bearer ${tokenForSnippet}\" \\\\\n -H \"Content-Type: application/json\" \\\\\n -d '{\n \"workspaceId\": \"${workspaceId ?? '<WORKSPACE_ID>'}\",\n \"ownerType\": \"${ownerType}\",\n \"ownerId\": \"${ownerId}\",\n \"metrics\": [\n { \"name\": \"Test coverage\", \"type\": \"TEST_COVERAGE\", \"value\": 86, \"threshold\": 80, \"unit\": \"%\", \"direction\": \"HIGHER_IS_BETTER\" },\n { \"name\": \"Open incidents\", \"type\": \"INCIDENT_COUNT\", \"value\": 0, \"threshold\": 0, \"unit\": \"count\", \"direction\": \"LOWER_IS_BETTER\" }\n ]\n }'`;\n\n const copy = async (text: string, key: string): Promise<void> => {\n try {\n await navigator.clipboard.writeText(text);\n setCopied(key);\n window.setTimeout(() => setCopied((c) => (c === key ? null : c)), 1500);\n } catch {\n // Clipboard may be unavailable (insecure context) — ignore silently.\n }\n };\n\n const handleGenerateToken = async (): Promise<void> => {\n const result = await rotateToken();\n const token = result.data?.rotateVibecontrolsScorecardPushToken.token;\n if (token) {\n setRevealedToken(token);\n await refetchToken();\n }\n };\n\n return (\n <div className=\"space-y-6\">\n {/* Intro */}\n <Card padding=\"lg\">\n <div className=\"flex items-start gap-3\">\n <div className=\"flex size-10 flex-shrink-0 items-center justify-center rounded-full bg-bg-sunken\">\n <BarChart3 className=\"size-5 text-text-secondary\" />\n </div>\n <div>\n <h3 className=\"text-lg font-semibold text-text-primary\">\n Track this {ownerType.toLowerCase()}'s health\n </h3>\n <p className=\"mt-1 text-sm text-text-secondary\">\n A scorecard rolls up the quality and reliability signals for this entity into a single\n 0–100 score. Each metric has a value, a target threshold, and a direction; its status\n (passing / warning / failing) is derived automatically from how the value compares to\n the threshold. Populate it one of three ways:\n </p>\n </div>\n </div>\n </Card>\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-3\">\n {/* 1. Apply a template */}\n <Card padding=\"lg\">\n <div className=\"flex items-center gap-2\">\n <LayoutTemplate className=\"size-4 text-action-primary-text\" />\n <h4 className=\"font-medium text-text-primary\">Apply a template</h4>\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">\n Start from a standard set of checks defined for your workspace.\n </p>\n <div className=\"mt-3 space-y-2\">\n {templatesLoading && templates.length === 0 && (\n <p className=\"text-xs text-text-muted\">Loading templates…</p>\n )}\n {templates.map((tpl) => (\n <button\n key={tpl.id}\n type=\"button\"\n onClick={() => onApplyTemplate(tpl.id)}\n disabled={applyingTemplateId !== null}\n className=\"flex w-full items-center justify-between rounded-md border border-border-default px-3 py-2 text-left text-sm text-text-primary hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-60\"\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate font-medium\">{tpl.name}</span>\n <span className=\"block truncate text-xs text-text-muted\">\n {tpl.metrics.length} metric{tpl.metrics.length === 1 ? '' : 's'}\n {tpl.isBuiltIn ? ' · starter' : ''}\n </span>\n </span>\n <span className=\"ml-2 flex-shrink-0 text-xs text-action-primary-text\">\n {applyingTemplateId === tpl.id ? 'Applying…' : 'Apply'}\n </span>\n </button>\n ))}\n {!templatesLoading && templates.length === 0 && (\n <p className=\"text-xs text-text-muted\">No templates yet.</p>\n )}\n </div>\n </Card>\n\n {/* 2. Add a metric manually */}\n <Card padding=\"lg\">\n <div className=\"flex items-center gap-2\">\n <Plus className=\"size-4 text-action-primary-text\" />\n <h4 className=\"font-medium text-text-primary\">Add a metric</h4>\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">\n Define a single metric by hand — name, value, threshold, and direction.\n </p>\n <button\n type=\"button\"\n onClick={onAddManual}\n className=\"mt-3 flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm text-action-primary-text hover:bg-action-primary-bg-hover\"\n >\n <Plus className=\"size-4\" />\n Add metric\n </button>\n </Card>\n\n {/* 3. Push from CI */}\n <Card padding=\"lg\">\n <div className=\"flex items-center gap-2\">\n <TerminalSquare className=\"size-4 text-action-primary-text\" />\n <h4 className=\"font-medium text-text-primary\">Push from CI</h4>\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">\n Keep metrics fresh automatically from your pipeline via the push webhook.\n </p>\n <div className=\"mt-3 flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={handleGenerateToken}\n disabled={rotating}\n className=\"flex items-center gap-1.5 rounded-md border border-border-default px-3 py-1.5 text-xs text-text-primary hover:bg-bg-sunken disabled:opacity-60\"\n >\n <KeyRound className=\"size-3.5\" />\n {tokenInfo?.exists ? 'Regenerate token' : 'Generate token'}\n </button>\n {revealedToken && (\n <button\n type=\"button\"\n onClick={() => copy(revealedToken, 'token')}\n className=\"flex items-center gap-1 text-xs text-action-primary-text\"\n >\n {copied === 'token' ? (\n <Check className=\"size-3.5\" />\n ) : (\n <Copy className=\"size-3.5\" />\n )}\n Copy token\n </button>\n )}\n </div>\n {revealedToken && (\n <p className=\"mt-2 rounded-md border border-status-warning-border bg-status-warning-bg px-2 py-1.5 text-[11px] text-status-warning-text\">\n Copy this token now — it is shown only once. Store it in your CI secrets.\n </p>\n )}\n </Card>\n </div>\n\n {/* CI snippet */}\n <Card padding=\"lg\">\n <div className=\"flex items-center justify-between\">\n <h4 className=\"text-sm font-medium text-text-primary\">CI push example</h4>\n <button\n type=\"button\"\n onClick={() => copy(curlSnippet, 'curl')}\n className=\"flex items-center gap-1 text-xs text-action-primary-text\"\n >\n {copied === 'curl' ? <Check className=\"size-3.5\" /> : <Copy className=\"size-3.5\" />}\n {copied === 'curl' ? 'Copied' : 'Copy'}\n </button>\n </div>\n <pre className=\"mt-2 overflow-x-auto rounded-md bg-bg-sunken p-3 text-[11px] leading-relaxed text-text-primary\">\n <code>{curlSnippet}</code>\n </pre>\n <p className=\"mt-2 text-[11px] text-text-muted\">\n Set <code className=\"rounded bg-bg-sunken px-1\">WORKSPACES_API_BASE</code> to your\n workspaces API base URL. Status is derived from value vs threshold; pass an explicit{' '}\n <code className=\"rounded bg-bg-sunken px-1\">status</code> only to override it.\n </p>\n </Card>\n\n {/* How scoring works */}\n <Card padding=\"lg\">\n <button\n type=\"button\"\n onClick={() => setShowHelp((s) => !s)}\n className=\"flex w-full items-center gap-2 text-left text-sm font-medium text-text-primary\"\n >\n <HelpCircle className=\"size-4 text-text-secondary\" />\n How metrics are measured\n </button>\n {showHelp && (\n <div className=\"mt-3 space-y-2 text-xs text-text-secondary\">\n <p>\n <strong className=\"text-text-primary\">Score</strong> — each metric is scored 0–100.\n For <em>higher-is-better</em> metrics the score is{' '}\n <code className=\"rounded bg-bg-sunken px-1\">value / threshold × 100</code>; for{' '}\n <em>lower-is-better</em> metrics (incidents, latency) it is{' '}\n <code className=\"rounded bg-bg-sunken px-1\">threshold / value × 100</code>. Both are\n capped at 100.\n </p>\n <p>\n <strong className=\"text-text-primary\">Status</strong> — meeting the threshold passes;\n within 10% of it warns; below that fails. You can pin a status manually if you need\n to.\n </p>\n <p>\n <strong className=\"text-text-primary\">Overall score</strong> — the weighted average of\n all metric scores. Give critical metrics a higher weight to make them count for more.\n </p>\n </div>\n )}\n </Card>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ScorecardPanel.js","names":[],"sources":["../../../src/components/scorecard/ScorecardPanel.tsx"],"sourcesContent":["import type { FC, FormEvent, MouseEvent } from 'react';\nimport { useState, useCallback, useEffect } from 'react';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport { Plus, TrendingUp, RefreshCw, Pencil, Trash2, Info } from 'lucide-react';\nimport { LoadingState, ErrorState, Card } from '@/components/shared';\nimport { MotionList, MotionItem } from '@burdenoff/fe-libs/shared/motion';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useActiveContext } from '@burdenoff/fe-libs/shared/providers/shell';\nimport {\n useGetScorecardQuery,\n useUpsertScorecardMutation,\n useAddScorecardMetricMutation,\n useUpdateScorecardMetricMutation,\n useRemoveScorecardMetricMutation,\n useApplyScorecardTemplateMutation,\n} from '@/generated/wspace-operations';\nimport {\n type ScorecardMetricType,\n type ScorecardMetricStatus,\n type ScorecardMetricDirection,\n METRIC_TYPE_OPTIONS,\n DIRECTION_OPTIONS,\n STATUS_OPTIONS,\n statusConfig,\n deriveStatus,\n} from './scorecardConstants';\nimport { ScorecardEmptyState } from './ScorecardEmptyState';\nimport { ScorecardsOverviewPanel } from './ScorecardsOverviewPanel';\nimport { MetricTrend } from './MetricTrend';\n\ninterface ScorecardMetric {\n id: string;\n name: string;\n description: string | null;\n type: ScorecardMetricType;\n value: number | null;\n threshold: number | null;\n unit: string | null;\n direction: ScorecardMetricDirection;\n weight: number;\n status: ScorecardMetricStatus;\n statusOverride: ScorecardMetricStatus | null;\n remediation: string | null;\n iframeUrl: string | null;\n iframeHeight: number | null;\n}\n\nexport interface ScorecardPanelProps {\n ownerType: 'CATALOG' | 'COMPONENT' | 'VIBE';\n ownerId: string;\n onAddMetric?: () => void;\n}\n\ntype StatusMode = 'AUTO' | ScorecardMetricStatus;\n\ninterface MetricFormState {\n name: string;\n description: string;\n type: ScorecardMetricType;\n value: string;\n threshold: string;\n unit: string;\n direction: ScorecardMetricDirection;\n weight: string;\n statusMode: StatusMode;\n remediation: string;\n iframeUrl: string;\n iframeHeight: string;\n}\n\nconst DEFAULT_METRIC_FORM: MetricFormState = {\n name: '',\n description: '',\n type: 'CUSTOM',\n value: '',\n threshold: '',\n unit: '',\n direction: 'HIGHER_IS_BETTER',\n weight: '1',\n statusMode: 'AUTO',\n remediation: '',\n iframeUrl: '',\n iframeHeight: '',\n};\n\nconst inputClass =\n 'w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary outline-none focus:border-action-primary-bg';\n\nexport const ScorecardPanel: FC<ScorecardPanelProps> = ({ ownerType, ownerId, onAddMetric }) => {\n const { t } = useI18n();\n const tr = useCallback(\n (key: string, fallback: string): string => {\n const v = t(key);\n return v === key ? fallback : v;\n },\n [t]\n );\n\n const { workspaceId } = useActiveContext();\n\n const [expandedMetric, setExpandedMetric] = useState<string | null>(null);\n const [isAddMetricOpen, setIsAddMetricOpen] = useState(false);\n const [editingMetricId, setEditingMetricId] = useState<string | null>(null);\n const [deletingMetric, setDeletingMetric] = useState<{ id: string; name: string } | null>(null);\n const [deleteError, setDeleteError] = useState<string | null>(null);\n const [metricForm, setMetricForm] = useState<MetricFormState>(DEFAULT_METRIC_FORM);\n const [formError, setFormError] = useState<string | null>(null);\n const [applyingTemplateId, setApplyingTemplateId] = useState<string | null>(null);\n\n const { data, loading, error, refetch } = useGetScorecardQuery({\n variables: { ownerType, ownerId },\n skip: !ownerId,\n fetchPolicy: 'cache-and-network',\n });\n\n const [upsertScorecard, { loading: upsertingScorecard }] = useUpsertScorecardMutation();\n const [addScorecardMetric, { loading: addingMetric }] = useAddScorecardMetricMutation();\n const [updateScorecardMetric, { loading: updatingMetric }] = useUpdateScorecardMetricMutation();\n const [removeScorecardMetric, { loading: removingMetric }] = useRemoveScorecardMetricMutation();\n const [applyTemplate] = useApplyScorecardTemplateMutation();\n\n const scorecard = data?.vibecontrolsScorecard ?? null;\n const isSavingMetric = upsertingScorecard || addingMetric || updatingMetric;\n const isEditing = editingMetricId !== null;\n\n const resetMetricForm = (): void => {\n setMetricForm(DEFAULT_METRIC_FORM);\n setFormError(null);\n setEditingMetricId(null);\n };\n\n const handleAddMetricClick = (): void => {\n if (onAddMetric) {\n onAddMetric();\n return;\n }\n resetMetricForm();\n setIsAddMetricOpen(true);\n };\n\n const handleEditMetricClick = (event: MouseEvent, metric: ScorecardMetric): void => {\n event.stopPropagation();\n setMetricForm({\n name: metric.name,\n description: metric.description ?? '',\n type: metric.type,\n value: metric.value !== null && metric.value !== undefined ? String(metric.value) : '',\n threshold:\n metric.threshold !== null && metric.threshold !== undefined ? String(metric.threshold) : '',\n unit: metric.unit ?? '',\n direction: metric.direction ?? 'HIGHER_IS_BETTER',\n weight: metric.weight !== null && metric.weight !== undefined ? String(metric.weight) : '1',\n statusMode: metric.statusOverride ?? 'AUTO',\n remediation: metric.remediation ?? '',\n iframeUrl: metric.iframeUrl ?? '',\n iframeHeight:\n metric.iframeHeight !== null && metric.iframeHeight !== undefined\n ? String(metric.iframeHeight)\n : '',\n });\n setFormError(null);\n setEditingMetricId(metric.id);\n setIsAddMetricOpen(true);\n };\n\n const handleRemoveMetricClick = (event: MouseEvent, metric: ScorecardMetric): void => {\n event.stopPropagation();\n setDeleteError(null);\n setDeletingMetric({ id: metric.id, name: metric.name });\n };\n\n const closeAddMetricDialog = (): void => {\n if (isSavingMetric) return;\n setIsAddMetricOpen(false);\n resetMetricForm();\n };\n\n const closeDeleteDialog = useCallback((): void => {\n if (removingMetric) return;\n setDeletingMetric(null);\n setDeleteError(null);\n }, [removingMetric]);\n\n useEffect(() => {\n if (!deletingMetric) return undefined;\n const handler = (event: KeyboardEvent): void => {\n if (event.key === 'Escape') closeDeleteDialog();\n };\n window.addEventListener('keydown', handler);\n return () => window.removeEventListener('keydown', handler);\n }, [deletingMetric, closeDeleteDialog]);\n\n const confirmRemoveMetric = async (): Promise<void> => {\n if (!deletingMetric) return;\n setDeleteError(null);\n try {\n await removeScorecardMetric({ variables: { id: deletingMetric.id } });\n await refetch();\n setDeletingMetric(null);\n } catch (caughtError) {\n setDeleteError(caughtError instanceof Error ? caughtError.message : 'Failed to remove metric');\n }\n };\n\n const updateMetricForm = (field: keyof MetricFormState, value: string): void => {\n setMetricForm((current) => ({ ...current, [field]: value }));\n };\n\n const parseOptionalNumber = (value: string, label: string): number | undefined => {\n const trimmed = value.trim();\n if (!trimmed) return undefined;\n const parsed = Number(trimmed);\n if (!Number.isFinite(parsed)) throw new Error(`${label} must be a valid number`);\n return parsed;\n };\n\n const ensureScorecardId = async (): Promise<string> => {\n if (scorecard?.id) return scorecard.id;\n const created = await upsertScorecard({ variables: { input: { ownerType, ownerId } } });\n const id = created.data?.upsertVibecontrolsScorecard.id;\n if (!id) throw new Error(tr('scorecard.scorecardCreateFailed', 'Unable to create scorecard'));\n return id;\n };\n\n const handleApplyTemplate = async (templateId: string): Promise<void> => {\n setApplyingTemplateId(templateId);\n try {\n await applyTemplate({ variables: { templateId, ownerType, ownerId } });\n await refetch();\n } finally {\n setApplyingTemplateId(null);\n }\n };\n\n const handleAddMetricSubmit = async (event: FormEvent<HTMLFormElement>): Promise<void> => {\n event.preventDefault();\n setFormError(null);\n\n const name = metricForm.name.trim();\n if (!name) {\n setFormError(tr('scorecard.metricNameRequired', 'Metric name is required'));\n return;\n }\n\n // 'AUTO' → derive status (send no explicit status). Otherwise pin it.\n const statusOverride =\n metricForm.statusMode === 'AUTO' ? undefined : (metricForm.statusMode as ScorecardMetricStatus);\n\n try {\n const sharedInput = {\n name,\n description: metricForm.description.trim() || null,\n type: metricForm.type,\n value: parseOptionalNumber(metricForm.value, tr('scorecard.value', 'Value')) ?? null,\n threshold:\n parseOptionalNumber(metricForm.threshold, tr('scorecard.threshold', 'Threshold')) ?? null,\n unit: metricForm.unit.trim() || null,\n direction: metricForm.direction,\n weight: parseOptionalNumber(metricForm.weight, tr('scorecard.weight', 'Weight')) ?? 1,\n status: statusOverride ?? null,\n remediation: metricForm.remediation.trim() || null,\n iframeUrl: metricForm.iframeUrl.trim() || null,\n iframeHeight:\n parseOptionalNumber(metricForm.iframeHeight, tr('scorecard.iframeHeight', 'Iframe height')) ??\n null,\n };\n\n if (editingMetricId) {\n await updateScorecardMetric({ variables: { id: editingMetricId, input: sharedInput } });\n } else {\n const scorecardId = await ensureScorecardId();\n await addScorecardMetric({ variables: { scorecardId, input: sharedInput } });\n }\n\n await refetch();\n setIsAddMetricOpen(false);\n resetMetricForm();\n } catch (caughtError) {\n setFormError(\n caughtError instanceof Error\n ? caughtError.message\n : editingMetricId\n ? 'Failed to update metric'\n : 'Failed to add metric'\n );\n }\n };\n\n // Live status preview for the form.\n const previewStatus =\n metricForm.statusMode === 'AUTO'\n ? deriveStatus(\n metricForm.value.trim() ? Number(metricForm.value) : null,\n metricForm.threshold.trim() ? Number(metricForm.threshold) : null,\n metricForm.direction\n )\n : (metricForm.statusMode as ScorecardMetricStatus);\n\n const renderDeleteMetricDialog = () => (\n <AnimatePresence>\n {deletingMetric && (\n <motion.div\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-bg-overlay p-4\"\n onClick={closeDeleteDialog}\n >\n <motion.div\n initial={{ scale: 0.96, y: 12 }}\n animate={{ scale: 1, y: 0 }}\n exit={{ scale: 0.96, y: 12 }}\n className=\"w-full max-w-md rounded-xl border border-border-default bg-bg-surface shadow-xl\"\n onClick={(event) => event.stopPropagation()}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"scorecard-delete-title\"\n >\n <div className=\"border-b border-border-default px-5 py-4\">\n <h3 id=\"scorecard-delete-title\" className=\"text-lg font-semibold text-text-primary\">\n {tr('scorecard.removeMetricTitle', 'Remove metric')}\n </h3>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n 'scorecard.removeMetricConfirm',\n 'Are you sure you want to remove this metric? This cannot be undone.'\n )}\n </p>\n </div>\n <div className=\"px-5 py-4 text-sm text-text-primary\">\n <p className=\"font-medium\">{deletingMetric.name}</p>\n {deleteError && (\n <p className=\"mt-3 rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-sm text-status-error-text\">\n {deleteError}\n </p>\n )}\n </div>\n <div className=\"flex items-center justify-end gap-3 border-t border-border-default px-5 py-4\">\n <button\n type=\"button\"\n onClick={closeDeleteDialog}\n disabled={removingMetric}\n className=\"rounded-md border border-border-default px-4 py-2 text-sm text-text-primary hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-60\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n <button\n type=\"button\"\n onClick={confirmRemoveMetric}\n disabled={removingMetric}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm text-status-error-text hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60\"\n >\n {removingMetric\n ? tr('scorecard.removingMetric', 'Removing...')\n : tr('scorecard.removeMetric', 'Remove metric')}\n </button>\n </div>\n </motion.div>\n </motion.div>\n )}\n </AnimatePresence>\n );\n\n const renderAddMetricDialog = () => {\n const previewConf = statusConfig[previewStatus] ?? statusConfig.UNKNOWN;\n return (\n <AnimatePresence>\n {isAddMetricOpen && (\n <motion.div\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-bg-overlay p-4\"\n onClick={closeAddMetricDialog}\n >\n <motion.div\n initial={{ scale: 0.96, y: 12 }}\n animate={{ scale: 1, y: 0 }}\n exit={{ scale: 0.96, y: 12 }}\n className=\"w-full max-w-2xl rounded-xl border border-border-default bg-bg-surface shadow-xl\"\n onClick={(event) => event.stopPropagation()}\n >\n <form onSubmit={handleAddMetricSubmit} className=\"flex max-h-[85vh] flex-col\">\n <div className=\"border-b border-border-default px-5 py-4\">\n <h3 className=\"text-lg font-semibold text-text-primary\">\n {isEditing\n ? tr('scorecard.editMetricTitle', 'Edit scorecard metric')\n : tr('scorecard.addMetricTitle', 'Add scorecard metric')}\n </h3>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n 'scorecard.addMetricDescription',\n 'Define a metric. Status is derived from value vs threshold unless you pin it.'\n )}\n </p>\n </div>\n\n <div className=\"space-y-4 overflow-y-auto px-5 py-4\">\n {formError && (\n <div className=\"rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-sm text-status-error-text\">\n {formError}\n </div>\n )}\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.metricName', 'Metric name')}\n </span>\n <input\n aria-label=\"control\"\n value={metricForm.name}\n onChange={(event) => updateMetricForm('name', event.target.value)}\n className={inputClass}\n placeholder={tr('scorecard.metricNamePlaceholder', 'Test coverage')}\n />\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.metricDescription', 'Description')}\n </span>\n <textarea\n aria-label=\"control\"\n value={metricForm.description}\n onChange={(event) => updateMetricForm('description', event.target.value)}\n className={`min-h-16 ${inputClass}`}\n placeholder={tr(\n 'scorecard.metricDescriptionPlaceholder',\n 'Explain what this metric measures'\n )}\n />\n </label>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.metricType', 'Type')}\n </span>\n <select\n value={metricForm.type}\n onChange={(event) =>\n updateMetricForm('type', event.target.value as ScorecardMetricType)\n }\n className={inputClass}\n >\n {METRIC_TYPE_OPTIONS.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.metricDirection', 'Direction')}\n </span>\n <select\n value={metricForm.direction}\n onChange={(event) =>\n updateMetricForm('direction', event.target.value as ScorecardMetricDirection)\n }\n className={inputClass}\n >\n {DIRECTION_OPTIONS.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </label>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-4\">\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.value', 'Value')}\n </span>\n <input\n aria-label=\"value\"\n value={metricForm.value}\n onChange={(event) => updateMetricForm('value', event.target.value)}\n inputMode=\"decimal\"\n className={inputClass}\n placeholder=\"86\"\n />\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.threshold', 'Threshold')}\n </span>\n <input\n aria-label=\"threshold\"\n value={metricForm.threshold}\n onChange={(event) => updateMetricForm('threshold', event.target.value)}\n inputMode=\"decimal\"\n className={inputClass}\n placeholder=\"80\"\n />\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.unit', 'Unit')}\n </span>\n <input\n aria-label=\"unit\"\n value={metricForm.unit}\n onChange={(event) => updateMetricForm('unit', event.target.value)}\n className={inputClass}\n placeholder=\"%\"\n />\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.weight', 'Weight')}\n </span>\n <input\n aria-label=\"weight\"\n value={metricForm.weight}\n onChange={(event) => updateMetricForm('weight', event.target.value)}\n inputMode=\"decimal\"\n className={inputClass}\n placeholder=\"1\"\n />\n </label>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.metricStatus', 'Status')}\n </span>\n <select\n value={metricForm.statusMode}\n onChange={(event) =>\n updateMetricForm('statusMode', event.target.value as StatusMode)\n }\n className={inputClass}\n >\n {STATUS_OPTIONS.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </label>\n\n <div className=\"flex items-end\">\n <div\n className={`flex items-center gap-2 rounded-md px-3 py-2 text-sm ${previewConf.bg} ${previewConf.text}`}\n >\n <previewConf.icon className=\"size-4\" />\n <span>\n {metricForm.statusMode === 'AUTO' ? 'Derived: ' : 'Pinned: '}\n {previewConf.label}\n </span>\n </div>\n </div>\n </div>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.remediation', 'Remediation guidance')}\n </span>\n <textarea\n aria-label=\"control\"\n value={metricForm.remediation}\n onChange={(event) => updateMetricForm('remediation', event.target.value)}\n className={`min-h-16 ${inputClass}`}\n placeholder={tr(\n 'scorecard.remediationPlaceholder',\n 'What should someone do when this metric is failing?'\n )}\n />\n </label>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-[1fr_10rem]\">\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.iframeUrl', 'Embed URL')}\n </span>\n <input\n aria-label=\"control\"\n value={metricForm.iframeUrl}\n onChange={(event) => updateMetricForm('iframeUrl', event.target.value)}\n className={inputClass}\n placeholder={tr('scorecard.iframeUrlPlaceholder', 'Optional dashboard URL')}\n />\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.iframeHeight', 'Iframe height')}\n </span>\n <input\n aria-label=\"iframe height\"\n value={metricForm.iframeHeight}\n onChange={(event) => updateMetricForm('iframeHeight', event.target.value)}\n inputMode=\"numeric\"\n className={inputClass}\n placeholder=\"320\"\n />\n </label>\n </div>\n </div>\n\n <div className=\"flex items-center justify-end gap-3 border-t border-border-default px-5 py-4\">\n <button\n type=\"button\"\n onClick={closeAddMetricDialog}\n disabled={isSavingMetric}\n className=\"rounded-md border border-border-default px-4 py-2 text-sm text-text-primary hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-60\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n <button\n type=\"submit\"\n disabled={isSavingMetric}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm text-action-primary-text hover:bg-action-primary-bg-hover disabled:cursor-not-allowed disabled:opacity-60\"\n >\n {isSavingMetric\n ? tr('scorecard.savingMetric', 'Saving...')\n : isEditing\n ? tr('scorecard.updateMetric', 'Update Metric')\n : tr('scorecard.saveMetric', 'Save Metric')}\n </button>\n </div>\n </form>\n </motion.div>\n </motion.div>\n )}\n </AnimatePresence>\n );\n };\n\n if (loading && !scorecard) {\n return <LoadingState message={tr('scorecard.loading', 'Loading scorecard...')} />;\n }\n\n if (error && !scorecard) {\n return (\n <ErrorState\n title={tr('scorecard.failedLoad', 'Failed to load scorecard')}\n message={error.message}\n onRetry={() => refetch()}\n />\n );\n }\n\n if (!scorecard || scorecard.metrics.length === 0) {\n return (\n <>\n <ScorecardEmptyState\n ownerType={ownerType}\n ownerId={ownerId}\n workspaceId={workspaceId}\n applyingTemplateId={applyingTemplateId}\n onApplyTemplate={handleApplyTemplate}\n onAddManual={handleAddMetricClick}\n />\n {renderAddMetricDialog()}\n {renderDeleteMetricDialog()}\n </>\n );\n }\n\n const overallScore = scorecard.overallScore ?? 0;\n const scoreColor =\n overallScore >= 80\n ? 'text-status-success-text'\n : overallScore >= 50\n ? 'text-status-warning-text'\n : 'text-status-error-text';\n\n return (\n <div className=\"space-y-6\">\n {/* Overall Score */}\n <Card padding=\"lg\">\n <div className=\"flex items-center justify-between\">\n <div>\n <p className=\"flex items-center gap-1.5 text-sm font-medium text-text-secondary\">\n {tr('scorecard.overallScore', 'Overall Score')}\n <span title=\"Weighted average of each metric's score (0–100). Status is derived from value vs threshold.\">\n <Info className=\"size-3.5 text-text-muted\" />\n </span>\n </p>\n <p className={`text-4xl font-bold ${scoreColor}`}>\n {overallScore}\n <span className=\"text-lg text-text-muted\">/100</span>\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <TrendingUp className={`size-6 ${scoreColor}`} />\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"rounded-md p-2 text-text-secondary transition-colors hover:bg-bg-sunken\"\n title={tr('common.refresh', 'Refresh')}\n >\n <RefreshCw className=\"size-4\" />\n </button>\n <button\n type=\"button\"\n onClick={handleAddMetricClick}\n className=\"flex items-center gap-2 rounded-md bg-action-primary-bg px-3 py-2 text-sm text-action-primary-text transition-colors hover:bg-action-primary-bg-hover\"\n >\n <Plus className=\"size-4\" />\n {tr('scorecard.addMetric', 'Add Metric')}\n </button>\n </div>\n </div>\n </Card>\n\n {/* Metrics Grid */}\n <MotionList stagger={0.04} className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {scorecard.metrics.map((metric: ScorecardMetric) => {\n const config = statusConfig[metric.status] || statusConfig.UNKNOWN;\n const StatusIcon = config.icon;\n const value = metric.value ?? 0;\n const threshold = metric.threshold ?? 100;\n const percentage = threshold > 0 ? Math.min(100, Math.max(0, (value / threshold) * 100)) : 0;\n const isExpanded = expandedMetric === metric.id;\n const showRemediation =\n metric.remediation && (metric.status === 'WARNING' || metric.status === 'FAILING');\n\n return (\n <MotionItem key={metric.id}>\n <Card hoverable padding=\"none\">\n <div\n role=\"button\"\n tabIndex={0}\n onClick={() => setExpandedMetric(isExpanded ? null : metric.id)}\n onKeyDown={(event) => {\n if (event.target !== event.currentTarget) return;\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n setExpandedMetric(isExpanded ? null : metric.id);\n }\n }}\n className=\"cursor-pointer rounded-lg p-4 focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n aria-expanded={isExpanded}\n aria-label={`${metric.name} metric details`}\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{metric.name}</p>\n <p className=\"mt-0.5 flex items-center gap-1.5 text-xs text-text-muted\">\n <span>{metric.direction === 'LOWER_IS_BETTER' ? '↓ lower better' : '↑ higher better'}</span>\n {metric.weight && metric.weight !== 1 ? <span>· ×{metric.weight}</span> : null}\n {metric.statusOverride ? <span>· pinned</span> : null}\n </p>\n </div>\n <div className=\"flex flex-shrink-0 items-center gap-1\">\n <button\n type=\"button\"\n onClick={(event) => handleEditMetricClick(event, metric)}\n className=\"rounded-md p-1 text-text-secondary hover:bg-bg-sunken hover:text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n aria-label={tr('scorecard.editMetric', 'Edit metric')}\n title={tr('scorecard.editMetric', 'Edit metric')}\n >\n <Pencil className=\"size-4\" />\n </button>\n <button\n type=\"button\"\n onClick={(event) => handleRemoveMetricClick(event, metric)}\n className=\"rounded-md p-1 text-text-secondary hover:bg-status-error-bg hover:text-status-error-text focus:outline-none focus:ring-2 focus:ring-status-error-text\"\n aria-label={tr('scorecard.removeMetric', 'Remove metric')}\n title={tr('scorecard.removeMetric', 'Remove metric')}\n >\n <Trash2 className=\"size-4\" />\n </button>\n <div className={`flex size-7 items-center justify-center rounded-full ${config.bg}`}>\n <StatusIcon className={`size-4 ${config.text}`} />\n </div>\n </div>\n </div>\n\n {/* Value and progress */}\n <div className=\"mt-3\">\n <div className=\"flex items-baseline justify-between\">\n <span className=\"text-lg font-bold text-text-primary\">\n {value}\n {metric.unit ? ` ${metric.unit}` : ''}\n </span>\n <span className=\"text-xs text-text-muted\">\n / {threshold}\n {metric.unit ? ` ${metric.unit}` : ''}\n </span>\n </div>\n <div className=\"mt-1.5 h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <motion.div\n initial={{ width: 0 }}\n animate={{ width: `${percentage}%` }}\n transition={{ duration: 0.5, ease: 'easeOut' }}\n className={`h-full rounded-full ${config.bar}`}\n />\n </div>\n </div>\n\n {showRemediation && (\n <p className=\"mt-2 rounded-md border border-status-warning-border bg-status-warning-bg px-2 py-1.5 text-xs text-status-warning-text\">\n {metric.remediation}\n </p>\n )}\n </div>\n\n {/* Expanded: trend + optional embed */}\n <AnimatePresence>\n {isExpanded && (\n <motion.div\n initial={{ height: 0, opacity: 0 }}\n animate={{ height: 'auto', opacity: 1 }}\n exit={{ height: 0, opacity: 0 }}\n transition={{ duration: 0.2 }}\n className=\"overflow-hidden border-t border-border-default\"\n >\n <div className=\"px-4 py-3\">\n <MetricTrend metricId={metric.id} colorClass={config.text} />\n </div>\n {metric.iframeUrl && (\n <iframe\n src={metric.iframeUrl}\n className=\"w-full border-0\"\n style={{ height: metric.iframeHeight ? `${metric.iframeHeight}px` : '200px' }}\n title={`${metric.name} embed`}\n sandbox=\"allow-scripts allow-same-origin\"\n />\n )}\n </motion.div>\n )}\n </AnimatePresence>\n </Card>\n </MotionItem>\n );\n })}\n </MotionList>\n\n <ScorecardsOverviewPanel ownerType={ownerType} highlightOwnerId={ownerId} />\n\n {renderAddMetricDialog()}\n {renderDeleteMetricDialog()}\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ScorecardPanel.js","names":[],"sources":["../../../src/components/scorecard/ScorecardPanel.tsx"],"sourcesContent":["import type { FC, FormEvent, MouseEvent } from 'react';\nimport { useState, useCallback, useEffect } from 'react';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport { Plus, TrendingUp, RefreshCw, Pencil, Trash2, Info } from 'lucide-react';\nimport { LoadingState, ErrorState, Card } from '@/components/shared';\nimport { MotionList, MotionItem } from '@burdenoff/fe-libs/shared/motion';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useActiveContext } from '@burdenoff/fe-libs/shared/providers/shell';\nimport {\n useGetScorecardQuery,\n useUpsertScorecardMutation,\n useAddScorecardMetricMutation,\n useUpdateScorecardMetricMutation,\n useRemoveScorecardMetricMutation,\n useApplyScorecardTemplateMutation,\n} from '@/generated/wspace-operations';\nimport {\n type ScorecardMetricType,\n type ScorecardMetricStatus,\n type ScorecardMetricDirection,\n METRIC_TYPE_OPTIONS,\n DIRECTION_OPTIONS,\n STATUS_OPTIONS,\n statusConfig,\n deriveStatus,\n} from './scorecardConstants';\nimport { ScorecardEmptyState } from './ScorecardEmptyState';\nimport { ScorecardsOverviewPanel } from './ScorecardsOverviewPanel';\nimport { MetricTrend } from './MetricTrend';\n\ninterface ScorecardMetric {\n id: string;\n name: string;\n description: string | null;\n type: ScorecardMetricType;\n value: number | null;\n threshold: number | null;\n unit: string | null;\n direction: ScorecardMetricDirection;\n weight: number;\n status: ScorecardMetricStatus;\n statusOverride: ScorecardMetricStatus | null;\n remediation: string | null;\n iframeUrl: string | null;\n iframeHeight: number | null;\n}\n\nexport interface ScorecardPanelProps {\n ownerType: 'CATALOG' | 'COMPONENT' | 'VIBE';\n ownerId: string;\n onAddMetric?: () => void;\n}\n\ntype StatusMode = 'AUTO' | ScorecardMetricStatus;\n\ninterface MetricFormState {\n name: string;\n description: string;\n type: ScorecardMetricType;\n value: string;\n threshold: string;\n unit: string;\n direction: ScorecardMetricDirection;\n weight: string;\n statusMode: StatusMode;\n remediation: string;\n iframeUrl: string;\n iframeHeight: string;\n}\n\nconst DEFAULT_METRIC_FORM: MetricFormState = {\n name: '',\n description: '',\n type: 'CUSTOM',\n value: '',\n threshold: '',\n unit: '',\n direction: 'HIGHER_IS_BETTER',\n weight: '1',\n statusMode: 'AUTO',\n remediation: '',\n iframeUrl: '',\n iframeHeight: '',\n};\n\nconst inputClass =\n 'w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary outline-none focus:border-action-primary-bg';\n\nexport const ScorecardPanel: FC<ScorecardPanelProps> = ({ ownerType, ownerId, onAddMetric }) => {\n const { t } = useI18n();\n const tr = useCallback(\n (key: string, fallback: string): string => {\n const v = t(key);\n return v === key ? fallback : v;\n },\n [t]\n );\n\n const { workspaceId } = useActiveContext();\n\n const [expandedMetric, setExpandedMetric] = useState<string | null>(null);\n const [isAddMetricOpen, setIsAddMetricOpen] = useState(false);\n const [editingMetricId, setEditingMetricId] = useState<string | null>(null);\n const [deletingMetric, setDeletingMetric] = useState<{ id: string; name: string } | null>(null);\n const [deleteError, setDeleteError] = useState<string | null>(null);\n const [metricForm, setMetricForm] = useState<MetricFormState>(DEFAULT_METRIC_FORM);\n const [formError, setFormError] = useState<string | null>(null);\n const [applyingTemplateId, setApplyingTemplateId] = useState<string | null>(null);\n\n const { data, loading, error, refetch } = useGetScorecardQuery({\n variables: { ownerType, ownerId },\n skip: !ownerId,\n fetchPolicy: 'cache-and-network',\n });\n\n const [upsertScorecard, { loading: upsertingScorecard }] = useUpsertScorecardMutation();\n const [addScorecardMetric, { loading: addingMetric }] = useAddScorecardMetricMutation();\n const [updateScorecardMetric, { loading: updatingMetric }] = useUpdateScorecardMetricMutation();\n const [removeScorecardMetric, { loading: removingMetric }] = useRemoveScorecardMetricMutation();\n const [applyTemplate] = useApplyScorecardTemplateMutation();\n\n const scorecard = data?.vibecontrolsScorecard ?? null;\n const isSavingMetric = upsertingScorecard || addingMetric || updatingMetric;\n const isEditing = editingMetricId !== null;\n\n const resetMetricForm = (): void => {\n setMetricForm(DEFAULT_METRIC_FORM);\n setFormError(null);\n setEditingMetricId(null);\n };\n\n const handleAddMetricClick = (): void => {\n if (onAddMetric) {\n onAddMetric();\n return;\n }\n resetMetricForm();\n setIsAddMetricOpen(true);\n };\n\n const handleEditMetricClick = (event: MouseEvent, metric: ScorecardMetric): void => {\n event.stopPropagation();\n setMetricForm({\n name: metric.name,\n description: metric.description ?? '',\n type: metric.type,\n value: metric.value !== null && metric.value !== undefined ? String(metric.value) : '',\n threshold:\n metric.threshold !== null && metric.threshold !== undefined ? String(metric.threshold) : '',\n unit: metric.unit ?? '',\n direction: metric.direction ?? 'HIGHER_IS_BETTER',\n weight: metric.weight !== null && metric.weight !== undefined ? String(metric.weight) : '1',\n statusMode: metric.statusOverride ?? 'AUTO',\n remediation: metric.remediation ?? '',\n iframeUrl: metric.iframeUrl ?? '',\n iframeHeight:\n metric.iframeHeight !== null && metric.iframeHeight !== undefined\n ? String(metric.iframeHeight)\n : '',\n });\n setFormError(null);\n setEditingMetricId(metric.id);\n setIsAddMetricOpen(true);\n };\n\n const handleRemoveMetricClick = (event: MouseEvent, metric: ScorecardMetric): void => {\n event.stopPropagation();\n setDeleteError(null);\n setDeletingMetric({ id: metric.id, name: metric.name });\n };\n\n const closeAddMetricDialog = (): void => {\n if (isSavingMetric) return;\n setIsAddMetricOpen(false);\n resetMetricForm();\n };\n\n const closeDeleteDialog = useCallback((): void => {\n if (removingMetric) return;\n setDeletingMetric(null);\n setDeleteError(null);\n }, [removingMetric]);\n\n useEffect(() => {\n if (!deletingMetric) return undefined;\n const handler = (event: KeyboardEvent): void => {\n if (event.key === 'Escape') closeDeleteDialog();\n };\n window.addEventListener('keydown', handler);\n return () => window.removeEventListener('keydown', handler);\n }, [deletingMetric, closeDeleteDialog]);\n\n const confirmRemoveMetric = async (): Promise<void> => {\n if (!deletingMetric) return;\n setDeleteError(null);\n try {\n await removeScorecardMetric({ variables: { id: deletingMetric.id } });\n await refetch();\n setDeletingMetric(null);\n } catch (caughtError) {\n setDeleteError(\n caughtError instanceof Error ? caughtError.message : 'Failed to remove metric'\n );\n }\n };\n\n const updateMetricForm = (field: keyof MetricFormState, value: string): void => {\n setMetricForm((current) => ({ ...current, [field]: value }));\n };\n\n const parseOptionalNumber = (value: string, label: string): number | undefined => {\n const trimmed = value.trim();\n if (!trimmed) return undefined;\n const parsed = Number(trimmed);\n if (!Number.isFinite(parsed)) throw new Error(`${label} must be a valid number`);\n return parsed;\n };\n\n const ensureScorecardId = async (): Promise<string> => {\n if (scorecard?.id) return scorecard.id;\n const created = await upsertScorecard({ variables: { input: { ownerType, ownerId } } });\n const id = created.data?.upsertVibecontrolsScorecard.id;\n if (!id) throw new Error(tr('scorecard.scorecardCreateFailed', 'Unable to create scorecard'));\n return id;\n };\n\n const handleApplyTemplate = async (templateId: string): Promise<void> => {\n setApplyingTemplateId(templateId);\n try {\n await applyTemplate({ variables: { templateId, ownerType, ownerId } });\n await refetch();\n } finally {\n setApplyingTemplateId(null);\n }\n };\n\n const handleAddMetricSubmit = async (event: FormEvent<HTMLFormElement>): Promise<void> => {\n event.preventDefault();\n setFormError(null);\n\n const name = metricForm.name.trim();\n if (!name) {\n setFormError(tr('scorecard.metricNameRequired', 'Metric name is required'));\n return;\n }\n\n // 'AUTO' → derive status (send no explicit status). Otherwise pin it.\n const statusOverride =\n metricForm.statusMode === 'AUTO'\n ? undefined\n : (metricForm.statusMode as ScorecardMetricStatus);\n\n try {\n const sharedInput = {\n name,\n description: metricForm.description.trim() || null,\n type: metricForm.type,\n value: parseOptionalNumber(metricForm.value, tr('scorecard.value', 'Value')) ?? null,\n threshold:\n parseOptionalNumber(metricForm.threshold, tr('scorecard.threshold', 'Threshold')) ?? null,\n unit: metricForm.unit.trim() || null,\n direction: metricForm.direction,\n weight: parseOptionalNumber(metricForm.weight, tr('scorecard.weight', 'Weight')) ?? 1,\n status: statusOverride ?? null,\n remediation: metricForm.remediation.trim() || null,\n iframeUrl: metricForm.iframeUrl.trim() || null,\n iframeHeight:\n parseOptionalNumber(\n metricForm.iframeHeight,\n tr('scorecard.iframeHeight', 'Iframe height')\n ) ?? null,\n };\n\n if (editingMetricId) {\n await updateScorecardMetric({ variables: { id: editingMetricId, input: sharedInput } });\n } else {\n const scorecardId = await ensureScorecardId();\n await addScorecardMetric({ variables: { scorecardId, input: sharedInput } });\n }\n\n await refetch();\n setIsAddMetricOpen(false);\n resetMetricForm();\n } catch (caughtError) {\n setFormError(\n caughtError instanceof Error\n ? caughtError.message\n : editingMetricId\n ? 'Failed to update metric'\n : 'Failed to add metric'\n );\n }\n };\n\n // Live status preview for the form.\n const previewStatus =\n metricForm.statusMode === 'AUTO'\n ? deriveStatus(\n metricForm.value.trim() ? Number(metricForm.value) : null,\n metricForm.threshold.trim() ? Number(metricForm.threshold) : null,\n metricForm.direction\n )\n : (metricForm.statusMode as ScorecardMetricStatus);\n\n const renderDeleteMetricDialog = () => (\n <AnimatePresence>\n {deletingMetric && (\n <motion.div\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-bg-overlay p-4\"\n onClick={closeDeleteDialog}\n >\n <motion.div\n initial={{ scale: 0.96, y: 12 }}\n animate={{ scale: 1, y: 0 }}\n exit={{ scale: 0.96, y: 12 }}\n className=\"w-full max-w-md rounded-xl border border-border-default bg-bg-surface shadow-xl\"\n onClick={(event) => event.stopPropagation()}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"scorecard-delete-title\"\n >\n <div className=\"border-b border-border-default px-5 py-4\">\n <h3 id=\"scorecard-delete-title\" className=\"text-lg font-semibold text-text-primary\">\n {tr('scorecard.removeMetricTitle', 'Remove metric')}\n </h3>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n 'scorecard.removeMetricConfirm',\n 'Are you sure you want to remove this metric? This cannot be undone.'\n )}\n </p>\n </div>\n <div className=\"px-5 py-4 text-sm text-text-primary\">\n <p className=\"font-medium\">{deletingMetric.name}</p>\n {deleteError && (\n <p className=\"mt-3 rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-sm text-status-error-text\">\n {deleteError}\n </p>\n )}\n </div>\n <div className=\"flex items-center justify-end gap-3 border-t border-border-default px-5 py-4\">\n <button\n type=\"button\"\n onClick={closeDeleteDialog}\n disabled={removingMetric}\n className=\"rounded-md border border-border-default px-4 py-2 text-sm text-text-primary hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-60\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n <button\n type=\"button\"\n onClick={confirmRemoveMetric}\n disabled={removingMetric}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm text-status-error-text hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60\"\n >\n {removingMetric\n ? tr('scorecard.removingMetric', 'Removing...')\n : tr('scorecard.removeMetric', 'Remove metric')}\n </button>\n </div>\n </motion.div>\n </motion.div>\n )}\n </AnimatePresence>\n );\n\n const renderAddMetricDialog = () => {\n const previewConf = statusConfig[previewStatus] ?? statusConfig.UNKNOWN;\n return (\n <AnimatePresence>\n {isAddMetricOpen && (\n <motion.div\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-bg-overlay p-4\"\n onClick={closeAddMetricDialog}\n >\n <motion.div\n initial={{ scale: 0.96, y: 12 }}\n animate={{ scale: 1, y: 0 }}\n exit={{ scale: 0.96, y: 12 }}\n className=\"w-full max-w-2xl rounded-xl border border-border-default bg-bg-surface shadow-xl\"\n onClick={(event) => event.stopPropagation()}\n >\n <form onSubmit={handleAddMetricSubmit} className=\"flex max-h-[85vh] flex-col\">\n <div className=\"border-b border-border-default px-5 py-4\">\n <h3 className=\"text-lg font-semibold text-text-primary\">\n {isEditing\n ? tr('scorecard.editMetricTitle', 'Edit scorecard metric')\n : tr('scorecard.addMetricTitle', 'Add scorecard metric')}\n </h3>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n 'scorecard.addMetricDescription',\n 'Define a metric. Status is derived from value vs threshold unless you pin it.'\n )}\n </p>\n </div>\n\n <div className=\"space-y-4 overflow-y-auto px-5 py-4\">\n {formError && (\n <div className=\"rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-sm text-status-error-text\">\n {formError}\n </div>\n )}\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.metricName', 'Metric name')}\n </span>\n <input\n aria-label=\"control\"\n value={metricForm.name}\n onChange={(event) => updateMetricForm('name', event.target.value)}\n className={inputClass}\n placeholder={tr('scorecard.metricNamePlaceholder', 'Test coverage')}\n />\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.metricDescription', 'Description')}\n </span>\n <textarea\n aria-label=\"control\"\n value={metricForm.description}\n onChange={(event) => updateMetricForm('description', event.target.value)}\n className={`min-h-16 ${inputClass}`}\n placeholder={tr(\n 'scorecard.metricDescriptionPlaceholder',\n 'Explain what this metric measures'\n )}\n />\n </label>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.metricType', 'Type')}\n </span>\n <select\n value={metricForm.type}\n onChange={(event) =>\n updateMetricForm('type', event.target.value as ScorecardMetricType)\n }\n className={inputClass}\n >\n {METRIC_TYPE_OPTIONS.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.metricDirection', 'Direction')}\n </span>\n <select\n value={metricForm.direction}\n onChange={(event) =>\n updateMetricForm(\n 'direction',\n event.target.value as ScorecardMetricDirection\n )\n }\n className={inputClass}\n >\n {DIRECTION_OPTIONS.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </label>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-4\">\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.value', 'Value')}\n </span>\n <input\n aria-label=\"value\"\n value={metricForm.value}\n onChange={(event) => updateMetricForm('value', event.target.value)}\n inputMode=\"decimal\"\n className={inputClass}\n placeholder=\"86\"\n />\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.threshold', 'Threshold')}\n </span>\n <input\n aria-label=\"threshold\"\n value={metricForm.threshold}\n onChange={(event) => updateMetricForm('threshold', event.target.value)}\n inputMode=\"decimal\"\n className={inputClass}\n placeholder=\"80\"\n />\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.unit', 'Unit')}\n </span>\n <input\n aria-label=\"unit\"\n value={metricForm.unit}\n onChange={(event) => updateMetricForm('unit', event.target.value)}\n className={inputClass}\n placeholder=\"%\"\n />\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.weight', 'Weight')}\n </span>\n <input\n aria-label=\"weight\"\n value={metricForm.weight}\n onChange={(event) => updateMetricForm('weight', event.target.value)}\n inputMode=\"decimal\"\n className={inputClass}\n placeholder=\"1\"\n />\n </label>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.metricStatus', 'Status')}\n </span>\n <select\n value={metricForm.statusMode}\n onChange={(event) =>\n updateMetricForm('statusMode', event.target.value as StatusMode)\n }\n className={inputClass}\n >\n {STATUS_OPTIONS.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </label>\n\n <div className=\"flex items-end\">\n <div\n className={`flex items-center gap-2 rounded-md px-3 py-2 text-sm ${previewConf.bg} ${previewConf.text}`}\n >\n <previewConf.icon className=\"size-4\" />\n <span>\n {metricForm.statusMode === 'AUTO' ? 'Derived: ' : 'Pinned: '}\n {previewConf.label}\n </span>\n </div>\n </div>\n </div>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.remediation', 'Remediation guidance')}\n </span>\n <textarea\n aria-label=\"control\"\n value={metricForm.remediation}\n onChange={(event) => updateMetricForm('remediation', event.target.value)}\n className={`min-h-16 ${inputClass}`}\n placeholder={tr(\n 'scorecard.remediationPlaceholder',\n 'What should someone do when this metric is failing?'\n )}\n />\n </label>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-[1fr_10rem]\">\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.iframeUrl', 'Embed URL')}\n </span>\n <input\n aria-label=\"control\"\n value={metricForm.iframeUrl}\n onChange={(event) => updateMetricForm('iframeUrl', event.target.value)}\n className={inputClass}\n placeholder={tr('scorecard.iframeUrlPlaceholder', 'Optional dashboard URL')}\n />\n </label>\n\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('scorecard.iframeHeight', 'Iframe height')}\n </span>\n <input\n aria-label=\"iframe height\"\n value={metricForm.iframeHeight}\n onChange={(event) => updateMetricForm('iframeHeight', event.target.value)}\n inputMode=\"numeric\"\n className={inputClass}\n placeholder=\"320\"\n />\n </label>\n </div>\n </div>\n\n <div className=\"flex items-center justify-end gap-3 border-t border-border-default px-5 py-4\">\n <button\n type=\"button\"\n onClick={closeAddMetricDialog}\n disabled={isSavingMetric}\n className=\"rounded-md border border-border-default px-4 py-2 text-sm text-text-primary hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-60\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n <button\n type=\"submit\"\n disabled={isSavingMetric}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm text-action-primary-text hover:bg-action-primary-bg-hover disabled:cursor-not-allowed disabled:opacity-60\"\n >\n {isSavingMetric\n ? tr('scorecard.savingMetric', 'Saving...')\n : isEditing\n ? tr('scorecard.updateMetric', 'Update Metric')\n : tr('scorecard.saveMetric', 'Save Metric')}\n </button>\n </div>\n </form>\n </motion.div>\n </motion.div>\n )}\n </AnimatePresence>\n );\n };\n\n if (loading && !scorecard) {\n return <LoadingState message={tr('scorecard.loading', 'Loading scorecard...')} />;\n }\n\n if (error && !scorecard) {\n return (\n <ErrorState\n title={tr('scorecard.failedLoad', 'Failed to load scorecard')}\n message={error.message}\n onRetry={() => refetch()}\n />\n );\n }\n\n if (!scorecard || scorecard.metrics.length === 0) {\n return (\n <>\n <ScorecardEmptyState\n ownerType={ownerType}\n ownerId={ownerId}\n workspaceId={workspaceId}\n applyingTemplateId={applyingTemplateId}\n onApplyTemplate={handleApplyTemplate}\n onAddManual={handleAddMetricClick}\n />\n {renderAddMetricDialog()}\n {renderDeleteMetricDialog()}\n </>\n );\n }\n\n const overallScore = scorecard.overallScore ?? 0;\n const scoreColor =\n overallScore >= 80\n ? 'text-status-success-text'\n : overallScore >= 50\n ? 'text-status-warning-text'\n : 'text-status-error-text';\n\n return (\n <div className=\"space-y-6\">\n {/* Overall Score */}\n <Card padding=\"lg\">\n <div className=\"flex items-center justify-between\">\n <div>\n <p className=\"flex items-center gap-1.5 text-sm font-medium text-text-secondary\">\n {tr('scorecard.overallScore', 'Overall Score')}\n <span title=\"Weighted average of each metric's score (0–100). Status is derived from value vs threshold.\">\n <Info className=\"size-3.5 text-text-muted\" />\n </span>\n </p>\n <p className={`text-4xl font-bold ${scoreColor}`}>\n {overallScore}\n <span className=\"text-lg text-text-muted\">/100</span>\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <TrendingUp className={`size-6 ${scoreColor}`} />\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"rounded-md p-2 text-text-secondary transition-colors hover:bg-bg-sunken\"\n title={tr('common.refresh', 'Refresh')}\n >\n <RefreshCw className=\"size-4\" />\n </button>\n <button\n type=\"button\"\n onClick={handleAddMetricClick}\n className=\"flex items-center gap-2 rounded-md bg-action-primary-bg px-3 py-2 text-sm text-action-primary-text transition-colors hover:bg-action-primary-bg-hover\"\n >\n <Plus className=\"size-4\" />\n {tr('scorecard.addMetric', 'Add Metric')}\n </button>\n </div>\n </div>\n </Card>\n\n {/* Metrics Grid */}\n <MotionList stagger={0.04} className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {scorecard.metrics.map((metric: ScorecardMetric) => {\n const config = statusConfig[metric.status] || statusConfig.UNKNOWN;\n const StatusIcon = config.icon;\n const value = metric.value ?? 0;\n const threshold = metric.threshold ?? 100;\n const percentage =\n threshold > 0 ? Math.min(100, Math.max(0, (value / threshold) * 100)) : 0;\n const isExpanded = expandedMetric === metric.id;\n const showRemediation =\n metric.remediation && (metric.status === 'WARNING' || metric.status === 'FAILING');\n\n return (\n <MotionItem key={metric.id}>\n <Card hoverable padding=\"none\">\n <div\n role=\"button\"\n tabIndex={0}\n onClick={() => setExpandedMetric(isExpanded ? null : metric.id)}\n onKeyDown={(event) => {\n if (event.target !== event.currentTarget) return;\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n setExpandedMetric(isExpanded ? null : metric.id);\n }\n }}\n className=\"cursor-pointer rounded-lg p-4 focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n aria-expanded={isExpanded}\n aria-label={`${metric.name} metric details`}\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {metric.name}\n </p>\n <p className=\"mt-0.5 flex items-center gap-1.5 text-xs text-text-muted\">\n <span>\n {metric.direction === 'LOWER_IS_BETTER'\n ? '↓ lower better'\n : '↑ higher better'}\n </span>\n {metric.weight && metric.weight !== 1 ? (\n <span>· ×{metric.weight}</span>\n ) : null}\n {metric.statusOverride ? <span>· pinned</span> : null}\n </p>\n </div>\n <div className=\"flex flex-shrink-0 items-center gap-1\">\n <button\n type=\"button\"\n onClick={(event) => handleEditMetricClick(event, metric)}\n className=\"rounded-md p-1 text-text-secondary hover:bg-bg-sunken hover:text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n aria-label={tr('scorecard.editMetric', 'Edit metric')}\n title={tr('scorecard.editMetric', 'Edit metric')}\n >\n <Pencil className=\"size-4\" />\n </button>\n <button\n type=\"button\"\n onClick={(event) => handleRemoveMetricClick(event, metric)}\n className=\"rounded-md p-1 text-text-secondary hover:bg-status-error-bg hover:text-status-error-text focus:outline-none focus:ring-2 focus:ring-status-error-text\"\n aria-label={tr('scorecard.removeMetric', 'Remove metric')}\n title={tr('scorecard.removeMetric', 'Remove metric')}\n >\n <Trash2 className=\"size-4\" />\n </button>\n <div\n className={`flex size-7 items-center justify-center rounded-full ${config.bg}`}\n >\n <StatusIcon className={`size-4 ${config.text}`} />\n </div>\n </div>\n </div>\n\n {/* Value and progress */}\n <div className=\"mt-3\">\n <div className=\"flex items-baseline justify-between\">\n <span className=\"text-lg font-bold text-text-primary\">\n {value}\n {metric.unit ? ` ${metric.unit}` : ''}\n </span>\n <span className=\"text-xs text-text-muted\">\n / {threshold}\n {metric.unit ? ` ${metric.unit}` : ''}\n </span>\n </div>\n <div className=\"mt-1.5 h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <motion.div\n initial={{ width: 0 }}\n animate={{ width: `${percentage}%` }}\n transition={{ duration: 0.5, ease: 'easeOut' }}\n className={`h-full rounded-full ${config.bar}`}\n />\n </div>\n </div>\n\n {showRemediation && (\n <p className=\"mt-2 rounded-md border border-status-warning-border bg-status-warning-bg px-2 py-1.5 text-xs text-status-warning-text\">\n {metric.remediation}\n </p>\n )}\n </div>\n\n {/* Expanded: trend + optional embed */}\n <AnimatePresence>\n {isExpanded && (\n <motion.div\n initial={{ height: 0, opacity: 0 }}\n animate={{ height: 'auto', opacity: 1 }}\n exit={{ height: 0, opacity: 0 }}\n transition={{ duration: 0.2 }}\n className=\"overflow-hidden border-t border-border-default\"\n >\n <div className=\"px-4 py-3\">\n <MetricTrend metricId={metric.id} colorClass={config.text} />\n </div>\n {metric.iframeUrl && (\n <iframe\n src={metric.iframeUrl}\n className=\"w-full border-0\"\n style={{\n height: metric.iframeHeight ? `${metric.iframeHeight}px` : '200px',\n }}\n title={`${metric.name} embed`}\n sandbox=\"allow-scripts allow-same-origin\"\n />\n )}\n </motion.div>\n )}\n </AnimatePresence>\n </Card>\n </MotionItem>\n );\n })}\n </MotionList>\n\n <ScorecardsOverviewPanel ownerType={ownerType} highlightOwnerId={ownerId} />\n\n {renderAddMetricDialog()}\n {renderDeleteMetricDialog()}\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ScorecardsOverviewPanel.js","names":[],"sources":["../../../src/components/scorecard/ScorecardsOverviewPanel.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState } from 'react';\nimport { Layers, ChevronDown, ChevronRight } from 'lucide-react';\nimport { Card } from '@/components/shared';\nimport { useGetScorecardsQuery, type GetScorecardsQuery } from '@/generated/wspace-operations';\n\ntype OverviewScorecard = GetScorecardsQuery['vibecontrolsScorecards'][number];\ntype OverviewMetric = OverviewScorecard['metrics'][number];\n\ninterface ScorecardsOverviewPanelProps {\n /** Restrict the comparison to one owner type (e.g. only VIBE scorecards). */\n ownerType?: 'CATALOG' | 'COMPONENT' | 'VIBE';\n /** Highlight the scorecard for this owner in the list. */\n highlightOwnerId?: string;\n}\n\nfunction scoreColor(score: number | null | undefined): string {\n if (score === null || score === undefined) return 'text-text-muted';\n if (score >= 80) return 'text-status-success-text';\n if (score >= 50) return 'text-status-warning-text';\n return 'text-status-error-text';\n}\n\n/**\n * A compact portfolio view comparing scorecards across the workspace, lowest\n * score first so the entities that need attention surface at the top.\n */\nexport const ScorecardsOverviewPanel: FC<ScorecardsOverviewPanelProps> = ({\n ownerType,\n highlightOwnerId,\n}) => {\n const [open, setOpen] = useState(false);\n const { data, loading } = useGetScorecardsQuery({\n variables: { ownerType },\n fetchPolicy: 'cache-and-network',\n skip: !open,\n });\n\n const scorecards = data?.vibecontrolsScorecards ?? [];\n\n return (\n <Card padding=\"lg\">\n <button\n type=\"button\"\n onClick={() => setOpen((o) => !o)}\n className=\"flex w-full items-center justify-between text-left\"\n >\n <span className=\"flex items-center gap-2 text-sm font-medium text-text-primary\">\n <Layers className=\"size-4 text-text-secondary\" />\n Compare across {ownerType ? `${ownerType.toLowerCase()}s` : 'the workspace'}\n </span>\n {open ? (\n <ChevronDown className=\"size-4 text-text-secondary\" />\n ) : (\n <ChevronRight className=\"size-4 text-text-secondary\" />\n )}\n </button>\n\n {open && (\n <div className=\"mt-3 space-y-1.5\">\n {loading && scorecards.length === 0 && (\n <p className=\"text-xs text-text-muted\">Loading scorecards…</p>\n )}\n {!loading && scorecards.length === 0 && (\n <p className=\"text-xs text-text-muted\">No other scorecards in this workspace yet.</p>\n )}\n {scorecards.map((sc: OverviewScorecard) => {\n const failing = sc.metrics.filter((m: OverviewMetric) => m.status === 'FAILING').length;\n const warning = sc.metrics.filter((m: OverviewMetric) => m.status === 'WARNING').length;\n const isCurrent = sc.ownerId === highlightOwnerId;\n return (\n <div\n key={sc.id}\n className={`flex items-center justify-between rounded-md border px-3 py-2 text-sm ${\n isCurrent ? 'border-action-primary-bg bg-bg-sunken' : 'border-border-default'\n }`}\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate font-medium text-text-primary\">\n {sc.ownerType} · {sc.ownerId.slice(0, 8)}\n {isCurrent ? ' (this one)' : ''}\n </span>\n <span className=\"block text-xs text-text-muted\">\n {sc.metrics.length} metric{sc.metrics.length === 1 ? '' : 's'}\n {failing ? ` · ${failing} failing` : ''}\n {warning ? ` · ${warning} warning` : ''}\n </span>\n </span>\n <span
|
|
1
|
+
{"version":3,"file":"ScorecardsOverviewPanel.js","names":[],"sources":["../../../src/components/scorecard/ScorecardsOverviewPanel.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState } from 'react';\nimport { Layers, ChevronDown, ChevronRight } from 'lucide-react';\nimport { Card } from '@/components/shared';\nimport { useGetScorecardsQuery, type GetScorecardsQuery } from '@/generated/wspace-operations';\n\ntype OverviewScorecard = GetScorecardsQuery['vibecontrolsScorecards'][number];\ntype OverviewMetric = OverviewScorecard['metrics'][number];\n\ninterface ScorecardsOverviewPanelProps {\n /** Restrict the comparison to one owner type (e.g. only VIBE scorecards). */\n ownerType?: 'CATALOG' | 'COMPONENT' | 'VIBE';\n /** Highlight the scorecard for this owner in the list. */\n highlightOwnerId?: string;\n}\n\nfunction scoreColor(score: number | null | undefined): string {\n if (score === null || score === undefined) return 'text-text-muted';\n if (score >= 80) return 'text-status-success-text';\n if (score >= 50) return 'text-status-warning-text';\n return 'text-status-error-text';\n}\n\n/**\n * A compact portfolio view comparing scorecards across the workspace, lowest\n * score first so the entities that need attention surface at the top.\n */\nexport const ScorecardsOverviewPanel: FC<ScorecardsOverviewPanelProps> = ({\n ownerType,\n highlightOwnerId,\n}) => {\n const [open, setOpen] = useState(false);\n const { data, loading } = useGetScorecardsQuery({\n variables: { ownerType },\n fetchPolicy: 'cache-and-network',\n skip: !open,\n });\n\n const scorecards = data?.vibecontrolsScorecards ?? [];\n\n return (\n <Card padding=\"lg\">\n <button\n type=\"button\"\n onClick={() => setOpen((o) => !o)}\n className=\"flex w-full items-center justify-between text-left\"\n >\n <span className=\"flex items-center gap-2 text-sm font-medium text-text-primary\">\n <Layers className=\"size-4 text-text-secondary\" />\n Compare across {ownerType ? `${ownerType.toLowerCase()}s` : 'the workspace'}\n </span>\n {open ? (\n <ChevronDown className=\"size-4 text-text-secondary\" />\n ) : (\n <ChevronRight className=\"size-4 text-text-secondary\" />\n )}\n </button>\n\n {open && (\n <div className=\"mt-3 space-y-1.5\">\n {loading && scorecards.length === 0 && (\n <p className=\"text-xs text-text-muted\">Loading scorecards…</p>\n )}\n {!loading && scorecards.length === 0 && (\n <p className=\"text-xs text-text-muted\">No other scorecards in this workspace yet.</p>\n )}\n {scorecards.map((sc: OverviewScorecard) => {\n const failing = sc.metrics.filter((m: OverviewMetric) => m.status === 'FAILING').length;\n const warning = sc.metrics.filter((m: OverviewMetric) => m.status === 'WARNING').length;\n const isCurrent = sc.ownerId === highlightOwnerId;\n return (\n <div\n key={sc.id}\n className={`flex items-center justify-between rounded-md border px-3 py-2 text-sm ${\n isCurrent ? 'border-action-primary-bg bg-bg-sunken' : 'border-border-default'\n }`}\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate font-medium text-text-primary\">\n {sc.ownerType} · {sc.ownerId.slice(0, 8)}\n {isCurrent ? ' (this one)' : ''}\n </span>\n <span className=\"block text-xs text-text-muted\">\n {sc.metrics.length} metric{sc.metrics.length === 1 ? '' : 's'}\n {failing ? ` · ${failing} failing` : ''}\n {warning ? ` · ${warning} warning` : ''}\n </span>\n </span>\n <span\n className={`flex-shrink-0 text-base font-bold ${scoreColor(sc.overallScore)}`}\n >\n {sc.overallScore ?? '—'}\n <span className=\"text-xs text-text-muted\">/100</span>\n </span>\n </div>\n );\n })}\n </div>\n )}\n </Card>\n );\n};\n"],"mappings":";;;;;;;AAgBA,SAAS,EAAW,GAA0C;AAI5D,QAHI,KAAU,OAAoC,oBAC9C,KAAS,KAAW,6BACpB,KAAS,KAAW,6BACjB;;AAOT,IAAa,KAA6D,EACxE,cACA,0BACI;CACJ,IAAM,CAAC,GAAM,KAAW,EAAS,GAAM,EACjC,EAAE,SAAM,eAAY,EAAsB;EAC9C,WAAW,EAAE,cAAW;EACxB,aAAa;EACb,MAAM,CAAC;EACR,CAAC,EAEI,IAAa,GAAM,0BAA0B,EAAE;AAErD,QACE,kBAAC,GAAD;EAAM,SAAQ;YAAd,CACE,kBAAC,UAAD;GACE,MAAK;GACL,eAAe,GAAS,MAAM,CAAC,EAAE;GACjC,WAAU;aAHZ,CAKE,kBAAC,QAAD;IAAM,WAAU;cAAhB;KACE,kBAAC,GAAD,EAAQ,WAAU,8BAA+B,CAAA;;KACjC,IAAY,GAAG,EAAU,aAAa,CAAC,KAAK;KACvD;OAEL,EADD,IACE,IAEA,GAFD,EAAa,WAAU,8BAA+B,CAEC,CAElD;MAER,KACC,kBAAC,OAAD;GAAK,WAAU;aAAf;IACG,KAAW,EAAW,WAAW,KAChC,kBAAC,KAAD;KAAG,WAAU;eAA0B;KAAuB,CAAA;IAE/D,CAAC,KAAW,EAAW,WAAW,KACjC,kBAAC,KAAD;KAAG,WAAU;eAA0B;KAA8C,CAAA;IAEtF,EAAW,KAAK,MAA0B;KACzC,IAAM,IAAU,EAAG,QAAQ,QAAQ,MAAsB,EAAE,WAAW,UAAU,CAAC,QAC3E,IAAU,EAAG,QAAQ,QAAQ,MAAsB,EAAE,WAAW,UAAU,CAAC,QAC3E,IAAY,EAAG,YAAY;AACjC,YACE,kBAAC,OAAD;MAEE,WAAW,yEACT,IAAY,0CAA0C;gBAH1D,CAME,kBAAC,QAAD;OAAM,WAAU;iBAAhB,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACG,EAAG;SAAU;SAAI,EAAG,QAAQ,MAAM,GAAG,EAAE;SACvC,IAAY,gBAAgB;SACxB;WACP,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACG,EAAG,QAAQ;SAAO;SAAQ,EAAG,QAAQ,WAAW,IAAI,KAAK;SACzD,IAAU,MAAM,EAAQ,YAAY;SACpC,IAAU,MAAM,EAAQ,YAAY;SAChC;UACF;UACP,kBAAC,QAAD;OACE,WAAW,qCAAqC,EAAW,EAAG,aAAa;iBAD7E,CAGG,EAAG,gBAAgB,KACpB,kBAAC,QAAD;QAAM,WAAU;kBAA0B;QAAW,CAAA,CAChD;SACH;QAtBC,EAAG,GAsBJ;MAER;IACE;KAEH"}
|