import { useState, useMemo, useEffect, useCallback } from 'react'; import { GitCommitDrawer } from '../components/GitCommitDrawer'; import { Button, Callout, Divider, Drawer, FormGroup, HTMLSelect, HTMLTable, InputGroup, Intent, NonIdealState, Spinner, Tab, Tabs, Tag, TextArea, Tooltip, EditableText, } from '@blueprintjs/core'; import type { Opc, OpcArtifact, OpcNote, OpcPriority, OpcStatus, OpcType, ArtifactType } from '../types/opc'; import { listOpcs, createOpc, updateOpc, getNextNumber, listNotes, addNote, listArtifacts, createArtifact, updateArtifact, deleteArtifact, getLinkedCommits, getPinnedCommits, pinCommit, unpinCommit, getBranchCoverageForRepo, listGiteaBranches, createGiteaBranch, aiAssist, type LinkedCommit, type PinnedCommit, type BranchCoverage, type GiteaBranch, } from '../api/opcApi'; // -- Label / intent maps ------------------------------------------------------- const TYPE_LABELS: Record = { ChangeOrder: 'Change Order', NonDevTask: 'Non-Dev Task', QaTask: 'QA Task', BusinessRequirement: 'Business Req.', Feature: 'Feature', General: 'General', }; const TYPE_INTENT: Record = { ChangeOrder: Intent.PRIMARY, NonDevTask: Intent.NONE, QaTask: Intent.WARNING, BusinessRequirement: Intent.SUCCESS, Feature: Intent.SUCCESS, General: Intent.NONE, }; const STATUS_INTENT: Record = { New: Intent.PRIMARY, InProgress: Intent.WARNING, InReview: Intent.PRIMARY, Blocked: Intent.DANGER, Closed: Intent.SUCCESS, Cancelled: Intent.NONE, }; const STATUS_LABELS: Record = { New: 'New', InProgress: 'In Progress', InReview: 'In Review', Blocked: 'Blocked', Closed: 'Closed', Cancelled: 'Cancelled', }; const PRIORITY_INTENT: Record = { Low: Intent.NONE, Medium: Intent.WARNING, High: Intent.DANGER, Critical: Intent.DANGER, }; const ARTIFACT_TABS: { type: ArtifactType; label: string; placeholder: string }[] = [ { type: 'BusinessRequirement', label: 'Business Req.', placeholder: 'Document business requirements, JAD/JAR outputs, acceptance criteria...' }, { type: 'Rule', label: 'Rules', placeholder: 'Define rule engine rules, validation logic, business rules...' }, { type: 'Spec', label: 'Spec', placeholder: 'Technical specification — data contracts, API shapes, architecture decisions...' }, { type: 'Documentation', label: 'Docs', placeholder: 'End-user or developer documentation for this change...' }, { type: 'QaTestPath', label: 'QA Test Paths', placeholder: 'Step-by-step QA test scenarios, edge cases, regression checks...' }, ]; // -- SDLC delivery chain ------------------------------------------------------- const SDLC_STAGES: { branch: string; label: string; intent: Intent }[] = [ { branch: 'develop', label: 'Dev', intent: Intent.PRIMARY }, { branch: 'staging', label: 'Staging', intent: Intent.WARNING }, { branch: 'uat', label: 'UAT', intent: Intent.DANGER }, { branch: 'master', label: 'Production', intent: Intent.SUCCESS }, ]; function deriveSdlcSummary(coverage: BranchCoverage[]): { label: string; intent: Intent } | null { for (let i = SDLC_STAGES.length - 1; i >= 0; i--) { const stage = SDLC_STAGES[i]; const hit = coverage.find(c => c.branch === stage.branch); if (hit?.contains) return { label: stage.label, intent: stage.intent }; } return null; } // Aggregate per-repo branch coverage into a single view. // A stage is "reached" only when every repo that recognised at least one hash // reports contains=true for that branch. Repos that recognised no hashes are // excluded from the constraint (they have no code linked to this OPC). const KNOWN_REPOS = ['Clarity', 'OPC', 'Gateway'] as const; type RepoCoverage = { repoKey: string; coverage: BranchCoverage[] }; function aggregateCoverage(perRepo: RepoCoverage[]): BranchCoverage[] { const active = perRepo.filter(r => r.coverage.length > 0); if (active.length === 0) return []; const branches = [...new Set(active.flatMap(r => r.coverage.map(c => c.branch)))]; return branches.map(branch => { const entries = active .map(r => r.coverage.find(c => c.branch === branch)) .filter((c): c is BranchCoverage => c !== undefined); return { branch, contains: entries.length > 0 && entries.every(c => c.contains), tipHash: entries[0]?.tipHash ?? '', isHead: entries.some(c => c.isHead), }; }); } // -- Helpers ------------------------------------------------------------------- function fmtDate(iso: string): string { return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); } function fmtDateTime(iso: string): string { return new Date(iso).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); } // -- AI Assist Box ------------------------------------------------------------- function AiAssistBox({ context, onApply }: { context?: string; onApply: (text: string) => void }) { const [prompt, setPrompt] = useState(''); const [result, setResult] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const run = async () => { if (!prompt.trim()) return; setLoading(true); setError(null); setResult(''); try { const text = await aiAssist(prompt.trim(), context); setResult(text); } catch (e) { setError(String(e)); } finally { setLoading(false); } }; return (
AI Assist
setPrompt(e.target.value)} onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); run(); } }} rightElement={
{error && {error}} {result && (
{result}
)}
); } // -- Artifact panel ------------------------------------------------------------ function ArtifactPanel({ opcId, opcNumber, artifactType, placeholder }: { opcId: string; opcNumber: string; artifactType: ArtifactType; placeholder: string; }) { const [artifacts, setArtifacts] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [editId, setEditId] = useState(null); const [editTitle, setEditTitle] = useState(''); const [editBody, setEditBody] = useState(''); const [adding, setAdding] = useState(false); const load = useCallback(async () => { setLoading(true); try { setArtifacts(await listArtifacts(opcId, artifactType)); } catch { /* API may not be up */ } finally { setLoading(false); } }, [opcId, artifactType]); useEffect(() => { load(); }, [load]); const startAdd = () => { setEditId(null); setEditTitle(''); setEditBody(''); setAdding(true); }; const startEdit = (a: OpcArtifact) => { setAdding(false); setEditId(a.id); setEditTitle(a.title); setEditBody(a.content); }; const cancel = () => { setAdding(false); setEditId(null); }; const save = async () => { setSaving(true); try { if (editId) { const u = await updateArtifact(editId, { artifactType, title: editTitle, content: editBody }); setArtifacts(prev => prev.map(a => a.id === editId ? u : a)); } else { const c = await createArtifact(opcId, { artifactType, title: editTitle, content: editBody }); setArtifacts(prev => [...prev, c]); } cancel(); } catch { /* no-op */ } finally { setSaving(false); } }; const remove = async (id: string) => { await deleteArtifact(id); setArtifacts(prev => prev.filter(a => a.id !== id)); }; const isEditing = adding || editId !== null; if (loading) return
; return (
{!isEditing && ( <> {artifacts.length === 0 ? ( } /> ) : ( <> {artifacts.map(a => (
{a.title || '(untitled)'}
{a.content}
{fmtDate(a.updatedAt)}
))}