Skip to content

Commit de6d186

Browse files
committed
Merge branch 'feature/download-results' into 'develop'
Feature/download results See merge request genaiic-reusable-assets/engagement-artifacts/genaiic-idp-accelerator!464
2 parents 634ce10 + 282dce2 commit de6d186

File tree

3 files changed

+181
-9
lines changed

3 files changed

+181
-9
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ SPDX-License-Identifier: MIT-0
77

88
### Added
99

10+
- **Section Data Download Feature for Document Results Export**
11+
- Added compact "Download" dropdown button in Document Sections panel for exporting section processing results
12+
- **Two Download Options**:
13+
- "Download Data" - Downloads prediction results from OutputBucket (always available)
14+
- "Download Baseline" - Downloads baseline/ground truth data from EvaluationBaselineBucket (only shown when baseline exists)
15+
1016
- **Configuration Library Import Feature for Enhanced Configuration Management**
1117
- Added Configuration Library browser enabling users to import pre-configured document processing workflows directly from the solution's configuration library
1218
- **Dual Import Options**: Users can now choose between importing from local files (existing) or from the Configuration Library (new)

src/ui/src/components/document-viewer/JSONViewer.jsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ const FileEditorView = ({ fileContent, onChange, isReadOnly = true, fileType = '
523523
);
524524
};
525525

526-
const JSONViewer = ({ fileUri, fileType = 'text', buttonText = 'View File', sectionData }) => {
526+
const JSONViewer = ({ fileUri, fileType = 'text', buttonText = 'View File', sectionData, onOpen, onClose }) => {
527527
const [fileContent, setFileContent] = useState(null);
528528
const [isLoading, setIsLoading] = useState(false);
529529
const [error, setError] = useState(null);
@@ -553,6 +553,11 @@ const JSONViewer = ({ fileUri, fileType = 'text', buttonText = 'View File', sect
553553
logger.debug('Received content:', `${fetchedContent.substring(0, 100)}...`);
554554
setFileContent(fetchedContent);
555555
setEditedContent(fetchedContent); // Initialize edited content for always-on edit mode
556+
557+
// Notify parent that viewer is now open
558+
if (onOpen) {
559+
onOpen();
560+
}
556561
} catch (err) {
557562
logger.error('Error fetching content:', err);
558563
setError(`Failed to load ${fileType} content. Please try again.`);
@@ -640,6 +645,11 @@ const JSONViewer = ({ fileUri, fileType = 'text', buttonText = 'View File', sect
640645
const closeViewer = () => {
641646
setFileContent(null);
642647
setEditedContent(null);
648+
649+
// Notify parent that viewer is now closed
650+
if (onClose) {
651+
onClose();
652+
}
643653
};
644654

645655
if (!fileUri) {

src/ui/src/components/sections-panel/SectionsPanel.jsx

Lines changed: 164 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Table,
1111
StatusIndicator,
1212
Button,
13+
ButtonDropdown,
1314
Header,
1415
FormField,
1516
Select,
@@ -19,14 +20,17 @@ import {
1920
Alert,
2021
} from '@cloudscape-design/components';
2122
import { generateClient } from 'aws-amplify/api';
23+
import { ConsoleLogger } from 'aws-amplify/utils';
2224

2325
import FileViewer from '../document-viewer/JSONViewer';
2426
import { getSectionConfidenceAlertCount, getSectionConfidenceAlerts } from '../common/confidence-alerts-utils';
2527
import useConfiguration from '../../hooks/use-configuration';
2628
import useSettingsContext from '../../contexts/settings';
2729
import processChanges from '../../graphql/queries/processChanges';
30+
import getFileContents from '../../graphql/queries/getFileContents';
2831

2932
const client = generateClient();
33+
const logger = new ConsoleLogger('SectionsPanel');
3034

3135
// Cell renderer components
3236
const IdCell = ({ item }) => <span>{item.Id}</span>;
@@ -51,14 +55,166 @@ const ConfidenceAlertsCell = ({ item, mergedConfig }) => {
5155
return <StatusIndicator type="warning">{alertCount}</StatusIndicator>;
5256
};
5357

54-
const ActionsCell = ({ item, pages, documentItem, mergedConfig }) => (
55-
<FileViewer
56-
fileUri={item.OutputJSONUri}
57-
fileType="json"
58-
buttonText="View/Edit Data"
59-
sectionData={{ ...item, pages, documentItem, mergedConfig }}
60-
/>
61-
);
58+
const ActionsCell = ({ item, pages, documentItem, mergedConfig }) => {
59+
const [isDownloading, setIsDownloading] = React.useState(false);
60+
const [isViewerOpen, setIsViewerOpen] = React.useState(false);
61+
const { settings } = useSettingsContext();
62+
63+
// Check if baseline is available based on evaluation status
64+
const isBaselineAvailable = documentItem?.evaluationStatus === 'BASELINE_AVAILABLE' || documentItem?.evaluationStatus === 'COMPLETED';
65+
66+
// Construct baseline URI by replacing output bucket with evaluation baseline bucket
67+
const constructBaselineUri = (outputUri) => {
68+
if (!outputUri) return null;
69+
70+
// Get actual bucket names from settings
71+
const outputBucketName = settings?.OutputBucket;
72+
const baselineBucketName = settings?.EvaluationBaselineBucket;
73+
74+
if (!outputBucketName || !baselineBucketName) {
75+
logger.error('Bucket names not available in settings');
76+
logger.debug('Settings:', settings);
77+
return null;
78+
}
79+
80+
// Parse the S3 URI to extract bucket and key
81+
// Format: s3://bucket-name/path/to/file
82+
const match = outputUri.match(/^s3:\/\/([^/]+)\/(.+)$/);
83+
if (!match) {
84+
logger.error('Invalid S3 URI format:', outputUri);
85+
return null;
86+
}
87+
88+
const [, bucketName, objectKey] = match;
89+
90+
// Verify this is actually the output bucket before replacing
91+
if (bucketName !== outputBucketName) {
92+
logger.warn(`URI bucket (${bucketName}) does not match expected output bucket (${outputBucketName})`);
93+
}
94+
95+
// Replace the output bucket with the baseline bucket (same object key)
96+
const baselineUri = `s3://${baselineBucketName}/${objectKey}`;
97+
98+
logger.info(`Converted output URI to baseline URI:`);
99+
logger.info(` Output: ${outputUri}`);
100+
logger.info(` Baseline: ${baselineUri}`);
101+
102+
return baselineUri;
103+
};
104+
105+
// Generate download filename
106+
const generateFilename = (documentKey, sectionId, type) => {
107+
// Sanitize document key by replacing forward slashes with underscores
108+
const sanitizedDocId = documentKey.replace(/\//g, '_');
109+
return `${sanitizedDocId}_section${sectionId}_${type}.json`;
110+
};
111+
112+
// Download handler for both prediction and baseline data
113+
const handleDownload = async (type) => {
114+
setIsDownloading(true);
115+
116+
try {
117+
const fileUri = type === 'baseline' ? constructBaselineUri(item.OutputJSONUri) : item.OutputJSONUri;
118+
119+
if (!fileUri) {
120+
alert('File URI not available');
121+
return;
122+
}
123+
124+
logger.info(`Downloading ${type} data from:`, fileUri);
125+
126+
// Fetch file contents using GraphQL
127+
const response = await client.graphql({
128+
query: getFileContents,
129+
variables: { s3Uri: fileUri },
130+
});
131+
132+
const result = response.data.getFileContents;
133+
134+
if (result.isBinary) {
135+
alert('This file contains binary content that cannot be downloaded');
136+
return;
137+
}
138+
139+
const content = result.content;
140+
141+
// Create blob and download
142+
const blob = new Blob([content], { type: 'application/json' });
143+
const url = URL.createObjectURL(blob);
144+
const link = document.createElement('a');
145+
146+
// Generate filename
147+
const documentKey = documentItem?.objectKey || documentItem?.ObjectKey || 'document';
148+
const filename = generateFilename(documentKey, item.Id, type);
149+
150+
link.href = url;
151+
link.download = filename;
152+
document.body.appendChild(link);
153+
link.click();
154+
document.body.removeChild(link);
155+
URL.revokeObjectURL(url);
156+
157+
logger.info(`Successfully downloaded ${type} data as ${filename}`);
158+
} catch (error) {
159+
logger.error(`Error downloading ${type} data:`, error);
160+
161+
let errorMessage = `Failed to download ${type} data`;
162+
163+
if (type === 'baseline' && error.message?.includes('not found')) {
164+
errorMessage = 'Baseline data not found. The baseline may not have been set for this document yet.';
165+
} else if (error.message) {
166+
errorMessage = `Failed to download ${type} data: ${error.message}`;
167+
}
168+
169+
alert(errorMessage);
170+
} finally {
171+
setIsDownloading(false);
172+
}
173+
};
174+
175+
// Build dropdown menu items
176+
const downloadMenuItems = [
177+
{
178+
id: 'prediction',
179+
text: 'Download Data',
180+
iconName: 'download',
181+
},
182+
];
183+
184+
// Add baseline option if available
185+
if (isBaselineAvailable) {
186+
downloadMenuItems.push({
187+
id: 'baseline',
188+
text: 'Download Baseline',
189+
iconName: 'download',
190+
});
191+
}
192+
193+
return (
194+
<SpaceBetween direction="horizontal" size="xs">
195+
<FileViewer
196+
fileUri={item.OutputJSONUri}
197+
fileType="json"
198+
buttonText="View/Edit Data"
199+
sectionData={{ ...item, pages, documentItem, mergedConfig }}
200+
onOpen={() => setIsViewerOpen(true)}
201+
onClose={() => setIsViewerOpen(false)}
202+
/>
203+
{!isViewerOpen && (
204+
<ButtonDropdown
205+
items={downloadMenuItems}
206+
onItemClick={({ detail }) => handleDownload(detail.id)}
207+
disabled={isDownloading}
208+
loading={isDownloading}
209+
variant="normal"
210+
expandToViewport
211+
>
212+
Download
213+
</ButtonDropdown>
214+
)}
215+
</SpaceBetween>
216+
);
217+
};
62218

63219
// Editable cell components for edit mode (moved outside render)
64220
const EditableIdCell = ({ item, validationErrors, updateSectionId }) => (

0 commit comments

Comments
 (0)