cmms/frontend/src/stores/audioRecording.js

539 lines
16 KiB
JavaScript

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { audioRecordingRepository } from '@/services/repositories'
/**
* Audio Recording Store
* Manages global audio recording state and operations
*/
export const useAudioRecordingStore = defineStore('audioRecording', () => {
// State
const recordings = ref(new Map()) // workOrderId -> recordings array
const activeRecordingSessions = ref(new Map()) // userId -> session data
const transcriptionJobs = ref(new Map()) // recordingId -> transcription status
const audioFormats = ref([])
const transcriptionConfig = ref({})
const loading = ref(false)
const error = ref(null)
// Recording state
const globalRecordingActive = ref(false)
const activeRecordingCount = ref(0)
// Audio analysis cache
const waveformData = ref(new Map()) // recordingId -> waveform data
const analysisData = ref(new Map()) // recordingId -> analysis data
// Getters
const getRecordingsForWorkOrder = computed(() => {
return (workOrderId) => recordings.value.get(workOrderId) || []
})
const getActiveRecordingSession = computed(() => {
return (userId) => activeRecordingSessions.value.get(userId)
})
const hasActiveRecording = computed(() => {
return (userId) => activeRecordingSessions.value.has(userId)
})
const getTranscriptionStatus = computed(() => {
return (recordingId) => transcriptionJobs.value.get(recordingId)
})
const getTotalDurationForWorkOrder = computed(() => {
return (workOrderId) => {
const workOrderRecordings = recordings.value.get(workOrderId) || []
return workOrderRecordings.reduce((total, recording) => {
return total + (recording.duration || 0)
}, 0)
}
})
const getTotalSizeForWorkOrder = computed(() => {
return (workOrderId) => {
const workOrderRecordings = recordings.value.get(workOrderId) || []
return workOrderRecordings.reduce((total, recording) => {
return total + (recording.size || 0)
}, 0)
}
})
const getTranscribedCount = computed(() => {
return (workOrderId) => {
const workOrderRecordings = recordings.value.get(workOrderId) || []
return workOrderRecordings.filter(recording => recording.transcription).length
}
})
const getWaveformData = computed(() => {
return (recordingId) => waveformData.value.get(recordingId)
})
const getAnalysisData = computed(() => {
return (recordingId) => analysisData.value.get(recordingId)
})
// Actions
const loadRecordings = async (workOrderId) => {
try {
loading.value = true
error.value = null
const recordingsList = await audioRecordingRepository.getByWorkOrderId(workOrderId)
recordings.value.set(workOrderId, recordingsList)
return recordingsList
} catch (err) {
error.value = err.message || 'Failed to load recordings'
throw err
} finally {
loading.value = false
}
}
const uploadRecording = async (workOrderId, recordingData) => {
try {
loading.value = true
error.value = null
const uploadedRecording = await audioRecordingRepository.upload({
...recordingData,
workOrderId
})
// Update local state
const currentRecordings = recordings.value.get(workOrderId) || []
recordings.value.set(workOrderId, [uploadedRecording, ...currentRecordings])
return uploadedRecording
} catch (err) {
error.value = err.message || 'Failed to upload recording'
throw err
} finally {
loading.value = false
}
}
const updateRecording = async (workOrderId, recordingId, updateData) => {
try {
loading.value = true
error.value = null
const updatedRecording = await audioRecordingRepository.update(recordingId, updateData)
// Update local state
const currentRecordings = recordings.value.get(workOrderId) || []
const index = currentRecordings.findIndex(recording => recording.id === recordingId)
if (index !== -1) {
currentRecordings[index] = updatedRecording
recordings.value.set(workOrderId, [...currentRecordings])
}
return updatedRecording
} catch (err) {
error.value = err.message || 'Failed to update recording'
throw err
} finally {
loading.value = false
}
}
const deleteRecording = async (workOrderId, recordingId) => {
try {
loading.value = true
error.value = null
await audioRecordingRepository.delete(recordingId)
// Update local state
const currentRecordings = recordings.value.get(workOrderId) || []
const filteredRecordings = currentRecordings.filter(recording => recording.id !== recordingId)
recordings.value.set(workOrderId, filteredRecordings)
// Clear related data
waveformData.value.delete(recordingId)
analysisData.value.delete(recordingId)
transcriptionJobs.value.delete(recordingId)
return true
} catch (err) {
error.value = err.message || 'Failed to delete recording'
throw err
} finally {
loading.value = false
}
}
const startRecordingSession = async (userId, workOrderId, sessionData = {}) => {
try {
loading.value = true
error.value = null
// Check if user already has an active session
if (activeRecordingSessions.value.has(userId)) {
throw new Error('User already has an active recording session')
}
const session = await audioRecordingRepository.startRecordingSession({
userId,
workOrderId,
...sessionData
})
// Update local state
activeRecordingSessions.value.set(userId, {
...session,
workOrderId,
startTime: new Date(),
localDuration: 0
})
activeRecordingCount.value = activeRecordingSessions.value.size
globalRecordingActive.value = activeRecordingCount.value > 0
return session
} catch (err) {
error.value = err.message || 'Failed to start recording session'
throw err
} finally {
loading.value = false
}
}
const stopRecordingSession = async (userId, recordingData) => {
try {
loading.value = true
error.value = null
const activeSession = activeRecordingSessions.value.get(userId)
if (!activeSession) {
throw new Error('No active recording session found for user')
}
const recording = await audioRecordingRepository.stopRecordingSession(activeSession.id, recordingData)
// Update recordings for the work order
const workOrderId = activeSession.workOrderId
const currentRecordings = recordings.value.get(workOrderId) || []
recordings.value.set(workOrderId, [recording, ...currentRecordings])
// Remove from active sessions
activeRecordingSessions.value.delete(userId)
activeRecordingCount.value = activeRecordingSessions.value.size
globalRecordingActive.value = activeRecordingCount.value > 0
return recording
} catch (err) {
error.value = err.message || 'Failed to stop recording session'
throw err
} finally {
loading.value = false
}
}
const updateSessionDuration = (userId, duration) => {
const session = activeRecordingSessions.value.get(userId)
if (session) {
session.localDuration = duration
activeRecordingSessions.value.set(userId, { ...session })
}
}
const transcribeRecording = async (recordingId, options = {}) => {
try {
loading.value = true
error.value = null
const transcriptionJob = await audioRecordingRepository.transcribe(recordingId, options)
// Track transcription status
transcriptionJobs.value.set(recordingId, {
...transcriptionJob,
status: 'pending',
startedAt: new Date().toISOString()
})
// Start polling for completion
pollTranscriptionStatus(recordingId)
return transcriptionJob
} catch (err) {
error.value = err.message || 'Failed to start transcription'
throw err
} finally {
loading.value = false
}
}
const pollTranscriptionStatus = async (recordingId) => {
try {
const status = await audioRecordingRepository.getTranscriptionStatus(recordingId)
transcriptionJobs.value.set(recordingId, status)
if (status.status === 'completed') {
// Update the recording with transcription data
updateRecordingTranscription(recordingId, status.transcription)
} else if (status.status === 'processing') {
// Continue polling
setTimeout(() => pollTranscriptionStatus(recordingId), 5000)
} else if (status.status === 'failed') {
console.error('Transcription failed:', status.error)
}
} catch (err) {
console.error('Failed to poll transcription status:', err)
}
}
const updateRecordingTranscription = (recordingId, transcriptionData) => {
// Find and update the recording in all work orders
for (const [workOrderId, recordingsList] of recordings.value.entries()) {
const recordingIndex = recordingsList.findIndex(r => r.id === recordingId)
if (recordingIndex !== -1) {
recordingsList[recordingIndex] = {
...recordingsList[recordingIndex],
transcription: transcriptionData.text,
transcriptionConfidence: transcriptionData.confidence,
transcriptionLanguage: transcriptionData.language,
transcriptionUpdatedAt: new Date().toISOString()
}
recordings.value.set(workOrderId, [...recordingsList])
break
}
}
}
const generateWaveform = async (recordingId) => {
try {
const waveform = await audioRecordingRepository.generateWaveform(recordingId)
waveformData.value.set(recordingId, waveform)
return waveform
} catch (err) {
console.error('Failed to generate waveform:', err)
return null
}
}
const getAudioAnalysis = async (recordingId) => {
try {
const analysis = await audioRecordingRepository.getAudioAnalysis(recordingId)
analysisData.value.set(recordingId, analysis)
return analysis
} catch (err) {
console.error('Failed to get audio analysis:', err)
return null
}
}
const convertAudioFormat = async (recordingId, format) => {
try {
loading.value = true
error.value = null
const convertedBlob = await audioRecordingRepository.convertFormat(recordingId, format)
// Create download
const url = URL.createObjectURL(convertedBlob)
const link = document.createElement('a')
link.href = url
link.download = `recording-${recordingId}.${format}`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
return true
} catch (err) {
error.value = err.message || 'Failed to convert audio format'
throw err
} finally {
loading.value = false
}
}
const searchRecordings = async (query, filters = {}) => {
try {
loading.value = true
error.value = null
const searchResults = await audioRecordingRepository.searchByContent(query, filters)
return searchResults
} catch (err) {
error.value = err.message || 'Failed to search recordings'
throw err
} finally {
loading.value = false
}
}
const loadActiveSessions = async (userId) => {
try {
const activeSessions = await audioRecordingRepository.getActiveSessions(userId)
// Restore active sessions to local state
activeSessions.forEach(session => {
activeRecordingSessions.value.set(session.userId, {
...session,
startTime: new Date(session.startTime),
localDuration: 0
})
})
activeRecordingCount.value = activeRecordingSessions.value.size
globalRecordingActive.value = activeRecordingCount.value > 0
return activeSessions
} catch (err) {
console.error('Failed to load active recording sessions:', err)
return []
}
}
const loadSupportedFormats = async () => {
try {
const formats = await audioRecordingRepository.getSupportedFormats()
audioFormats.value = formats
return formats
} catch (err) {
console.error('Failed to load supported audio formats:', err)
// Provide default supported formats if API call fails
const defaultFormats = [
{ mimeType: 'audio/webm', extension: 'webm', maxSize: 10485760 }, // 10MB
{ mimeType: 'audio/mp4', extension: 'mp4', maxSize: 10485760 },
{ mimeType: 'audio/ogg', extension: 'ogg', maxSize: 10485760 }
]
audioFormats.value = defaultFormats
return defaultFormats
}
}
const loadTranscriptionConfig = async () => {
try {
const config = await audioRecordingRepository.getTranscriptionConfig()
transcriptionConfig.value = config
return config
} catch (err) {
console.error('Failed to load transcription config:', err)
// Provide default transcription config if API call fails
const defaultConfig = {
enabled: false,
supportedLanguages: ['en', 'es', 'fr'],
maxFileSize: 10485760, // 10MB
timeout: 30000 // 30 seconds
}
transcriptionConfig.value = defaultConfig
return defaultConfig
}
}
const getStatistics = async (workOrderId) => {
try {
return await audioRecordingRepository.getStatistics(workOrderId)
} catch (err) {
error.value = err.message || 'Failed to get recording statistics'
throw err
}
}
const validateAudioFile = async (audioFile) => {
try {
return await audioRecordingRepository.validateAudioFile(audioFile)
} catch (err) {
error.value = err.message || 'Failed to validate audio file'
throw err
}
}
const bulkOperation = async (recordingIds, operation, options = {}) => {
try {
loading.value = true
error.value = null
const result = await audioRecordingRepository.bulkOperation(recordingIds, operation, options)
// Update local state based on operation
if (operation === 'delete') {
// Remove deleted recordings from local state
recordingIds.forEach(recordingId => {
for (const [workOrderId, recordingsList] of recordings.value.entries()) {
const filteredRecordings = recordingsList.filter(r => r.id !== recordingId)
if (filteredRecordings.length !== recordingsList.length) {
recordings.value.set(workOrderId, filteredRecordings)
}
}
})
}
return result
} catch (err) {
error.value = err.message || 'Failed to perform bulk operation'
throw err
} finally {
loading.value = false
}
}
const clearWorkOrderData = (workOrderId) => {
recordings.value.delete(workOrderId)
}
const clearUserSession = (userId) => {
activeRecordingSessions.value.delete(userId)
activeRecordingCount.value = activeRecordingSessions.value.size
globalRecordingActive.value = activeRecordingCount.value > 0
}
const clearError = () => {
error.value = null
}
return {
// State
recordings,
activeRecordingSessions,
transcriptionJobs,
audioFormats,
transcriptionConfig,
loading,
error,
globalRecordingActive,
activeRecordingCount,
waveformData,
analysisData,
// Getters
getRecordingsForWorkOrder,
getActiveRecordingSession,
hasActiveRecording,
getTranscriptionStatus,
getTotalDurationForWorkOrder,
getTotalSizeForWorkOrder,
getTranscribedCount,
getWaveformData,
getAnalysisData,
// Actions
loadRecordings,
uploadRecording,
updateRecording,
deleteRecording,
startRecordingSession,
stopRecordingSession,
updateSessionDuration,
transcribeRecording,
generateWaveform,
getAudioAnalysis,
convertAudioFormat,
searchRecordings,
loadActiveSessions,
loadSupportedFormats,
loadTranscriptionConfig,
getStatistics,
validateAudioFile,
bulkOperation,
clearWorkOrderData,
clearUserSession,
clearError
}
})