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