diff --git a/.github/workflows/docker:build&push.yaml b/.github/workflows/docker:build&push.yaml index b80d3c9..f0d24bd 100644 --- a/.github/workflows/docker:build&push.yaml +++ b/.github/workflows/docker:build&push.yaml @@ -24,6 +24,19 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - + name: Set date + id: date + run: echo "date=$(date +'%Y-%m-%d')" >> "$GITHUB_OUTPUT" + - + name: Generate Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: sergion14/uxcaptain-webapp + tags: | + type=raw,value=${{ github.ref_name }} + type=raw,value=${{ github.ref_name }}-${{ steps.date.outputs.date }} - name: Build and push uses: docker/build-push-action@v6 @@ -31,7 +44,7 @@ jobs: context: . platforms: linux/amd64 #,linux/arm64 - Not building for ARM, since ubuntu server is just amd64 push: true - tags: sergion14/uxcaptain:webapp-${{ github.ref_name }} + tags: ${{ steps.meta.outputs.tags }} build-args: | VITE_API_BASE_URL=${{ vars.VITE_API_BASE_URL }} VITE_PUBLIC_POSTHOG_KEY=${{ secrets.VITE_PUBLIC_POSTHOG_KEY }} diff --git a/src/components/partials/AnalysisStepNavigator.jsx b/src/components/partials/AnalysisStepNavigator.jsx index f18b01a..89d211e 100644 --- a/src/components/partials/AnalysisStepNavigator.jsx +++ b/src/components/partials/AnalysisStepNavigator.jsx @@ -5,7 +5,7 @@ import { IconAlertCircle, IconCheck, IconUpload, IconCircleCheck } from '@tabler import { useMediaPermissions } from '../../contexts/MediaPermissionsContext'; import apiClient from '../../config/API/axiosConfig.mjs'; -export const AnalysisStepNavigator = ({ steps = [], onExit, analysisData, analysisEntryId, analysisId, analysisPresignedUploadUrl }) => { +export const AnalysisStepNavigator = ({ steps = [], onExit, analysisData, analysisEntryId, analysisEntryPresignedUploadUrl }) => { const { hasPermissions, permissionStatus, @@ -67,9 +67,8 @@ export const AnalysisStepNavigator = ({ steps = [], onExit, analysisData, analys // Step 5: Upload recording using presigned URL const uploadSuccess = await uploadRecording( recordingData.blob, - analysisId, analysisEntryId, - analysisPresignedUploadUrl + analysisEntryPresignedUploadUrl ); if (!uploadSuccess) { @@ -88,8 +87,6 @@ export const AnalysisStepNavigator = ({ steps = [], onExit, analysisData, analys try { await apiClient.patch('/api/v1/analysisEntry', { analysisEntryId, - analysisEntryStatus: 'submitted', - analysisId }); } catch (patchErr) { setFinishError('Failed to update analysis entry after upload.'); @@ -239,6 +236,5 @@ AnalysisStepNavigator.propTypes = { onExit: PropTypes.func, analysisData: PropTypes.object, analysisEntryId: PropTypes.string, - analysisId: PropTypes.string, - analysisPresignedUploadUrl: PropTypes.string + analysisEntryPresignedUploadUrl: PropTypes.string }; \ No newline at end of file diff --git a/src/components/partials/VideoPlayerSidebar.jsx b/src/components/partials/VideoPlayerSidebar.jsx index 8de1c6b..d287fd8 100644 --- a/src/components/partials/VideoPlayerSidebar.jsx +++ b/src/components/partials/VideoPlayerSidebar.jsx @@ -10,6 +10,7 @@ import { Tabs, } from '@mantine/core'; import { useMemo } from 'react'; +import { transformTranscript } from '../../utils/transcriptTransformer'; export const VideoPlayerSidebar = ({ transcript, @@ -21,31 +22,23 @@ export const VideoPlayerSidebar = ({ notes = [], scenario = '', }) => { + // Standardized time formatting fallback function to ensure consistency + const formatTimeFallback = (seconds) => { + if (typeof seconds !== 'number' || isNaN(seconds)) return '0:00'; + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs < 10 ? '0' : ''}${secs}`; + }; + // Memoized transcript transformation to avoid expensive recalculations const transformedTranscript = useMemo(() => { - if (!transcript) return null; - - // Handle case where transcript is already the transcription array - if (Array.isArray(transcript)) { - return transcript.map((segment, index) => ({ - id: index, - start: segment.start_time, - text: segment.transcript, - end_time: segment.end_time - })); - } - - // Handle new API response format - if (transcript.transcriptionSegments && Array.isArray(transcript.transcriptionSegments)) { - return transcript.transcriptionSegments.map((segment, index) => ({ - id: index, - start: segment.start_time, - text: segment.transcript, - end_time: segment.end_time - })); + // If transcript is already in transformed format (has id, start, text properties), return as-is + if (transcript && Array.isArray(transcript) && transcript.length > 0 && transcript[0].id !== undefined) { + return transcript; } - return null; + // Otherwise, transform from raw API format + return transformTranscript(transcript); }, [transcript]); // Simple check for transcript tab visibility @@ -129,7 +122,7 @@ export const VideoPlayerSidebar = ({ {note.content || 'Sin contenido'} {note.timestamp && ( - Tiempo: {formatTime ? formatTime(note.timestamp) : note.timestamp} + Tiempo: {formatTime ? formatTime(note.timestamp) : formatTimeFallback(note.timestamp)} )} ))} @@ -164,7 +157,7 @@ export const VideoPlayerSidebar = ({ > - {formatTime && segment.start ? formatTime(segment.start) : segment.start?.toFixed(1) || '0.0'} + {formatTime ? formatTime(segment.start) : formatTimeFallback(segment.start)} diff --git a/src/contexts/MediaPermissionsContext.jsx b/src/contexts/MediaPermissionsContext.jsx index 55d4051..fedadb6 100644 --- a/src/contexts/MediaPermissionsContext.jsx +++ b/src/contexts/MediaPermissionsContext.jsx @@ -198,7 +198,7 @@ export const MediaPermissionsProvider = ({ children }) => { }, []); // Upload recording - const uploadRecording = useCallback(async (blob, analysisId, analysisEntryId, presignedUrl) => { + const uploadRecording = useCallback(async (blob, analysisEntryId, presignedUrl) => { if (!blob) { setUploadError('Missing recording data'); return false; diff --git a/src/pages/participate/ParticipateStepRouter.jsx b/src/pages/participate/ParticipateStepRouter.jsx index 5fb19b5..515381c 100644 --- a/src/pages/participate/ParticipateStepRouter.jsx +++ b/src/pages/participate/ParticipateStepRouter.jsx @@ -11,9 +11,8 @@ export const ParticipateStepRouter = ({ currentStep, setCurrentStep, analysisData, - analysisId, analysisEntryId, - analysisPresignedUploadUrl, + analysisEntryPresignedUploadUrl, error, setError, validationLoading, @@ -84,8 +83,7 @@ export const ParticipateStepRouter = ({ diff --git a/src/pages/participate/ParticipateWrapper.jsx b/src/pages/participate/ParticipateWrapper.jsx index d1dbdd4..7c6b89b 100644 --- a/src/pages/participate/ParticipateWrapper.jsx +++ b/src/pages/participate/ParticipateWrapper.jsx @@ -17,7 +17,7 @@ const ParticipateContent = () => { const [analysisData, setAnalysisData] = useState(null); const [analysisId, setAnalysisId] = useState(null); const [analysisEntryId, setAnalysisEntryId] = useState(null); - const [analysisPresignedUploadUrl, setAnalysisPresignedUploadUrl] = useState(null); + const [analysisEntryPresignedUploadUrl, setAnalysisEntryPresignedUploadUrl] = useState(null); const [error, setError] = useState(null); // Loading states for different steps @@ -85,7 +85,7 @@ const ParticipateContent = () => { if (response?.data?.success) { setAnalysisData(response.data.analysisData); setAnalysisEntryId(response.data.analysisEntryId); - setAnalysisPresignedUploadUrl(response.data.analysisPresignedUploadUrl); + setAnalysisEntryPresignedUploadUrl(response.data.analysisEntryPresignedUploadUrl); setCurrentStep('analysis'); } else { setError("Error al obtener los datos del análisis. Por favor, inténtalo de nuevo."); @@ -118,7 +118,7 @@ const ParticipateContent = () => { setAnalysisData(null); setAnalysisId(null); setAnalysisEntryId(null); - setAnalysisPresignedUploadUrl(null); + setAnalysisEntryPresignedUploadUrl(null); setError(null); setCurrentStep('input'); setShowSecurityModal(false); @@ -229,9 +229,8 @@ const ParticipateContent = () => { currentStep={currentStep} setCurrentStep={setCurrentStep} analysisData={analysisData} - analysisId={analysisId} analysisEntryId={analysisEntryId} - analysisPresignedUploadUrl={analysisPresignedUploadUrl} + analysisEntryPresignedUploadUrl={analysisEntryPresignedUploadUrl} error={error} setError={setError} validationLoading={validationLoading} diff --git a/src/pages/participate/steps/RecordingStep.jsx b/src/pages/participate/steps/RecordingStep.jsx index c34d94a..6123142 100644 --- a/src/pages/participate/steps/RecordingStep.jsx +++ b/src/pages/participate/steps/RecordingStep.jsx @@ -8,8 +8,7 @@ import { useMediaPermissions } from "../../../contexts/MediaPermissionsContext"; export const RecordingStep = ({ analysisData, analysisEntryId, - analysisId, - analysisPresignedUploadUrl, + analysisEntryPresignedUploadUrl, onExit, buildAnalysisSteps }) => { @@ -42,8 +41,7 @@ export const RecordingStep = ({ onExit={onExit} analysisData={analysisData} analysisEntryId={analysisEntryId} - analysisId={analysisId} - analysisPresignedUploadUrl={analysisPresignedUploadUrl} + analysisEntryPresignedUploadUrl={analysisEntryPresignedUploadUrl} /> diff --git a/src/pages/user/VideoPlayerPage.jsx b/src/pages/user/VideoPlayerPage.jsx index 74f25d9..9869600 100644 --- a/src/pages/user/VideoPlayerPage.jsx +++ b/src/pages/user/VideoPlayerPage.jsx @@ -4,6 +4,7 @@ import apiClient from '../../config/API/axiosConfig.mjs'; import { Container, Text, Loader, Alert, Stack, Button, Group, Box } from '@mantine/core'; import VideoPlayer from '../../components/partials/VideoPlayer'; import { VideoPlayerSidebar } from '../../components/partials/VideoPlayerSidebar'; +import { transformTranscript } from '../../utils/transcriptTransformer'; export const VideoPlayerPage = () => { const { analysisId, entryId } = useParams(); @@ -29,11 +30,12 @@ export const VideoPlayerPage = () => { const videoResponse = await apiClient.get(`/api/v1/analysisEntry/${entryId}`); setVideoUrl(videoResponse.data.analysisEntryGetRecordingPresignedUrl); - // Handle direct transcript data from server response - const transcriptData = videoResponse.data.transcriptionSegments; + // Transform transcript data for consistent format across components + const rawTranscriptData = videoResponse.data.transcriptionSegments; - if (transcriptData) { - setTranscript(transcriptData); + if (rawTranscriptData) { + const transformedData = transformTranscript(rawTranscriptData); + setTranscript(transformedData || null); } else { setTranscript(null); } @@ -73,21 +75,15 @@ export const VideoPlayerPage = () => { const handleTimeUpdate = (time) => { setCurrentTime(time); - // Find active transcript segment - handle both transformed and raw transcript formats + // Find active transcript segment + // Since transcript is always transformed, we can use simplified logic if (transcript && Array.isArray(transcript)) { - const currentSegment = transcript.find((segment, index) => { - // Handle transformed transcript format (from sidebar) - if (segment.id !== undefined && segment.start !== undefined && segment.end_time !== undefined) { - return time >= segment.start && time <= segment.end_time; - } - // Handle raw transcript format (from API) - return time >= segment.start_time && time <= segment.end_time; - }); + const currentSegment = transcript.find(segment => + time >= segment.start && time <= segment.end_time + ); if (currentSegment) { - // Use the index as ID to match the sidebar transformation - const segmentIndex = transcript.indexOf(currentSegment); - setActiveTranscriptId(segmentIndex); + setActiveTranscriptId(currentSegment.id); } } }; diff --git a/src/utils/transcriptTransformer.js b/src/utils/transcriptTransformer.js new file mode 100644 index 0000000..c4edece --- /dev/null +++ b/src/utils/transcriptTransformer.js @@ -0,0 +1,80 @@ +/** + * Utility function to transform transcript data from API response format + * to a standardized format used throughout the application. + * + * API Response Format: + * { + * transcriptionSegments: [ + * { + * start_time: number, + * end_time: number, + * transcript: string + * } + * ] + * } + * + * Transformed Format: + * [ + * { + * id: number, + * start: number, + * end_time: number, + * text: string + * } + * ] + */ + +/** + * Transforms transcript data from API format to application format + * @param {Object|Array} transcriptData - Raw transcript data from API + * @returns {Array|null} - Transformed transcript array or null if invalid + */ +export const transformTranscript = (transcriptData) => { + if (!transcriptData) return null; + + // Handle case where transcript is already the transcription array + if (Array.isArray(transcriptData)) { + return transcriptData.map((segment, index) => ({ + id: index, + start: segment.start_time, + text: segment.transcript, + end_time: segment.end_time + })); + } + + // Handle API response format with transcriptionSegments property + if (transcriptData.transcriptionSegments && Array.isArray(transcriptData.transcriptionSegments)) { + return transcriptData.transcriptionSegments.map((segment, index) => ({ + id: index, + start: segment.start_time, + text: segment.transcript, + end_time: segment.end_time + })); + } + + return null; +}; + +/** + * Finds the active transcript segment for a given time + * @param {Array} transcriptSegments - Array of transcript segments + * @param {number} currentTime - Current time in seconds + * @returns {Object|null} - Active segment or null if not found + */ +export const findActiveSegment = (transcriptSegments, currentTime) => { + if (!transcriptSegments || !Array.isArray(transcriptSegments)) return null; + + return transcriptSegments.find(segment => { + // Handle transformed transcript format + if (segment.id !== undefined && segment.start !== undefined && segment.end_time !== undefined) { + return currentTime >= segment.start && currentTime <= segment.end_time; + } + + // Handle raw transcript format + if (segment.start_time !== undefined && segment.end_time !== undefined) { + return currentTime >= segment.start_time && currentTime <= segment.end_time; + } + + return false; + }); +}; \ No newline at end of file