281 lines
8.5 KiB
JavaScript
281 lines
8.5 KiB
JavaScript
/**
|
|
* Upload composable
|
|
* Handles file upload queue and processing
|
|
*/
|
|
|
|
import { ref, computed, nextTick } from 'vue';
|
|
import { uploadFile } from '../utils/apiClient.js';
|
|
|
|
export function useUpload() {
|
|
// State
|
|
const uploadQueue = ref([]);
|
|
const currentlyProcessingFile = ref(null);
|
|
const processingProgress = ref(0);
|
|
const processingMessage = ref('');
|
|
const isProcessingActive = ref(false);
|
|
const pollInterval = ref(null);
|
|
const progressPopupMinimized = ref(false);
|
|
const progressPopupClosed = ref(false);
|
|
const maxFileSizeMB = ref(250);
|
|
const chunkingEnabled = ref(true);
|
|
const dragover = ref(false);
|
|
|
|
// Computed
|
|
const hasQueuedFiles = computed(() => {
|
|
return uploadQueue.value.some(item => item.status === 'queued');
|
|
});
|
|
|
|
const processingCount = computed(() => {
|
|
return uploadQueue.value.filter(item => item.status === 'processing' || item.status === 'queued').length;
|
|
});
|
|
|
|
const completedCount = computed(() => {
|
|
return uploadQueue.value.filter(item => item.status === 'completed').length;
|
|
});
|
|
|
|
const errorCount = computed(() => {
|
|
return uploadQueue.value.filter(item => item.status === 'error').length;
|
|
});
|
|
|
|
// Methods
|
|
const addFilesToQueue = (files) => {
|
|
const maxFileSize = maxFileSizeMB.value * 1024 * 1024;
|
|
|
|
for (const file of files) {
|
|
if (file.size > maxFileSize) {
|
|
uploadQueue.value.push({
|
|
file,
|
|
status: 'error',
|
|
error: `File exceeds maximum size of ${maxFileSizeMB.value}MB`,
|
|
clientId: Date.now() + Math.random()
|
|
});
|
|
continue;
|
|
}
|
|
|
|
const isAudio = file.type.startsWith('audio/') ||
|
|
file.type.startsWith('video/') ||
|
|
/\.(mp3|wav|ogg|m4a|flac|webm|weba|mp4|mov|avi|mkv)$/i.test(file.name);
|
|
|
|
if (!isAudio) {
|
|
uploadQueue.value.push({
|
|
file,
|
|
status: 'error',
|
|
error: 'File type not supported',
|
|
clientId: Date.now() + Math.random()
|
|
});
|
|
continue;
|
|
}
|
|
|
|
uploadQueue.value.push({
|
|
file,
|
|
status: 'queued',
|
|
recordingId: null,
|
|
clientId: Date.now() + Math.random(),
|
|
error: null
|
|
});
|
|
}
|
|
|
|
if (!isProcessingActive.value && hasQueuedFiles.value) {
|
|
startProcessingQueue();
|
|
}
|
|
};
|
|
|
|
const startProcessingQueue = async () => {
|
|
if (isProcessingActive.value) return;
|
|
|
|
const nextItem = uploadQueue.value.find(item => item.status === 'queued');
|
|
if (!nextItem) {
|
|
isProcessingActive.value = false;
|
|
return;
|
|
}
|
|
|
|
isProcessingActive.value = true;
|
|
currentlyProcessingFile.value = nextItem;
|
|
nextItem.status = 'uploading';
|
|
processingProgress.value = 0;
|
|
processingMessage.value = 'Uploading...';
|
|
|
|
try {
|
|
const data = await uploadFile('/api/recordings/upload', nextItem.file, (progress) => {
|
|
processingProgress.value = progress;
|
|
processingMessage.value = `Uploading... ${Math.round(progress)}%`;
|
|
});
|
|
|
|
nextItem.recordingId = data.recording_id;
|
|
nextItem.status = 'processing';
|
|
processingMessage.value = 'Processing...';
|
|
|
|
// Start polling for status
|
|
pollProcessingStatus(nextItem);
|
|
|
|
} catch (error) {
|
|
nextItem.status = 'error';
|
|
nextItem.error = error.message;
|
|
currentlyProcessingFile.value = null;
|
|
isProcessingActive.value = false;
|
|
|
|
// Continue with next file
|
|
if (hasQueuedFiles.value) {
|
|
await nextTick();
|
|
startProcessingQueue();
|
|
}
|
|
}
|
|
};
|
|
|
|
const pollProcessingStatus = (queueItem) => {
|
|
if (pollInterval.value) {
|
|
clearInterval(pollInterval.value);
|
|
}
|
|
|
|
pollInterval.value = setInterval(async () => {
|
|
try {
|
|
const response = await fetch(`/api/recordings/${queueItem.recordingId}/status`);
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'COMPLETED') {
|
|
clearInterval(pollInterval.value);
|
|
pollInterval.value = null;
|
|
|
|
queueItem.status = 'completed';
|
|
currentlyProcessingFile.value = null;
|
|
isProcessingActive.value = false;
|
|
processingProgress.value = 100;
|
|
processingMessage.value = 'Complete!';
|
|
|
|
// Continue with next file
|
|
if (hasQueuedFiles.value) {
|
|
await nextTick();
|
|
startProcessingQueue();
|
|
}
|
|
|
|
} else if (data.status === 'ERROR') {
|
|
clearInterval(pollInterval.value);
|
|
pollInterval.value = null;
|
|
|
|
queueItem.status = 'error';
|
|
queueItem.error = data.error_message || 'Processing failed';
|
|
currentlyProcessingFile.value = null;
|
|
isProcessingActive.value = false;
|
|
|
|
// Continue with next file
|
|
if (hasQueuedFiles.value) {
|
|
await nextTick();
|
|
startProcessingQueue();
|
|
}
|
|
|
|
} else {
|
|
// Still processing
|
|
if (data.status === 'SUMMARIZING') {
|
|
processingMessage.value = 'Generating summary...';
|
|
processingProgress.value = 80;
|
|
} else {
|
|
processingMessage.value = 'Transcribing...';
|
|
processingProgress.value = 50;
|
|
}
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error polling status:', error);
|
|
}
|
|
}, 3000);
|
|
};
|
|
|
|
const removeFromQueue = (clientId) => {
|
|
const index = uploadQueue.value.findIndex(item => item.clientId === clientId);
|
|
if (index > -1) {
|
|
uploadQueue.value.splice(index, 1);
|
|
}
|
|
};
|
|
|
|
const clearCompletedFromQueue = () => {
|
|
uploadQueue.value = uploadQueue.value.filter(item =>
|
|
item.status !== 'completed' && item.status !== 'error'
|
|
);
|
|
};
|
|
|
|
const handleDragEnter = (event) => {
|
|
event.preventDefault();
|
|
dragover.value = true;
|
|
};
|
|
|
|
const handleDragLeave = (event) => {
|
|
event.preventDefault();
|
|
dragover.value = false;
|
|
};
|
|
|
|
const handleDrop = (event) => {
|
|
event.preventDefault();
|
|
dragover.value = false;
|
|
|
|
const files = Array.from(event.dataTransfer.files);
|
|
if (files.length > 0) {
|
|
addFilesToQueue(files);
|
|
}
|
|
};
|
|
|
|
const handleFileSelect = (event) => {
|
|
const files = Array.from(event.target.files);
|
|
if (files.length > 0) {
|
|
addFilesToQueue(files);
|
|
}
|
|
event.target.value = '';
|
|
};
|
|
|
|
const minimizeProgressPopup = () => {
|
|
progressPopupMinimized.value = true;
|
|
};
|
|
|
|
const maximizeProgressPopup = () => {
|
|
progressPopupMinimized.value = false;
|
|
};
|
|
|
|
const closeProgressPopup = () => {
|
|
progressPopupClosed.value = true;
|
|
};
|
|
|
|
const loadConfig = async () => {
|
|
try {
|
|
const response = await fetch('/api/config');
|
|
const data = await response.json();
|
|
maxFileSizeMB.value = data.max_file_size_mb || 250;
|
|
chunkingEnabled.value = data.chunking_enabled !== false;
|
|
} catch (error) {
|
|
console.error('Error loading config:', error);
|
|
}
|
|
};
|
|
|
|
return {
|
|
// State
|
|
uploadQueue,
|
|
currentlyProcessingFile,
|
|
processingProgress,
|
|
processingMessage,
|
|
isProcessingActive,
|
|
progressPopupMinimized,
|
|
progressPopupClosed,
|
|
maxFileSizeMB,
|
|
chunkingEnabled,
|
|
dragover,
|
|
|
|
// Computed
|
|
hasQueuedFiles,
|
|
processingCount,
|
|
completedCount,
|
|
errorCount,
|
|
|
|
// Methods
|
|
addFilesToQueue,
|
|
startProcessingQueue,
|
|
removeFromQueue,
|
|
clearCompletedFromQueue,
|
|
handleDragEnter,
|
|
handleDragLeave,
|
|
handleDrop,
|
|
handleFileSelect,
|
|
minimizeProgressPopup,
|
|
maximizeProgressPopup,
|
|
closeProgressPopup,
|
|
loadConfig
|
|
};
|
|
}
|