@agile-vibe-coding/avc 0.2.3 → 0.3.2
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/README.md +475 -3
- package/cli/agents/agent-selector.md +23 -0
- package/cli/agents/code-implementer.md +117 -0
- package/cli/agents/code-validator.md +80 -0
- package/cli/agents/context-reviewer-epic.md +101 -0
- package/cli/agents/context-reviewer-story.md +92 -0
- package/cli/agents/context-writer-epic.md +145 -0
- package/cli/agents/context-writer-story.md +111 -0
- package/cli/agents/doc-writer-epic.md +42 -0
- package/cli/agents/doc-writer-story.md +43 -0
- package/cli/agents/duplicate-detector.md +110 -0
- package/cli/agents/epic-story-decomposer.md +318 -39
- package/cli/agents/mission-scope-generator.md +68 -4
- package/cli/agents/mission-scope-validator.md +40 -6
- package/cli/agents/project-context-extractor.md +21 -6
- package/cli/agents/scaffolding-generator.md +99 -0
- package/cli/agents/seed-validator.md +71 -0
- package/cli/agents/story-scope-reviewer.md +147 -0
- package/cli/agents/story-splitter.md +83 -0
- package/cli/agents/validator-documentation.json +31 -0
- package/cli/agents/validator-documentation.md +3 -1
- package/cli/api-reference-tool.js +368 -0
- package/cli/checks/catalog.json +76 -0
- package/cli/checks/code/quality.json +26 -0
- package/cli/checks/code/testing.json +14 -0
- package/cli/checks/code/traceability.json +26 -0
- package/cli/checks/cross-refs/epic.json +171 -0
- package/cli/checks/cross-refs/story.json +149 -0
- package/cli/checks/epic/api.json +114 -0
- package/cli/checks/epic/backend.json +126 -0
- package/cli/checks/epic/cloud.json +126 -0
- package/cli/checks/epic/data.json +102 -0
- package/cli/checks/epic/database.json +114 -0
- package/cli/checks/epic/developer.json +182 -0
- package/cli/checks/epic/devops.json +174 -0
- package/cli/checks/epic/frontend.json +162 -0
- package/cli/checks/epic/mobile.json +102 -0
- package/cli/checks/epic/qa.json +90 -0
- package/cli/checks/epic/security.json +184 -0
- package/cli/checks/epic/solution-architect.json +192 -0
- package/cli/checks/epic/test-architect.json +90 -0
- package/cli/checks/epic/ui.json +102 -0
- package/cli/checks/epic/ux.json +90 -0
- package/cli/checks/fixes/epic-fix-template.md +10 -0
- package/cli/checks/fixes/story-fix-template.md +10 -0
- package/cli/checks/story/api.json +186 -0
- package/cli/checks/story/backend.json +102 -0
- package/cli/checks/story/cloud.json +102 -0
- package/cli/checks/story/data.json +210 -0
- package/cli/checks/story/database.json +102 -0
- package/cli/checks/story/developer.json +168 -0
- package/cli/checks/story/devops.json +102 -0
- package/cli/checks/story/frontend.json +174 -0
- package/cli/checks/story/mobile.json +102 -0
- package/cli/checks/story/qa.json +210 -0
- package/cli/checks/story/security.json +198 -0
- package/cli/checks/story/solution-architect.json +230 -0
- package/cli/checks/story/test-architect.json +210 -0
- package/cli/checks/story/ui.json +102 -0
- package/cli/checks/story/ux.json +102 -0
- package/cli/coding-order.js +401 -0
- package/cli/dependency-checker.js +72 -0
- package/cli/epic-story-validator.js +284 -799
- package/cli/index.js +0 -0
- package/cli/init-model-config.js +17 -10
- package/cli/init.js +514 -92
- package/cli/kanban-server-manager.js +1 -2
- package/cli/llm-claude.js +98 -31
- package/cli/llm-gemini.js +29 -5
- package/cli/llm-local.js +493 -0
- package/cli/llm-openai.js +262 -41
- package/cli/llm-provider.js +147 -8
- package/cli/llm-token-limits.js +113 -4
- package/cli/llm-verifier.js +209 -1
- package/cli/llm-xiaomi.js +143 -0
- package/cli/message-constants.js +3 -12
- package/cli/messaging-api.js +6 -12
- package/cli/micro-check-fixer.js +335 -0
- package/cli/micro-check-runner.js +449 -0
- package/cli/micro-check-scorer.js +148 -0
- package/cli/micro-check-validator.js +538 -0
- package/cli/model-pricing.js +23 -0
- package/cli/model-selector.js +3 -2
- package/cli/prompt-logger.js +57 -0
- package/cli/repl-ink.js +106 -346
- package/cli/repl-old.js +1 -2
- package/cli/seed-processor.js +194 -24
- package/cli/sprint-planning-processor.js +2638 -289
- package/cli/template-processor.js +50 -3
- package/cli/token-tracker.js +50 -23
- package/cli/tools/generate-story-validators.js +1 -1
- package/cli/validation-router.js +70 -8
- package/cli/worktree-runner.js +654 -0
- package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
- package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
- package/kanban/client/dist/index.html +2 -2
- package/kanban/client/src/App.jsx +43 -14
- package/kanban/client/src/components/ceremony/AskArchPopup.jsx +7 -3
- package/kanban/client/src/components/ceremony/AskModelPopup.jsx +23 -10
- package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +320 -133
- package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
- package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +80 -13
- package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +156 -22
- package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +11 -11
- package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +3 -21
- package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +214 -10
- package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +23 -2
- package/kanban/client/src/components/kanban/CardDetailModal.jsx +97 -10
- package/kanban/client/src/components/kanban/GroupingSelector.jsx +7 -1
- package/kanban/client/src/components/kanban/KanbanCard.jsx +23 -14
- package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +9 -14
- package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
- package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
- package/kanban/client/src/components/settings/AgentsTab.jsx +103 -75
- package/kanban/client/src/components/settings/ApiKeysTab.jsx +31 -2
- package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +9 -2
- package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
- package/kanban/client/src/components/settings/CostThresholdsTab.jsx +3 -2
- package/kanban/client/src/components/settings/ModelPricingTab.jsx +72 -7
- package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
- package/kanban/client/src/components/settings/SettingsModal.jsx +4 -4
- package/kanban/client/src/components/stats/CostModal.jsx +34 -3
- package/kanban/client/src/hooks/useGrouping.js +59 -0
- package/kanban/client/src/lib/api.js +118 -4
- package/kanban/client/src/lib/status-grouping.js +10 -0
- package/kanban/client/src/store/kanbanStore.js +8 -0
- package/kanban/server/index.js +23 -2
- package/kanban/server/routes/ceremony.js +153 -4
- package/kanban/server/routes/costs.js +9 -3
- package/kanban/server/routes/openai-oauth.js +366 -0
- package/kanban/server/routes/settings.js +447 -14
- package/kanban/server/routes/websocket.js +7 -2
- package/kanban/server/routes/work-items.js +141 -1
- package/kanban/server/services/CeremonyService.js +275 -24
- package/kanban/server/services/TaskRunnerService.js +261 -0
- package/kanban/server/workers/run-task-worker.js +121 -0
- package/kanban/server/workers/seed-worker.js +94 -0
- package/kanban/server/workers/sponsor-call-worker.js +14 -6
- package/kanban/server/workers/sprint-planning-worker.js +94 -12
- package/package.json +2 -3
- package/cli/agents/solver-epic-api.json +0 -15
- package/cli/agents/solver-epic-api.md +0 -39
- package/cli/agents/solver-epic-backend.json +0 -15
- package/cli/agents/solver-epic-backend.md +0 -39
- package/cli/agents/solver-epic-cloud.json +0 -15
- package/cli/agents/solver-epic-cloud.md +0 -39
- package/cli/agents/solver-epic-data.json +0 -15
- package/cli/agents/solver-epic-data.md +0 -39
- package/cli/agents/solver-epic-database.json +0 -15
- package/cli/agents/solver-epic-database.md +0 -39
- package/cli/agents/solver-epic-developer.json +0 -15
- package/cli/agents/solver-epic-developer.md +0 -39
- package/cli/agents/solver-epic-devops.json +0 -15
- package/cli/agents/solver-epic-devops.md +0 -39
- package/cli/agents/solver-epic-frontend.json +0 -15
- package/cli/agents/solver-epic-frontend.md +0 -39
- package/cli/agents/solver-epic-mobile.json +0 -15
- package/cli/agents/solver-epic-mobile.md +0 -39
- package/cli/agents/solver-epic-qa.json +0 -15
- package/cli/agents/solver-epic-qa.md +0 -39
- package/cli/agents/solver-epic-security.json +0 -15
- package/cli/agents/solver-epic-security.md +0 -39
- package/cli/agents/solver-epic-solution-architect.json +0 -15
- package/cli/agents/solver-epic-solution-architect.md +0 -39
- package/cli/agents/solver-epic-test-architect.json +0 -15
- package/cli/agents/solver-epic-test-architect.md +0 -39
- package/cli/agents/solver-epic-ui.json +0 -15
- package/cli/agents/solver-epic-ui.md +0 -39
- package/cli/agents/solver-epic-ux.json +0 -15
- package/cli/agents/solver-epic-ux.md +0 -39
- package/cli/agents/solver-story-api.json +0 -15
- package/cli/agents/solver-story-api.md +0 -39
- package/cli/agents/solver-story-backend.json +0 -15
- package/cli/agents/solver-story-backend.md +0 -39
- package/cli/agents/solver-story-cloud.json +0 -15
- package/cli/agents/solver-story-cloud.md +0 -39
- package/cli/agents/solver-story-data.json +0 -15
- package/cli/agents/solver-story-data.md +0 -39
- package/cli/agents/solver-story-database.json +0 -15
- package/cli/agents/solver-story-database.md +0 -39
- package/cli/agents/solver-story-developer.json +0 -15
- package/cli/agents/solver-story-developer.md +0 -39
- package/cli/agents/solver-story-devops.json +0 -15
- package/cli/agents/solver-story-devops.md +0 -39
- package/cli/agents/solver-story-frontend.json +0 -15
- package/cli/agents/solver-story-frontend.md +0 -39
- package/cli/agents/solver-story-mobile.json +0 -15
- package/cli/agents/solver-story-mobile.md +0 -39
- package/cli/agents/solver-story-qa.json +0 -15
- package/cli/agents/solver-story-qa.md +0 -39
- package/cli/agents/solver-story-security.json +0 -15
- package/cli/agents/solver-story-security.md +0 -39
- package/cli/agents/solver-story-solution-architect.json +0 -15
- package/cli/agents/solver-story-solution-architect.md +0 -39
- package/cli/agents/solver-story-test-architect.json +0 -15
- package/cli/agents/solver-story-test-architect.md +0 -39
- package/cli/agents/solver-story-ui.json +0 -15
- package/cli/agents/solver-story-ui.md +0 -39
- package/cli/agents/solver-story-ux.json +0 -15
- package/cli/agents/solver-story-ux.md +0 -39
- package/cli/agents/validator-epic-api.json +0 -93
- package/cli/agents/validator-epic-api.md +0 -137
- package/cli/agents/validator-epic-backend.json +0 -93
- package/cli/agents/validator-epic-backend.md +0 -130
- package/cli/agents/validator-epic-cloud.json +0 -93
- package/cli/agents/validator-epic-cloud.md +0 -137
- package/cli/agents/validator-epic-data.json +0 -93
- package/cli/agents/validator-epic-data.md +0 -130
- package/cli/agents/validator-epic-database.json +0 -93
- package/cli/agents/validator-epic-database.md +0 -137
- package/cli/agents/validator-epic-developer.json +0 -74
- package/cli/agents/validator-epic-developer.md +0 -153
- package/cli/agents/validator-epic-devops.json +0 -74
- package/cli/agents/validator-epic-devops.md +0 -153
- package/cli/agents/validator-epic-frontend.json +0 -74
- package/cli/agents/validator-epic-frontend.md +0 -153
- package/cli/agents/validator-epic-mobile.json +0 -93
- package/cli/agents/validator-epic-mobile.md +0 -130
- package/cli/agents/validator-epic-qa.json +0 -93
- package/cli/agents/validator-epic-qa.md +0 -130
- package/cli/agents/validator-epic-security.json +0 -74
- package/cli/agents/validator-epic-security.md +0 -154
- package/cli/agents/validator-epic-solution-architect.json +0 -74
- package/cli/agents/validator-epic-solution-architect.md +0 -156
- package/cli/agents/validator-epic-test-architect.json +0 -93
- package/cli/agents/validator-epic-test-architect.md +0 -130
- package/cli/agents/validator-epic-ui.json +0 -93
- package/cli/agents/validator-epic-ui.md +0 -130
- package/cli/agents/validator-epic-ux.json +0 -93
- package/cli/agents/validator-epic-ux.md +0 -130
- package/cli/agents/validator-story-api.json +0 -104
- package/cli/agents/validator-story-api.md +0 -152
- package/cli/agents/validator-story-backend.json +0 -104
- package/cli/agents/validator-story-backend.md +0 -152
- package/cli/agents/validator-story-cloud.json +0 -104
- package/cli/agents/validator-story-cloud.md +0 -152
- package/cli/agents/validator-story-data.json +0 -104
- package/cli/agents/validator-story-data.md +0 -152
- package/cli/agents/validator-story-database.json +0 -104
- package/cli/agents/validator-story-database.md +0 -152
- package/cli/agents/validator-story-developer.json +0 -104
- package/cli/agents/validator-story-developer.md +0 -152
- package/cli/agents/validator-story-devops.json +0 -104
- package/cli/agents/validator-story-devops.md +0 -152
- package/cli/agents/validator-story-frontend.json +0 -104
- package/cli/agents/validator-story-frontend.md +0 -152
- package/cli/agents/validator-story-mobile.json +0 -104
- package/cli/agents/validator-story-mobile.md +0 -152
- package/cli/agents/validator-story-qa.json +0 -104
- package/cli/agents/validator-story-qa.md +0 -152
- package/cli/agents/validator-story-security.json +0 -104
- package/cli/agents/validator-story-security.md +0 -152
- package/cli/agents/validator-story-solution-architect.json +0 -104
- package/cli/agents/validator-story-solution-architect.md +0 -152
- package/cli/agents/validator-story-test-architect.json +0 -104
- package/cli/agents/validator-story-test-architect.md +0 -152
- package/cli/agents/validator-story-ui.json +0 -104
- package/cli/agents/validator-story-ui.md +0 -152
- package/cli/agents/validator-story-ux.json +0 -104
- package/cli/agents/validator-story-ux.md +0 -152
- package/kanban/client/dist/assets/index-CiD8PS2e.js +0 -306
- package/kanban/client/dist/assets/index-nLh0m82Q.css +0 -1
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { X, RotateCcw, ChevronDown, ChevronRight, Plus, Trash2, Info } from 'lucide-react';
|
|
3
|
+
import { getCheckContent, saveCheckContent, resetCheck } from '../../lib/api';
|
|
4
|
+
|
|
5
|
+
// ── Severity config ──────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
const SEVERITY_OPTIONS = ['critical', 'major', 'minor'];
|
|
8
|
+
const SEVERITY_STYLE = {
|
|
9
|
+
critical: 'bg-red-100 text-red-800 border-red-200',
|
|
10
|
+
major: 'bg-amber-100 text-amber-800 border-amber-200',
|
|
11
|
+
minor: 'bg-slate-100 text-slate-600 border-slate-200',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// ── Intro / help panel ───────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
function HelpPanel({ isCrossRef }) {
|
|
17
|
+
const [open, setOpen] = useState(false);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="mb-2">
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
onClick={() => setOpen(!open)}
|
|
24
|
+
className="flex items-center gap-1.5 text-[11px] text-blue-600 hover:text-blue-800 transition-colors"
|
|
25
|
+
>
|
|
26
|
+
<Info className="w-3.5 h-3.5" />
|
|
27
|
+
{open ? 'Hide guide' : 'How micro-checks work & field reference'}
|
|
28
|
+
</button>
|
|
29
|
+
{open && (
|
|
30
|
+
<div className="mt-2 text-[11px] leading-relaxed text-slate-600 bg-blue-50/60 border border-blue-100 rounded-lg px-4 py-3 space-y-3">
|
|
31
|
+
<div>
|
|
32
|
+
<p className="font-semibold text-slate-700 mb-1">How micro-checks are used in ceremonies</p>
|
|
33
|
+
<p>
|
|
34
|
+
During Sprint Planning validation, each epic and story is evaluated by
|
|
35
|
+
hundreds of small, independent checks instead of a single monolithic LLM call.
|
|
36
|
+
{isCrossRef
|
|
37
|
+
? ' Cross-reference checks (Tier 2) run after all domain checks complete. They verify consistency across perspectives — for example, that security requirements match API endpoint access controls, or that database PII fields align with privacy policies.'
|
|
38
|
+
: ' Domain checks (Tier 1) run first, in parallel. Each check makes 1–2 short LLM calls: an optional applicability gate, then a YES/NO quality question. Failed checks are scored deterministically and critical/major failures trigger atomic auto-fixes (Tier 3).'}
|
|
39
|
+
</p>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div>
|
|
43
|
+
<p className="font-semibold text-slate-700 mb-1">Severity levels</p>
|
|
44
|
+
<ul className="list-none space-y-0.5 ml-1">
|
|
45
|
+
<li><span className="font-mono font-semibold text-red-700">critical</span> — Failure blocks the score below 70. A single critical failure caps the score at 60; each additional one drops it by 10. Auto-fix is always attempted.</li>
|
|
46
|
+
<li><span className="font-mono font-semibold text-amber-700">major</span> — Failure caps the score between 70–89. Each major failure reduces the cap by 5. Auto-fix is attempted.</li>
|
|
47
|
+
<li><span className="font-mono font-semibold text-slate-500">minor</span> — Only minor failures allow scores of 95–100. Minor failures are reported but not auto-fixed.</li>
|
|
48
|
+
</ul>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div>
|
|
52
|
+
<p className="font-semibold text-slate-700 mb-1">Field reference</p>
|
|
53
|
+
<dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 ml-1">
|
|
54
|
+
<dt className="font-mono text-slate-700">severity</dt>
|
|
55
|
+
<dd>How much a failure impacts the score (see above).</dd>
|
|
56
|
+
<dt className="font-mono text-slate-700">category</dt>
|
|
57
|
+
<dd>Grouping label for reporting (e.g. "security", "data-modeling"). Helps cluster related failures.</dd>
|
|
58
|
+
{!isCrossRef && <>
|
|
59
|
+
<dt className="font-mono text-slate-700">applicabilityQuestion</dt>
|
|
60
|
+
<dd>Optional gate question asked before the main check. If the LLM answers NO, the check is skipped as not applicable. Leave empty to always run. Example: "Does this epic involve user authentication?"</dd>
|
|
61
|
+
<dt className="font-mono text-slate-700">universal</dt>
|
|
62
|
+
<dd>If true, the applicability gate is skipped and the check always runs. Useful for checks that apply to every epic/story regardless of domain.</dd>
|
|
63
|
+
</>}
|
|
64
|
+
<dt className="font-mono text-slate-700">question</dt>
|
|
65
|
+
<dd>The YES/NO quality question sent to the LLM. A YES answer means the check passes. Write it so that YES = good quality. Example: "Does this story include acceptance criteria that cover error scenarios?"</dd>
|
|
66
|
+
<dt className="font-mono text-slate-700">failDescription</dt>
|
|
67
|
+
<dd>Human-readable explanation shown when the check fails. Describes what quality gap was detected.</dd>
|
|
68
|
+
<dt className="font-mono text-slate-700">failSuggestion</dt>
|
|
69
|
+
<dd>Actionable guidance for the auto-fixer (or a human) on how to address the failure. Be specific — this drives the Tier 3 atomic fix prompt.</dd>
|
|
70
|
+
{isCrossRef && <>
|
|
71
|
+
<dt className="font-mono text-slate-700">perspectives</dt>
|
|
72
|
+
<dd>Which domain perspectives this cross-reference check bridges (e.g. ["security", "api"]). Read-only — set in the JSON directly.</dd>
|
|
73
|
+
<dt className="font-mono text-slate-700">dependsOn</dt>
|
|
74
|
+
<dd>Tier 1 check IDs whose evidence this check needs. The cross-ref check only runs if all dependencies have results. Read-only — set in the JSON directly.</dd>
|
|
75
|
+
</>}
|
|
76
|
+
</dl>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<p className="text-[10px] text-slate-400 italic">
|
|
80
|
+
Customized checks are saved to <span className="font-mono">.avc/customized-agents/checks/</span> and
|
|
81
|
+
override the built-in defaults. Use "Reset to default" to revert.
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Field editor components ──────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
function TextField({ label, value, onChange, placeholder, rows = 2 }) {
|
|
92
|
+
return (
|
|
93
|
+
<label className="block">
|
|
94
|
+
<span className="text-[11px] font-medium text-slate-500 uppercase tracking-wide">{label}</span>
|
|
95
|
+
<textarea
|
|
96
|
+
value={value || ''}
|
|
97
|
+
onChange={e => onChange(e.target.value)}
|
|
98
|
+
placeholder={placeholder}
|
|
99
|
+
rows={rows}
|
|
100
|
+
className="mt-0.5 w-full text-xs text-slate-800 border border-slate-200 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400 resize-none leading-relaxed"
|
|
101
|
+
/>
|
|
102
|
+
</label>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function SeveritySelect({ value, onChange }) {
|
|
107
|
+
return (
|
|
108
|
+
<label className="block">
|
|
109
|
+
<span className="text-[11px] font-medium text-slate-500 uppercase tracking-wide">Severity</span>
|
|
110
|
+
<select
|
|
111
|
+
value={value || 'major'}
|
|
112
|
+
onChange={e => onChange(e.target.value)}
|
|
113
|
+
className="mt-0.5 w-full text-xs border border-slate-200 rounded-md px-2 py-1.5 bg-white text-slate-800 focus:outline-none focus:ring-1 focus:ring-blue-400"
|
|
114
|
+
>
|
|
115
|
+
{SEVERITY_OPTIONS.map(s => (
|
|
116
|
+
<option key={s} value={s}>{s.charAt(0).toUpperCase() + s.slice(1)}</option>
|
|
117
|
+
))}
|
|
118
|
+
</select>
|
|
119
|
+
</label>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function InlineField({ label, value }) {
|
|
124
|
+
return (
|
|
125
|
+
<span className="text-[10px] text-slate-400">
|
|
126
|
+
<span className="font-medium">{label}:</span> <span className="font-mono text-slate-500">{value}</span>
|
|
127
|
+
</span>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Single check card ────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
function CheckCard({ check, index, isCrossRef, expanded, onToggle, onChange, onDelete }) {
|
|
134
|
+
const sev = SEVERITY_STYLE[check.severity] || SEVERITY_STYLE.minor;
|
|
135
|
+
|
|
136
|
+
const update = (field, value) => {
|
|
137
|
+
onChange(index, { ...check, [field]: value });
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Truncate question for collapsed header
|
|
141
|
+
const shortQuestion = (check.question || '').length > 90
|
|
142
|
+
? check.question.slice(0, 90) + '…'
|
|
143
|
+
: (check.question || '(no question)');
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div className={`border rounded-lg overflow-hidden ${expanded ? 'border-blue-200 shadow-sm' : 'border-slate-200'}`}>
|
|
147
|
+
{/* Header — always visible */}
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
onClick={onToggle}
|
|
151
|
+
className={`w-full flex items-center gap-2 px-3 py-2 text-left transition-colors ${expanded ? 'bg-blue-50/50' : 'bg-white hover:bg-slate-50'}`}
|
|
152
|
+
>
|
|
153
|
+
{expanded
|
|
154
|
+
? <ChevronDown className="w-3 h-3 text-slate-400 flex-shrink-0" />
|
|
155
|
+
: <ChevronRight className="w-3 h-3 text-slate-400 flex-shrink-0" />
|
|
156
|
+
}
|
|
157
|
+
<span className={`flex-shrink-0 text-[10px] font-semibold px-1.5 py-0.5 rounded border ${sev}`}>
|
|
158
|
+
{check.severity || 'major'}
|
|
159
|
+
</span>
|
|
160
|
+
<span className="flex-shrink-0 text-[10px] font-mono text-slate-400">{check.id}</span>
|
|
161
|
+
<span className="flex-1 text-xs text-slate-600 truncate min-w-0">{shortQuestion}</span>
|
|
162
|
+
</button>
|
|
163
|
+
|
|
164
|
+
{/* Expanded form */}
|
|
165
|
+
{expanded && (
|
|
166
|
+
<div className="px-3 pb-3 pt-1 bg-white border-t border-slate-100">
|
|
167
|
+
{/* Read-only metadata row */}
|
|
168
|
+
<div className="flex items-center gap-3 mb-2 flex-wrap">
|
|
169
|
+
<InlineField label="id" value={check.id} />
|
|
170
|
+
{check.category && <InlineField label="category" value={check.category} />}
|
|
171
|
+
{isCrossRef && check.perspectives && (
|
|
172
|
+
<InlineField label="perspectives" value={check.perspectives.join(', ')} />
|
|
173
|
+
)}
|
|
174
|
+
{isCrossRef && check.dependsOn && (
|
|
175
|
+
<InlineField label="dependsOn" value={check.dependsOn.join(', ')} />
|
|
176
|
+
)}
|
|
177
|
+
{!isCrossRef && check.universal !== undefined && (
|
|
178
|
+
<InlineField label="universal" value={check.universal ? 'yes' : 'no'} />
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div className="grid grid-cols-[1fr_auto] gap-x-3 gap-y-2">
|
|
183
|
+
<div className="col-span-2 sm:col-span-1">
|
|
184
|
+
<SeveritySelect value={check.severity} onChange={v => update('severity', v)} />
|
|
185
|
+
</div>
|
|
186
|
+
<div className="col-span-2 sm:col-span-1">
|
|
187
|
+
<label className="block">
|
|
188
|
+
<span className="text-[11px] font-medium text-slate-500 uppercase tracking-wide">Category</span>
|
|
189
|
+
<input
|
|
190
|
+
type="text"
|
|
191
|
+
value={check.category || ''}
|
|
192
|
+
onChange={e => update('category', e.target.value)}
|
|
193
|
+
className="mt-0.5 w-full text-xs border border-slate-200 rounded-md px-2.5 py-1.5 text-slate-800 focus:outline-none focus:ring-1 focus:ring-blue-400"
|
|
194
|
+
/>
|
|
195
|
+
</label>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<div className="mt-2 flex flex-col gap-2">
|
|
200
|
+
{!isCrossRef && !check.universal && (
|
|
201
|
+
<TextField
|
|
202
|
+
label="Applicability Question"
|
|
203
|
+
value={check.applicabilityQuestion}
|
|
204
|
+
onChange={v => update('applicabilityQuestion', v)}
|
|
205
|
+
placeholder="When should this check run? (leave empty = always)"
|
|
206
|
+
rows={2}
|
|
207
|
+
/>
|
|
208
|
+
)}
|
|
209
|
+
<TextField
|
|
210
|
+
label="Question"
|
|
211
|
+
value={check.question}
|
|
212
|
+
onChange={v => update('question', v)}
|
|
213
|
+
placeholder="The YES/NO quality question asked to the LLM"
|
|
214
|
+
rows={3}
|
|
215
|
+
/>
|
|
216
|
+
<TextField
|
|
217
|
+
label="Fail Description"
|
|
218
|
+
value={check.failDescription}
|
|
219
|
+
onChange={v => update('failDescription', v)}
|
|
220
|
+
placeholder="What's wrong when this check fails"
|
|
221
|
+
rows={2}
|
|
222
|
+
/>
|
|
223
|
+
<TextField
|
|
224
|
+
label="Fail Suggestion"
|
|
225
|
+
value={check.failSuggestion}
|
|
226
|
+
onChange={v => update('failSuggestion', v)}
|
|
227
|
+
placeholder="How to fix the failure"
|
|
228
|
+
rows={2}
|
|
229
|
+
/>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
{/* Delete button */}
|
|
233
|
+
<div className="mt-3 flex justify-end">
|
|
234
|
+
<button
|
|
235
|
+
type="button"
|
|
236
|
+
onClick={() => onDelete(index)}
|
|
237
|
+
className="flex items-center gap-1 text-[11px] text-red-500 hover:text-red-700 transition-colors"
|
|
238
|
+
>
|
|
239
|
+
<Trash2 className="w-3 h-3" />
|
|
240
|
+
Remove check
|
|
241
|
+
</button>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
</div>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Main popup ───────────────────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
export function CheckEditorPopup({ scope, perspective, onClose, onSaved, onReset }) {
|
|
252
|
+
const [fileData, setFileData] = useState(null); // raw API response
|
|
253
|
+
const [parsed, setParsed] = useState(null); // full parsed object
|
|
254
|
+
const [checks, setChecks] = useState([]); // the checks array being edited
|
|
255
|
+
const [expandedIdx, setExpandedIdx] = useState(null); // which card is expanded
|
|
256
|
+
const [loading, setLoading] = useState(true);
|
|
257
|
+
const [saving, setSaving] = useState(false);
|
|
258
|
+
const [saved, setSaved] = useState(false);
|
|
259
|
+
const [error, setError] = useState(null);
|
|
260
|
+
const [pendingReset, setPendingReset] = useState(false);
|
|
261
|
+
|
|
262
|
+
const isCrossRef = scope === 'cross-refs';
|
|
263
|
+
const displayName = isCrossRef
|
|
264
|
+
? `cross-refs/${perspective}.json`
|
|
265
|
+
: `${scope}/${perspective}.json`;
|
|
266
|
+
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
setLoading(true);
|
|
269
|
+
setError(null);
|
|
270
|
+
setPendingReset(false);
|
|
271
|
+
setExpandedIdx(null);
|
|
272
|
+
getCheckContent(scope, perspective)
|
|
273
|
+
.then(d => {
|
|
274
|
+
setFileData(d);
|
|
275
|
+
try {
|
|
276
|
+
const obj = JSON.parse(d.content);
|
|
277
|
+
setParsed(obj);
|
|
278
|
+
setChecks(JSON.parse(JSON.stringify(obj.checks || [])));
|
|
279
|
+
} catch {
|
|
280
|
+
setError('Failed to parse check file');
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
.catch(err => setError(err.message))
|
|
284
|
+
.finally(() => setLoading(false));
|
|
285
|
+
}, [scope, perspective]);
|
|
286
|
+
|
|
287
|
+
// Dirty detection: compare current checks to original
|
|
288
|
+
const isDirty = (() => {
|
|
289
|
+
if (!fileData) return false;
|
|
290
|
+
try {
|
|
291
|
+
const original = JSON.parse(fileData.content);
|
|
292
|
+
return JSON.stringify(checks) !== JSON.stringify(original.checks || []);
|
|
293
|
+
} catch {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
})();
|
|
297
|
+
|
|
298
|
+
const handleCheckChange = (index, updatedCheck) => {
|
|
299
|
+
setChecks(prev => prev.map((c, i) => i === index ? updatedCheck : c));
|
|
300
|
+
setPendingReset(false);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const handleDelete = (index) => {
|
|
304
|
+
setChecks(prev => prev.filter((_, i) => i !== index));
|
|
305
|
+
if (expandedIdx === index) setExpandedIdx(null);
|
|
306
|
+
else if (expandedIdx > index) setExpandedIdx(expandedIdx - 1);
|
|
307
|
+
setPendingReset(false);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const handleAdd = () => {
|
|
311
|
+
const prefix = isCrossRef ? 'xref' : (scope === 'epic' ? perspective.slice(0, 3) + '-epic' : perspective.slice(0, 3) + '-story');
|
|
312
|
+
const newId = `${prefix}-${String(checks.length + 1).padStart(2, '0')}`;
|
|
313
|
+
const newCheck = isCrossRef
|
|
314
|
+
? { id: newId, tier: 2, perspectives: [], severity: 'major', category: 'consistency', dependsOn: [], question: '', failDescription: '', failSuggestion: '' }
|
|
315
|
+
: { id: newId, tier: 1, perspective, severity: 'major', category: '', universal: false, applicabilityQuestion: '', question: '', failDescription: '', failSuggestion: '' };
|
|
316
|
+
setChecks(prev => [...prev, newCheck]);
|
|
317
|
+
setExpandedIdx(checks.length);
|
|
318
|
+
setPendingReset(false);
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const handleSave = async () => {
|
|
322
|
+
if (!fileData) return;
|
|
323
|
+
setSaving(true);
|
|
324
|
+
setError(null);
|
|
325
|
+
try {
|
|
326
|
+
if (pendingReset) {
|
|
327
|
+
await resetCheck(scope, perspective);
|
|
328
|
+
const obj = JSON.parse(fileData.defaultContent);
|
|
329
|
+
setParsed(obj);
|
|
330
|
+
setChecks(JSON.parse(JSON.stringify(obj.checks || [])));
|
|
331
|
+
setFileData(prev => ({ ...prev, content: prev.defaultContent, isCustomized: false }));
|
|
332
|
+
setPendingReset(false);
|
|
333
|
+
onReset?.();
|
|
334
|
+
} else {
|
|
335
|
+
// Rebuild full JSON preserving top-level fields
|
|
336
|
+
const output = { ...parsed, checks };
|
|
337
|
+
const content = JSON.stringify(output, null, 2);
|
|
338
|
+
await saveCheckContent(scope, perspective, content);
|
|
339
|
+
setFileData(prev => ({ ...prev, content, isCustomized: true }));
|
|
340
|
+
onSaved?.();
|
|
341
|
+
}
|
|
342
|
+
setSaved(true);
|
|
343
|
+
setTimeout(() => setSaved(false), 2000);
|
|
344
|
+
} catch (err) {
|
|
345
|
+
setError(err.message);
|
|
346
|
+
} finally {
|
|
347
|
+
setSaving(false);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const handleReset = () => {
|
|
352
|
+
if (!fileData?.defaultContent) return;
|
|
353
|
+
try {
|
|
354
|
+
const obj = JSON.parse(fileData.defaultContent);
|
|
355
|
+
setChecks(JSON.parse(JSON.stringify(obj.checks || [])));
|
|
356
|
+
} catch {}
|
|
357
|
+
setPendingReset(true);
|
|
358
|
+
setExpandedIdx(null);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const canReset = fileData?.isCustomized && !pendingReset;
|
|
362
|
+
|
|
363
|
+
// Severity summary
|
|
364
|
+
const counts = { critical: 0, major: 0, minor: 0 };
|
|
365
|
+
checks.forEach(c => { if (counts[c.severity] !== undefined) counts[c.severity]++; });
|
|
366
|
+
|
|
367
|
+
return (
|
|
368
|
+
<div
|
|
369
|
+
className="fixed inset-0 z-[90] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
|
|
370
|
+
onClick={e => { if (e.target === e.currentTarget) onClose(); }}
|
|
371
|
+
>
|
|
372
|
+
<div
|
|
373
|
+
className="w-full max-w-3xl bg-white rounded-2xl shadow-2xl flex flex-col overflow-hidden"
|
|
374
|
+
style={{ height: '85vh' }}
|
|
375
|
+
>
|
|
376
|
+
{/* Header */}
|
|
377
|
+
<div className="flex items-center justify-between px-5 py-3 border-b border-slate-100 flex-shrink-0">
|
|
378
|
+
<div className="flex items-center gap-2 min-w-0 flex-wrap">
|
|
379
|
+
<span className="text-sm font-mono font-medium text-slate-700 truncate">
|
|
380
|
+
{displayName}
|
|
381
|
+
</span>
|
|
382
|
+
<span className="flex-shrink-0 text-[10px] font-medium px-1.5 py-0.5 rounded bg-blue-50 text-blue-600">
|
|
383
|
+
{checks.length} checks
|
|
384
|
+
</span>
|
|
385
|
+
{counts.critical > 0 && (
|
|
386
|
+
<span className={`flex-shrink-0 text-[10px] font-medium px-1.5 py-0.5 rounded border ${SEVERITY_STYLE.critical}`}>
|
|
387
|
+
{counts.critical} critical
|
|
388
|
+
</span>
|
|
389
|
+
)}
|
|
390
|
+
{counts.major > 0 && (
|
|
391
|
+
<span className={`flex-shrink-0 text-[10px] font-medium px-1.5 py-0.5 rounded border ${SEVERITY_STYLE.major}`}>
|
|
392
|
+
{counts.major} major
|
|
393
|
+
</span>
|
|
394
|
+
)}
|
|
395
|
+
{counts.minor > 0 && (
|
|
396
|
+
<span className={`flex-shrink-0 text-[10px] font-medium px-1.5 py-0.5 rounded border ${SEVERITY_STYLE.minor}`}>
|
|
397
|
+
{counts.minor} minor
|
|
398
|
+
</span>
|
|
399
|
+
)}
|
|
400
|
+
{fileData?.isCustomized && !pendingReset && (
|
|
401
|
+
<span className="flex-shrink-0 text-[10px] font-medium px-1.5 py-0.5 rounded bg-amber-100 text-amber-700">
|
|
402
|
+
Custom
|
|
403
|
+
</span>
|
|
404
|
+
)}
|
|
405
|
+
{pendingReset && (
|
|
406
|
+
<span className="flex-shrink-0 text-[10px] font-medium px-1.5 py-0.5 rounded bg-blue-100 text-blue-700">
|
|
407
|
+
Reset pending — save to apply
|
|
408
|
+
</span>
|
|
409
|
+
)}
|
|
410
|
+
</div>
|
|
411
|
+
<button
|
|
412
|
+
type="button"
|
|
413
|
+
onClick={onClose}
|
|
414
|
+
className="text-slate-400 hover:text-slate-600 transition-colors ml-4 flex-shrink-0"
|
|
415
|
+
aria-label="Close"
|
|
416
|
+
>
|
|
417
|
+
<X className="w-4 h-4" />
|
|
418
|
+
</button>
|
|
419
|
+
</div>
|
|
420
|
+
|
|
421
|
+
{/* Body */}
|
|
422
|
+
{loading ? (
|
|
423
|
+
<div className="flex-1 flex items-center justify-center text-sm text-slate-400">
|
|
424
|
+
<span className="w-4 h-4 border border-slate-300 border-t-slate-600 rounded-full animate-spin mr-2" />
|
|
425
|
+
Loading…
|
|
426
|
+
</div>
|
|
427
|
+
) : error && !fileData ? (
|
|
428
|
+
<div className="flex-1 flex items-center justify-center text-sm text-red-500 px-6 text-center">
|
|
429
|
+
{error}
|
|
430
|
+
</div>
|
|
431
|
+
) : (
|
|
432
|
+
<div className="flex-1 overflow-y-auto min-h-0 px-4 py-3">
|
|
433
|
+
<div className="flex flex-col gap-1.5">
|
|
434
|
+
<HelpPanel isCrossRef={isCrossRef} />
|
|
435
|
+
{checks.map((check, i) => (
|
|
436
|
+
<CheckCard
|
|
437
|
+
key={check.id || i}
|
|
438
|
+
check={check}
|
|
439
|
+
index={i}
|
|
440
|
+
isCrossRef={isCrossRef}
|
|
441
|
+
expanded={expandedIdx === i}
|
|
442
|
+
onToggle={() => setExpandedIdx(expandedIdx === i ? null : i)}
|
|
443
|
+
onChange={handleCheckChange}
|
|
444
|
+
onDelete={handleDelete}
|
|
445
|
+
/>
|
|
446
|
+
))}
|
|
447
|
+
|
|
448
|
+
{/* Add check button */}
|
|
449
|
+
<button
|
|
450
|
+
type="button"
|
|
451
|
+
onClick={handleAdd}
|
|
452
|
+
className="flex items-center justify-center gap-1.5 py-2 border-2 border-dashed border-slate-200 rounded-lg text-xs text-slate-400 hover:text-blue-600 hover:border-blue-300 transition-colors"
|
|
453
|
+
>
|
|
454
|
+
<Plus className="w-3.5 h-3.5" />
|
|
455
|
+
Add check
|
|
456
|
+
</button>
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
)}
|
|
460
|
+
|
|
461
|
+
{/* Footer */}
|
|
462
|
+
<div className="flex items-center justify-between px-5 py-3 border-t border-slate-100 flex-shrink-0">
|
|
463
|
+
<div>
|
|
464
|
+
{error && !loading && (
|
|
465
|
+
<p className="text-xs text-red-600">{error}</p>
|
|
466
|
+
)}
|
|
467
|
+
{saved && (
|
|
468
|
+
<p className="text-xs text-green-600 font-medium">Saved</p>
|
|
469
|
+
)}
|
|
470
|
+
</div>
|
|
471
|
+
<div className="flex items-center gap-2">
|
|
472
|
+
<button
|
|
473
|
+
type="button"
|
|
474
|
+
onClick={handleReset}
|
|
475
|
+
disabled={!canReset}
|
|
476
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-slate-500 hover:text-amber-600 transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
477
|
+
title={canReset ? 'Revert to built-in defaults' : 'Only available for customized checks'}
|
|
478
|
+
>
|
|
479
|
+
<RotateCcw className="w-3 h-3" />
|
|
480
|
+
Reset to default
|
|
481
|
+
</button>
|
|
482
|
+
<button
|
|
483
|
+
type="button"
|
|
484
|
+
onClick={onClose}
|
|
485
|
+
className="px-3 py-1.5 text-xs font-medium text-slate-500 hover:text-slate-700 transition-colors"
|
|
486
|
+
>
|
|
487
|
+
Cancel
|
|
488
|
+
</button>
|
|
489
|
+
<button
|
|
490
|
+
type="button"
|
|
491
|
+
onClick={handleSave}
|
|
492
|
+
disabled={(!isDirty && !pendingReset) || saving}
|
|
493
|
+
className="px-3 py-1.5 text-xs font-medium bg-slate-900 text-white rounded-md hover:bg-slate-700 transition-colors disabled:opacity-40"
|
|
494
|
+
>
|
|
495
|
+
{saving ? (
|
|
496
|
+
<span className="inline-flex items-center gap-1">
|
|
497
|
+
<span className="w-3 h-3 border border-white/40 border-t-white rounded-full animate-spin" />
|
|
498
|
+
Saving
|
|
499
|
+
</span>
|
|
500
|
+
) : pendingReset ? 'Save & Reset' : 'Save'}
|
|
501
|
+
</button>
|
|
502
|
+
</div>
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
);
|
|
507
|
+
}
|
|
@@ -3,8 +3,9 @@ import { saveCostThresholds } from '../../lib/api';
|
|
|
3
3
|
|
|
4
4
|
const CEREMONIES = [
|
|
5
5
|
{ key: 'sponsor-call', label: 'Sponsor Call', desc: 'Wizard to define project mission, scope, and architecture' },
|
|
6
|
-
{ key: 'sprint-planning', label: 'Sprint Planning', desc: '
|
|
7
|
-
{ key: 'seed', label: 'Seed', desc: '
|
|
6
|
+
{ key: 'sprint-planning', label: 'Sprint Planning', desc: 'Decomposes project scope into epics, stories, and contexts' },
|
|
7
|
+
{ key: 'seed', label: 'Seed', desc: 'Decomposes stories into implementable tasks and subtasks' },
|
|
8
|
+
{ key: 'run', label: 'Run', desc: 'Implements task code in worktree with AI-powered generation and validation' },
|
|
8
9
|
];
|
|
9
10
|
|
|
10
11
|
function initState(costThresholds) {
|
|
@@ -2,10 +2,20 @@ import { useState } from 'react';
|
|
|
2
2
|
import { ExternalLink } from 'lucide-react';
|
|
3
3
|
import { saveModelPricing } from '../../lib/api';
|
|
4
4
|
|
|
5
|
+
const PROVIDERS = ['claude', 'gemini', 'openai', 'xiaomi'];
|
|
6
|
+
const PROVIDER_LABELS = { claude: 'Claude', gemini: 'Gemini', openai: 'OpenAI', xiaomi: 'Xiaomi MiMo' };
|
|
7
|
+
const PROVIDER_TAB_COLORS = {
|
|
8
|
+
claude: { active: 'border-orange-500 text-orange-700', badge: 'bg-orange-50 text-orange-700 border-orange-200' },
|
|
9
|
+
gemini: { active: 'border-blue-500 text-blue-700', badge: 'bg-blue-50 text-blue-700 border-blue-200' },
|
|
10
|
+
openai: { active: 'border-green-500 text-green-700', badge: 'bg-green-50 text-green-700 border-green-200' },
|
|
11
|
+
xiaomi: { active: 'border-purple-500 text-purple-700', badge: 'bg-purple-50 text-purple-700 border-purple-200' },
|
|
12
|
+
};
|
|
13
|
+
|
|
5
14
|
const PROVIDER_COLORS = {
|
|
6
15
|
claude: 'bg-orange-50 text-orange-700 border-orange-200',
|
|
7
16
|
gemini: 'bg-blue-50 text-blue-700 border-blue-200',
|
|
8
17
|
openai: 'bg-green-50 text-green-700 border-green-200',
|
|
18
|
+
xiaomi: 'bg-purple-50 text-purple-700 border-purple-200',
|
|
9
19
|
};
|
|
10
20
|
|
|
11
21
|
const UNIT_OPTIONS = [
|
|
@@ -17,8 +27,9 @@ function initState(models) {
|
|
|
17
27
|
const state = {};
|
|
18
28
|
for (const [modelId, info] of Object.entries(models)) {
|
|
19
29
|
state[modelId] = {
|
|
20
|
-
input: String(info.pricing?.input
|
|
21
|
-
|
|
30
|
+
input: String(info.pricing?.input ?? ''),
|
|
31
|
+
inputCached: String(info.pricing?.inputCached ?? ''),
|
|
32
|
+
output: String(info.pricing?.output ?? ''),
|
|
22
33
|
unit: info.pricing?.unit ?? 'million',
|
|
23
34
|
source: info.pricing?.source ?? '',
|
|
24
35
|
lastUpdated: info.pricing?.lastUpdated ?? '',
|
|
@@ -38,7 +49,15 @@ export function ModelPricingTab({ settings, onSaved }) {
|
|
|
38
49
|
const [pricing, setPricing] = useState(() => initState(models));
|
|
39
50
|
const [status, setStatus] = useState(null); // null | 'saving' | 'saved' | 'error'
|
|
40
51
|
|
|
41
|
-
|
|
52
|
+
// Determine which providers actually have models, preserve stable order
|
|
53
|
+
const availableProviders = PROVIDERS.filter(p =>
|
|
54
|
+
Object.values(models).some(m => m.provider === p)
|
|
55
|
+
);
|
|
56
|
+
const [activeProvider, setActiveProvider] = useState(() => availableProviders[0] || 'claude');
|
|
57
|
+
|
|
58
|
+
const modelEntries = Object.entries(models).filter(
|
|
59
|
+
([, info]) => info.provider === activeProvider
|
|
60
|
+
);
|
|
42
61
|
|
|
43
62
|
const update = (modelId, field, value) => {
|
|
44
63
|
setPricing((prev) => ({
|
|
@@ -55,10 +74,11 @@ export function ModelPricingTab({ settings, onSaved }) {
|
|
|
55
74
|
for (const [modelId, p] of Object.entries(pricing)) {
|
|
56
75
|
payload[modelId] = {
|
|
57
76
|
pricing: {
|
|
58
|
-
input:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
77
|
+
input: parseFloat(p.input) || 0,
|
|
78
|
+
inputCached: parseFloat(p.inputCached) || 0,
|
|
79
|
+
output: parseFloat(p.output) || 0,
|
|
80
|
+
unit: p.unit,
|
|
81
|
+
source: p.source,
|
|
62
82
|
},
|
|
63
83
|
};
|
|
64
84
|
}
|
|
@@ -89,6 +109,36 @@ export function ModelPricingTab({ settings, onSaved }) {
|
|
|
89
109
|
to estimate LLM spend. Prices are in <strong>USD</strong>.
|
|
90
110
|
</p>
|
|
91
111
|
|
|
112
|
+
{/* Provider tabs */}
|
|
113
|
+
{availableProviders.length > 1 && (
|
|
114
|
+
<div className="flex border-b border-slate-200 -mx-5 px-5">
|
|
115
|
+
{availableProviders.map((p) => {
|
|
116
|
+
const colors = PROVIDER_TAB_COLORS[p] || { active: 'border-slate-900 text-slate-900' };
|
|
117
|
+
const isActive = p === activeProvider;
|
|
118
|
+
const count = Object.values(models).filter(m => m.provider === p).length;
|
|
119
|
+
return (
|
|
120
|
+
<button
|
|
121
|
+
key={p}
|
|
122
|
+
type="button"
|
|
123
|
+
onClick={() => setActiveProvider(p)}
|
|
124
|
+
className={`px-4 py-2.5 text-sm font-medium border-b-2 -mb-px transition-colors ${
|
|
125
|
+
isActive
|
|
126
|
+
? colors.active
|
|
127
|
+
: 'border-transparent text-slate-500 hover:text-slate-700'
|
|
128
|
+
}`}
|
|
129
|
+
>
|
|
130
|
+
{PROVIDER_LABELS[p] || p}
|
|
131
|
+
<span className={`ml-1.5 text-xs px-1.5 py-0.5 rounded-full border ${
|
|
132
|
+
isActive ? colors.badge : 'bg-slate-100 text-slate-400 border-slate-200'
|
|
133
|
+
}`}>
|
|
134
|
+
{count}
|
|
135
|
+
</span>
|
|
136
|
+
</button>
|
|
137
|
+
);
|
|
138
|
+
})}
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
|
|
92
142
|
<div className="flex flex-col gap-2">
|
|
93
143
|
{modelEntries.map(([modelId, info]) => {
|
|
94
144
|
const p = pricing[modelId] ?? { input: '', output: '', unit: 'million' };
|
|
@@ -140,6 +190,21 @@ export function ModelPricingTab({ settings, onSaved }) {
|
|
|
140
190
|
))}
|
|
141
191
|
</select>
|
|
142
192
|
|
|
193
|
+
{/* Cache Read row */}
|
|
194
|
+
<label className="text-slate-600 font-medium">Cache Read</label>
|
|
195
|
+
<input
|
|
196
|
+
type="number"
|
|
197
|
+
min="0"
|
|
198
|
+
step="0.01"
|
|
199
|
+
value={p.inputCached}
|
|
200
|
+
onChange={(e) => update(modelId, 'inputCached', e.target.value)}
|
|
201
|
+
placeholder="0.00"
|
|
202
|
+
className="rounded-md border border-slate-300 px-2 py-1.5 text-slate-900 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full max-w-[120px]"
|
|
203
|
+
/>
|
|
204
|
+
<span className="text-slate-400 text-xs">
|
|
205
|
+
{UNIT_OPTIONS.find((o) => o.value === p.unit)?.label} · discounted
|
|
206
|
+
</span>
|
|
207
|
+
|
|
143
208
|
{/* Output row */}
|
|
144
209
|
<label className="text-slate-600 font-medium">Output</label>
|
|
145
210
|
<input
|