@skyhook-io/radar-app 1.14.4 → 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/components/diagnose/AIMarkdown.tsx +8 -3
- package/src/components/diagnose/AssessmentCard.tsx +249 -237
- 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 +4 -1
- package/src/components/diagnose/investigationCase.test.tsx +56 -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 +8 -5
- package/src/components/diagnose/investigationEvidence/types.ts +1 -1
- package/src/components/diagnose/parts.test.tsx +141 -2
- package/src/components/rightsizing/RightsizingScanView.tsx +177 -65
- 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/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
1
|
+
import { useState, type ComponentProps } from "react";
|
|
2
2
|
import { AlertTriangle, Copy, Check } from "lucide-react";
|
|
3
3
|
import { Markdown } from "../ui/Markdown";
|
|
4
4
|
import { Tooltip } from "../ui/Tooltip";
|
|
@@ -59,7 +59,7 @@ export function CopyButton({ text, label }: { text: string; label: string }) {
|
|
|
59
59
|
// it as a fence — it leaks the literal ``` and renders an empty code box. Coerce
|
|
60
60
|
// fence markers onto their own lines and push trailing content off the opener so
|
|
61
61
|
// the block renders. (Well-formed markdown is unaffected.)
|
|
62
|
-
function tidyFences(md: string): string {
|
|
62
|
+
export function tidyFences(md: string): string {
|
|
63
63
|
if (!md || !md.includes("```")) return md;
|
|
64
64
|
return md
|
|
65
65
|
.replace(/([^\n])```/g, "$1\n\n```") // opener/closer must start a line
|
|
@@ -75,12 +75,17 @@ const SOFT_INLINE_CODE =
|
|
|
75
75
|
export function AIMarkdown({
|
|
76
76
|
className,
|
|
77
77
|
children,
|
|
78
|
+
codeActions,
|
|
78
79
|
}: {
|
|
79
80
|
className?: string;
|
|
80
81
|
children: string;
|
|
82
|
+
codeActions?: ComponentProps<typeof Markdown>["codeActions"];
|
|
81
83
|
}) {
|
|
82
84
|
return (
|
|
83
|
-
<Markdown
|
|
85
|
+
<Markdown
|
|
86
|
+
className={`${SOFT_INLINE_CODE} ${className ?? ""}`}
|
|
87
|
+
codeActions={codeActions}
|
|
88
|
+
>
|
|
84
89
|
{tidyFences(children)}
|
|
85
90
|
</Markdown>
|
|
86
91
|
);
|
|
@@ -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
|
}
|
|
@@ -675,8 +673,201 @@ export type AssessmentExplanation = {
|
|
|
675
673
|
openRequest?: number;
|
|
676
674
|
};
|
|
677
675
|
|
|
678
|
-
|
|
679
|
-
|
|
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
|
+
|
|
680
871
|
function DiagnosisResult({
|
|
681
872
|
diagnosis,
|
|
682
873
|
onApply,
|
|
@@ -719,37 +910,9 @@ function DiagnosisResult({
|
|
|
719
910
|
? storyPlainText(diagnosis.report)
|
|
720
911
|
: diagnosis.report;
|
|
721
912
|
const showAnalysisDisclosure = !storyShape || storyInline;
|
|
722
|
-
const [detail, setDetail] = useState<"analysis" | "explanation" | null>(
|
|
723
|
-
explanation?.status === "running" ? "explanation" : null,
|
|
724
|
-
);
|
|
725
|
-
const showAnalysis = detail === "analysis";
|
|
726
|
-
const analysisReveal = useDisclosureReveal<HTMLDivElement>();
|
|
727
|
-
const { elementRef: analysisElementRef, revealAfterToggle: revealAnalysis } =
|
|
728
|
-
analysisReveal;
|
|
729
|
-
useEffect(() => {
|
|
730
|
-
if (explanation?.openRequest) {
|
|
731
|
-
setDetail("explanation");
|
|
732
|
-
revealAnalysis(true);
|
|
733
|
-
}
|
|
734
|
-
}, [explanation?.openRequest, revealAnalysis]);
|
|
735
|
-
useEffect(() => {
|
|
736
|
-
if (
|
|
737
|
-
detail === "explanation" &&
|
|
738
|
-
(explanation?.status === "done" || explanation?.status === "error")
|
|
739
|
-
) {
|
|
740
|
-
const element = analysisElementRef.current;
|
|
741
|
-
const scroller = element?.closest("[data-investigation-findings-scroll]");
|
|
742
|
-
if (element && scroller) {
|
|
743
|
-
const top = element.getBoundingClientRect().top;
|
|
744
|
-
const viewport = scroller.getBoundingClientRect();
|
|
745
|
-
if (top >= viewport.top && top < viewport.bottom) revealAnalysis(true);
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
}, [explanation?.status, detail, analysisElementRef, revealAnalysis]);
|
|
749
913
|
const [showAllSteps, setShowAllSteps] = useState(false);
|
|
750
914
|
const stepsId = useId();
|
|
751
915
|
const stepsReveal = useDisclosureReveal<HTMLDivElement>();
|
|
752
|
-
const analysisId = useId();
|
|
753
916
|
// Only a real structured cause anchors the amber card; the full prose lives in
|
|
754
917
|
// "Full analysis" (never relabel the report as a causal assessment).
|
|
755
918
|
const rootCause = diagnosis.rootCause;
|
|
@@ -784,6 +947,7 @@ function DiagnosisResult({
|
|
|
784
947
|
}) => {
|
|
785
948
|
const isRec = recValid && i === recIdx! - 1;
|
|
786
949
|
const step = typedSteps ? steps[i] : undefined;
|
|
950
|
+
const commands = remediationCommands(r);
|
|
787
951
|
return (
|
|
788
952
|
<div
|
|
789
953
|
key={i}
|
|
@@ -842,17 +1006,6 @@ function DiagnosisResult({
|
|
|
842
1006
|
Apply…
|
|
843
1007
|
</button>
|
|
844
1008
|
)}
|
|
845
|
-
{remediationCommands(r).map((command, c, all) => (
|
|
846
|
-
<CopyButton
|
|
847
|
-
key={c}
|
|
848
|
-
text={command}
|
|
849
|
-
label={
|
|
850
|
-
all.length > 1
|
|
851
|
-
? `Copy command ${c + 1} of step ${i + 1}`
|
|
852
|
-
: `Copy command from step ${i + 1}`
|
|
853
|
-
}
|
|
854
|
-
/>
|
|
855
|
-
))}
|
|
856
1009
|
</div>
|
|
857
1010
|
</div>
|
|
858
1011
|
{/* The condition and the reason are read before the command is
|
|
@@ -873,7 +1026,19 @@ function DiagnosisResult({
|
|
|
873
1026
|
{diagnosis.recommendedReason}
|
|
874
1027
|
</p>
|
|
875
1028
|
)}
|
|
876
|
-
<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
|
+
>
|
|
877
1042
|
{r}
|
|
878
1043
|
</AIMarkdown>
|
|
879
1044
|
</div>
|
|
@@ -1034,152 +1199,17 @@ function DiagnosisResult({
|
|
|
1034
1199
|
</div>
|
|
1035
1200
|
)}
|
|
1036
1201
|
|
|
1037
|
-
{
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
className="flex flex-wrap items-center gap-x-3 gap-y-1 pt-2"
|
|
1049
|
-
data-assessment-actions
|
|
1050
|
-
>
|
|
1051
|
-
{((showAnalysisDisclosure && diagnosis.report) ||
|
|
1052
|
-
(showAnalysisDisclosure && diagnosis.confidence != null) ||
|
|
1053
|
-
assessmentSources) && (
|
|
1054
|
-
<button
|
|
1055
|
-
type="button"
|
|
1056
|
-
aria-expanded={showAnalysis}
|
|
1057
|
-
aria-controls={`${analysisId}-analysis`}
|
|
1058
|
-
onClick={() => {
|
|
1059
|
-
setDetail(showAnalysis ? null : "analysis");
|
|
1060
|
-
analysisReveal.revealAfterToggle(!showAnalysis);
|
|
1061
|
-
}}
|
|
1062
|
-
className="flex items-center gap-1.5 rounded-md py-2 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary"
|
|
1063
|
-
>
|
|
1064
|
-
<CollapseChevron
|
|
1065
|
-
open={showAnalysis}
|
|
1066
|
-
className="h-3.5 w-3.5"
|
|
1067
|
-
/>
|
|
1068
|
-
{showAnalysisDisclosure && diagnosis.report
|
|
1069
|
-
? "Full analysis"
|
|
1070
|
-
: "Assessment details"}
|
|
1071
|
-
</button>
|
|
1072
|
-
)}
|
|
1073
|
-
{explanation && (
|
|
1074
|
-
<Tooltip
|
|
1075
|
-
content={
|
|
1076
|
-
explanation.status === "idle"
|
|
1077
|
-
? explanation.onGenerate
|
|
1078
|
-
? "Ask the agent to explain this assessment and its proposed next steps in plain language."
|
|
1079
|
-
: "Wait for the current agent request to finish before requesting an explanation."
|
|
1080
|
-
: explanation.status === "running"
|
|
1081
|
-
? "The agent is preparing an explanation. You can close this and return while it runs."
|
|
1082
|
-
: explanation.status === "error"
|
|
1083
|
-
? "View the explanation error and retry when the agent is available."
|
|
1084
|
-
: "Show the saved plain-language explanation. No new request is needed."
|
|
1085
|
-
}
|
|
1086
|
-
>
|
|
1087
|
-
<button
|
|
1088
|
-
type="button"
|
|
1089
|
-
aria-expanded={detail === "explanation"}
|
|
1090
|
-
aria-controls={`${analysisId}-explanation`}
|
|
1091
|
-
disabled={
|
|
1092
|
-
explanation.status === "idle" && !explanation.onGenerate
|
|
1093
|
-
}
|
|
1094
|
-
onClick={() => {
|
|
1095
|
-
const open = detail !== "explanation";
|
|
1096
|
-
setDetail(open ? "explanation" : null);
|
|
1097
|
-
analysisReveal.revealAfterToggle(open);
|
|
1098
|
-
if (open && explanation.status === "idle")
|
|
1099
|
-
explanation.onGenerate?.();
|
|
1100
|
-
}}
|
|
1101
|
-
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"
|
|
1102
|
-
>
|
|
1103
|
-
<HelpCircle className="h-3 w-3" />
|
|
1104
|
-
Explain simply
|
|
1105
|
-
</button>
|
|
1106
|
-
</Tooltip>
|
|
1107
|
-
)}
|
|
1108
|
-
{assessmentAction && (
|
|
1109
|
-
<div className="ml-auto">{assessmentAction}</div>
|
|
1110
|
-
)}
|
|
1111
|
-
</div>
|
|
1112
|
-
<div id={analysisId} ref={analysisReveal.elementRef}>
|
|
1113
|
-
<div id={`${analysisId}-analysis`}>
|
|
1114
|
-
<Collapse open={detail === "analysis"} mountLazily>
|
|
1115
|
-
<div className="border-t border-theme-border/60 px-3 py-2">
|
|
1116
|
-
{showAnalysisDisclosure ? (
|
|
1117
|
-
<>
|
|
1118
|
-
<p className="mb-2 text-xs text-theme-text-tertiary">
|
|
1119
|
-
Agent confidence:{" "}
|
|
1120
|
-
{diagnosis.confidence != null
|
|
1121
|
-
? confidenceLabel(diagnosis.confidence)
|
|
1122
|
-
: "not stated"}
|
|
1123
|
-
{diagnosis.confidence != null
|
|
1124
|
-
? " · self-reported"
|
|
1125
|
-
: ""}
|
|
1126
|
-
</p>
|
|
1127
|
-
<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">
|
|
1128
|
-
{analysisText}
|
|
1129
|
-
</AIMarkdown>
|
|
1130
|
-
</>
|
|
1131
|
-
) : null}
|
|
1132
|
-
{assessmentSources}
|
|
1133
|
-
</div>
|
|
1134
|
-
</Collapse>
|
|
1135
|
-
</div>
|
|
1136
|
-
<div id={`${analysisId}-explanation`}>
|
|
1137
|
-
<Collapse open={detail === "explanation"} mountLazily>
|
|
1138
|
-
<div className="border-t border-theme-border/60 px-3 py-2">
|
|
1139
|
-
{explanation?.status === "running" ? (
|
|
1140
|
-
<div
|
|
1141
|
-
role="status"
|
|
1142
|
-
className="flex items-center gap-2 py-2 text-sm text-theme-text-secondary"
|
|
1143
|
-
>
|
|
1144
|
-
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
|
1145
|
-
Explaining this assessment…
|
|
1146
|
-
</div>
|
|
1147
|
-
) : explanation?.status === "done" ? (
|
|
1148
|
-
<div className="flex items-start gap-2">
|
|
1149
|
-
<AIMarkdown className="min-w-0 flex-1 text-sm text-theme-text-secondary [overflow-wrap:anywhere]">
|
|
1150
|
-
{explanation.text || ""}
|
|
1151
|
-
</AIMarkdown>
|
|
1152
|
-
<CopyButton
|
|
1153
|
-
text={explanation.text || ""}
|
|
1154
|
-
label="Copy explanation"
|
|
1155
|
-
/>
|
|
1156
|
-
</div>
|
|
1157
|
-
) : explanation?.status === "error" ? (
|
|
1158
|
-
<div
|
|
1159
|
-
role="alert"
|
|
1160
|
-
className="flex flex-wrap items-center gap-2 py-2 text-sm text-theme-text-secondary"
|
|
1161
|
-
>
|
|
1162
|
-
<span>
|
|
1163
|
-
{explanation.error ||
|
|
1164
|
-
"The agent did not return an explanation."}
|
|
1165
|
-
</span>
|
|
1166
|
-
{explanation.onGenerate && (
|
|
1167
|
-
<button
|
|
1168
|
-
type="button"
|
|
1169
|
-
onClick={explanation.onGenerate}
|
|
1170
|
-
className="rounded-md px-2 py-1 text-xs font-medium text-accent-text hover:bg-theme-hover"
|
|
1171
|
-
>
|
|
1172
|
-
Try again
|
|
1173
|
-
</button>
|
|
1174
|
-
)}
|
|
1175
|
-
</div>
|
|
1176
|
-
) : null}
|
|
1177
|
-
</div>
|
|
1178
|
-
</Collapse>
|
|
1179
|
-
</div>
|
|
1180
|
-
</div>
|
|
1181
|
-
</div>
|
|
1182
|
-
)}
|
|
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
|
+
)}
|
|
1183
1213
|
|
|
1184
1214
|
{showConclusion && showDisclaimer && (
|
|
1185
1215
|
<div className="flex items-start gap-1 px-0.5 text-[11px] text-theme-text-tertiary">
|
|
@@ -1204,6 +1234,7 @@ function healthFlagSentence(flag: {
|
|
|
1204
1234
|
|
|
1205
1235
|
export function AllClearCard({
|
|
1206
1236
|
diagnosis,
|
|
1237
|
+
explanation,
|
|
1207
1238
|
animate,
|
|
1208
1239
|
showDisclaimer,
|
|
1209
1240
|
coverageLimited,
|
|
@@ -1222,6 +1253,7 @@ export function AllClearCard({
|
|
|
1222
1253
|
animate: boolean;
|
|
1223
1254
|
showDisclaimer: boolean;
|
|
1224
1255
|
coverageLimited: boolean;
|
|
1256
|
+
explanation?: AssessmentExplanation;
|
|
1225
1257
|
evidenceConflict: boolean;
|
|
1226
1258
|
evidenceConflictExplainedBy?: string[];
|
|
1227
1259
|
assessmentAction?: ReactNode;
|
|
@@ -1236,9 +1268,6 @@ export function AllClearCard({
|
|
|
1236
1268
|
onRevealSource?: (sourceId: string) => void;
|
|
1237
1269
|
}) {
|
|
1238
1270
|
const storyShape = diagnosisHasStoryShape(diagnosis);
|
|
1239
|
-
const [showAnalysis, setShowAnalysis] = useState(false);
|
|
1240
|
-
const analysisReveal = useDisclosureReveal<HTMLDivElement>();
|
|
1241
|
-
const analysisId = useId();
|
|
1242
1271
|
const report =
|
|
1243
1272
|
(storyShape && !storyInline ? "" : storyPlainText(diagnosis.report)) ||
|
|
1244
1273
|
(storyShape
|
|
@@ -1283,43 +1312,14 @@ export function AllClearCard({
|
|
|
1283
1312
|
// the disclaimer are the same in both shapes.
|
|
1284
1313
|
const trailing = (
|
|
1285
1314
|
<>
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
type="button"
|
|
1295
|
-
aria-expanded={showAnalysis}
|
|
1296
|
-
aria-controls={analysisId}
|
|
1297
|
-
onClick={() => {
|
|
1298
|
-
setShowAnalysis(!showAnalysis);
|
|
1299
|
-
analysisReveal.revealAfterToggle(!showAnalysis);
|
|
1300
|
-
}}
|
|
1301
|
-
className="flex items-center gap-1.5 rounded-md py-2 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary"
|
|
1302
|
-
>
|
|
1303
|
-
<CollapseChevron open={showAnalysis} className="h-3.5 w-3.5" />
|
|
1304
|
-
{detailed ? "Full analysis" : "Assessment details"}
|
|
1305
|
-
</button>
|
|
1306
|
-
)}
|
|
1307
|
-
{assessmentAction && (
|
|
1308
|
-
<div className="ml-auto">{assessmentAction}</div>
|
|
1309
|
-
)}
|
|
1310
|
-
</div>
|
|
1311
|
-
<div id={analysisId} ref={analysisReveal.elementRef}>
|
|
1312
|
-
<Collapse open={showAnalysis}>
|
|
1313
|
-
<div className="border-t border-theme-border/60 px-3 py-2">
|
|
1314
|
-
<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">
|
|
1315
|
-
{detailed ? report : ""}
|
|
1316
|
-
</AIMarkdown>
|
|
1317
|
-
{assessmentSources}
|
|
1318
|
-
</div>
|
|
1319
|
-
</Collapse>
|
|
1320
|
-
</div>
|
|
1321
|
-
</div>
|
|
1322
|
-
) : null}
|
|
1315
|
+
<AssessmentDetails
|
|
1316
|
+
diagnosis={diagnosis}
|
|
1317
|
+
explanation={explanation}
|
|
1318
|
+
assessmentAction={assessmentAction}
|
|
1319
|
+
assessmentSources={assessmentSources}
|
|
1320
|
+
showAnalysisDisclosure={detailed}
|
|
1321
|
+
analysisText={report}
|
|
1322
|
+
/>
|
|
1323
1323
|
{showDisclaimer ? (
|
|
1324
1324
|
<div className="flex items-start gap-1 px-0.5 text-[11px] text-theme-text-tertiary">
|
|
1325
1325
|
<ShieldCheck className="mt-0.5 h-3 w-3 shrink-0" />
|
|
@@ -1442,6 +1442,9 @@ export function AllClearCard({
|
|
|
1442
1442
|
// all-clear) — so "I couldn't tell" never reads as "you're fine."
|
|
1443
1443
|
export function InconclusiveCard({
|
|
1444
1444
|
diagnosis,
|
|
1445
|
+
explanation,
|
|
1446
|
+
assessmentSources,
|
|
1447
|
+
assessmentAction,
|
|
1445
1448
|
animate,
|
|
1446
1449
|
storyInline = false,
|
|
1447
1450
|
revisedAfter,
|
|
@@ -1451,7 +1454,10 @@ export function InconclusiveCard({
|
|
|
1451
1454
|
diagnosis: Diagnosis;
|
|
1452
1455
|
animate: boolean;
|
|
1453
1456
|
storyInline?: boolean;
|
|
1457
|
+
explanation?: AssessmentExplanation;
|
|
1454
1458
|
revisedAfter?: string;
|
|
1459
|
+
assessmentSources?: ReactNode;
|
|
1460
|
+
assessmentAction?: ReactNode;
|
|
1455
1461
|
/** Reads Radar could not complete for this assessment; listed under Still open. */
|
|
1456
1462
|
assessmentLimits?: string[];
|
|
1457
1463
|
assessmentCopy?: Pick<AssessmentCopyRadar, "context" | "receipts">;
|
|
@@ -1496,6 +1502,12 @@ export function InconclusiveCard({
|
|
|
1496
1502
|
addressing any errors shown in Activity.
|
|
1497
1503
|
</span>
|
|
1498
1504
|
</div>
|
|
1505
|
+
<AssessmentDetails
|
|
1506
|
+
diagnosis={diagnosis}
|
|
1507
|
+
explanation={explanation}
|
|
1508
|
+
assessmentSources={assessmentSources}
|
|
1509
|
+
assessmentAction={assessmentAction}
|
|
1510
|
+
/>
|
|
1499
1511
|
</div>
|
|
1500
1512
|
);
|
|
1501
1513
|
}
|
|
@@ -1547,10 +1559,10 @@ export function remediationHeadline(step: string): string {
|
|
|
1547
1559
|
*/
|
|
1548
1560
|
export function remediationCommands(step: string): string[] {
|
|
1549
1561
|
const commands: string[] = [];
|
|
1550
|
-
|
|
1562
|
+
const normalized = tidyFences(step);
|
|
1551
1563
|
const code = /```[a-zA-Z]*\n([\s\S]*?)```|`([^`\n]+)`/g;
|
|
1552
1564
|
let match: RegExpExecArray | null;
|
|
1553
|
-
while ((match = code.exec(
|
|
1565
|
+
while ((match = code.exec(normalized))) {
|
|
1554
1566
|
if (match[1] !== undefined) {
|
|
1555
1567
|
const body = match[1].trim();
|
|
1556
1568
|
if (body) commands.push(body);
|