diff --git a/application/single_app/config.py b/application/single_app/config.py
index 4e90e6aef..77f9f7c89 100644
--- a/application/single_app/config.py
+++ b/application/single_app/config.py
@@ -103,7 +103,7 @@
EXECUTOR_MAX_WORKERS = 30
CHAT_STREAM_EXECUTOR_MAX_WORKERS = max(8, int(os.getenv('SIMPLECHAT_CHAT_STREAM_WORKERS', '32') or '32'))
SESSION_TYPE = 'filesystem'
-VERSION = "0.261.423"
+VERSION = "0.261.424"
IS_DEVELOPMENT = is_development_env_enabled()
# Opt-out for deployments where App Service Easy Auth is active but the platform
diff --git a/application/single_app/functions_orchestration_result_runtime.py b/application/single_app/functions_orchestration_result_runtime.py
index 2a860bef4..e268320eb 100644
--- a/application/single_app/functions_orchestration_result_runtime.py
+++ b/application/single_app/functions_orchestration_result_runtime.py
@@ -1,7 +1,7 @@
# functions_orchestration_result_runtime.py
"""Typed runtime handoffs; no clients, model calls, publication, or new storage.
-Version: 0.261.365
+Version: 0.261.424
"""
from copy import deepcopy
@@ -274,13 +274,39 @@ def read_result_source_coverage(context, reference):
return None
+def _native_inventory_evidence_state(prepared, document_id):
+ """An inspected empty inventory is evidence of absence, not missing excerpts."""
+ coverage = prepared.get('coverage')
+ inventories = coverage.get('native_inspection') if type(coverage) is dict else None
+ if type(inventories) is not list:
+ return 'unknown'
+ matches = [
+ item for item in inventories
+ if type(item) is dict and item.get('document_id') == document_id
+ ]
+ if len(matches) != 1:
+ return 'unknown'
+ inventory = matches[0]
+ if inventory.get('execution_mode') != 'native_inspection' or inventory.get('section') != 'comments':
+ return 'unknown'
+ total, returned = inventory.get('total_records'), inventory.get('returned_records')
+ if type(total) is not int or type(returned) is not int or not 0 <= returned <= total:
+ return 'unknown'
+ flags = ('partial', 'has_more', 'limit_reached')
+ if returned < total or any(inventory.get(flag) is True for flag in flags):
+ return 'partial'
+ if inventory.get('inventory_complete') is True and all(inventory.get(flag) is False for flag in flags):
+ return 'complete'
+ return 'unknown'
+
+
def _read_result_document_citations(service, reference, *, evidence_status_out=None):
root = service.open_result(reference, allow_partial=True, require_current_sources=True)
if root.metadata()['source_count'] == 0:
return []
sources, citations, visited, expanded = {}, {}, set(), set()
coverage_values = {}
- search_counts, source_names = {}, {}
+ search_counts, source_names, source_outcomes = {}, {}, {}
citation_fields = (
'citation_id', 'chunk_id', 'file_name', 'title', 'page_number', 'chunk_sequence',
'sheet_name', 'location_label', 'location_value', 'classification', 'document_classification', 'score',
@@ -327,6 +353,7 @@ def visit(current):
visit(ResultRef.from_dict(parent))
retained_citations = []
native_reference = None
+ producer_counts = {}
if (
producer.capability_id == 'document_search'
and (current.output_name, current.kind) in (
@@ -360,7 +387,9 @@ def visit(current):
if prepared['content_scope'] in ('native_excel_annotations', 'native_document_annotations'):
native_reference = prepared_reader.reference.to_dict()
for source in lineage['sources']:
- search_counts.setdefault(source_key(source), set())
+ identity = source_key(source)
+ search_counts.setdefault(identity, set())
+ producer_counts[identity] = set()
elif producer.capability_id == CAPABILITY_ACTION_INVOKE and lineage['sources']:
prepared = read_complete_input(reader)
if (
@@ -419,14 +448,21 @@ def visit(current):
producer.capability_id == 'document_search' and identity_key in search_counts
and citation.get('evidence_kind') != 'native_annotation_coverage'
):
- search_counts[identity_key].add((
+ evidence_key = (
producer, current.manifest_sha256,
canonical_digest({name: value for name, value in projected.items() if name != 'evidence_id'}),
- ))
+ )
+ search_counts[identity_key].add(evidence_key)
+ producer_counts[identity_key].add(evidence_key)
identity = {key: value for key, value in projected.items() if key != 'evidence_id'}
citations.setdefault(source_key(matches[0]), {}).setdefault(
canonical_digest(identity), projected,
)
+ for identity, counts in producer_counts.items():
+ state = _native_inventory_evidence_state(prepared, identity[2]) if native_reference is not None else (
+ 'complete' if counts else 'missing'
+ )
+ source_outcomes.setdefault(identity, {})[key] = state
reader.recheck()
visit(reference)
@@ -440,15 +476,20 @@ def visit(current):
rows = []
for index, (identity, source) in enumerate(sources.items(), start=1):
count = len(search_counts[identity]) if identity in search_counts else None
+ outcome = evidence_status_from_sources([
+ {'state': state} for state in source_outcomes.get(identity, {}).values()
+ ])
row = {
"name": str(source_names.get(identity) or source.get("file_name") or f"Selected source {index}")[:160],
"document_id": source["document_id"],
- "state": "missing" if count == 0 else "complete" if count is not None else "unknown",
+ "state": outcome["state"] if outcome is not None else "unknown",
}
if count is not None:
row["evidence_count"] = count
rows.append(row)
- evidence_status_out["value"] = evidence_status_from_sources(rows)
+ evidence_status_out["value"] = evidence_status_from_sources(
+ rows, partial=root.completeness.status == 'partial',
+ )
return projected
diff --git a/application/single_app/static/js/chat/chat-analysis-results.js b/application/single_app/static/js/chat/chat-analysis-results.js
index 5996caa98..3f37db6bd 100644
--- a/application/single_app/static/js/chat/chat-analysis-results.js
+++ b/application/single_app/static/js/chat/chat-analysis-results.js
@@ -1,5 +1,5 @@
// chat-analysis-results.js
-// Saved Analyze findings supplement the answer; only an authorized complete-record page is read.
+// Saved Analyze findings live in Sources; only an authorized complete-record page is read.
const pageSize = 25;
const validationNotices = {
@@ -96,6 +96,56 @@ export function readSavedAnalyses(metadata) {
return results;
}
+function savedAnalysisLimitation(descriptors, unavailable) {
+ if (unavailable || descriptors.some(descriptor => !descriptor || descriptor.available === false)) {
+ return 'Saved analysis unavailable';
+ }
+ const labels = {
+ invalid: 'Analysis validation failed',
+ partial: 'Partial analysis',
+ pending: 'Analysis validation pending',
+ not_validated: 'Analysis not validated',
+ };
+ for (const status of Object.keys(labels)) {
+ if (descriptors.some(descriptor => descriptor?.validation_status === status)) {
+ return labels[status];
+ }
+ }
+ return null;
+}
+
+function renderAnalysisWarning(message) {
+ const label = savedAnalysisLimitation(message._savedAnalysisDescriptors,
+ message.dataset.savedAnalysisUnavailable === 'true');
+ let warning = message._savedAnalysisWarning;
+ if (!label) {
+ warning?.remove();
+ message._savedAnalysisWarning = null;
+ return;
+ }
+ if (!warning) {
+ warning = node('div', 'saved-analysis-warning mt-2');
+ warning.setAttribute('role', 'status');
+ const button = node('button', 'btn btn-sm alert alert-warning py-1 px-2 mb-0 text-start');
+ button.type = 'button';
+ button.addEventListener('click', () => {
+ const toggle = message._savedAnalysisSources.parentElement.querySelector(
+ ':scope > .message-footer .citation-toggle-btn',
+ );
+ if (toggle?.getAttribute('aria-expanded') !== 'true') {
+ toggle?.click();
+ }
+ });
+ warning.append(button);
+ message._savedAnalysisSources.parentElement.querySelector(':scope > .message-text')?.after(warning);
+ message._savedAnalysisWarning = warning;
+ }
+ const button = warning.querySelector('button');
+ button.textContent = `${label}. View sources`;
+ button.setAttribute('aria-controls', message._savedAnalysisSources.id);
+ warning.classList.toggle('d-none', message.dataset.savedAnalysisMasked === 'true');
+}
+
export function syncSavedAnalysisConversation(conversationId = activeConversationId()) {
const next = String(conversationId || '').trim() || null;
if (contextConversationId !== next) {
@@ -304,8 +354,9 @@ export function disposeSavedAnalysisViews() {
function hideUnavailableActions(message) {
message.dataset.savedAnalysisUnavailable = 'true';
- message.querySelectorAll('.saved-analysis-downloads, .generated-tabular-outputs-container, .assistant-inline-export-actions, .message-footer')
+ message.querySelectorAll('.saved-analysis-downloads, .generated-tabular-outputs-container, .assistant-inline-export-actions, .message-footer .message-actions')
.forEach(element => element.classList.add('d-none'));
+ renderAnalysisWarning(message);
}
export function hydrateSavedAnalysisResult(message, fullMessage) {
@@ -315,18 +366,32 @@ export function hydrateSavedAnalysisResult(message, fullMessage) {
}
message.dataset.savedAnalysis = 'true';
message._savedAnalysisDescriptors = descriptors;
- let anchor = message.querySelector('.message-text');
+ const sources = message.querySelector(':scope > .message-content > .message-bubble > .message-sources');
+ message._savedAnalysisSources = sources;
for (const descriptor of descriptors) {
- anchor = hydrateAnalysisResult(message, descriptor, anchor);
+ hydrateAnalysisResult(message, descriptor, sources);
}
+ renderAnalysisWarning(message);
+ message.addEventListener('chat:sources-toggled', event => {
+ if (!event.detail.open) {
+ views.forEach(view => {
+ if (message.contains(view.root)) {
+ view.dispose();
+ }
+ });
+ sources.querySelectorAll('.saved-analysis-result details[open]').forEach(details => {
+ details.open = false;
+ });
+ }
+ });
}
-function hydrateAnalysisResult(message, descriptor, anchor) {
- const root = node('section', 'saved-analysis-result border rounded p-3 mt-3');
+function hydrateAnalysisResult(message, descriptor, sources) {
+ const root = node('section', 'saved-analysis-result border-top pt-3 mt-3');
root.setAttribute('aria-label', descriptor?.label ? `Saved analysis: ${descriptor.label}` : 'Saved analysis');
const heading = node('h3', 'h6 mb-1 text-break', descriptor?.label || 'Saved analysis');
root.append(heading);
- anchor?.after(root);
+ sources.append(root);
const controllers = new Set();
const view = { root, dispose: () => {
controllers.forEach(controller => controller.abort());
@@ -380,7 +445,7 @@ function hydrateAnalysisResult(message, descriptor, anchor) {
}
};
const status = node('p', 'small text-muted mb-1',
- `0 of ${descriptor.record_count} records displayed · ${descriptor.source_count} sources in the saved analysis.`);
+ `${descriptor.record_count} saved findings · ${descriptor.source_count} sources.`);
status.setAttribute('role', 'status');
status.setAttribute('aria-live', 'polite');
const validationNotice = status => descriptor.semantics?.mode === 'source_observations'
@@ -544,7 +609,7 @@ function hydrateAnalysisResult(message, descriptor, anchor) {
} else {
view.dispose();
content.replaceChildren();
- status.textContent = `0 of ${descriptor.record_count} records displayed · ${descriptor.source_count} sources in the saved analysis.`;
+ status.textContent = `${descriptor.record_count} saved findings · ${descriptor.source_count} sources.`;
}
});
retry.addEventListener('click', () => void loadPage());
@@ -582,6 +647,7 @@ export function setSavedAnalysisMasked(message, metadata) {
element.classList.toggle('d-none', masked || (element.classList.contains('saved-analysis-downloads') &&
message.dataset.savedAnalysisUnavailable === 'true'));
});
+ message._savedAnalysisWarning?.classList.toggle('d-none', masked);
if (masked && message._savedAnalysisDescriptors.some(descriptor => sameAnalysis(selectedContext, descriptor))) {
clearSavedAnalysisContext();
}
diff --git a/application/single_app/static/js/chat/chat-messages.js b/application/single_app/static/js/chat/chat-messages.js
index a9bfaa142..84858d06f 100644
--- a/application/single_app/static/js/chat/chat-messages.js
+++ b/application/single_app/static/js/chat/chat-messages.js
@@ -6144,13 +6144,11 @@ export function appendMessage(
`Condition check (Boolean(citationsButtonsHtml) && hasAnyCitations): ${shouldShowCitations}`
);
- if (shouldShowCitations) {
+ const hasSavedAnalysis = readSavedAnalyses(fullMessageObject?.metadata).length > 0;
+ if (shouldShowCitations || hasSavedAnalysis) {
console.log(">>> Will generate and include citation elements.");
- const citationsContainerId = `citations-${messageId || Date.now()}`;
- citationToggleHtml = ``;
- // citationsButtonsHtml already contains a
wrapper
- // Just add ID and display style by wrapping minimally
- citationContentContainerHtml = `
${citationsButtonsHtml}
`;
+ citationToggleHtml = '
';
+ citationContentContainerHtml = '
';
} else {
console.log(">>> Will NOT generate citation elements.");
}
@@ -6185,6 +6183,16 @@ export function appendMessage(
`;
+ const sourcesContainer = messageDiv.querySelector(':scope > .message-content > .message-bubble > .message-sources');
+ const sourcesToggle = messageDiv.querySelector(
+ ':scope > .message-content > .message-bubble > .message-footer .citation-toggle-btn',
+ );
+ if (sourcesContainer) {
+ sourcesContainer.id = `citations-${messageId || Date.now()}`;
+ sourcesContainer.innerHTML = sanitizeHtmlSafely(citationsButtonsHtml);
+ sourcesToggle.setAttribute('aria-controls', sourcesContainer.id);
+ }
+
messageDiv.dataset.replySenderName = stripHtmlTags(senderLabel).replace(/\s+/g, " ").trim() || "AI";
messageDiv.dataset.replyPreviewText = buildPlainTextPreview(renderedAiContent.previewMarkdown);
@@ -6381,24 +6389,25 @@ export function appendMessage(
showToast("Failed to copy text.", "warning");
});
});
- const toggleBtn = messageDiv.querySelector(".citation-toggle-btn");
+ const toggleBtn = sourcesToggle;
if (toggleBtn) {
toggleBtn.addEventListener("click", () => {
/* ... toggle logic ... */
- const targetId = toggleBtn.getAttribute("aria-controls");
- const citationsContainer = messageDiv.querySelector(`#${targetId}`);
+ const citationsContainer = sourcesContainer;
if (!citationsContainer) return;
// Store current scroll position to maintain user's view
const currentScrollTop = document.getElementById('chat-messages-container')?.scrollTop || window.pageYOffset;
- const isExpanded = citationsContainer.style.display !== "none";
- citationsContainer.style.display = isExpanded ? "none" : "block";
+ const isExpanded = !citationsContainer.classList.contains('d-none');
+ citationsContainer.classList.toggle('d-none', isExpanded);
toggleBtn.setAttribute("aria-expanded", !isExpanded);
toggleBtn.title = isExpanded ? "Show sources" : "Hide sources";
+ toggleBtn.setAttribute("aria-label", toggleBtn.title);
toggleBtn.innerHTML = isExpanded
? ''
: '';
+ messageDiv.dispatchEvent(new CustomEvent('chat:sources-toggled', { detail: { open: !isExpanded } }));
// Note: Removed scrollChatToBottom() to prevent jumping when expanding citations
// Restore scroll position after DOM changes
diff --git a/application/v2_ui/src/components/chat/AnalysisResult.tsx b/application/v2_ui/src/components/chat/AnalysisResult.tsx
index 7c876800f..5ae7ea84d 100644
--- a/application/v2_ui/src/components/chat/AnalysisResult.tsx
+++ b/application/v2_ui/src/components/chat/AnalysisResult.tsx
@@ -1,5 +1,5 @@
// AnalysisResult.tsx
-// Lazy, complete-record pages supporting the readable answer above them.
+// Lazy, complete-record pages in a message's Sources disclosure.
import { useCallback, useEffect, useState } from 'react';
import { fetchAnalysisEvidence, fetchAnalysisRecords } from '../../lib/endpoints';
@@ -99,9 +99,11 @@ function Evidence({
export function AnalysisResult({
descriptor,
onUnavailable,
+ knownUnavailable,
}: {
descriptor: SavedAnalysisDescriptor;
- onUnavailable: (descriptor: SavedAnalysisDescriptor) => void;
+ onUnavailable?: (descriptor: SavedAnalysisDescriptor, message: string) => void;
+ knownUnavailable?: string;
}) {
const [open, setOpen] = useState(false);
const [offset, setOffset] = useState(0);
@@ -113,12 +115,12 @@ export function AnalysisResult({
const activeConversationId = useChatStore((state) => state.activeConversationId);
const selectAnalysisResult = useChatStore((state) => state.selectAnalysisResult);
const belongsHere = descriptor.conversation_id === activeConversationId;
- const blocked = descriptor.available === false || !belongsHere || Boolean(unavailable);
+ const blocked = descriptor.available === false || !belongsHere || Boolean(unavailable || knownUnavailable);
const reportUnavailable = useCallback((message: string) => {
setUnavailable(message);
setPage(null);
- onUnavailable(descriptor);
+ onUnavailable?.(descriptor, message);
const store = useChatStore.getState();
if (sameAnalysis(store.analysisResultContext, descriptor)) {
store.clearAnalysisResultContext();
@@ -158,11 +160,11 @@ export function AnalysisResult({
return (
- {descriptor.label || 'Saved analysis'}
+ className="min-w-0 border-b border-edge pb-4">
+ {descriptor.label || 'Saved analysis'}
{blocked ? (
- {unavailable || analysisUnavailableMessage()}
+ {unavailable || knownUnavailable || analysisUnavailableMessage()}
) : (
<>
diff --git a/application/v2_ui/src/components/chat/EvidenceStatus.tsx b/application/v2_ui/src/components/chat/EvidenceStatus.tsx
index ee57e5141..665970954 100644
--- a/application/v2_ui/src/components/chat/EvidenceStatus.tsx
+++ b/application/v2_ui/src/components/chat/EvidenceStatus.tsx
@@ -14,20 +14,23 @@ export function EvidenceSourceBadge({ source }: { source: EvidenceSource }) {
);
}
-export function EvidenceStatusFooter({ status, expanded, onOpen, controlsId }: {
+export function EvidenceStatusFooter({ status, analysisNotice, expanded, onOpen, controlsId }: {
status: EvidenceStatus | null; expanded: boolean; onOpen: () => void; controlsId?: string;
+ analysisNotice?: string | null;
}) {
- if (!status || status.state === 'complete' || status.state === 'unknown') return null;
+ const state = status && status.state !== 'complete' && status.state !== 'unknown' ? status.state : null;
+ const label = state ? EVIDENCE_LABELS[state] : analysisNotice;
+ if (!label) return null;
return (
);
@@ -47,7 +50,11 @@ export function EvidenceStatusDetails({ status }: { status: EvidenceStatus | nul
{source.name}
- {source.evidence_count !== undefined && {source.evidence_count} evidence item{source.evidence_count === 1 ? '' : 's'} returned}
+ {source.evidence_count !== undefined &&
+ {source.state === 'complete' && source.evidence_count === 0
+ ? 'Inspection completed with no matching records'
+ : `${source.evidence_count} evidence item${source.evidence_count === 1 ? '' : 's'} returned`}
+ }
))}
diff --git a/application/v2_ui/src/components/chat/MessageActions.tsx b/application/v2_ui/src/components/chat/MessageActions.tsx
index 90ed03459..b590d4f73 100644
--- a/application/v2_ui/src/components/chat/MessageActions.tsx
+++ b/application/v2_ui/src/components/chat/MessageActions.tsx
@@ -668,6 +668,8 @@ export function MessageActions({
: 'Show sources'
}
active={inspector === 'sources'}
+ expanded={inspector === 'sources'}
+ controls={`message-inspector-${message.id}`}
onClick={() => inspect('sources')}
>
diff --git a/application/v2_ui/src/components/chat/MessageInspector.tsx b/application/v2_ui/src/components/chat/MessageInspector.tsx
index 64c0c0718..e27beedfe 100644
--- a/application/v2_ui/src/components/chat/MessageInspector.tsx
+++ b/application/v2_ui/src/components/chat/MessageInspector.tsx
@@ -4,7 +4,7 @@
// One panel with three sections rather than three separate panels: a user comparing what a
// response cited against how it was generated should not have to close one to open another.
-import { useEffect, useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import { clsx } from 'clsx';
import {
Braces,
@@ -31,7 +31,9 @@ import {
import { normalizeOrchestrationAttempt } from '../../lib/orchestration';
import { openOrchestrationRecovery } from '../../lib/orchestrationController';
import { m365ChipLabel, m365SecondaryLine } from '../../lib/m365Citations';
-import type { ChatMessage, Json, PersistedThought } from '../../lib/types';
+import type { ChatMessage, Json, PersistedThought, SavedAnalysisDescriptor } from '../../lib/types';
+import { analysisUnavailableMessage, readSavedAnalyses } from '../../lib/savedAnalysis';
+import { AnalysisResult } from './AnalysisResult';
import { buildToolResultView, type RowMode } from '../../lib/agentCitationRows';
import { GlassButton } from '../ui/primitives';
import { M365KindIcon, M365OpenLink } from './M365CitationChip';
@@ -170,15 +172,32 @@ function DetailsSection({ message }: { message: ChatMessage }) {
);
}
-function SourcesSection({ sources, evidence }: { sources: MessageSources; evidence: EvidenceStatus | null }) {
- if (sources.total === 0 && !evidence) {
+function SourcesSection({ sources, evidence, analyses, onAnalysisUnavailable, unavailableAnalyses }: {
+ sources: MessageSources;
+ evidence: EvidenceStatus | null;
+ analyses: (SavedAnalysisDescriptor | null)[];
+ onAnalysisUnavailable?: (descriptor: SavedAnalysisDescriptor, message: string) => void;
+ unavailableAnalyses?: ReadonlyMap;
+}) {
+ if (sources.total === 0 && !evidence && !analyses.length) {
return This response did not cite any sources.;
}
return (
- {sources.total === 0 &&
This response did not cite any sources.}
+ {analyses.map((descriptor, index) => descriptor ? (
+
+ ) : (
+
+ {analysisUnavailableMessage()}
+
+ ))}
+ {sources.total === 0 && !analyses.length &&
This response did not cite any sources.}
{sources.conversations.length > 0 && (
@@ -454,15 +473,21 @@ export function MessageInspector({
section,
onSection,
onClose,
+ onAnalysisUnavailable,
+ unavailableAnalyses,
}: {
message: ChatMessage;
section: InspectorSection;
onSection: (next: InspectorSection) => void;
onClose: () => void;
+ onAnalysisUnavailable?: (descriptor: SavedAnalysisDescriptor, message: string) => void;
+ unavailableAnalyses?: ReadonlyMap;
}) {
const sources = readSources(message as unknown as Json);
const masks = readMaskState(message);
const evidence = masks.hasAnyMask ? null : readEvidenceStatus(message.metadata?.evidence_status);
+ const analyses = useMemo(() => masks.hasAnyMask ? [] : readSavedAnalyses(message.metadata),
+ [message.metadata, masks.hasAnyMask]);
// An uploaded file cites nothing and has no reasoning. Its sources are what ingestion
// extracted from it, which is the material any later citation of the file comes from.
const upload = message.role === 'file';
@@ -527,7 +552,9 @@ export function MessageInspector({
This upload has no stored extraction to show.
)
)}
- {active === 'sources' && !upload && }
+ {active === 'sources' && !upload && }
{active === 'reasoning' && }
diff --git a/application/v2_ui/src/components/chat/MessageList.tsx b/application/v2_ui/src/components/chat/MessageList.tsx
index b8713b663..f445d11ad 100644
--- a/application/v2_ui/src/components/chat/MessageList.tsx
+++ b/application/v2_ui/src/components/chat/MessageList.tsx
@@ -43,7 +43,6 @@ import {
import { InlineMapCards } from './InlineMapCard';
import { ChatFilePreview } from './ChatFilePreview';
import { GeneratedArtifactCard } from './GeneratedArtifactCard';
-import { AnalysisResult } from './AnalysisResult';
import type { SavedAnalysisDescriptor } from '../../lib/types';
import { IconButton, MessageActions } from './MessageActions';
import { isCheckingFailureNotice, OrchestrationMessageRecovery } from './OrchestrationRecoveryNotice';
@@ -116,7 +115,7 @@ import { orchestrationStartedWorkflow } from '../../lib/orchestrationWorkflowRun
import { orchestrationHandedOffWorkflow } from '../../lib/workflowHandoffs';
import { readWorkflowDelivery } from '../../lib/workflowDelivery';
import { workflowRunTrackerShouldRun } from '../../lib/workflowRunTracker';
-import { analysisUnavailableMessage, readSavedAnalyses, sameAnalysis } from '../../lib/savedAnalysis';
+import { readSavedAnalyses, sameAnalysis, savedAnalysisLimitation } from '../../lib/savedAnalysis';
import { readMessagePrompt } from '../../lib/messagePrompt';
import { PromptCard } from './PromptCard';
import {
@@ -916,14 +915,14 @@ function MessageBubbleInner({
const managedOutputs = orchestration.outputs !== undefined || liveOutputs !== undefined;
const savedAnalyses = useMemo(() => readSavedAnalyses(message.metadata), [message.metadata]);
const hasSavedAnalysis = savedAnalyses.length > 0;
- const [unavailableAnalysisKeys, setUnavailableAnalysisKeys] = useState>(() => new Set());
- const markAnalysisUnavailable = useCallback((descriptor: SavedAnalysisDescriptor) => {
+ const [unavailableAnalyses, setUnavailableAnalyses] = useState