Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .github/workflows/docker:build&push.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,27 @@ 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
with:
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 }}
Expand Down
10 changes: 3 additions & 7 deletions src/components/partials/AnalysisStepNavigator.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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.');
Expand Down Expand Up @@ -239,6 +236,5 @@ AnalysisStepNavigator.propTypes = {
onExit: PropTypes.func,
analysisData: PropTypes.object,
analysisEntryId: PropTypes.string,
analysisId: PropTypes.string,
analysisPresignedUploadUrl: PropTypes.string
analysisEntryPresignedUploadUrl: PropTypes.string
};
39 changes: 16 additions & 23 deletions src/components/partials/VideoPlayerSidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Tabs,
} from '@mantine/core';
import { useMemo } from 'react';
import { transformTranscript } from '../../utils/transcriptTransformer';

export const VideoPlayerSidebar = ({
transcript,
Expand All @@ -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
Expand Down Expand Up @@ -129,7 +122,7 @@ export const VideoPlayerSidebar = ({
<Box key={index}>
<Text size="sm">{note.content || 'Sin contenido'}</Text>
{note.timestamp && (
<Text size="xs" color="dimmed">Tiempo: {formatTime ? formatTime(note.timestamp) : note.timestamp}</Text>
<Text size="xs" color="dimmed">Tiempo: {formatTime ? formatTime(note.timestamp) : formatTimeFallback(note.timestamp)}</Text>
)}
</Box>
))}
Expand Down Expand Up @@ -164,7 +157,7 @@ export const VideoPlayerSidebar = ({
>
<Group position="apart" align="flex-start">
<Text size="xs" color="blue" fw={500} w={60} style={{ flexShrink: 0 }}>
{formatTime && segment.start ? formatTime(segment.start) : segment.start?.toFixed(1) || '0.0'}
{formatTime ? formatTime(segment.start) : formatTimeFallback(segment.start)}
</Text>
<Text size="sm" style={{ flex: 1 }} component="div">
<Highlight highlight={activeTranscriptId === segment.id ? [] : []}>
Expand Down
2 changes: 1 addition & 1 deletion src/contexts/MediaPermissionsContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 2 additions & 4 deletions src/pages/participate/ParticipateStepRouter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ export const ParticipateStepRouter = ({
currentStep,
setCurrentStep,
analysisData,
analysisId,
analysisEntryId,
analysisPresignedUploadUrl,
analysisEntryPresignedUploadUrl,
error,
setError,
validationLoading,
Expand Down Expand Up @@ -84,8 +83,7 @@ export const ParticipateStepRouter = ({
<RecordingStep
analysisData={analysisData}
analysisEntryId={analysisEntryId}
analysisId={analysisId}
analysisPresignedUploadUrl={analysisPresignedUploadUrl}
analysisEntryPresignedUploadUrl={analysisEntryPresignedUploadUrl}
onExit={handleExitAnalysis}
buildAnalysisSteps={buildAnalysisSteps}
/>
Expand Down
9 changes: 4 additions & 5 deletions src/pages/participate/ParticipateWrapper.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.");
Expand Down Expand Up @@ -118,7 +118,7 @@ const ParticipateContent = () => {
setAnalysisData(null);
setAnalysisId(null);
setAnalysisEntryId(null);
setAnalysisPresignedUploadUrl(null);
setAnalysisEntryPresignedUploadUrl(null);
setError(null);
setCurrentStep('input');
setShowSecurityModal(false);
Expand Down Expand Up @@ -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}
Expand Down
6 changes: 2 additions & 4 deletions src/pages/participate/steps/RecordingStep.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import { useMediaPermissions } from "../../../contexts/MediaPermissionsContext";
export const RecordingStep = ({
analysisData,
analysisEntryId,
analysisId,
analysisPresignedUploadUrl,
analysisEntryPresignedUploadUrl,
onExit,
buildAnalysisSteps
}) => {
Expand Down Expand Up @@ -42,8 +41,7 @@ export const RecordingStep = ({
onExit={onExit}
analysisData={analysisData}
analysisEntryId={analysisEntryId}
analysisId={analysisId}
analysisPresignedUploadUrl={analysisPresignedUploadUrl}
analysisEntryPresignedUploadUrl={analysisEntryPresignedUploadUrl}
/>
</Card>
</>
Expand Down
28 changes: 12 additions & 16 deletions src/pages/user/VideoPlayerPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}
}
};
Expand Down
80 changes: 80 additions & 0 deletions src/utils/transcriptTransformer.js
Original file line number Diff line number Diff line change
@@ -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;
});
};