2941 lines
96 KiB
TypeScript
2941 lines
96 KiB
TypeScript
/*
|
|
* File: SeriesDetailScreen.tsx
|
|
* Description: Detailed series information screen with predictions and feedback
|
|
* Design & Developed by Tech4Biz Solutions
|
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
|
*
|
|
* Features:
|
|
* - Complete series information and metadata
|
|
* - AI predictions and findings for the series
|
|
* - Feedback history from physicians
|
|
* - Floating feedback button for new feedback
|
|
* - Responsive design for different screen sizes
|
|
* - Integration with patient data and feedback system
|
|
* - Tabbed interface for better organization
|
|
*/
|
|
|
|
import React, { useEffect, useState, useCallback } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
ScrollView,
|
|
TouchableOpacity,
|
|
Alert,
|
|
Dimensions,
|
|
Image,
|
|
FlatList,
|
|
RefreshControl,
|
|
TextInput,
|
|
} from 'react-native';
|
|
import { theme } from '../../../theme/theme';
|
|
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
|
import Icon from 'react-native-vector-icons/Feather';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { DicomViewerModal } from '../../../shared/components';
|
|
|
|
// Import types and API
|
|
import { patientAPI } from '../services/patientAPI';
|
|
import { selectUser } from '../../Auth/redux/authSelectors';
|
|
import { API_CONFIG } from '../../../shared/utils';
|
|
import { SeriesDetailScreenProps } from '../navigation/navigationTypes';
|
|
|
|
// Get screen dimensions
|
|
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
|
|
|
|
// ============================================================================
|
|
// INTERFACES
|
|
// ============================================================================
|
|
|
|
interface Feedback {
|
|
feedback_id: string;
|
|
user_id: string;
|
|
feedback_text: string;
|
|
is_positive: boolean;
|
|
email: string;
|
|
created_at: string;
|
|
prediction_id: number;
|
|
prediction_file_path: string;
|
|
series_number: string;
|
|
feedback_type: string;
|
|
}
|
|
|
|
interface Prediction {
|
|
id: number;
|
|
file_path: string;
|
|
prediction: {
|
|
label: string;
|
|
finding_type: string;
|
|
clinical_urgency: string;
|
|
confidence_score: number;
|
|
detailed_results: any;
|
|
finding_category: string;
|
|
primary_severity: string;
|
|
anatomical_location: string;
|
|
// Hemorrhage type properties
|
|
epidural?: boolean;
|
|
subdural?: boolean;
|
|
intraparenchymal?: boolean;
|
|
subarachnoid?: boolean;
|
|
intraventricular?: boolean;
|
|
epidural_percentage?: number;
|
|
subdural_percentage?: number;
|
|
intraparenchymal_percentage?: number;
|
|
subarachnoid_percentage?: number;
|
|
intraventricular_percentage?: number;
|
|
midline_shift?: number;
|
|
};
|
|
processed_at: string;
|
|
preview: string;
|
|
}
|
|
|
|
// Tab types
|
|
type TabType = 'series' | 'ai' | 'feedback';
|
|
|
|
// ============================================================================
|
|
// SERIES DETAIL SCREEN COMPONENT
|
|
// ============================================================================
|
|
|
|
/**
|
|
* SeriesDetailScreen Component
|
|
*
|
|
* Purpose: Detailed view of a specific DICOM series with predictions and feedback
|
|
*
|
|
* Features:
|
|
* - Complete series information and metadata
|
|
* - AI predictions and findings display
|
|
* - Feedback history from physicians
|
|
* - Floating feedback button for new feedback
|
|
* - Responsive design for different screen sizes
|
|
* - Integration with patient data and feedback system
|
|
* - Tabbed interface for better organization
|
|
*/
|
|
const SeriesDetailScreen: React.FC<SeriesDetailScreenProps> = ({ navigation, route }) => {
|
|
// ============================================================================
|
|
// STATE MANAGEMENT
|
|
// ============================================================================
|
|
|
|
const dispatch = useAppDispatch();
|
|
|
|
// Route parameters
|
|
const { patientId, patientName, seriesNumber, seriesData, patientData, onFeedbackSubmitted } = route.params;
|
|
|
|
// Redux state
|
|
const user = useAppSelector(selectUser);
|
|
|
|
// Local state
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Tab state
|
|
const [activeTab, setActiveTab] = useState<TabType>('series');
|
|
|
|
// Local patient data state for real-time updates
|
|
const [localPatientData, setLocalPatientData] = useState(patientData);
|
|
|
|
// Feedback state
|
|
const [showFeedbackModal, setShowFeedbackModal] = useState(false);
|
|
const [selectedPrediction, setSelectedPrediction] = useState<Prediction | null>(null);
|
|
const [feedbackText, setFeedbackText] = useState('');
|
|
const [isPositive, setIsPositive] = useState<boolean | null>(null);
|
|
const [isSubmittingFeedback, setIsSubmittingFeedback] = useState(false);
|
|
|
|
// Feedback result modal state
|
|
const [showFeedbackResultModal, setShowFeedbackResultModal] = useState(false);
|
|
const [feedbackResult, setFeedbackResult] = useState<{
|
|
type: 'success' | 'error';
|
|
title: string;
|
|
message: string;
|
|
} | null>(null);
|
|
|
|
// Track newly added feedback for visual indication
|
|
const [newFeedbackIds, setNewFeedbackIds] = useState<Set<string>>(new Set());
|
|
|
|
// DICOM Modal state
|
|
const [dicomModalVisible, setDicomModalVisible] = useState(false);
|
|
const [selectedDicomData, setSelectedDicomData] = useState<{
|
|
dicomUrl: string;
|
|
prediction: Prediction;
|
|
imageIndex: number;
|
|
} | null>(null);
|
|
|
|
// ============================================================================
|
|
// EFFECTS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Component Mount Effect
|
|
*
|
|
* Purpose: Set navigation title and initialize screen
|
|
*/
|
|
useEffect(() => {
|
|
navigation.setOptions({
|
|
title: `Series ${seriesNumber}`,
|
|
headerShown: false,
|
|
});
|
|
}, [navigation, seriesNumber]);
|
|
|
|
/**
|
|
* Sync Local Patient Data Effect
|
|
*
|
|
* Purpose: Keep local patient data in sync with route params
|
|
*/
|
|
useEffect(() => {
|
|
setLocalPatientData(patientData);
|
|
}, [patientData]);
|
|
|
|
// ============================================================================
|
|
// EVENT HANDLERS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Refresh Patient Data
|
|
*
|
|
* Purpose: Fetch updated patient data including new feedback
|
|
*/
|
|
const refreshPatientData = useCallback(async () => {
|
|
if (!user?.access_token) return;
|
|
|
|
try {
|
|
const response: any = await patientAPI.getPatientDetailsById(patientId, user.access_token);
|
|
|
|
if (response.ok && response.data && response.data.data) {
|
|
// Update the local patient data with fresh data from API
|
|
// This will include the newly submitted feedback
|
|
const updatedPatientData = response.data.data;
|
|
setLocalPatientData(updatedPatientData);
|
|
|
|
// Also update the route params for consistency
|
|
route.params.patientData = updatedPatientData;
|
|
|
|
setError(null);
|
|
}
|
|
} catch (err: any) {
|
|
console.log('Error refreshing patient data:', err.message);
|
|
// Don't show error to user for background refresh
|
|
}
|
|
}, [patientId, user?.access_token, route.params]);
|
|
|
|
/**
|
|
* Handle Back Navigation
|
|
*
|
|
* Purpose: Navigate back to previous screen
|
|
*/
|
|
const handleBackPress = useCallback(() => {
|
|
navigation.goBack();
|
|
}, [navigation]);
|
|
|
|
/**
|
|
* Handle DICOM Image Press
|
|
*
|
|
* Purpose: Open DICOM viewer modal for selected prediction image
|
|
*
|
|
* @param prediction - Prediction data containing DICOM file path
|
|
* @param imageIndex - Index of the image in the series
|
|
*/
|
|
const handleDicomImagePress = useCallback((prediction: Prediction, imageIndex: number) => {
|
|
if (prediction?.file_path) {
|
|
const dicomUrl = API_CONFIG.BASE_URL + '/api/dicom' + prediction.file_path;
|
|
console.log('DICOM URL:', dicomUrl);
|
|
|
|
setSelectedDicomData({
|
|
dicomUrl,
|
|
prediction,
|
|
imageIndex,
|
|
});
|
|
setDicomModalVisible(true);
|
|
} else {
|
|
Alert.alert(
|
|
'No DICOM Available',
|
|
'No DICOM file path is available for this image.',
|
|
[{ text: 'OK' }]
|
|
);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Handle Close DICOM Modal
|
|
*
|
|
* Purpose: Close DICOM viewer modal and reset state
|
|
*/
|
|
const handleCloseDicomModal = useCallback(() => {
|
|
setDicomModalVisible(false);
|
|
setSelectedDicomData(null);
|
|
}, []);
|
|
|
|
/**
|
|
* Handle Refresh
|
|
*
|
|
* Purpose: Pull-to-refresh functionality
|
|
*/
|
|
const handleRefresh = useCallback(async () => {
|
|
setIsRefreshing(true);
|
|
// Refresh patient data to get latest information
|
|
await refreshPatientData();
|
|
setIsRefreshing(false);
|
|
}, [refreshPatientData]);
|
|
|
|
/**
|
|
* Handle Open Feedback Modal
|
|
*
|
|
* Purpose: Open feedback modal for a specific prediction
|
|
*
|
|
* @param prediction - Prediction data for feedback
|
|
*/
|
|
const handleOpenFeedback = useCallback((prediction: Prediction) => {
|
|
setSelectedPrediction(prediction);
|
|
setFeedbackText('');
|
|
setIsPositive(null);
|
|
setShowFeedbackModal(true);
|
|
}, []);
|
|
|
|
/**
|
|
* Handle Submit Feedback
|
|
*
|
|
* Purpose: Submit feedback to API
|
|
*/
|
|
const handleSubmitFeedback = useCallback(async () => {
|
|
if (!selectedPrediction || !feedbackText.trim() || isPositive === null) {
|
|
setFeedbackResult({
|
|
type: 'error',
|
|
title: 'Validation Error',
|
|
message: 'Please provide all required feedback information'
|
|
});
|
|
setShowFeedbackResultModal(true);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setIsSubmittingFeedback(true);
|
|
|
|
if (!patientId) {
|
|
throw new Error('Patient ID not available');
|
|
}
|
|
|
|
const feedbackPayload = {
|
|
patid: patientId,
|
|
prediction_id: selectedPrediction.id,
|
|
feedback_text: feedbackText.trim(),
|
|
is_positive: isPositive
|
|
};
|
|
|
|
console.log('Submitting feedback payload:', feedbackPayload);
|
|
|
|
// Call the actual API
|
|
const response = await patientAPI.submitFeedback(feedbackPayload, user?.access_token);
|
|
console.log('Feedback response:', response);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(response.problem || 'Failed to submit feedback');
|
|
}
|
|
|
|
// Show success message
|
|
setFeedbackResult({
|
|
type: 'success',
|
|
title: 'Feedback Submitted',
|
|
message: 'Your feedback has been recorded successfully.'
|
|
});
|
|
setShowFeedbackResultModal(true);
|
|
|
|
// Track this feedback as newly added for visual indication
|
|
const newFeedbackId = `new_${Date.now()}`;
|
|
setNewFeedbackIds(prev => new Set(prev).add(newFeedbackId));
|
|
|
|
// Refresh patient data to get updated feedback
|
|
await refreshPatientData();
|
|
|
|
// Notify parent screen to refresh its data as well
|
|
// This ensures PatientDetailsScreen shows updated information when user navigates back
|
|
if (onFeedbackSubmitted) {
|
|
onFeedbackSubmitted();
|
|
}
|
|
} catch (error: any) {
|
|
setFeedbackResult({
|
|
type: 'error',
|
|
title: 'Error',
|
|
message: error.message || 'Failed to submit feedback. Please try again.'
|
|
});
|
|
setShowFeedbackResultModal(true);
|
|
} finally {
|
|
setIsSubmittingFeedback(false);
|
|
}
|
|
}, [selectedPrediction, feedbackText, isPositive, patientId, user?.access_token, refreshPatientData, onFeedbackSubmitted]);
|
|
|
|
/**
|
|
* Handle Close Feedback Modal
|
|
*
|
|
* Purpose: Close feedback modal and reset state
|
|
*/
|
|
const handleCloseFeedback = useCallback(() => {
|
|
setShowFeedbackModal(false);
|
|
setSelectedPrediction(null);
|
|
setFeedbackText('');
|
|
setIsPositive(null);
|
|
}, []);
|
|
|
|
/**
|
|
* Handle Feedback Result Modal Close
|
|
*
|
|
* Purpose: Close feedback result modal and reset form if success
|
|
*/
|
|
const handleFeedbackResultClose = useCallback(() => {
|
|
setShowFeedbackResultModal(false);
|
|
setFeedbackResult(null);
|
|
|
|
// If it was a success, also close the feedback modal and reset form
|
|
if (feedbackResult?.type === 'success') {
|
|
setShowFeedbackModal(false);
|
|
setSelectedPrediction(null);
|
|
setFeedbackText('');
|
|
setIsPositive(null);
|
|
}
|
|
}, [feedbackResult?.type]);
|
|
|
|
/**
|
|
* Auto-close Success Modal Effect
|
|
*
|
|
* Purpose: Automatically close success modal after 2 seconds
|
|
*/
|
|
useEffect(() => {
|
|
if (feedbackResult?.type === 'success') {
|
|
const timer = setTimeout(() => {
|
|
handleFeedbackResultClose();
|
|
}, 2000);
|
|
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [feedbackResult?.type, handleFeedbackResultClose]);
|
|
|
|
/**
|
|
* Clear New Feedback Badges Effect
|
|
*
|
|
* Purpose: Clear "New" badges after 30 seconds to avoid UI clutter
|
|
*/
|
|
useEffect(() => {
|
|
if (newFeedbackIds.size > 0) {
|
|
const timer = setTimeout(() => {
|
|
setNewFeedbackIds(new Set());
|
|
}, 30000); // 30 seconds
|
|
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [newFeedbackIds.size]);
|
|
|
|
// ============================================================================
|
|
// UTILITY FUNCTIONS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Get Clinical Urgency Color
|
|
*
|
|
* Purpose: Get appropriate color for clinical urgency
|
|
*
|
|
* @param urgency - Clinical urgency level
|
|
*/
|
|
const getUrgencyColor = (urgency: string) => {
|
|
switch (urgency.toLowerCase()) {
|
|
case 'urgent':
|
|
return theme.colors.error;
|
|
case 'semi-urgent':
|
|
return theme.colors.warning;
|
|
case 'non-urgent':
|
|
return theme.colors.success;
|
|
default:
|
|
return theme.colors.info;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get Feedback Type Color
|
|
*
|
|
* Purpose: Get appropriate color for feedback type
|
|
*
|
|
* @param feedbackType - Feedback type (positive/negative)
|
|
*/
|
|
const getFeedbackTypeColor = (feedbackType: string) => {
|
|
switch (feedbackType.toLowerCase()) {
|
|
case 'positive':
|
|
return theme.colors.success;
|
|
case 'negative':
|
|
return theme.colors.error;
|
|
default:
|
|
return theme.colors.info;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get Percentage Color
|
|
*
|
|
* Purpose: Get appropriate color based on percentage value
|
|
*
|
|
* @param percentage - Percentage value (0-100)
|
|
*/
|
|
const getPercentageColor = (percentage: number) => {
|
|
if (percentage >= 70) {
|
|
return theme.colors.error; // High detection - Red
|
|
} else if (percentage >= 40) {
|
|
return theme.colors.warning; // Medium detection - Orange
|
|
} else if (percentage >= 10) {
|
|
return theme.colors.info; // Low detection - Blue
|
|
} else {
|
|
return theme.colors.success; // No detection - Green
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get Percentage Status
|
|
*
|
|
* Purpose: Get status text based on percentage value
|
|
*
|
|
* @param percentage - Percentage value (0-100)
|
|
*/
|
|
const getPercentageStatus = (percentage: number) => {
|
|
if (percentage >= 70) {
|
|
return 'HIGH';
|
|
} else if (percentage >= 40) {
|
|
return 'MEDIUM';
|
|
} else if (percentage >= 10) {
|
|
return 'LOW';
|
|
} else {
|
|
return 'NONE';
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get Percentage Value
|
|
*
|
|
* Purpose: Extract percentage value from prediction data with flexible field naming
|
|
*
|
|
* @param prediction - Prediction object
|
|
* @param type - Hemorrhage type (epidural, subdural, etc.)
|
|
*/
|
|
const getPercentageValue = (prediction: any, type: string): number => {
|
|
// First, try to get from detailed_results.hemorrhage_detection
|
|
if (prediction.detailed_results?.hemorrhage_detection) {
|
|
const hemorrhageData = prediction.detailed_results.hemorrhage_detection;
|
|
|
|
// Map our types to the actual field names in the data
|
|
const fieldMapping: { [key: string]: string } = {
|
|
'epidural': 'Epidural',
|
|
'subdural': 'Subdural',
|
|
'subarachnoid': 'Subarachnoid',
|
|
'intraparenchymal': 'Intraparenchymal',
|
|
'intraventricular': 'Intraventricular',
|
|
'midline_shift': 'Midline shift'
|
|
};
|
|
|
|
const actualFieldName = fieldMapping[type];
|
|
if (actualFieldName && hemorrhageData[actualFieldName] !== undefined) {
|
|
const value = hemorrhageData[actualFieldName];
|
|
|
|
// Convert decimal (0-1) to percentage (0-100)
|
|
if (typeof value === 'number' && value >= 0 && value <= 1) {
|
|
return value * 100;
|
|
}
|
|
|
|
// If it's already a percentage, return as is
|
|
if (typeof value === 'number' && value >= 0 && value <= 100) {
|
|
return value;
|
|
}
|
|
|
|
// Handle string values
|
|
if (typeof value === 'string') {
|
|
const numValue = parseFloat(value);
|
|
if (!isNaN(numValue)) {
|
|
if (numValue >= 0 && numValue <= 1) {
|
|
return numValue * 100;
|
|
}
|
|
if (numValue >= 0 && numValue <= 100) {
|
|
return numValue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: try different possible field names for the percentage
|
|
const possibleFields = [
|
|
`${type}_percentage`,
|
|
`${type}_score`,
|
|
`${type}_value`,
|
|
`${type}_detection`,
|
|
`${type}_probability`,
|
|
type
|
|
];
|
|
|
|
for (const field of possibleFields) {
|
|
if (prediction[field] !== undefined && prediction[field] !== null) {
|
|
const value = prediction[field];
|
|
|
|
// Handle different data types
|
|
if (typeof value === 'number') {
|
|
// If it's already a percentage (0-100), return as is
|
|
if (value >= 0 && value <= 100) {
|
|
return value;
|
|
}
|
|
// If it's a decimal (0-1), convert to percentage
|
|
if (value >= 0 && value <= 1) {
|
|
return value * 100;
|
|
}
|
|
// If it's a large number, assume it's already a percentage
|
|
return value;
|
|
}
|
|
|
|
// Handle string values
|
|
if (typeof value === 'string') {
|
|
const numValue = parseFloat(value);
|
|
if (!isNaN(numValue)) {
|
|
// If it's already a percentage (0-100), return as is
|
|
if (numValue >= 0 && numValue <= 100) {
|
|
return numValue;
|
|
}
|
|
// If it's a decimal (0-1), convert to percentage
|
|
if (numValue >= 0 && numValue <= 1) {
|
|
return numValue * 100;
|
|
}
|
|
// If it's a large number, assume it's already a percentage
|
|
return numValue;
|
|
}
|
|
}
|
|
|
|
// Handle boolean values (convert to 0% or 100%)
|
|
if (typeof value === 'boolean') {
|
|
return value ? 100 : 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no valid percentage found, return 0
|
|
return 0;
|
|
};
|
|
|
|
/**
|
|
* Get Series Predictions
|
|
*
|
|
* Purpose: Get predictions for the current series
|
|
*/
|
|
const getSeriesPredictions = () => {
|
|
if (!localPatientData?.predictions_by_series) return [];
|
|
return localPatientData.predictions_by_series[seriesNumber] || [];
|
|
};
|
|
|
|
/**
|
|
* Get Series Feedback
|
|
*
|
|
* Purpose: Get feedback for the current series
|
|
*/
|
|
const getSeriesFeedback = () => {
|
|
if (!localPatientData?.feedback_by_series) return [];
|
|
return localPatientData.feedback_by_series[seriesNumber] || [];
|
|
};
|
|
|
|
/**
|
|
* Check if Feedback is New
|
|
*
|
|
* Purpose: Check if feedback was recently added for visual indication
|
|
*
|
|
* @param feedbackId - Feedback ID to check
|
|
*/
|
|
const isFeedbackNew = (feedbackId: string) => {
|
|
return newFeedbackIds.has(feedbackId);
|
|
};
|
|
|
|
// ============================================================================
|
|
// RENDER HELPERS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Render Tab Navigation
|
|
*
|
|
* Purpose: Render tab navigation for switching between different sections
|
|
*/
|
|
const renderTabNavigation = () => {
|
|
const tabs = [
|
|
{ id: 'series' as TabType, label: 'Series Info', icon: 'info' },
|
|
{ id: 'ai' as TabType, label: 'AI Analysis', icon: 'activity' },
|
|
{ id: 'feedback' as TabType, label: 'Feedback', icon: 'message-circle' }
|
|
];
|
|
|
|
return (
|
|
<View style={styles.tabContainer}>
|
|
{tabs.map((tab) => (
|
|
<TouchableOpacity
|
|
key={tab.id}
|
|
style={[
|
|
styles.tabButton,
|
|
activeTab === tab.id && styles.tabButtonActive
|
|
]}
|
|
onPress={() => setActiveTab(tab.id)}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Icon
|
|
name={tab.icon as any}
|
|
size={16}
|
|
color={activeTab === tab.id ? theme.colors.background : theme.colors.textSecondary}
|
|
/>
|
|
<Text style={[
|
|
styles.tabButtonText,
|
|
activeTab === tab.id && styles.tabButtonTextActive
|
|
]}>
|
|
{tab.label}
|
|
</Text>
|
|
{/* Active Tab Indicator */}
|
|
{activeTab === tab.id && (
|
|
<View style={styles.tabIndicator} />
|
|
)}
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Render Series Header
|
|
*
|
|
* Purpose: Render series identification and basic information
|
|
*/
|
|
const renderSeriesHeader = () => {
|
|
if (!seriesData) return null;
|
|
|
|
return (
|
|
<View style={styles.seriesHeader}>
|
|
<View style={styles.seriesHeaderContent}>
|
|
<View style={styles.seriesHeaderLeft}>
|
|
<Text style={styles.seriesTitle}>
|
|
Series {seriesData.series_num}
|
|
</Text>
|
|
<Text style={styles.seriesDescription}>
|
|
{seriesData.series_description || 'No description available'}
|
|
</Text>
|
|
<View style={styles.seriesMetaRow}>
|
|
<View style={styles.metaItem}>
|
|
<Icon name="image" size={16} color={theme.colors.textSecondary} />
|
|
<Text style={styles.metaText}>{seriesData.total_images} images</Text>
|
|
</View>
|
|
<View style={styles.metaItem}>
|
|
<Icon name="activity" size={16} color={theme.colors.textSecondary} />
|
|
<Text style={styles.metaText}>{seriesData.modality} modality</Text>
|
|
</View>
|
|
{seriesData.body_part && (
|
|
<View style={styles.metaItem}>
|
|
<Icon name="target" size={16} color={theme.colors.textSecondary} />
|
|
<Text style={styles.metaText}>{seriesData.body_part}</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.seriesHeaderRight}>
|
|
<View style={styles.seriesStatusBadge}>
|
|
<Icon name="check-circle" size={16} color={'white'} />
|
|
<Text style={styles.seriesStatusText}>Processed</Text>
|
|
</View>
|
|
{seriesData.study_date && (
|
|
<Text style={styles.studyDate}>
|
|
{new Date(seriesData.study_date).toLocaleDateString()}
|
|
</Text>
|
|
)}
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Render Series Details
|
|
*
|
|
* Purpose: Render detailed series information in a comprehensive format
|
|
*/
|
|
const renderSeriesDetails = () => {
|
|
if (!seriesData) return null;
|
|
|
|
return (
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>Series Information</Text>
|
|
|
|
{/* Series Summary Bar */}
|
|
<View style={styles.seriesSummaryBar}>
|
|
<View style={styles.summaryItem}>
|
|
<Text style={styles.summaryLabel}>Series Number</Text>
|
|
<View style={styles.summaryValueContainer}>
|
|
<Text style={styles.summaryValue}>{seriesData.series_num}</Text>
|
|
</View>
|
|
</View>
|
|
<View style={styles.summaryItem}>
|
|
<Text style={styles.summaryLabel}>Total Images</Text>
|
|
<View style={styles.summaryValueContainer}>
|
|
<Text style={styles.summaryValue}>{seriesData.total_images}</Text>
|
|
</View>
|
|
</View>
|
|
<View style={styles.summaryItem}>
|
|
<Text style={styles.summaryLabel}>Modality</Text>
|
|
<View style={styles.summaryValueContainer}>
|
|
<Text style={styles.summaryValue}>{seriesData.modality}</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Detailed Series Information Card */}
|
|
<View style={styles.seriesDetailsCard}>
|
|
<View style={styles.cardHeader}>
|
|
<Text style={styles.cardTitle}>Series Details</Text>
|
|
<Text style={styles.cardSubtitle}>Complete series metadata and information</Text>
|
|
</View>
|
|
|
|
<View style={styles.detailsGrid}>
|
|
<View style={styles.detailRow}>
|
|
<View style={styles.detailColumn}>
|
|
<Text style={styles.detailLabel}>Series Number</Text>
|
|
<Text style={styles.detailValue}>{seriesData.series_num}</Text>
|
|
</View>
|
|
<View style={styles.detailColumn}>
|
|
<Text style={styles.detailLabel}>Series Description</Text>
|
|
<Text style={styles.detailValue} numberOfLines={2}>
|
|
{seriesData.series_description || 'No description available'}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.detailRow}>
|
|
<View style={styles.detailColumn}>
|
|
<Text style={styles.detailLabel}>Total Images</Text>
|
|
<Text style={styles.detailValue}>{seriesData.total_images}</Text>
|
|
</View>
|
|
<View style={styles.detailColumn}>
|
|
<Text style={styles.detailLabel}>Modality</Text>
|
|
<Text style={styles.detailValue}>{seriesData.modality}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.detailRow}>
|
|
<View style={styles.detailColumn}>
|
|
<Text style={styles.detailLabel}>Patient ID</Text>
|
|
<Text style={styles.detailValue}>{patientId}</Text>
|
|
</View>
|
|
<View style={styles.detailColumn}>
|
|
<Text style={styles.detailLabel}>Patient Name</Text>
|
|
<Text style={styles.detailValue}>{patientName}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{seriesData.body_part && (
|
|
<View style={styles.detailRow}>
|
|
<View style={styles.detailColumn}>
|
|
<Text style={styles.detailLabel}>Body Part</Text>
|
|
<Text style={styles.detailValue}>{seriesData.body_part}</Text>
|
|
</View>
|
|
<View style={styles.detailColumn}>
|
|
<Text style={styles.detailLabel}>Study Date</Text>
|
|
<Text style={styles.detailValue}>
|
|
{seriesData.study_date ? new Date(seriesData.study_date).toLocaleDateString() : 'Not specified'}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{seriesData.institution_name && (
|
|
<View style={styles.detailRow}>
|
|
<View style={styles.detailColumn}>
|
|
<Text style={styles.detailLabel}>Institution</Text>
|
|
<Text style={styles.detailValue}>{seriesData.institution_name}</Text>
|
|
</View>
|
|
<View style={styles.detailColumn}>
|
|
<Text style={styles.detailLabel}>Manufacturer</Text>
|
|
<Text style={styles.detailValue}>{seriesData.manufacturer || 'Not specified'}</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
{/* Processing Information */}
|
|
<View style={styles.processingInfo}>
|
|
<Text style={styles.processingTitle}>Processing Information</Text>
|
|
<View style={styles.processingGrid}>
|
|
<View style={styles.processingItem}>
|
|
<Text style={styles.processingLabel}>File Type</Text>
|
|
<Text style={styles.processingValue}>DICOM</Text>
|
|
</View>
|
|
<View style={styles.processingItem}>
|
|
<Text style={styles.processingLabel}>Multiframe</Text>
|
|
<Text style={styles.processingValue}>
|
|
{seriesData.total_images > 1 ? 'Yes' : 'No'}
|
|
</Text>
|
|
</View>
|
|
<View style={styles.processingItem}>
|
|
<Text style={styles.processingLabel}>Frames</Text>
|
|
<Text style={styles.processingValue}>{seriesData.total_images}</Text>
|
|
</View>
|
|
<View style={styles.processingItem}>
|
|
<Text style={styles.processingLabel}>Status</Text>
|
|
<View style={styles.statusBadge}>
|
|
<Text style={styles.statusText}>Processed</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Render AI Predictions
|
|
*
|
|
* Purpose: Render AI predictions and findings for the series
|
|
*/
|
|
const renderAIPredictions = () => {
|
|
const predictions = getSeriesPredictions();
|
|
|
|
// Debug: Log the prediction data structure
|
|
|
|
if (predictions.length === 0) {
|
|
return (
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>AI Analysis Results</Text>
|
|
<View style={styles.emptyState}>
|
|
<Icon name="brain" size={48} color={theme.colors.textMuted} />
|
|
<Text style={styles.emptyStateTitle}>No AI Predictions</Text>
|
|
<Text style={styles.emptyStateSubtitle}>
|
|
No AI predictions are available for this series yet
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
// Calculate summary metrics
|
|
const totalPredictions = predictions.length;
|
|
const highPriorityCount = predictions.filter((p: Prediction) =>
|
|
p.prediction.clinical_urgency?.toLowerCase() === 'urgent'
|
|
).length;
|
|
const avgConfidence = predictions.reduce((sum: number, p: Prediction) =>
|
|
sum + (p.prediction.confidence_score || 0), 0
|
|
) / totalPredictions;
|
|
|
|
return (
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>AI Analysis Results</Text>
|
|
|
|
{/* AI Summary Bar */}
|
|
<View style={styles.aiSummaryBar}>
|
|
<View style={styles.aiSummaryItem}>
|
|
<Text style={styles.aiSummaryLabel}>Total Predictions</Text>
|
|
<View style={styles.aiSummaryValueContainer}>
|
|
<Text style={styles.aiSummaryValue}>{totalPredictions} predictions found</Text>
|
|
</View>
|
|
</View>
|
|
<View style={styles.aiSummaryItem}>
|
|
<Text style={styles.aiSummaryLabel}>High Priority</Text>
|
|
<View style={styles.aiSummaryValueContainer}>
|
|
<Text style={styles.aiSummaryValue}>
|
|
{highPriorityCount > 0 ? `${highPriorityCount} urgent` : 'None'}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
<View style={styles.aiSummaryItem}>
|
|
<Text style={styles.aiSummaryLabel}>Avg Confidence</Text>
|
|
<View style={styles.aiSummaryValueContainer}>
|
|
<Text style={styles.aiSummaryValue}>
|
|
{(avgConfidence * 100).toFixed(1)}%
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* AI Predictions Card */}
|
|
<View style={styles.aiPredictionsCard}>
|
|
<View style={styles.cardHeader}>
|
|
<Text style={styles.cardTitle}>AI Predictions Analysis</Text>
|
|
<Text style={styles.cardSubtitle}>Medical scan analysis results</Text>
|
|
</View>
|
|
|
|
{predictions.map((prediction: Prediction) => (
|
|
<View key={prediction.id} style={styles.predictionCard}>
|
|
<View style={styles.predictionHeader}>
|
|
<Text style={styles.predictionLabel}>{prediction.prediction.label}</Text>
|
|
<View style={[
|
|
styles.urgencyBadge,
|
|
{ backgroundColor: getUrgencyColor(prediction.prediction.clinical_urgency) }
|
|
]}>
|
|
<Text style={styles.urgencyText}>
|
|
{prediction.prediction.clinical_urgency}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.predictionDetails}>
|
|
<View style={styles.predictionDetailItem}>
|
|
<Text style={styles.predictionDetailLabel}>Finding Type:</Text>
|
|
<Text style={styles.predictionDetailValue}>
|
|
{prediction.prediction.finding_type}
|
|
</Text>
|
|
</View>
|
|
<View style={styles.predictionDetailItem}>
|
|
<Text style={styles.predictionDetailLabel}>Confidence:</Text>
|
|
<Text style={styles.predictionDetailValue}>
|
|
{(prediction.prediction.confidence_score * 100).toFixed(1)}%
|
|
</Text>
|
|
</View>
|
|
<View style={styles.predictionDetailItem}>
|
|
<Text style={styles.predictionDetailLabel}>Category:</Text>
|
|
<Text style={styles.predictionDetailValue}>
|
|
{prediction.prediction.finding_category}
|
|
</Text>
|
|
</View>
|
|
<View style={styles.predictionDetailItem}>
|
|
<Text style={styles.predictionDetailLabel}>Severity:</Text>
|
|
<Text style={styles.predictionDetailValue}>
|
|
{prediction.prediction.primary_severity}
|
|
</Text>
|
|
</View>
|
|
<View style={styles.predictionDetailItem}>
|
|
<Text style={styles.predictionDetailLabel}>Location:</Text>
|
|
<Text style={styles.predictionDetailValue}>
|
|
{prediction.prediction.anatomical_location}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
|
|
|
|
{/* Show specific hemorrhage types if they exist */}
|
|
{prediction.prediction.epidural !== undefined && (
|
|
<View style={styles.additionalFindingItem}>
|
|
<Text style={styles.additionalFindingLabel}>Epidural Hemorrhage:</Text>
|
|
<Text style={styles.additionalFindingValue}>
|
|
{prediction.prediction.epidural ? 'Detected' : 'Not Detected'}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{prediction.prediction.subdural !== undefined && (
|
|
<View style={styles.additionalFindingItem}>
|
|
<Text style={styles.additionalFindingLabel}>Subdural Hemorrhage:</Text>
|
|
<Text style={styles.additionalFindingValue}>
|
|
{prediction.prediction.subdural ? 'Detected' : 'Not Detected'}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{prediction.prediction.intraparenchymal !== undefined && (
|
|
<View style={styles.additionalFindingItem}>
|
|
<Text style={styles.additionalFindingLabel}>Intraparenchymal Hemorrhage:</Text>
|
|
<Text style={styles.additionalFindingValue}>
|
|
{prediction.prediction.intraparenchymal ? 'Detected' : 'Not Detected'}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{prediction.prediction.subarachnoid !== undefined && (
|
|
<View style={styles.additionalFindingItem}>
|
|
<Text style={styles.additionalFindingLabel}>Subarachnoid Hemorrhage:</Text>
|
|
<Text style={styles.additionalFindingValue}>
|
|
{prediction.prediction.subarachnoid ? 'Detected' : 'Not Detected'}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{prediction.prediction.intraventricular !== undefined && (
|
|
<View style={styles.additionalFindingItem}>
|
|
<Text style={styles.additionalFindingLabel}>Intraventricular Hemorrhage:</Text>
|
|
<Text style={styles.additionalFindingValue}>
|
|
{prediction.prediction.intraventricular ? 'Detected' : 'Not Detected'}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{/* Additional Findings from detailed_results */}
|
|
{prediction.prediction.detailed_results && (
|
|
<View style={styles.additionalFindingsSection}>
|
|
<Text style={styles.additionalFindingsTitle}>Additional Analysis Results</Text>
|
|
|
|
{/* Stroke Detection */}
|
|
{prediction.prediction.detailed_results.stroke_detection && (
|
|
<View style={styles.additionalFindingItem}>
|
|
<Text style={styles.additionalFindingLabel}>Stroke Detection:</Text>
|
|
<View style={styles.additionalFindingValues}>
|
|
<Text style={styles.additionalFindingValue}>
|
|
Normal: {((prediction.prediction.detailed_results.stroke_detection.Normal || 0) * 100).toFixed(1)}%
|
|
</Text>
|
|
<Text style={styles.additionalFindingValue}>
|
|
Stroke: {((prediction.prediction.detailed_results.stroke_detection.Stroke || 0) * 100).toFixed(1)}%
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Binary Hemorrhage */}
|
|
{prediction.prediction.detailed_results.binary_hemorrhage && (
|
|
<View style={styles.additionalFindingItem}>
|
|
<Text style={styles.additionalFindingLabel}>Hemorrhage Detection:</Text>
|
|
<View style={styles.additionalFindingValues}>
|
|
<Text style={styles.additionalFindingValue}>
|
|
Normal: {((prediction.prediction.detailed_results.binary_hemorrhage.Normal || 0) * 100).toFixed(1)}%
|
|
</Text>
|
|
<Text style={styles.additionalFindingValue}>
|
|
Hemorrhage: {((prediction.prediction.detailed_results.binary_hemorrhage.Hemorrhage || 0) * 100).toFixed(1)}%
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
</View>
|
|
)}
|
|
|
|
{/* Visual Indicators Section - Show detailed findings with percentage indicators */}
|
|
<View style={styles.visualIndicatorsSection}>
|
|
<Text style={styles.visualIndicatorsTitle}>Detailed Findings Analysis</Text>
|
|
|
|
{/* Compact Hemorrhage Type Percentage Indicators with Progress Bars */}
|
|
<View style={styles.compactIndicatorsContainer}>
|
|
{/* Epidural */}
|
|
<View style={styles.compactIndicatorItem}>
|
|
<View style={styles.indicatorHeader}>
|
|
<Icon
|
|
name="activity"
|
|
size={16}
|
|
color={getPercentageColor(getPercentageValue(prediction.prediction, 'epidural'))}
|
|
/>
|
|
<Text style={styles.compactIndicatorTitle}>Epidural</Text>
|
|
<Text style={[
|
|
styles.compactIndicatorPercentage,
|
|
{ color: getPercentageColor(getPercentageValue(prediction.prediction, 'epidural')) }
|
|
]}>
|
|
{getPercentageValue(prediction.prediction, 'epidural').toFixed(1)}%
|
|
</Text>
|
|
</View>
|
|
<View style={styles.progressBarContainer}>
|
|
<View
|
|
style={[
|
|
styles.progressBar,
|
|
{
|
|
width: `${getPercentageValue(prediction.prediction, 'epidural')}%`,
|
|
backgroundColor: getPercentageColor(getPercentageValue(prediction.prediction, 'epidural'))
|
|
}
|
|
]}
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Subdural */}
|
|
<View style={styles.compactIndicatorItem}>
|
|
<View style={styles.indicatorHeader}>
|
|
<Icon
|
|
name="activity"
|
|
size={16}
|
|
color={getPercentageColor(getPercentageValue(prediction.prediction, 'subdural'))}
|
|
/>
|
|
<Text style={styles.compactIndicatorTitle}>Subdural</Text>
|
|
<Text style={[
|
|
styles.compactIndicatorPercentage,
|
|
{ color: getPercentageColor(getPercentageValue(prediction.prediction, 'subdural')) }
|
|
]}>
|
|
{getPercentageValue(prediction.prediction, 'subdural').toFixed(1)}%
|
|
</Text>
|
|
</View>
|
|
<View style={styles.progressBarContainer}>
|
|
<View
|
|
style={[
|
|
styles.progressBar,
|
|
{
|
|
width: `${getPercentageValue(prediction.prediction, 'subdural')}%`,
|
|
backgroundColor: getPercentageColor(getPercentageValue(prediction.prediction, 'subdural'))
|
|
}
|
|
]}
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Intraparenchymal */}
|
|
<View style={styles.compactIndicatorItem}>
|
|
<View style={styles.indicatorHeader}>
|
|
<Icon
|
|
name="activity"
|
|
size={16}
|
|
color={getPercentageColor(getPercentageValue(prediction.prediction, 'intraparenchymal'))}
|
|
/>
|
|
<Text style={styles.compactIndicatorTitle}>Intraparenchymal</Text>
|
|
<Text style={[
|
|
styles.compactIndicatorPercentage,
|
|
{ color: getPercentageColor(getPercentageValue(prediction.prediction, 'intraparenchymal')) }
|
|
]}>
|
|
{getPercentageValue(prediction.prediction, 'intraparenchymal').toFixed(1)}%
|
|
</Text>
|
|
</View>
|
|
<View style={styles.progressBarContainer}>
|
|
<View
|
|
style={[
|
|
styles.progressBar,
|
|
{
|
|
width: `${getPercentageValue(prediction.prediction, 'intraparenchymal')}%`,
|
|
backgroundColor: getPercentageColor(getPercentageValue(prediction.prediction, 'intraparenchymal'))
|
|
}
|
|
]}
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Subarachnoid */}
|
|
<View style={styles.compactIndicatorItem}>
|
|
<View style={styles.indicatorHeader}>
|
|
<Icon
|
|
name="activity"
|
|
size={16}
|
|
color={getPercentageColor(getPercentageValue(prediction.prediction, 'subarachnoid'))}
|
|
/>
|
|
<Text style={styles.compactIndicatorTitle}>Subarachnoid</Text>
|
|
<Text style={[
|
|
styles.compactIndicatorPercentage,
|
|
{ color: getPercentageColor(getPercentageValue(prediction.prediction, 'subarachnoid')) }
|
|
]}>
|
|
{getPercentageValue(prediction.prediction, 'subarachnoid').toFixed(1)}%
|
|
</Text>
|
|
</View>
|
|
<View style={styles.progressBarContainer}>
|
|
<View
|
|
style={[
|
|
styles.progressBar,
|
|
{
|
|
width: `${getPercentageValue(prediction.prediction, 'subarachnoid')}%`,
|
|
backgroundColor: getPercentageColor(getPercentageValue(prediction.prediction, 'subarachnoid'))
|
|
}
|
|
]}
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Intraventricular */}
|
|
<View style={styles.compactIndicatorItem}>
|
|
<View style={styles.indicatorHeader}>
|
|
<Icon
|
|
name="activity"
|
|
size={16}
|
|
color={getPercentageColor(getPercentageValue(prediction.prediction, 'intraventricular'))}
|
|
/>
|
|
<Text style={styles.compactIndicatorTitle}>Intraventricular</Text>
|
|
<Text style={[
|
|
styles.compactIndicatorPercentage,
|
|
{ color: getPercentageColor(getPercentageValue(prediction.prediction, 'intraventricular')) }
|
|
]}>
|
|
{getPercentageValue(prediction.prediction, 'intraventricular').toFixed(1)}%
|
|
</Text>
|
|
</View>
|
|
<View style={styles.progressBarContainer}>
|
|
<View
|
|
style={[
|
|
styles.progressBar,
|
|
{
|
|
width: `${getPercentageValue(prediction.prediction, 'intraventricular')}%`,
|
|
backgroundColor: getPercentageColor(getPercentageValue(prediction.prediction, 'intraventricular'))
|
|
}
|
|
]}
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Midline Shift */}
|
|
<View style={styles.compactIndicatorItem}>
|
|
<View style={styles.indicatorHeader}>
|
|
<Icon
|
|
name="activity"
|
|
size={16}
|
|
color={getPercentageColor(getPercentageValue(prediction.prediction, 'midline_shift'))}
|
|
/>
|
|
<Text style={styles.compactIndicatorTitle}>Midline Shift</Text>
|
|
<Text style={[
|
|
styles.compactIndicatorPercentage,
|
|
{ color: getPercentageColor(getPercentageValue(prediction.prediction, 'midline_shift')) }
|
|
]}>
|
|
{getPercentageValue(prediction.prediction, 'midline_shift').toFixed(1)}%
|
|
</Text>
|
|
</View>
|
|
<View style={styles.progressBarContainer}>
|
|
<View
|
|
style={[
|
|
styles.progressBar,
|
|
{
|
|
width: `${getPercentageValue(prediction.prediction, 'midline_shift')}%`,
|
|
backgroundColor: getPercentageValue(prediction.prediction, 'midline_shift') > 50 ? theme.colors.error : theme.colors.warning
|
|
}
|
|
]}
|
|
/>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Summary Indicator with Overall Percentage */}
|
|
<View style={styles.summaryIndicator}>
|
|
<Text style={styles.summaryIndicatorTitle}>Overall Assessment</Text>
|
|
|
|
{/* Overall Percentage Calculation */}
|
|
{(() => {
|
|
const percentages = [
|
|
getPercentageValue(prediction.prediction, 'epidural'),
|
|
getPercentageValue(prediction.prediction, 'subdural'),
|
|
getPercentageValue(prediction.prediction, 'intraparenchymal'),
|
|
getPercentageValue(prediction.prediction, 'subarachnoid'),
|
|
getPercentageValue(prediction.prediction, 'intraventricular'),
|
|
getPercentageValue(prediction.prediction, 'midline_shift')
|
|
];
|
|
const maxPercentage = Math.max(...percentages);
|
|
const hasHemorrhage = maxPercentage > 10; // Consider >10% as detected
|
|
|
|
return (
|
|
<View style={styles.overallPercentageContainer}>
|
|
<Text style={styles.overallPercentageLabel}>Highest Detection:</Text>
|
|
<Text style={styles.overallPercentageValue}>{maxPercentage.toFixed(1)}%</Text>
|
|
|
|
<View style={[
|
|
styles.summaryIndicatorStatus,
|
|
{ backgroundColor: hasHemorrhage ? theme.colors.error : theme.colors.success }
|
|
]}>
|
|
<Icon
|
|
name={hasHemorrhage ? "alert-circle" : "check-circle"}
|
|
size={24}
|
|
color={theme.colors.background}
|
|
/>
|
|
<Text style={styles.summaryIndicatorText}>
|
|
{hasHemorrhage ? 'HEMORRHAGE DETECTED' : 'NO HEMORRHAGE'}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
})()}
|
|
</View>
|
|
</View>
|
|
|
|
<Text style={styles.predictionTimestamp}>
|
|
Processed: {new Date(prediction.processed_at).toLocaleDateString()}
|
|
</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Render Series Images
|
|
*
|
|
* Purpose: Render DICOM image previews for the series
|
|
*/
|
|
const renderSeriesImages = () => {
|
|
const predictions = getSeriesPredictions();
|
|
|
|
if (predictions.length === 0) {
|
|
return (
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>DICOM Images</Text>
|
|
<View style={styles.emptyState}>
|
|
<Icon name="image" size={48} color={theme.colors.textMuted} />
|
|
<Text style={styles.emptyStateTitle}>No Images Available</Text>
|
|
<Text style={styles.emptyStateSubtitle}>
|
|
No DICOM images are available for this series
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
// Calculate image summary metrics
|
|
const totalImages = predictions.length;
|
|
const imagesWithPreview = predictions.filter((p: Prediction) => p.preview).length;
|
|
const imagesWithoutPreview = totalImages - imagesWithPreview;
|
|
|
|
return (
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>DICOM Images</Text>
|
|
|
|
{/* Images Summary Bar */}
|
|
<View style={styles.imagesSummaryBar}>
|
|
<View style={styles.imagesSummaryItem}>
|
|
<Text style={styles.imagesSummaryLabel}>Total Images</Text>
|
|
<View style={styles.imagesSummaryValueContainer}>
|
|
<Text style={styles.imagesSummaryValue}>{totalImages}</Text>
|
|
</View>
|
|
</View>
|
|
<View style={styles.imagesSummaryItem}>
|
|
<Text style={styles.imagesSummaryLabel}>With Preview</Text>
|
|
<View style={styles.imagesSummaryValueContainer}>
|
|
<Text style={styles.imagesSummaryValue}>{imagesWithPreview}</Text>
|
|
</View>
|
|
</View>
|
|
<View style={styles.imagesSummaryItem}>
|
|
<Text style={styles.imagesSummaryLabel}>Format</Text>
|
|
<View style={styles.imagesSummaryValueContainer}>
|
|
<Text style={styles.imagesSummaryValue}>DICOM</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Images Display Card */}
|
|
<View style={styles.imagesDisplayCard}>
|
|
<View style={styles.cardHeader}>
|
|
<Text style={styles.cardTitle}>Image Gallery</Text>
|
|
<Text style={styles.cardSubtitle}>Series image previews and metadata</Text>
|
|
</View>
|
|
|
|
<ScrollView
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
contentContainerStyle={styles.imageList}
|
|
>
|
|
{predictions.map((prediction: Prediction, index: number) => (
|
|
<View key={prediction.id} style={styles.imageContainer}>
|
|
{prediction.preview ? (
|
|
<TouchableOpacity
|
|
style={styles.imageClickable}
|
|
onPress={() => handleDicomImagePress(prediction, index)}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Image
|
|
source={{ uri: API_CONFIG.DICOM_BASE_URL + prediction.preview }}
|
|
style={styles.seriesImage}
|
|
resizeMode="cover"
|
|
/>
|
|
{/* Overlay to indicate clickable */}
|
|
<View style={styles.imageOverlay}>
|
|
<Icon name="maximize-2" size={20} color={theme.colors.background} />
|
|
<Text style={styles.imageOverlayText}>View DICOM</Text>
|
|
</View>
|
|
</TouchableOpacity>
|
|
) : (
|
|
<View style={styles.noImagePlaceholder}>
|
|
<Icon name="image" size={32} color={theme.colors.textMuted} />
|
|
<Text style={styles.noImageText}>No Preview</Text>
|
|
</View>
|
|
)}
|
|
<Text style={styles.imageLabel}>Image {index + 1}</Text>
|
|
<Text style={styles.imagePredictionLabel}>
|
|
{prediction.prediction.label}
|
|
</Text>
|
|
</View>
|
|
))}
|
|
</ScrollView>
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Render Feedback History
|
|
*
|
|
* Purpose: Render feedback history from physicians
|
|
*/
|
|
const renderFeedbackHistory = () => {
|
|
const feedback = getSeriesFeedback();
|
|
|
|
if (isRefreshing) {
|
|
return (
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>Feedback History</Text>
|
|
<View style={styles.loadingState}>
|
|
<Icon name="loader" size={24} color={theme.colors.primary} />
|
|
<Text style={styles.loadingStateText}>Refreshing feedback...</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (feedback.length === 0) {
|
|
return (
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>Feedback History</Text>
|
|
<View style={styles.emptyState}>
|
|
<Icon name="message-circle" size={48} color={theme.colors.textMuted} />
|
|
<Text style={styles.emptyStateTitle}>No Feedback Yet</Text>
|
|
<Text style={styles.emptyStateSubtitle}>
|
|
Be the first to provide feedback on this series
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
// Calculate feedback summary metrics
|
|
const totalFeedback = feedback.length;
|
|
const positiveFeedback = feedback.filter((f: Feedback) => f.is_positive).length;
|
|
const negativeFeedback = feedback.filter((f: Feedback) => !f.is_positive).length;
|
|
|
|
return (
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>Feedback History</Text>
|
|
|
|
{/* Feedback Summary Bar */}
|
|
<View style={styles.feedbackSummaryBar}>
|
|
<View style={styles.feedbackSummaryItem}>
|
|
<Text style={styles.feedbackSummaryLabel}>Total Feedback</Text>
|
|
<View style={styles.feedbackSummaryValueContainer}>
|
|
<Text style={styles.feedbackSummaryValue}>{totalFeedback} entries</Text>
|
|
</View>
|
|
</View>
|
|
<View style={styles.feedbackSummaryItem}>
|
|
<Text style={styles.feedbackSummaryLabel}>Positive</Text>
|
|
<View style={styles.feedbackSummaryValueContainer}>
|
|
<Text style={styles.feedbackSummaryValue}>{positiveFeedback}</Text>
|
|
</View>
|
|
</View>
|
|
<View style={styles.feedbackSummaryItem}>
|
|
<Text style={styles.feedbackSummaryLabel}>Negative</Text>
|
|
<View style={styles.feedbackSummaryValueContainer}>
|
|
<Text style={styles.feedbackSummaryValue}>{negativeFeedback}</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Feedback History Card */}
|
|
<View style={styles.feedbackHistoryCard}>
|
|
<View style={styles.cardHeader}>
|
|
<Text style={styles.cardTitle}>Clinical Feedback</Text>
|
|
<Text style={styles.cardSubtitle}>Radiologist insights and corrections</Text>
|
|
</View>
|
|
|
|
{feedback.map((feedbackItem: Feedback) => (
|
|
<View key={feedbackItem.feedback_id} style={styles.feedbackCard}>
|
|
<View style={styles.feedbackHeader}>
|
|
<View style={styles.feedbackHeaderLeft}>
|
|
<View style={[
|
|
styles.feedbackTypeBadge,
|
|
{ backgroundColor: getFeedbackTypeColor(feedbackItem.feedback_type) }
|
|
]}>
|
|
<Text style={styles.feedbackTypeText}>
|
|
{feedbackItem.feedback_type}
|
|
</Text>
|
|
</View>
|
|
{isFeedbackNew(feedbackItem.feedback_id) && (
|
|
<View style={styles.newFeedbackBadge}>
|
|
<Text style={styles.newFeedbackBadgeText}>NEW</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
<Text style={styles.feedbackDate}>
|
|
{new Date(feedbackItem.created_at).toLocaleDateString()}
|
|
</Text>
|
|
</View>
|
|
|
|
<Text style={styles.feedbackText}>{feedbackItem.feedback_text}</Text>
|
|
|
|
<View style={styles.feedbackFooter}>
|
|
<Text style={styles.feedbackEmail}>{feedbackItem.email}</Text>
|
|
<Text style={styles.feedbackPredictionId}>
|
|
Prediction ID: {feedbackItem.prediction_id}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
))}
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
// ============================================================================
|
|
// MAIN RENDER
|
|
// ============================================================================
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container}>
|
|
|
|
{/* Header */}
|
|
<View style={styles.header}>
|
|
<TouchableOpacity
|
|
style={styles.backButton}
|
|
onPress={handleBackPress}
|
|
>
|
|
<Icon name="arrow-left" size={24} color={theme.colors.textPrimary} />
|
|
</TouchableOpacity>
|
|
<View style={styles.headerTitle}>
|
|
<Text style={styles.headerTitleText}>Series {seriesNumber}</Text>
|
|
<Text style={styles.headerSubtitleText}>{patientName}</Text>
|
|
</View>
|
|
<TouchableOpacity
|
|
style={styles.refreshButton}
|
|
onPress={handleRefresh}
|
|
disabled={isRefreshing}
|
|
>
|
|
<Icon
|
|
name="refresh-cw"
|
|
size={20}
|
|
color={isRefreshing ? theme.colors.textMuted : theme.colors.primary}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Tab Navigation */}
|
|
{renderTabNavigation()}
|
|
|
|
{/* Content */}
|
|
<ScrollView
|
|
style={styles.content}
|
|
showsVerticalScrollIndicator={false}
|
|
refreshControl={
|
|
<RefreshControl
|
|
refreshing={isRefreshing}
|
|
onRefresh={handleRefresh}
|
|
colors={[theme.colors.primary]}
|
|
tintColor={theme.colors.primary}
|
|
/>
|
|
}
|
|
>
|
|
{/* Series Header */}
|
|
{renderSeriesHeader()}
|
|
|
|
{/* Tab Content */}
|
|
{activeTab === 'series' && (
|
|
<>
|
|
{/* Series Details */}
|
|
{renderSeriesDetails()}
|
|
|
|
{/* Series Images */}
|
|
{renderSeriesImages()}
|
|
</>
|
|
)}
|
|
|
|
{activeTab === 'ai' && (
|
|
<>
|
|
{/* AI Predictions */}
|
|
{renderAIPredictions()}
|
|
</>
|
|
)}
|
|
|
|
{activeTab === 'feedback' && (
|
|
<>
|
|
{/* Feedback History */}
|
|
{renderFeedbackHistory()}
|
|
</>
|
|
)}
|
|
</ScrollView>
|
|
|
|
{/* Floating Feedback Button - Show on all tabs */}
|
|
<TouchableOpacity
|
|
style={styles.floatingFeedbackButton}
|
|
onPress={() => {
|
|
const predictions = getSeriesPredictions();
|
|
if (predictions.length > 0) {
|
|
handleOpenFeedback(predictions[0]);
|
|
} else {
|
|
Alert.alert('No Predictions', 'No AI predictions available for feedback');
|
|
}
|
|
}}
|
|
activeOpacity={0.8}
|
|
>
|
|
<Icon name="message-circle" size={24} color={theme.colors.background} />
|
|
</TouchableOpacity>
|
|
|
|
{/* Feedback Modal */}
|
|
{showFeedbackModal && selectedPrediction && (
|
|
<View style={styles.modalOverlay}>
|
|
<View style={styles.feedbackModal}>
|
|
<View style={styles.modalHeader}>
|
|
<Text style={styles.modalTitle}>Provide Feedback</Text>
|
|
<TouchableOpacity
|
|
style={styles.closeButton}
|
|
onPress={handleCloseFeedback}
|
|
>
|
|
<Icon name="x" size={24} color={theme.colors.textSecondary} />
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
<View style={styles.modalContent}>
|
|
{/* Prediction Info */}
|
|
<View style={styles.feedbackPredictionInfo}>
|
|
<Text style={styles.feedbackPredictionTitle}>
|
|
AI Prediction: {selectedPrediction.prediction.label}
|
|
</Text>
|
|
<Text style={styles.feedbackPredictionMeta}>
|
|
Confidence: {(selectedPrediction.prediction.confidence_score * 100).toFixed(1)}% •
|
|
Type: {selectedPrediction.prediction.finding_type}
|
|
</Text>
|
|
</View>
|
|
|
|
{/* Prediction Accuracy Selection */}
|
|
<View style={styles.feedbackSection}>
|
|
<Text style={styles.feedbackSectionTitle}>Is this prediction accurate?</Text>
|
|
<View style={styles.predictionAccuracyContainer}>
|
|
{[
|
|
{ key: 'true', label: 'Yes (Positive)', color: theme.colors.success, icon: 'check-circle', value: true },
|
|
{ key: 'false', label: 'No (Negative)', color: theme.colors.error, icon: 'x-circle', value: false }
|
|
].map((option) => (
|
|
<TouchableOpacity
|
|
key={option.key}
|
|
onPress={() => setIsPositive(option.value)}
|
|
style={[
|
|
styles.predictionAccuracyButton,
|
|
isPositive === option.value && styles.predictionAccuracyButtonActive,
|
|
{ borderColor: option.color }
|
|
]}
|
|
>
|
|
<Icon
|
|
name={option.icon as any}
|
|
size={16}
|
|
color={isPositive === option.value ? theme.colors.background : option.color}
|
|
/>
|
|
<Text style={[
|
|
styles.predictionAccuracyButtonText,
|
|
isPositive === option.value && styles.predictionAccuracyButtonTextActive
|
|
]}>
|
|
{option.label}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
</View>
|
|
|
|
{/* Feedback Text Input */}
|
|
<View style={styles.feedbackSection}>
|
|
<Text style={styles.feedbackSectionTitle}>Your Feedback</Text>
|
|
<TextInput
|
|
style={styles.feedbackTextInput}
|
|
placeholder="Provide your clinical insights, suggestions, or corrections..."
|
|
placeholderTextColor={theme.colors.textMuted}
|
|
value={feedbackText}
|
|
onChangeText={setFeedbackText}
|
|
multiline
|
|
numberOfLines={4}
|
|
textAlignVertical="top"
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.modalFooter}>
|
|
<TouchableOpacity
|
|
style={styles.cancelButton}
|
|
onPress={handleCloseFeedback}
|
|
>
|
|
<Text style={styles.cancelButtonText}>Cancel</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.submitButton,
|
|
(!feedbackText.trim() || isPositive === null || isSubmittingFeedback) && styles.submitButtonDisabled
|
|
]}
|
|
onPress={handleSubmitFeedback}
|
|
disabled={!feedbackText.trim() || isPositive === null || isSubmittingFeedback}
|
|
>
|
|
{isSubmittingFeedback ? (
|
|
<View style={styles.submitButtonLoading}>
|
|
<Icon name="loader" size={16} color={theme.colors.background} />
|
|
<Text style={[styles.submitButtonText, { marginLeft: 8 }]}>Submitting...</Text>
|
|
</View>
|
|
) : (
|
|
<Text style={styles.submitButtonText}>Submit Feedback</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Feedback Result Modal */}
|
|
{showFeedbackResultModal && feedbackResult && (
|
|
<View style={styles.modalOverlay}>
|
|
<View style={styles.feedbackResultModal}>
|
|
<View style={styles.modalHeader}>
|
|
<Icon
|
|
name={feedbackResult.type === 'success' ? 'check-circle' : 'alert-circle'}
|
|
size={24}
|
|
color={feedbackResult.type === 'success' ? theme.colors.success : theme.colors.error}
|
|
/>
|
|
<Text style={[
|
|
styles.modalTitle,
|
|
{ color: feedbackResult.type === 'success' ? theme.colors.success : theme.colors.error }
|
|
]}>
|
|
{feedbackResult.title}
|
|
</Text>
|
|
</View>
|
|
|
|
<View style={styles.modalContent}>
|
|
<Text style={styles.feedbackResultMessage}>
|
|
{feedbackResult.message}
|
|
</Text>
|
|
</View>
|
|
|
|
<View style={styles.modalFooter}>
|
|
<TouchableOpacity
|
|
style={styles.okButton}
|
|
onPress={handleFeedbackResultClose}
|
|
>
|
|
<Text style={styles.okButtonText}>OK</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* DICOM Viewer Modal */}
|
|
{selectedDicomData && (
|
|
<DicomViewerModal
|
|
visible={dicomModalVisible}
|
|
dicomUrl={selectedDicomData.dicomUrl}
|
|
onClose={handleCloseDicomModal}
|
|
title={`${selectedDicomData.prediction.prediction.label}`}
|
|
patientName={patientName}
|
|
studyDescription={`Series ${seriesNumber} - Image ${selectedDicomData.imageIndex + 1} - ${selectedDicomData.prediction.prediction.finding_type}`}
|
|
/>
|
|
)}
|
|
|
|
</SafeAreaView>
|
|
);
|
|
};
|
|
|
|
// ============================================================================
|
|
// STYLES
|
|
// ============================================================================
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: theme.colors.background,
|
|
},
|
|
|
|
// Header Styles
|
|
header: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingHorizontal: theme.spacing.md,
|
|
paddingVertical: theme.spacing.sm,
|
|
backgroundColor: theme.colors.background,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: theme.colors.border,
|
|
},
|
|
backButton: {
|
|
padding: theme.spacing.sm,
|
|
marginRight: theme.spacing.sm,
|
|
},
|
|
headerTitle: {
|
|
flex: 1,
|
|
alignItems: 'center',
|
|
},
|
|
headerTitleText: {
|
|
fontSize: 18,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
},
|
|
headerSubtitleText: {
|
|
fontSize: 12,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
},
|
|
refreshButton: {
|
|
padding: theme.spacing.sm,
|
|
marginLeft: theme.spacing.sm,
|
|
},
|
|
|
|
// Content Styles
|
|
content: {
|
|
flex: 1,
|
|
},
|
|
|
|
// Section Styles
|
|
section: {
|
|
marginBottom: theme.spacing.lg,
|
|
paddingHorizontal: theme.spacing.md,
|
|
},
|
|
sectionTitle: {
|
|
fontSize: 18,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
marginBottom: theme.spacing.md,
|
|
},
|
|
|
|
// Series Header Styles
|
|
seriesHeader: {
|
|
paddingHorizontal: theme.spacing.md,
|
|
paddingVertical: theme.spacing.lg,
|
|
backgroundColor: theme.colors.background,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: theme.colors.border,
|
|
},
|
|
seriesHeaderContent: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
},
|
|
seriesHeaderLeft: {
|
|
flex: 1,
|
|
},
|
|
seriesHeaderRight: {
|
|
alignItems: 'flex-end',
|
|
},
|
|
seriesTitle: {
|
|
fontSize: 24,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
marginBottom: theme.spacing.xs,
|
|
},
|
|
seriesDescription: {
|
|
fontSize: 14,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
seriesMetaRow: {
|
|
flexDirection: 'row',
|
|
flexWrap: 'wrap',
|
|
marginTop: theme.spacing.xs,
|
|
},
|
|
metaItem: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginRight: theme.spacing.md,
|
|
marginBottom: theme.spacing.xs,
|
|
},
|
|
metaText: {
|
|
fontSize: 12,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
marginLeft: theme.spacing.sm,
|
|
},
|
|
studyDate: {
|
|
fontSize: 12,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
},
|
|
seriesStatusBadge: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: theme.colors.success,
|
|
paddingHorizontal: theme.spacing.md,
|
|
paddingVertical: theme.spacing.xs,
|
|
borderRadius: 12,
|
|
},
|
|
seriesStatusText: {
|
|
fontSize: 12,
|
|
color: theme.colors.background,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
marginLeft: theme.spacing.sm,
|
|
},
|
|
|
|
// Series Details Styles
|
|
seriesDetails: {
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
padding: theme.spacing.md,
|
|
borderRadius: 8,
|
|
},
|
|
seriesDetailItem: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'flex-start',
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
seriesDetailLabel: {
|
|
fontSize: 12,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
flex: 1,
|
|
},
|
|
seriesDetailValue: {
|
|
fontSize: 12,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
flex: 2,
|
|
textAlign: 'right',
|
|
},
|
|
|
|
// Prediction Styles
|
|
predictionCard: {
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
borderRadius: 8,
|
|
padding: theme.spacing.sm,
|
|
marginBottom: theme.spacing.sm,
|
|
// shadowColor: '#000',
|
|
// shadowOffset: { width: 0, height: 2 },
|
|
// shadowOpacity: 0.1,
|
|
// shadowRadius: 4,
|
|
// elevation: 2,
|
|
},
|
|
predictionHeader: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
predictionLabel: {
|
|
fontSize: 14,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
flex: 1,
|
|
},
|
|
urgencyBadge: {
|
|
paddingHorizontal: 10,
|
|
paddingVertical: 4,
|
|
borderRadius: 12,
|
|
},
|
|
urgencyText: {
|
|
fontSize: 10,
|
|
color: theme.colors.background,
|
|
textTransform: 'uppercase',
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
},
|
|
predictionDetails: {
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
predictionDetailItem: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'flex-start',
|
|
marginBottom: theme.spacing.xs,
|
|
},
|
|
predictionDetailLabel: {
|
|
fontSize: 12,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
flex: 1,
|
|
},
|
|
predictionDetailValue: {
|
|
fontSize: 12,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
flex: 2,
|
|
textAlign: 'right',
|
|
},
|
|
predictionTimestamp: {
|
|
fontSize: 12,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
textAlign: 'right',
|
|
},
|
|
|
|
// Image Styles
|
|
imageList: {
|
|
paddingRight: theme.spacing.md,
|
|
},
|
|
imageContainer: {
|
|
alignItems: 'center',
|
|
marginRight: theme.spacing.md,
|
|
},
|
|
imageClickable: {
|
|
position: 'relative',
|
|
marginBottom: theme.spacing.xs,
|
|
borderRadius: 8,
|
|
overflow: 'hidden',
|
|
},
|
|
seriesImage: {
|
|
width: 120,
|
|
height: 120,
|
|
borderRadius: 8,
|
|
},
|
|
imageOverlay: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
opacity: 1,
|
|
},
|
|
imageOverlayText: {
|
|
color: theme.colors.background,
|
|
fontSize: 10,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
marginTop: 4,
|
|
textAlign: 'center',
|
|
},
|
|
noImagePlaceholder: {
|
|
width: 120,
|
|
height: 120,
|
|
borderRadius: 8,
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
borderWidth: 1,
|
|
borderColor: theme.colors.border,
|
|
borderStyle: 'dashed',
|
|
marginBottom: theme.spacing.xs,
|
|
},
|
|
noImageText: {
|
|
fontSize: 10,
|
|
color: theme.colors.textMuted,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
textAlign: 'center',
|
|
},
|
|
imageLabel: {
|
|
fontSize: 12,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
},
|
|
imagePredictionLabel: {
|
|
fontSize: 10,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
marginTop: theme.spacing.xs,
|
|
},
|
|
|
|
// Feedback Styles
|
|
feedbackCard: {
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
borderRadius: 8,
|
|
padding: theme.spacing.md,
|
|
marginBottom: theme.spacing.sm,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.1,
|
|
shadowRadius: 4,
|
|
elevation: 2,
|
|
},
|
|
feedbackHeader: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
feedbackHeaderLeft: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
},
|
|
feedbackTypeBadge: {
|
|
paddingHorizontal: 8,
|
|
paddingVertical: 4,
|
|
borderRadius: 12,
|
|
},
|
|
feedbackTypeText: {
|
|
fontSize: 10,
|
|
color: theme.colors.background,
|
|
textTransform: 'uppercase',
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
},
|
|
newFeedbackBadge: {
|
|
backgroundColor: theme.colors.primary,
|
|
paddingHorizontal: 8,
|
|
paddingVertical: 4,
|
|
borderRadius: 12,
|
|
marginLeft: theme.spacing.sm,
|
|
},
|
|
newFeedbackBadgeText: {
|
|
fontSize: 10,
|
|
color: theme.colors.background,
|
|
textTransform: 'uppercase',
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
},
|
|
feedbackDate: {
|
|
fontSize: 12,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
},
|
|
feedbackText: {
|
|
fontSize: 14,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
lineHeight: 20,
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
feedbackFooter: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
},
|
|
feedbackEmail: {
|
|
fontSize: 12,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
},
|
|
feedbackPredictionId: {
|
|
fontSize: 12,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
},
|
|
|
|
// Empty State Styles
|
|
emptyState: {
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
paddingVertical: theme.spacing.xl,
|
|
},
|
|
emptyStateTitle: {
|
|
fontSize: 18,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
marginTop: theme.spacing.md,
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
emptyStateSubtitle: {
|
|
fontSize: 14,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
textAlign: 'center',
|
|
lineHeight: 20,
|
|
},
|
|
|
|
// Floating Feedback Button
|
|
floatingFeedbackButton: {
|
|
position: 'absolute',
|
|
bottom: theme.spacing.lg,
|
|
right: theme.spacing.lg,
|
|
width: 56,
|
|
height: 56,
|
|
borderRadius: 28,
|
|
backgroundColor: theme.colors.primary,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
shadowColor: theme.colors.primary,
|
|
shadowOffset: { width: 0, height: 4 },
|
|
shadowOpacity: 0.3,
|
|
shadowRadius: 8,
|
|
elevation: 8,
|
|
},
|
|
|
|
// Modal Styles
|
|
modalOverlay: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
zIndex: 1000,
|
|
},
|
|
feedbackModal: {
|
|
backgroundColor: theme.colors.background,
|
|
borderRadius: 12,
|
|
width: '90%',
|
|
maxWidth: 450,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 4 },
|
|
shadowOpacity: 0.3,
|
|
shadowRadius: 10,
|
|
elevation: 10,
|
|
},
|
|
modalHeader: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
padding: theme.spacing.md,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: theme.colors.border,
|
|
},
|
|
modalTitle: {
|
|
fontSize: 20,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
},
|
|
closeButton: {
|
|
padding: theme.spacing.sm,
|
|
},
|
|
modalContent: {
|
|
padding: theme.spacing.md,
|
|
},
|
|
feedbackSection: {
|
|
marginBottom: theme.spacing.md,
|
|
},
|
|
feedbackSectionTitle: {
|
|
fontSize: 14,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
feedbackPredictionInfo: {
|
|
paddingTop: theme.spacing.md,
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
feedbackPredictionTitle: {
|
|
fontSize: 14,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
marginBottom: theme.spacing.xs,
|
|
},
|
|
feedbackPredictionMeta: {
|
|
fontSize: 12,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
},
|
|
predictionAccuracyContainer: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-around',
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
predictionAccuracyButton: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingVertical: theme.spacing.sm,
|
|
paddingHorizontal: theme.spacing.md,
|
|
borderRadius: 12,
|
|
borderWidth: 1,
|
|
borderColor: theme.colors.border,
|
|
minWidth: 120,
|
|
justifyContent: 'center',
|
|
},
|
|
predictionAccuracyButtonActive: {
|
|
borderColor: theme.colors.primary,
|
|
backgroundColor: theme.colors.primary,
|
|
},
|
|
predictionAccuracyButtonText: {
|
|
fontSize: 14,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
marginLeft: theme.spacing.sm,
|
|
},
|
|
predictionAccuracyButtonTextActive: {
|
|
color: theme.colors.background,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
},
|
|
feedbackTextInput: {
|
|
borderWidth: 1,
|
|
borderColor: theme.colors.border,
|
|
borderRadius: 8,
|
|
padding: theme.spacing.md,
|
|
fontSize: 14,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
minHeight: 100,
|
|
textAlignVertical: 'top',
|
|
},
|
|
modalFooter: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-around',
|
|
padding: theme.spacing.md,
|
|
borderTopWidth: 1,
|
|
borderTopColor: theme.colors.border,
|
|
},
|
|
cancelButton: {
|
|
paddingVertical: theme.spacing.md,
|
|
paddingHorizontal: theme.spacing.lg,
|
|
borderRadius: 8,
|
|
borderWidth: 1,
|
|
borderColor: theme.colors.border,
|
|
},
|
|
cancelButtonText: {
|
|
fontSize: 16,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
},
|
|
submitButton: {
|
|
paddingVertical: theme.spacing.md,
|
|
paddingHorizontal: theme.spacing.lg,
|
|
borderRadius: 8,
|
|
backgroundColor: theme.colors.primary,
|
|
shadowColor: theme.colors.primary,
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.3,
|
|
shadowRadius: 4,
|
|
elevation: 4,
|
|
},
|
|
submitButtonDisabled: {
|
|
backgroundColor: theme.colors.textMuted,
|
|
opacity: 0.7,
|
|
},
|
|
submitButtonText: {
|
|
color: theme.colors.background,
|
|
fontSize: 16,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
},
|
|
submitButtonLoading: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
|
|
// Feedback Result Modal Styles
|
|
feedbackResultModal: {
|
|
backgroundColor: theme.colors.background,
|
|
borderRadius: 16,
|
|
padding: 0,
|
|
width: '90%',
|
|
maxWidth: 400,
|
|
shadowColor: '#000000',
|
|
shadowOffset: { width: 0, height: 4 },
|
|
shadowOpacity: 0.15,
|
|
shadowRadius: 8,
|
|
elevation: 8,
|
|
},
|
|
feedbackResultMessage: {
|
|
fontSize: 16,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
textAlign: 'center',
|
|
lineHeight: 24,
|
|
paddingHorizontal: theme.spacing.md,
|
|
},
|
|
okButton: {
|
|
paddingVertical: theme.spacing.md,
|
|
paddingHorizontal: theme.spacing.xl,
|
|
borderRadius: 8,
|
|
backgroundColor: theme.colors.primary,
|
|
shadowColor: theme.colors.primary,
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.3,
|
|
shadowRadius: 4,
|
|
elevation: 4,
|
|
minWidth: 100,
|
|
alignItems: 'center',
|
|
},
|
|
okButtonText: {
|
|
color: theme.colors.background,
|
|
fontSize: 16,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
},
|
|
loadingState: {
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
paddingVertical: theme.spacing.xl,
|
|
},
|
|
loadingStateText: {
|
|
fontSize: 14,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
marginTop: theme.spacing.sm,
|
|
},
|
|
// New styles for detailed series info
|
|
seriesSummaryBar: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-around',
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
paddingVertical: theme.spacing.sm,
|
|
paddingHorizontal: theme.spacing.md,
|
|
borderRadius: 8,
|
|
marginBottom: theme.spacing.md,
|
|
},
|
|
summaryItem: {
|
|
alignItems: 'center',
|
|
},
|
|
summaryLabel: {
|
|
fontSize: 12,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
marginBottom: theme.spacing.xs,
|
|
},
|
|
summaryValueContainer: {
|
|
backgroundColor: theme.colors.background,
|
|
paddingHorizontal: theme.spacing.md,
|
|
paddingVertical: theme.spacing.xs,
|
|
borderRadius: 12,
|
|
},
|
|
summaryValue: {
|
|
fontSize: 14,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
},
|
|
seriesDetailsCard: {
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
borderRadius: 12,
|
|
padding: theme.spacing.md,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.1,
|
|
shadowRadius: 4,
|
|
elevation: 2,
|
|
},
|
|
cardHeader: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
cardTitle: {
|
|
fontSize: 18,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
},
|
|
cardSubtitle: {
|
|
fontSize: 12,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
paddingHorizontal:theme.spacing.sm,
|
|
flex:1
|
|
},
|
|
detailsGrid: {
|
|
marginBottom: theme.spacing.md,
|
|
},
|
|
detailRow: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
detailColumn: {
|
|
flex: 1,
|
|
marginRight: theme.spacing.sm,
|
|
},
|
|
detailLabel: {
|
|
fontSize: 12,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
marginBottom: theme.spacing.xs,
|
|
},
|
|
detailValue: {
|
|
fontSize: 14,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
},
|
|
processingInfo: {
|
|
marginTop: theme.spacing.md,
|
|
paddingTop: theme.spacing.md,
|
|
borderTopWidth: 1,
|
|
borderTopColor: theme.colors.border,
|
|
},
|
|
processingTitle: {
|
|
fontSize: 16,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
processingGrid: {
|
|
flexDirection: 'row',
|
|
flexWrap: 'wrap',
|
|
justifyContent: 'space-around',
|
|
},
|
|
processingItem: {
|
|
alignItems: 'center',
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
processingLabel: {
|
|
fontSize: 12,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
},
|
|
processingValue: {
|
|
fontSize: 14,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
},
|
|
statusBadge: {
|
|
backgroundColor: theme.colors.success,
|
|
paddingHorizontal: theme.spacing.md,
|
|
paddingVertical: theme.spacing.xs,
|
|
borderRadius: 12,
|
|
},
|
|
statusText: {
|
|
fontSize: 12,
|
|
color: theme.colors.background,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
},
|
|
// New styles for AI summary bar
|
|
aiSummaryBar: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-around',
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
paddingVertical: theme.spacing.sm,
|
|
paddingHorizontal: theme.spacing.md,
|
|
borderRadius: 8,
|
|
marginBottom: theme.spacing.md,
|
|
},
|
|
aiSummaryItem: {
|
|
alignItems: 'center',
|
|
},
|
|
aiSummaryLabel: {
|
|
fontSize: 12,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
marginBottom: theme.spacing.xs,
|
|
},
|
|
aiSummaryValueContainer: {
|
|
backgroundColor: theme.colors.background,
|
|
paddingHorizontal: theme.spacing.md,
|
|
paddingVertical: theme.spacing.xs,
|
|
borderRadius: 12,
|
|
},
|
|
aiSummaryValue: {
|
|
fontSize: 14,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
},
|
|
// New styles for AI predictions card
|
|
aiPredictionsCard: {
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
borderRadius: 12,
|
|
padding: theme.spacing.md,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.1,
|
|
shadowRadius: 4,
|
|
elevation: 2,
|
|
},
|
|
// New styles for detailed results section
|
|
detailedResultsSection: {
|
|
marginTop: theme.spacing.md,
|
|
paddingTop: theme.spacing.md,
|
|
borderTopWidth: 1,
|
|
borderTopColor: theme.colors.border,
|
|
},
|
|
detailedResultsTitle: {
|
|
fontSize: 14,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
detailedResultsGrid: {
|
|
flexDirection: 'row',
|
|
flexWrap: 'wrap',
|
|
justifyContent: 'space-around',
|
|
},
|
|
detailedResultItem: {
|
|
alignItems: 'center',
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
detailedResultLabel: {
|
|
fontSize: 12,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
},
|
|
detailedResultValue: {
|
|
fontSize: 14,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
},
|
|
// New styles for additional findings
|
|
additionalFindingItem: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginTop: theme.spacing.sm,
|
|
paddingTop: theme.spacing.sm,
|
|
borderTopWidth: 1,
|
|
borderTopColor: theme.colors.border,
|
|
},
|
|
additionalFindingLabel: {
|
|
fontSize: 12,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
},
|
|
additionalFindingValue: {
|
|
fontSize: 14,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
},
|
|
// New styles for additional findings section
|
|
additionalFindingsSection: {
|
|
marginTop: theme.spacing.md,
|
|
paddingTop: theme.spacing.md,
|
|
borderTopWidth: 1,
|
|
borderTopColor: theme.colors.border,
|
|
},
|
|
additionalFindingsTitle: {
|
|
fontSize: 14,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
additionalFindingValues: {
|
|
alignItems: 'flex-end',
|
|
},
|
|
// New styles for images summary bar
|
|
imagesSummaryBar: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-around',
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
paddingVertical: theme.spacing.sm,
|
|
paddingHorizontal: theme.spacing.md,
|
|
borderRadius: 8,
|
|
marginBottom: theme.spacing.md,
|
|
},
|
|
imagesSummaryItem: {
|
|
alignItems: 'center',
|
|
},
|
|
imagesSummaryLabel: {
|
|
fontSize: 12,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
marginBottom: theme.spacing.xs,
|
|
},
|
|
imagesSummaryValueContainer: {
|
|
backgroundColor: theme.colors.background,
|
|
paddingHorizontal: theme.spacing.md,
|
|
paddingVertical: theme.spacing.xs,
|
|
borderRadius: 12,
|
|
},
|
|
imagesSummaryValue: {
|
|
fontSize: 14,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
},
|
|
// New styles for images display card
|
|
imagesDisplayCard: {
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
borderRadius: 12,
|
|
padding: theme.spacing.md,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.1,
|
|
shadowRadius: 4,
|
|
elevation: 2,
|
|
},
|
|
// New styles for feedback summary bar
|
|
feedbackSummaryBar: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-around',
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
paddingVertical: theme.spacing.sm,
|
|
paddingHorizontal: theme.spacing.md,
|
|
borderRadius: 8,
|
|
marginBottom: theme.spacing.md,
|
|
},
|
|
feedbackSummaryItem: {
|
|
alignItems: 'center',
|
|
},
|
|
feedbackSummaryLabel: {
|
|
fontSize: 12,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
marginBottom: theme.spacing.xs,
|
|
},
|
|
feedbackSummaryValueContainer: {
|
|
backgroundColor: theme.colors.background,
|
|
paddingHorizontal: theme.spacing.md,
|
|
paddingVertical: theme.spacing.xs,
|
|
borderRadius: 12,
|
|
},
|
|
feedbackSummaryValue: {
|
|
fontSize: 14,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
},
|
|
// New styles for feedback history card
|
|
feedbackHistoryCard: {
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
borderRadius: 12,
|
|
padding: theme.spacing.sm,
|
|
paddingBottom: theme.spacing.md,
|
|
// shadowColor: '#000',
|
|
// shadowOffset: { width: 0, height: 2 },
|
|
// shadowOpacity: 0.1,
|
|
// shadowRadius: 4,
|
|
// elevation: 2,
|
|
},
|
|
// New styles for visual indicators section
|
|
visualIndicatorsSection: {
|
|
marginTop: theme.spacing.md,
|
|
paddingTop: theme.spacing.md,
|
|
borderTopWidth: 1,
|
|
borderTopColor: theme.colors.border,
|
|
},
|
|
visualIndicatorsTitle: {
|
|
fontSize: 16,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
marginBottom: theme.spacing.md,
|
|
textAlign: 'center',
|
|
},
|
|
// New styles for compact indicators
|
|
compactIndicatorsContainer: {
|
|
flexDirection: 'row',
|
|
flexWrap: 'wrap',
|
|
justifyContent: 'space-between',
|
|
marginBottom: theme.spacing.md,
|
|
// paddingHorizontal: theme.spacing.sm,
|
|
},
|
|
compactIndicatorItem: {
|
|
width: '100%',
|
|
paddingVertical: theme.spacing.md,
|
|
paddingHorizontal: theme.spacing.md,
|
|
backgroundColor: theme.colors.background,
|
|
borderRadius: 12,
|
|
marginBottom: theme.spacing.sm,
|
|
borderWidth: 1,
|
|
borderColor: theme.colors.border,
|
|
// shadowColor: '#000',
|
|
// shadowOffset: { width: 0, height: 1 },
|
|
// shadowOpacity: 0.05,
|
|
// shadowRadius: 2,
|
|
// elevation: 1,
|
|
},
|
|
indicatorHeader: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
compactIndicatorTitle: {
|
|
fontSize: 14,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
flex: 1,
|
|
marginLeft: theme.spacing.sm,
|
|
},
|
|
compactIndicatorPercentage: {
|
|
fontSize: 16,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
marginLeft: theme.spacing.sm,
|
|
},
|
|
indicatorsGrid: {
|
|
flexDirection: 'row',
|
|
flexWrap: 'wrap',
|
|
justifyContent: 'space-around',
|
|
marginBottom: theme.spacing.md,
|
|
},
|
|
indicatorCard: {
|
|
backgroundColor: theme.colors.background,
|
|
borderRadius: 12,
|
|
padding: theme.spacing.md,
|
|
marginBottom: theme.spacing.sm,
|
|
minWidth: 140,
|
|
alignItems: 'center',
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.1,
|
|
shadowRadius: 4,
|
|
elevation: 2,
|
|
},
|
|
indicatorTitle: {
|
|
fontSize: 12,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
marginLeft: theme.spacing.xs,
|
|
textAlign: 'center',
|
|
},
|
|
indicatorStatus: {
|
|
paddingHorizontal: theme.spacing.sm,
|
|
paddingVertical: theme.spacing.xs,
|
|
borderRadius: 8,
|
|
minWidth: 100,
|
|
alignItems: 'center',
|
|
},
|
|
indicatorStatusText: {
|
|
fontSize: 10,
|
|
color: theme.colors.background,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
textTransform: 'uppercase',
|
|
textAlign: 'center',
|
|
},
|
|
summaryIndicator: {
|
|
alignItems: 'center',
|
|
marginTop: theme.spacing.md,
|
|
},
|
|
summaryIndicatorTitle: {
|
|
fontSize: 14,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
summaryIndicatorStatus: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingHorizontal: theme.spacing.md,
|
|
paddingVertical: theme.spacing.sm,
|
|
borderRadius: 12,
|
|
minWidth: 200,
|
|
justifyContent: 'center',
|
|
},
|
|
summaryIndicatorText: {
|
|
fontSize: 12,
|
|
color: theme.colors.background,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
textTransform: 'uppercase',
|
|
marginLeft: theme.spacing.sm,
|
|
textAlign: 'center',
|
|
},
|
|
percentageContainer: {
|
|
alignItems: 'center',
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
percentageValue: {
|
|
fontSize: 16,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
marginBottom: theme.spacing.xs,
|
|
},
|
|
progressBarContainer: {
|
|
width: '100%',
|
|
height: 6,
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
borderRadius: 3,
|
|
marginTop: theme.spacing.xs,
|
|
overflow: 'hidden',
|
|
},
|
|
progressBar: {
|
|
height: '100%',
|
|
borderRadius: 3,
|
|
minWidth: 4,
|
|
},
|
|
progressBarBackground: {
|
|
width: '100%',
|
|
height: '100%',
|
|
backgroundColor: theme.colors.background,
|
|
borderRadius: 4,
|
|
},
|
|
progressBarFill: {
|
|
height: '100%',
|
|
borderRadius: 4,
|
|
minWidth: 4, // Ensure minimum width for very small percentages
|
|
},
|
|
overallPercentageContainer: {
|
|
alignItems: 'center',
|
|
marginBottom: theme.spacing.md,
|
|
},
|
|
overallPercentageLabel: {
|
|
fontSize: 14,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
marginBottom: theme.spacing.xs,
|
|
},
|
|
overallPercentageValue: {
|
|
fontSize: 24,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
marginBottom: theme.spacing.md,
|
|
},
|
|
debugSection: {
|
|
marginTop: theme.spacing.md,
|
|
paddingTop: theme.spacing.md,
|
|
borderTopWidth: 1,
|
|
borderTopColor: theme.colors.border,
|
|
},
|
|
debugTitle: {
|
|
fontSize: 16,
|
|
color: theme.colors.textPrimary,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
debugText: {
|
|
fontSize: 14,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
textAlign: 'center',
|
|
paddingHorizontal: theme.spacing.md,
|
|
lineHeight: 20,
|
|
},
|
|
tabContainer: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-around',
|
|
paddingVertical: theme.spacing.sm,
|
|
paddingHorizontal: theme.spacing.md,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: theme.colors.border,
|
|
backgroundColor: theme.colors.background,
|
|
},
|
|
tabButton: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingHorizontal: theme.spacing.md,
|
|
paddingVertical: theme.spacing.sm,
|
|
borderRadius: 12,
|
|
borderWidth: 1,
|
|
borderColor: theme.colors.border,
|
|
minWidth: 110,
|
|
justifyContent: 'center',
|
|
position: 'relative',
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
},
|
|
tabButtonActive: {
|
|
borderColor: theme.colors.primary,
|
|
backgroundColor: theme.colors.primary,
|
|
shadowColor: theme.colors.primary,
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.2,
|
|
shadowRadius: 4,
|
|
elevation: 3,
|
|
},
|
|
tabButtonText: {
|
|
fontSize: 14,
|
|
color: theme.colors.textSecondary,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
marginLeft: theme.spacing.xs,
|
|
},
|
|
tabButtonTextActive: {
|
|
color: theme.colors.background,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
},
|
|
tabIndicator: {
|
|
position: 'absolute',
|
|
bottom: -theme.spacing.sm,
|
|
left: 0,
|
|
right: 0,
|
|
height: 3,
|
|
backgroundColor: theme.colors.primary,
|
|
borderRadius: 2,
|
|
},
|
|
});
|
|
|
|
export default SeriesDetailScreen;
|