539 lines
16 KiB
JavaScript
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
|
|
}
|
|
}) |