Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions frontend/src/components/report/SummaryPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ const SummaryPanel = ({
// Fallback: highlights (keyPoints) and SAST/engine keyFindings for concerns
const { oneLiner, keyPoints } = normalizeHighlights(rawScanResult);

// Partial-failure warnings surfaced from backend LLM nodes
const partialFailures = Array.isArray(rawScanResult?.partial_failures)
? rawScanResult.partial_failures
: [];

// SAST/engine keyFindings – use for Quick Summary concerns when they add value
const engineConcerns = (keyFindings || [])
.filter(f => f.severity === 'high' || f.severity === 'medium')
Expand Down Expand Up @@ -59,6 +64,17 @@ const SummaryPanel = ({
);
};

const partialFailureBanner = partialFailures.length > 0 ? (
<div className="summary-partial-failures" role="alert" aria-label="Some analysis features are temporarily unavailable">
{partialFailures.map((msg, idx) => (
<div key={idx} className="summary-partial-failure-item">
<span className="summary-partial-failure-icon">⚠️</span>
<span className="summary-partial-failure-text">{msg}</span>
</div>
))}
</div>
) : null;

if (showPlaceholder) {
return (
<section className="summary-panel summary-panel--unified">
Expand All @@ -69,6 +85,7 @@ const SummaryPanel = ({
</h2>
{getDecisionBadge()}
</div>
{partialFailureBanner}
<div className="summary-content">
<div className="summary-placeholder-wrapper">
<p className="summary-placeholder-line">Review this extension before installing.</p>
Expand All @@ -94,6 +111,20 @@ const SummaryPanel = ({
}

if (!hasAnySummary) {
if (partialFailures.length > 0) {
return (
<section className="summary-panel summary-panel--unified">
<div className="summary-header">
<h2 className="summary-title">
<span className="title-icon">✨</span>
Quick Summary
</h2>
{getDecisionBadge()}
</div>
{partialFailureBanner}
</section>
);
}
return null;
}

Expand All @@ -113,6 +144,7 @@ const SummaryPanel = ({
</h2>
{getDecisionBadge()}
</div>
{partialFailureBanner}

<div className="summary-content">
{/* Headline – short takeaway */}
Expand Down Expand Up @@ -189,6 +221,7 @@ const SummaryPanel = ({
</h2>
{getDecisionBadge()}
</div>
{partialFailureBanner}

<div className="summary-content">
{/* Verdict - the headline */}
Expand Down Expand Up @@ -272,6 +305,7 @@ const SummaryPanel = ({
</h2>
{getDecisionBadge()}
</div>
{partialFailureBanner}

<div className="summary-content">
{/* One-liner summary */}
Expand Down
41 changes: 41 additions & 0 deletions frontend/src/components/report/SummaryPanel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,47 @@
}
}

// Partial-failure warning banner (LLM analyses that could not complete)
.summary-panel .summary-partial-failures {
display: flex;
flex-direction: column;
gap: 6px;
margin: 0 0 14px 0;
padding: 12px 16px;
background: rgba(234, 179, 8, 0.08);
border: 1px solid rgba(234, 179, 8, 0.35);
border-radius: 10px;

.summary-partial-failure-item {
display: flex;
align-items: flex-start;
gap: 8px;
}

.summary-partial-failure-icon {
flex-shrink: 0;
font-size: 14px;
line-height: 1.5;
}

.summary-partial-failure-text {
font-size: var(--report-text-sm, 0.875rem);
line-height: 1.5;
color: rgba(253, 224, 71, 0.9);
font-weight: 500;
}
}

// Light theme overrides for partial-failure banner
.light .summary-panel .summary-partial-failures {
background: rgba(234, 179, 8, 0.06);
border-color: rgba(161, 120, 0, 0.35);

.summary-partial-failure-text {
color: #92600a;
}
}

// Light theme: decision-badge text WCAG-compliant, matches risk colors on light bg
.light .summary-panel .decision-badge {
&.decision-badge--allow {
Expand Down
2 changes: 2 additions & 0 deletions src/extension_shield/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1562,6 +1562,8 @@ async def run_analysis_workflow(url: str, extension_id: str):
"publisher_disclosures": build_publisher_disclosures(
metadata, final_state.get("governance_bundle")
),
# Partial-failure warnings from LLM nodes (empty list = all LLM analyses succeeded)
"partial_failures": final_state.get("llm_warnings") or [],
}

# Final sanitization pass to ensure JSON-serializability
Expand Down
16 changes: 15 additions & 1 deletion src/extension_shield/workflow/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,9 +432,13 @@ def summary_generation_node(state: WorkflowState) -> Command:
logger.warning("Summary generation failed, using fallback: %s", exc)
executive_summary = None

warnings = list(state.get("llm_warnings") or [])
if executive_summary is None:
warnings.append("Summary unavailable — LLM service temporarily failed")

return Command(
goto=IMPACT_ANALYSIS_NODE,
update={"executive_summary": executive_summary},
update={"executive_summary": executive_summary, "llm_warnings": warnings},
)


Expand Down Expand Up @@ -481,11 +485,16 @@ def impact_analysis_node(state: WorkflowState) -> Command:
updated_results = dict(analysis_results)
updated_results["impact_analysis"] = impact_analysis

warnings = list(state.get("llm_warnings") or [])
if impact_analysis is None:
warnings.append("Impact analysis unavailable — LLM service temporarily failed")

return Command(
goto=PRIVACY_COMPLIANCE_NODE,
update={
"analysis_results": updated_results,
"impact_analysis": impact_analysis,
"llm_warnings": warnings,
},
)

Expand Down Expand Up @@ -528,11 +537,16 @@ def privacy_compliance_node(state: WorkflowState) -> Command:
updated_results = dict(analysis_results)
updated_results["privacy_compliance"] = privacy_compliance

warnings = list(state.get("llm_warnings") or [])
if privacy_compliance is None:
warnings.append("Privacy compliance unavailable — LLM service temporarily failed")

return Command(
goto=GOVERNANCE_NODE,
update={
"analysis_results": updated_results,
"privacy_compliance": privacy_compliance,
"llm_warnings": warnings,
},
)

Expand Down
5 changes: 5 additions & 0 deletions src/extension_shield/workflow/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ class WorkflowState(TypedDict):
start_time (Optional[str]): ISO 8601 formatted start time of the workflow,
if available.
end_time (Optional[str]): ISO 8601 formatted end time of the workflow, if available.
llm_warnings (Optional[list]): Warning messages collected when LLM nodes fail
partially (e.g. summary or impact analysis unavailable). Each entry is a
human-readable string suitable for surfacing to the user.
error (Optional[str]): Error message if the workflow has failed, otherwise None.
"""

Expand All @@ -64,6 +67,8 @@ class WorkflowState(TypedDict):
governance_verdict: Optional[str]
governance_report: Optional[Dict]
governance_error: Optional[str]
# Partial-failure warnings accumulated by LLM nodes
llm_warnings: Optional[list]
# Status fields
status: WorkflowStatus
start_time: Optional[str]
Expand Down
Loading