@skyhook-io/radar-app 1.14.3 → 1.14.5
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/package.json +1 -1
- package/src/api/client.ts +2 -0
- package/src/api/diagnose.ts +7 -1
- package/src/components/diagnose/AIMarkdown.tsx +8 -3
- package/src/components/diagnose/ActivityTurn.tsx +1 -1
- package/src/components/diagnose/AssessmentCard.tsx +269 -238
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +47 -0
- package/src/components/diagnose/InvestigationEvidencePane.tsx +15 -2
- package/src/components/diagnose/InvestigationEvidenceSections.tsx +1 -1
- package/src/components/diagnose/InvestigationView.tsx +32 -24
- package/src/components/diagnose/investigationCase.test.tsx +83 -0
- package/src/components/diagnose/investigationCase.ts +3 -2
- package/src/components/diagnose/investigationEvidence/adapters/coverage.test.tsx +22 -0
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +8 -2
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +1 -1
- package/src/components/diagnose/investigationEvidence/adapters/listings.ts +2 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +60 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.ts +7 -3
- package/src/components/diagnose/investigationEvidence/bodies/streams.tsx +1 -1
- package/src/components/diagnose/investigationEvidence/observations.ts +10 -5
- package/src/components/diagnose/investigationEvidence/types.ts +1 -1
- package/src/components/diagnose/investigationState.test.ts +2 -2
- package/src/components/diagnose/investigationState.ts +2 -2
- package/src/components/diagnose/investigationStory.ts +1 -1
- package/src/components/diagnose/parts.test.tsx +153 -2
- package/src/components/home/mcpToolCatalog.ts +27 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +194 -63
- package/src/components/rightsizing/empty-state.test.tsx +67 -0
- package/src/components/rightsizing/model.test.ts +118 -1
- package/src/components/rightsizing/model.ts +36 -13
- package/src/components/rightsizing/notices.test.tsx +175 -0
- package/src/components/ui/Markdown.tsx +33 -6
- package/src/components/ui/Omnibar.tsx +82 -69
- package/src/components/workload/WorkloadView.tsx +7 -1
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
STEP_KIND_LABEL,
|
|
36
36
|
assessmentCopyText,
|
|
37
37
|
} from "./assessmentCopy";
|
|
38
|
-
import { AIMarkdown, CopyButton } from "./AIMarkdown";
|
|
38
|
+
import { AIMarkdown, CopyButton, tidyFences } from "./AIMarkdown";
|
|
39
39
|
import { prettyTool } from "./toolCallLabel";
|
|
40
40
|
|
|
41
41
|
export function ResultCard({
|
|
@@ -134,6 +134,7 @@ export function ResultCard({
|
|
|
134
134
|
return section === "actions" ? null : (
|
|
135
135
|
<AllClearCard
|
|
136
136
|
diagnosis={diagnosis}
|
|
137
|
+
explanation={explanation}
|
|
137
138
|
animate={animate}
|
|
138
139
|
showDisclaimer={showDisclaimer}
|
|
139
140
|
coverageLimited={coverageLimited}
|
|
@@ -170,6 +171,9 @@ export function ResultCard({
|
|
|
170
171
|
<>
|
|
171
172
|
<InconclusiveCard
|
|
172
173
|
diagnosis={diagnosis}
|
|
174
|
+
explanation={explanation}
|
|
175
|
+
assessmentSources={assessmentSources}
|
|
176
|
+
assessmentAction={assessmentAction}
|
|
173
177
|
animate={animate}
|
|
174
178
|
storyInline={storyInline}
|
|
175
179
|
revisedAfter={revisedAfter}
|
|
@@ -188,12 +192,6 @@ export function ResultCard({
|
|
|
188
192
|
readOnlyAssessment={readOnlyAssessment}
|
|
189
193
|
/>
|
|
190
194
|
) : null}
|
|
191
|
-
{assessmentSources ? (
|
|
192
|
-
<AssessmentSourceDetails>{assessmentSources}</AssessmentSourceDetails>
|
|
193
|
-
) : null}
|
|
194
|
-
{assessmentAction && (
|
|
195
|
-
<div className="mt-2 flex justify-end">{assessmentAction}</div>
|
|
196
|
-
)}
|
|
197
195
|
</>
|
|
198
196
|
);
|
|
199
197
|
}
|
|
@@ -446,6 +444,7 @@ export function AssessmentSources({
|
|
|
446
444
|
investigationCase,
|
|
447
445
|
unlinkedEvidence = 0,
|
|
448
446
|
evidenceMalformed = false,
|
|
447
|
+
omittedEntries = 0,
|
|
449
448
|
readOnly = false,
|
|
450
449
|
renderedGroupIds,
|
|
451
450
|
onViewSource,
|
|
@@ -464,6 +463,12 @@ export function AssessmentSources({
|
|
|
464
463
|
* and no count describes how many were lost.
|
|
465
464
|
*/
|
|
466
465
|
evidenceMalformed?: boolean;
|
|
466
|
+
/**
|
|
467
|
+
* Next steps, open items and ruled-out hypotheses the server left out of
|
|
468
|
+
* the record because the list ran past the page's limit. They were valid;
|
|
469
|
+
* the reader learns the list was longer than what is shown.
|
|
470
|
+
*/
|
|
471
|
+
omittedEntries?: number;
|
|
467
472
|
readOnly?: boolean;
|
|
468
473
|
/**
|
|
469
474
|
* Groups the Evidence pane actually rendered. A card-placed note whose card
|
|
@@ -475,7 +480,12 @@ export function AssessmentSources({
|
|
|
475
480
|
onViewSource: (sourceId: string) => void;
|
|
476
481
|
}) {
|
|
477
482
|
const rows = assessmentSourceRows(resolution, investigationCase);
|
|
478
|
-
if (
|
|
483
|
+
if (
|
|
484
|
+
rows.length === 0 &&
|
|
485
|
+
unlinkedEvidence === 0 &&
|
|
486
|
+
!evidenceMalformed &&
|
|
487
|
+
omittedEntries === 0
|
|
488
|
+
)
|
|
479
489
|
return null;
|
|
480
490
|
return (
|
|
481
491
|
<div className="mt-3 border-t border-theme-border/60 pt-2">
|
|
@@ -566,6 +576,13 @@ export function AssessmentSources({
|
|
|
566
576
|
: `${unlinkedEvidence} agent notes could not be linked to Radar results and are not shown.`}
|
|
567
577
|
</p>
|
|
568
578
|
) : null}
|
|
579
|
+
{omittedEntries > 0 ? (
|
|
580
|
+
<p className="mt-2 text-[11px] text-theme-text-tertiary">
|
|
581
|
+
{omittedEntries === 1
|
|
582
|
+
? "1 next step, open item or ruled-out hypothesis went past the page's limit and is not shown."
|
|
583
|
+
: `${omittedEntries} next steps, open items or ruled-out hypotheses went past the page's limits and are not shown.`}
|
|
584
|
+
</p>
|
|
585
|
+
) : null}
|
|
569
586
|
</div>
|
|
570
587
|
);
|
|
571
588
|
}
|
|
@@ -656,8 +673,201 @@ export type AssessmentExplanation = {
|
|
|
656
673
|
openRequest?: number;
|
|
657
674
|
};
|
|
658
675
|
|
|
659
|
-
|
|
660
|
-
|
|
676
|
+
function AssessmentDetails({
|
|
677
|
+
diagnosis,
|
|
678
|
+
explanation,
|
|
679
|
+
assessmentAction,
|
|
680
|
+
assessmentSources,
|
|
681
|
+
showAnalysisDisclosure = false,
|
|
682
|
+
showConfidence = false,
|
|
683
|
+
analysisText = diagnosis.report,
|
|
684
|
+
}: {
|
|
685
|
+
diagnosis: Diagnosis;
|
|
686
|
+
explanation?: AssessmentExplanation;
|
|
687
|
+
assessmentAction?: ReactNode;
|
|
688
|
+
assessmentSources?: ReactNode;
|
|
689
|
+
showAnalysisDisclosure?: boolean;
|
|
690
|
+
showConfidence?: boolean;
|
|
691
|
+
analysisText?: string;
|
|
692
|
+
}) {
|
|
693
|
+
const [detail, setDetail] = useState<"analysis" | "explanation" | null>(
|
|
694
|
+
explanation?.status === "running" ? "explanation" : null,
|
|
695
|
+
);
|
|
696
|
+
const showAnalysis = detail === "analysis";
|
|
697
|
+
const analysisReveal = useDisclosureReveal<HTMLDivElement>();
|
|
698
|
+
const { elementRef: analysisElementRef, revealAfterToggle: revealAnalysis } =
|
|
699
|
+
analysisReveal;
|
|
700
|
+
useEffect(() => {
|
|
701
|
+
if (explanation?.openRequest) {
|
|
702
|
+
setDetail("explanation");
|
|
703
|
+
revealAnalysis(true);
|
|
704
|
+
}
|
|
705
|
+
}, [explanation?.openRequest, revealAnalysis]);
|
|
706
|
+
useEffect(() => {
|
|
707
|
+
if (
|
|
708
|
+
detail === "explanation" &&
|
|
709
|
+
(explanation?.status === "done" || explanation?.status === "error")
|
|
710
|
+
) {
|
|
711
|
+
const element = analysisElementRef.current;
|
|
712
|
+
const scroller = element?.closest("[data-investigation-findings-scroll]");
|
|
713
|
+
if (element && scroller) {
|
|
714
|
+
const top = element.getBoundingClientRect().top;
|
|
715
|
+
const viewport = scroller.getBoundingClientRect();
|
|
716
|
+
if (top >= viewport.top && top < viewport.bottom) revealAnalysis(true);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}, [explanation?.status, detail, analysisElementRef, revealAnalysis]);
|
|
720
|
+
const analysisId = useId();
|
|
721
|
+
return (
|
|
722
|
+
<>
|
|
723
|
+
{/* Full analysis — the agent's detailed evidence, on demand. Under the
|
|
724
|
+
story contract Findings renders the story itself, so only the
|
|
725
|
+
explanation and sources remain here. */}
|
|
726
|
+
{((showAnalysisDisclosure && diagnosis.report) ||
|
|
727
|
+
(showAnalysisDisclosure && diagnosis.confidence != null) ||
|
|
728
|
+
explanation ||
|
|
729
|
+
assessmentAction ||
|
|
730
|
+
assessmentSources) && (
|
|
731
|
+
<div>
|
|
732
|
+
<div
|
|
733
|
+
className="flex flex-wrap items-center gap-x-3 gap-y-1 pt-2"
|
|
734
|
+
data-assessment-actions
|
|
735
|
+
>
|
|
736
|
+
{((showAnalysisDisclosure && diagnosis.report) ||
|
|
737
|
+
(showAnalysisDisclosure && diagnosis.confidence != null) ||
|
|
738
|
+
assessmentSources) && (
|
|
739
|
+
<button
|
|
740
|
+
type="button"
|
|
741
|
+
aria-expanded={showAnalysis}
|
|
742
|
+
aria-controls={`${analysisId}-analysis`}
|
|
743
|
+
onClick={() => {
|
|
744
|
+
setDetail(showAnalysis ? null : "analysis");
|
|
745
|
+
analysisReveal.revealAfterToggle(!showAnalysis);
|
|
746
|
+
}}
|
|
747
|
+
className="flex items-center gap-1.5 rounded-md py-2 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary"
|
|
748
|
+
>
|
|
749
|
+
<CollapseChevron open={showAnalysis} className="h-3.5 w-3.5" />
|
|
750
|
+
{showAnalysisDisclosure && diagnosis.report
|
|
751
|
+
? "Full analysis"
|
|
752
|
+
: "Assessment details"}
|
|
753
|
+
</button>
|
|
754
|
+
)}
|
|
755
|
+
{explanation && (
|
|
756
|
+
<Tooltip
|
|
757
|
+
content={
|
|
758
|
+
explanation.status === "idle"
|
|
759
|
+
? explanation.onGenerate
|
|
760
|
+
? "Ask the agent to explain this assessment and its proposed next steps in plain language."
|
|
761
|
+
: "Wait for the current agent request to finish before requesting an explanation."
|
|
762
|
+
: explanation.status === "running"
|
|
763
|
+
? "The agent is preparing an explanation. You can close this and return while it runs."
|
|
764
|
+
: explanation.status === "error"
|
|
765
|
+
? "View the explanation error and retry when the agent is available."
|
|
766
|
+
: "Show the saved plain-language explanation. No new request is needed."
|
|
767
|
+
}
|
|
768
|
+
>
|
|
769
|
+
<button
|
|
770
|
+
type="button"
|
|
771
|
+
aria-expanded={detail === "explanation"}
|
|
772
|
+
aria-controls={`${analysisId}-explanation`}
|
|
773
|
+
disabled={
|
|
774
|
+
explanation.status === "idle" && !explanation.onGenerate
|
|
775
|
+
}
|
|
776
|
+
onClick={() => {
|
|
777
|
+
const open = detail !== "explanation";
|
|
778
|
+
setDetail(open ? "explanation" : null);
|
|
779
|
+
analysisReveal.revealAfterToggle(open);
|
|
780
|
+
if (open && explanation.status === "idle")
|
|
781
|
+
explanation.onGenerate?.();
|
|
782
|
+
}}
|
|
783
|
+
className="inline-flex items-center gap-1 rounded-md px-2 py-1.5 text-xs font-medium text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary disabled:opacity-50"
|
|
784
|
+
>
|
|
785
|
+
<HelpCircle className="h-3 w-3" />
|
|
786
|
+
Explain simply
|
|
787
|
+
</button>
|
|
788
|
+
</Tooltip>
|
|
789
|
+
)}
|
|
790
|
+
{assessmentAction && (
|
|
791
|
+
<div className="ml-auto">{assessmentAction}</div>
|
|
792
|
+
)}
|
|
793
|
+
</div>
|
|
794
|
+
<div id={analysisId} ref={analysisReveal.elementRef}>
|
|
795
|
+
<div id={`${analysisId}-analysis`}>
|
|
796
|
+
<Collapse open={detail === "analysis"} mountLazily>
|
|
797
|
+
<div className="border-t border-theme-border/60 px-3 py-2">
|
|
798
|
+
{showAnalysisDisclosure ? (
|
|
799
|
+
<>
|
|
800
|
+
{showConfidence && (
|
|
801
|
+
<p className="mb-2 text-xs text-theme-text-tertiary">
|
|
802
|
+
Agent confidence:{" "}
|
|
803
|
+
{diagnosis.confidence != null
|
|
804
|
+
? confidenceLabel(diagnosis.confidence)
|
|
805
|
+
: "not stated"}
|
|
806
|
+
{diagnosis.confidence != null
|
|
807
|
+
? " · self-reported"
|
|
808
|
+
: ""}
|
|
809
|
+
</p>
|
|
810
|
+
)}
|
|
811
|
+
<AIMarkdown className="text-sm [overflow-wrap:anywhere] [&_h2:first-child]:mt-0 [&_h2]:mb-1.5 [&_h2]:mt-3 [&_h2]:text-xs [&_h2]:font-semibold [&_h2]:uppercase [&_h2]:tracking-wide [&_h2]:text-theme-text-tertiary [&_h3]:text-sm [&_li]:text-theme-text-secondary [&_p]:my-1.5 [&_p]:text-theme-text-secondary">
|
|
812
|
+
{analysisText}
|
|
813
|
+
</AIMarkdown>
|
|
814
|
+
</>
|
|
815
|
+
) : null}
|
|
816
|
+
{assessmentSources}
|
|
817
|
+
</div>
|
|
818
|
+
</Collapse>
|
|
819
|
+
</div>
|
|
820
|
+
<div id={`${analysisId}-explanation`}>
|
|
821
|
+
<Collapse open={detail === "explanation"} mountLazily>
|
|
822
|
+
<div className="border-t border-theme-border/60 px-3 py-2">
|
|
823
|
+
{explanation?.status === "running" ? (
|
|
824
|
+
<div
|
|
825
|
+
role="status"
|
|
826
|
+
className="flex items-center gap-2 py-2 text-sm text-theme-text-secondary"
|
|
827
|
+
>
|
|
828
|
+
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
|
829
|
+
Explaining this assessment…
|
|
830
|
+
</div>
|
|
831
|
+
) : explanation?.status === "done" ? (
|
|
832
|
+
<div className="flex items-start gap-2">
|
|
833
|
+
<AIMarkdown className="min-w-0 flex-1 text-sm text-theme-text-secondary [overflow-wrap:anywhere]">
|
|
834
|
+
{explanation.text || ""}
|
|
835
|
+
</AIMarkdown>
|
|
836
|
+
<CopyButton
|
|
837
|
+
text={explanation.text || ""}
|
|
838
|
+
label="Copy explanation"
|
|
839
|
+
/>
|
|
840
|
+
</div>
|
|
841
|
+
) : explanation?.status === "error" ? (
|
|
842
|
+
<div
|
|
843
|
+
role="alert"
|
|
844
|
+
className="flex flex-wrap items-center gap-2 py-2 text-sm text-theme-text-secondary"
|
|
845
|
+
>
|
|
846
|
+
<span>
|
|
847
|
+
{explanation.error ||
|
|
848
|
+
"The agent did not return an explanation."}
|
|
849
|
+
</span>
|
|
850
|
+
{explanation.onGenerate && (
|
|
851
|
+
<button
|
|
852
|
+
type="button"
|
|
853
|
+
onClick={explanation.onGenerate}
|
|
854
|
+
className="rounded-md px-2 py-1 text-xs font-medium text-accent-text hover:bg-theme-hover"
|
|
855
|
+
>
|
|
856
|
+
Try again
|
|
857
|
+
</button>
|
|
858
|
+
)}
|
|
859
|
+
</div>
|
|
860
|
+
) : null}
|
|
861
|
+
</div>
|
|
862
|
+
</Collapse>
|
|
863
|
+
</div>
|
|
864
|
+
</div>
|
|
865
|
+
</div>
|
|
866
|
+
)}
|
|
867
|
+
</>
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
|
|
661
871
|
function DiagnosisResult({
|
|
662
872
|
diagnosis,
|
|
663
873
|
onApply,
|
|
@@ -700,37 +910,9 @@ function DiagnosisResult({
|
|
|
700
910
|
? storyPlainText(diagnosis.report)
|
|
701
911
|
: diagnosis.report;
|
|
702
912
|
const showAnalysisDisclosure = !storyShape || storyInline;
|
|
703
|
-
const [detail, setDetail] = useState<"analysis" | "explanation" | null>(
|
|
704
|
-
explanation?.status === "running" ? "explanation" : null,
|
|
705
|
-
);
|
|
706
|
-
const showAnalysis = detail === "analysis";
|
|
707
|
-
const analysisReveal = useDisclosureReveal<HTMLDivElement>();
|
|
708
|
-
const { elementRef: analysisElementRef, revealAfterToggle: revealAnalysis } =
|
|
709
|
-
analysisReveal;
|
|
710
|
-
useEffect(() => {
|
|
711
|
-
if (explanation?.openRequest) {
|
|
712
|
-
setDetail("explanation");
|
|
713
|
-
revealAnalysis(true);
|
|
714
|
-
}
|
|
715
|
-
}, [explanation?.openRequest, revealAnalysis]);
|
|
716
|
-
useEffect(() => {
|
|
717
|
-
if (
|
|
718
|
-
detail === "explanation" &&
|
|
719
|
-
(explanation?.status === "done" || explanation?.status === "error")
|
|
720
|
-
) {
|
|
721
|
-
const element = analysisElementRef.current;
|
|
722
|
-
const scroller = element?.closest("[data-investigation-findings-scroll]");
|
|
723
|
-
if (element && scroller) {
|
|
724
|
-
const top = element.getBoundingClientRect().top;
|
|
725
|
-
const viewport = scroller.getBoundingClientRect();
|
|
726
|
-
if (top >= viewport.top && top < viewport.bottom) revealAnalysis(true);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
}, [explanation?.status, detail, analysisElementRef, revealAnalysis]);
|
|
730
913
|
const [showAllSteps, setShowAllSteps] = useState(false);
|
|
731
914
|
const stepsId = useId();
|
|
732
915
|
const stepsReveal = useDisclosureReveal<HTMLDivElement>();
|
|
733
|
-
const analysisId = useId();
|
|
734
916
|
// Only a real structured cause anchors the amber card; the full prose lives in
|
|
735
917
|
// "Full analysis" (never relabel the report as a causal assessment).
|
|
736
918
|
const rootCause = diagnosis.rootCause;
|
|
@@ -765,6 +947,7 @@ function DiagnosisResult({
|
|
|
765
947
|
}) => {
|
|
766
948
|
const isRec = recValid && i === recIdx! - 1;
|
|
767
949
|
const step = typedSteps ? steps[i] : undefined;
|
|
950
|
+
const commands = remediationCommands(r);
|
|
768
951
|
return (
|
|
769
952
|
<div
|
|
770
953
|
key={i}
|
|
@@ -823,17 +1006,6 @@ function DiagnosisResult({
|
|
|
823
1006
|
Apply…
|
|
824
1007
|
</button>
|
|
825
1008
|
)}
|
|
826
|
-
{remediationCommands(r).map((command, c, all) => (
|
|
827
|
-
<CopyButton
|
|
828
|
-
key={c}
|
|
829
|
-
text={command}
|
|
830
|
-
label={
|
|
831
|
-
all.length > 1
|
|
832
|
-
? `Copy command ${c + 1} of step ${i + 1}`
|
|
833
|
-
: `Copy command from step ${i + 1}`
|
|
834
|
-
}
|
|
835
|
-
/>
|
|
836
|
-
))}
|
|
837
1009
|
</div>
|
|
838
1010
|
</div>
|
|
839
1011
|
{/* The condition and the reason are read before the command is
|
|
@@ -854,7 +1026,19 @@ function DiagnosisResult({
|
|
|
854
1026
|
{diagnosis.recommendedReason}
|
|
855
1027
|
</p>
|
|
856
1028
|
)}
|
|
857
|
-
<AIMarkdown
|
|
1029
|
+
<AIMarkdown
|
|
1030
|
+
className="max-w-[100ch] text-sm [overflow-wrap:anywhere] [&_p]:my-0 [&_pre]:my-1.5"
|
|
1031
|
+
codeActions={(code) => {
|
|
1032
|
+
const command = code.trim();
|
|
1033
|
+
if (!commands.includes(command)) return null;
|
|
1034
|
+
return (
|
|
1035
|
+
<CopyButton
|
|
1036
|
+
text={command}
|
|
1037
|
+
label={`Copy command from step ${i + 1}`}
|
|
1038
|
+
/>
|
|
1039
|
+
);
|
|
1040
|
+
}}
|
|
1041
|
+
>
|
|
858
1042
|
{r}
|
|
859
1043
|
</AIMarkdown>
|
|
860
1044
|
</div>
|
|
@@ -1015,152 +1199,17 @@ function DiagnosisResult({
|
|
|
1015
1199
|
</div>
|
|
1016
1200
|
)}
|
|
1017
1201
|
|
|
1018
|
-
{
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
className="flex flex-wrap items-center gap-x-3 gap-y-1 pt-2"
|
|
1030
|
-
data-assessment-actions
|
|
1031
|
-
>
|
|
1032
|
-
{((showAnalysisDisclosure && diagnosis.report) ||
|
|
1033
|
-
(showAnalysisDisclosure && diagnosis.confidence != null) ||
|
|
1034
|
-
assessmentSources) && (
|
|
1035
|
-
<button
|
|
1036
|
-
type="button"
|
|
1037
|
-
aria-expanded={showAnalysis}
|
|
1038
|
-
aria-controls={`${analysisId}-analysis`}
|
|
1039
|
-
onClick={() => {
|
|
1040
|
-
setDetail(showAnalysis ? null : "analysis");
|
|
1041
|
-
analysisReveal.revealAfterToggle(!showAnalysis);
|
|
1042
|
-
}}
|
|
1043
|
-
className="flex items-center gap-1.5 rounded-md py-2 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary"
|
|
1044
|
-
>
|
|
1045
|
-
<CollapseChevron
|
|
1046
|
-
open={showAnalysis}
|
|
1047
|
-
className="h-3.5 w-3.5"
|
|
1048
|
-
/>
|
|
1049
|
-
{showAnalysisDisclosure && diagnosis.report
|
|
1050
|
-
? "Full analysis"
|
|
1051
|
-
: "Assessment details"}
|
|
1052
|
-
</button>
|
|
1053
|
-
)}
|
|
1054
|
-
{explanation && (
|
|
1055
|
-
<Tooltip
|
|
1056
|
-
content={
|
|
1057
|
-
explanation.status === "idle"
|
|
1058
|
-
? explanation.onGenerate
|
|
1059
|
-
? "Ask the agent to explain this assessment and its proposed next steps in plain language."
|
|
1060
|
-
: "Wait for the current agent request to finish before requesting an explanation."
|
|
1061
|
-
: explanation.status === "running"
|
|
1062
|
-
? "The agent is preparing an explanation. You can close this and return while it runs."
|
|
1063
|
-
: explanation.status === "error"
|
|
1064
|
-
? "View the explanation error and retry when the agent is available."
|
|
1065
|
-
: "Show the saved plain-language explanation. No new request is needed."
|
|
1066
|
-
}
|
|
1067
|
-
>
|
|
1068
|
-
<button
|
|
1069
|
-
type="button"
|
|
1070
|
-
aria-expanded={detail === "explanation"}
|
|
1071
|
-
aria-controls={`${analysisId}-explanation`}
|
|
1072
|
-
disabled={
|
|
1073
|
-
explanation.status === "idle" && !explanation.onGenerate
|
|
1074
|
-
}
|
|
1075
|
-
onClick={() => {
|
|
1076
|
-
const open = detail !== "explanation";
|
|
1077
|
-
setDetail(open ? "explanation" : null);
|
|
1078
|
-
analysisReveal.revealAfterToggle(open);
|
|
1079
|
-
if (open && explanation.status === "idle")
|
|
1080
|
-
explanation.onGenerate?.();
|
|
1081
|
-
}}
|
|
1082
|
-
className="inline-flex items-center gap-1 rounded-md px-2 py-1.5 text-xs font-medium text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary disabled:opacity-50"
|
|
1083
|
-
>
|
|
1084
|
-
<HelpCircle className="h-3 w-3" />
|
|
1085
|
-
Explain simply
|
|
1086
|
-
</button>
|
|
1087
|
-
</Tooltip>
|
|
1088
|
-
)}
|
|
1089
|
-
{assessmentAction && (
|
|
1090
|
-
<div className="ml-auto">{assessmentAction}</div>
|
|
1091
|
-
)}
|
|
1092
|
-
</div>
|
|
1093
|
-
<div id={analysisId} ref={analysisReveal.elementRef}>
|
|
1094
|
-
<div id={`${analysisId}-analysis`}>
|
|
1095
|
-
<Collapse open={detail === "analysis"} mountLazily>
|
|
1096
|
-
<div className="border-t border-theme-border/60 px-3 py-2">
|
|
1097
|
-
{showAnalysisDisclosure ? (
|
|
1098
|
-
<>
|
|
1099
|
-
<p className="mb-2 text-xs text-theme-text-tertiary">
|
|
1100
|
-
Agent confidence:{" "}
|
|
1101
|
-
{diagnosis.confidence != null
|
|
1102
|
-
? confidenceLabel(diagnosis.confidence)
|
|
1103
|
-
: "not stated"}
|
|
1104
|
-
{diagnosis.confidence != null
|
|
1105
|
-
? " · self-reported"
|
|
1106
|
-
: ""}
|
|
1107
|
-
</p>
|
|
1108
|
-
<AIMarkdown className="text-sm [overflow-wrap:anywhere] [&_h2:first-child]:mt-0 [&_h2]:mb-1.5 [&_h2]:mt-3 [&_h2]:text-xs [&_h2]:font-semibold [&_h2]:uppercase [&_h2]:tracking-wide [&_h2]:text-theme-text-tertiary [&_h3]:text-sm [&_li]:text-theme-text-secondary [&_p]:my-1.5 [&_p]:text-theme-text-secondary">
|
|
1109
|
-
{analysisText}
|
|
1110
|
-
</AIMarkdown>
|
|
1111
|
-
</>
|
|
1112
|
-
) : null}
|
|
1113
|
-
{assessmentSources}
|
|
1114
|
-
</div>
|
|
1115
|
-
</Collapse>
|
|
1116
|
-
</div>
|
|
1117
|
-
<div id={`${analysisId}-explanation`}>
|
|
1118
|
-
<Collapse open={detail === "explanation"} mountLazily>
|
|
1119
|
-
<div className="border-t border-theme-border/60 px-3 py-2">
|
|
1120
|
-
{explanation?.status === "running" ? (
|
|
1121
|
-
<div
|
|
1122
|
-
role="status"
|
|
1123
|
-
className="flex items-center gap-2 py-2 text-sm text-theme-text-secondary"
|
|
1124
|
-
>
|
|
1125
|
-
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
|
1126
|
-
Explaining this assessment…
|
|
1127
|
-
</div>
|
|
1128
|
-
) : explanation?.status === "done" ? (
|
|
1129
|
-
<div className="flex items-start gap-2">
|
|
1130
|
-
<AIMarkdown className="min-w-0 flex-1 text-sm text-theme-text-secondary [overflow-wrap:anywhere]">
|
|
1131
|
-
{explanation.text || ""}
|
|
1132
|
-
</AIMarkdown>
|
|
1133
|
-
<CopyButton
|
|
1134
|
-
text={explanation.text || ""}
|
|
1135
|
-
label="Copy explanation"
|
|
1136
|
-
/>
|
|
1137
|
-
</div>
|
|
1138
|
-
) : explanation?.status === "error" ? (
|
|
1139
|
-
<div
|
|
1140
|
-
role="alert"
|
|
1141
|
-
className="flex flex-wrap items-center gap-2 py-2 text-sm text-theme-text-secondary"
|
|
1142
|
-
>
|
|
1143
|
-
<span>
|
|
1144
|
-
{explanation.error ||
|
|
1145
|
-
"The agent did not return an explanation."}
|
|
1146
|
-
</span>
|
|
1147
|
-
{explanation.onGenerate && (
|
|
1148
|
-
<button
|
|
1149
|
-
type="button"
|
|
1150
|
-
onClick={explanation.onGenerate}
|
|
1151
|
-
className="rounded-md px-2 py-1 text-xs font-medium text-accent-text hover:bg-theme-hover"
|
|
1152
|
-
>
|
|
1153
|
-
Try again
|
|
1154
|
-
</button>
|
|
1155
|
-
)}
|
|
1156
|
-
</div>
|
|
1157
|
-
) : null}
|
|
1158
|
-
</div>
|
|
1159
|
-
</Collapse>
|
|
1160
|
-
</div>
|
|
1161
|
-
</div>
|
|
1162
|
-
</div>
|
|
1163
|
-
)}
|
|
1202
|
+
{showConclusion && (
|
|
1203
|
+
<AssessmentDetails
|
|
1204
|
+
diagnosis={diagnosis}
|
|
1205
|
+
explanation={explanation}
|
|
1206
|
+
assessmentAction={assessmentAction}
|
|
1207
|
+
assessmentSources={assessmentSources}
|
|
1208
|
+
showAnalysisDisclosure={showAnalysisDisclosure}
|
|
1209
|
+
showConfidence
|
|
1210
|
+
analysisText={analysisText}
|
|
1211
|
+
/>
|
|
1212
|
+
)}
|
|
1164
1213
|
|
|
1165
1214
|
{showConclusion && showDisclaimer && (
|
|
1166
1215
|
<div className="flex items-start gap-1 px-0.5 text-[11px] text-theme-text-tertiary">
|
|
@@ -1185,6 +1234,7 @@ function healthFlagSentence(flag: {
|
|
|
1185
1234
|
|
|
1186
1235
|
export function AllClearCard({
|
|
1187
1236
|
diagnosis,
|
|
1237
|
+
explanation,
|
|
1188
1238
|
animate,
|
|
1189
1239
|
showDisclaimer,
|
|
1190
1240
|
coverageLimited,
|
|
@@ -1203,6 +1253,7 @@ export function AllClearCard({
|
|
|
1203
1253
|
animate: boolean;
|
|
1204
1254
|
showDisclaimer: boolean;
|
|
1205
1255
|
coverageLimited: boolean;
|
|
1256
|
+
explanation?: AssessmentExplanation;
|
|
1206
1257
|
evidenceConflict: boolean;
|
|
1207
1258
|
evidenceConflictExplainedBy?: string[];
|
|
1208
1259
|
assessmentAction?: ReactNode;
|
|
@@ -1217,9 +1268,6 @@ export function AllClearCard({
|
|
|
1217
1268
|
onRevealSource?: (sourceId: string) => void;
|
|
1218
1269
|
}) {
|
|
1219
1270
|
const storyShape = diagnosisHasStoryShape(diagnosis);
|
|
1220
|
-
const [showAnalysis, setShowAnalysis] = useState(false);
|
|
1221
|
-
const analysisReveal = useDisclosureReveal<HTMLDivElement>();
|
|
1222
|
-
const analysisId = useId();
|
|
1223
1271
|
const report =
|
|
1224
1272
|
(storyShape && !storyInline ? "" : storyPlainText(diagnosis.report)) ||
|
|
1225
1273
|
(storyShape
|
|
@@ -1264,43 +1312,14 @@ export function AllClearCard({
|
|
|
1264
1312
|
// the disclaimer are the same in both shapes.
|
|
1265
1313
|
const trailing = (
|
|
1266
1314
|
<>
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
type="button"
|
|
1276
|
-
aria-expanded={showAnalysis}
|
|
1277
|
-
aria-controls={analysisId}
|
|
1278
|
-
onClick={() => {
|
|
1279
|
-
setShowAnalysis(!showAnalysis);
|
|
1280
|
-
analysisReveal.revealAfterToggle(!showAnalysis);
|
|
1281
|
-
}}
|
|
1282
|
-
className="flex items-center gap-1.5 rounded-md py-2 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary"
|
|
1283
|
-
>
|
|
1284
|
-
<CollapseChevron open={showAnalysis} className="h-3.5 w-3.5" />
|
|
1285
|
-
{detailed ? "Full analysis" : "Assessment details"}
|
|
1286
|
-
</button>
|
|
1287
|
-
)}
|
|
1288
|
-
{assessmentAction && (
|
|
1289
|
-
<div className="ml-auto">{assessmentAction}</div>
|
|
1290
|
-
)}
|
|
1291
|
-
</div>
|
|
1292
|
-
<div id={analysisId} ref={analysisReveal.elementRef}>
|
|
1293
|
-
<Collapse open={showAnalysis}>
|
|
1294
|
-
<div className="border-t border-theme-border/60 px-3 py-2">
|
|
1295
|
-
<AIMarkdown className="text-sm [overflow-wrap:anywhere] [&_p]:my-1.5 [&_p]:text-theme-text-secondary [&_p:first-child]:mt-0 [&_p:last-child]:mb-0">
|
|
1296
|
-
{detailed ? report : ""}
|
|
1297
|
-
</AIMarkdown>
|
|
1298
|
-
{assessmentSources}
|
|
1299
|
-
</div>
|
|
1300
|
-
</Collapse>
|
|
1301
|
-
</div>
|
|
1302
|
-
</div>
|
|
1303
|
-
) : null}
|
|
1315
|
+
<AssessmentDetails
|
|
1316
|
+
diagnosis={diagnosis}
|
|
1317
|
+
explanation={explanation}
|
|
1318
|
+
assessmentAction={assessmentAction}
|
|
1319
|
+
assessmentSources={assessmentSources}
|
|
1320
|
+
showAnalysisDisclosure={detailed}
|
|
1321
|
+
analysisText={report}
|
|
1322
|
+
/>
|
|
1304
1323
|
{showDisclaimer ? (
|
|
1305
1324
|
<div className="flex items-start gap-1 px-0.5 text-[11px] text-theme-text-tertiary">
|
|
1306
1325
|
<ShieldCheck className="mt-0.5 h-3 w-3 shrink-0" />
|
|
@@ -1423,6 +1442,9 @@ export function AllClearCard({
|
|
|
1423
1442
|
// all-clear) — so "I couldn't tell" never reads as "you're fine."
|
|
1424
1443
|
export function InconclusiveCard({
|
|
1425
1444
|
diagnosis,
|
|
1445
|
+
explanation,
|
|
1446
|
+
assessmentSources,
|
|
1447
|
+
assessmentAction,
|
|
1426
1448
|
animate,
|
|
1427
1449
|
storyInline = false,
|
|
1428
1450
|
revisedAfter,
|
|
@@ -1432,7 +1454,10 @@ export function InconclusiveCard({
|
|
|
1432
1454
|
diagnosis: Diagnosis;
|
|
1433
1455
|
animate: boolean;
|
|
1434
1456
|
storyInline?: boolean;
|
|
1457
|
+
explanation?: AssessmentExplanation;
|
|
1435
1458
|
revisedAfter?: string;
|
|
1459
|
+
assessmentSources?: ReactNode;
|
|
1460
|
+
assessmentAction?: ReactNode;
|
|
1436
1461
|
/** Reads Radar could not complete for this assessment; listed under Still open. */
|
|
1437
1462
|
assessmentLimits?: string[];
|
|
1438
1463
|
assessmentCopy?: Pick<AssessmentCopyRadar, "context" | "receipts">;
|
|
@@ -1477,6 +1502,12 @@ export function InconclusiveCard({
|
|
|
1477
1502
|
addressing any errors shown in Activity.
|
|
1478
1503
|
</span>
|
|
1479
1504
|
</div>
|
|
1505
|
+
<AssessmentDetails
|
|
1506
|
+
diagnosis={diagnosis}
|
|
1507
|
+
explanation={explanation}
|
|
1508
|
+
assessmentSources={assessmentSources}
|
|
1509
|
+
assessmentAction={assessmentAction}
|
|
1510
|
+
/>
|
|
1480
1511
|
</div>
|
|
1481
1512
|
);
|
|
1482
1513
|
}
|
|
@@ -1528,10 +1559,10 @@ export function remediationHeadline(step: string): string {
|
|
|
1528
1559
|
*/
|
|
1529
1560
|
export function remediationCommands(step: string): string[] {
|
|
1530
1561
|
const commands: string[] = [];
|
|
1531
|
-
|
|
1562
|
+
const normalized = tidyFences(step);
|
|
1532
1563
|
const code = /```[a-zA-Z]*\n([\s\S]*?)```|`([^`\n]+)`/g;
|
|
1533
1564
|
let match: RegExpExecArray | null;
|
|
1534
|
-
while ((match = code.exec(
|
|
1565
|
+
while ((match = code.exec(normalized))) {
|
|
1535
1566
|
if (match[1] !== undefined) {
|
|
1536
1567
|
const body = match[1].trim();
|
|
1537
1568
|
if (body) commands.push(body);
|