Files
dictia-public/static/js/modules/composables/upload.js

825 lines
33 KiB
JavaScript

/**
* Upload management composable
* Handles file uploads, queue processing, and progress tracking
*/
import * as FailedUploads from '../db/failed-uploads.js';
import * as IncognitoStorage from '../db/incognito-storage.js';
// Parse error message and return friendly error info
function getFriendlyError(errorMessage, t) {
const _t = t || ((key) => key);
if (!errorMessage) return { title: _t('errors.processingError'), message: _t('errors.processingErrorMessage') };
const lowerText = errorMessage.toLowerCase();
const patterns = [
{ patterns: ['maximum content size limit', 'file too large', '413', 'payload too large', 'exceeded'], title: _t('errors.fileTooLargeTitle'), guidance: _t('errors.enableChunkingGuidance') },
{ patterns: ['timed out', 'timeout', 'deadline exceeded'], title: _t('errors.processingTimeout'), guidance: _t('errors.splitAudioGuidance') },
{ patterns: ['401', 'unauthorized', 'invalid api key', 'authentication failed', 'incorrect api key'], title: _t('errors.authenticationError'), guidance: _t('errors.checkApiKeyGuidance') },
{ patterns: ['rate limit', 'too many requests', '429', 'quota exceeded'], title: _t('errors.rateLimitExceeded'), guidance: _t('errors.waitAndRetryGuidance') },
{ patterns: ['connection refused', 'connection reset', 'could not connect', 'network unreachable'], title: _t('errors.connectionError'), guidance: _t('errors.checkNetworkGuidance') },
{ patterns: ['503', '502', '500', 'service unavailable', 'server error', 'internal server error'], title: _t('errors.serviceUnavailable'), guidance: _t('errors.tryAgainLaterGuidance') },
{ patterns: ['invalid file format', 'unsupported format', 'could not decode', 'corrupt'], title: _t('errors.invalidAudioFormat'), guidance: _t('errors.convertFormatGuidance') },
{ patterns: ['audio extraction failed', 'ffmpeg failed', 'no audio stream'], title: _t('errors.audioExtractionFailed'), guidance: _t('errors.convertStandardGuidance') },
];
for (const pattern of patterns) {
for (const p of pattern.patterns) {
if (lowerText.includes(p)) return { title: pattern.title, guidance: pattern.guidance };
}
}
return { title: _t('errors.processingError'), guidance: _t('errors.processingErrorFallbackGuidance') };
}
export function useUpload(state, utils) {
const {
uploadQueue, currentlyProcessingFile, processingProgress, processingMessage,
isProcessingActive, pollInterval, progressPopupMinimized, progressPopupClosed,
maxFileSizeMB, chunkingEnabled, chunkingMode, chunkingLimit, maxConcurrentUploads,
recordings, selectedRecording, totalRecordings, globalError,
selectedTagIds, uploadLanguage, uploadMinSpeakers, uploadMaxSpeakers, uploadHotwords, uploadInitialPrompt,
useAsrEndpoint, connectorSupportsDiarization, asrLanguage, asrMinSpeakers, asrMaxSpeakers,
dragover, availableTags, uploadTagSearchFilter,
// Folder state
availableFolders, selectedFolderId,
// Incognito mode state
incognitoMode, incognitoRecording, incognitoProcessing,
// View state
currentView,
// Upload disclaimer state
uploadDisclaimer, showUploadDisclaimerModal
} = state;
const { computed, nextTick, ref } = Vue;
const { setGlobalError, showToast, formatFileSize, onChatComplete, t } = utils;
// Compute selected tags from IDs
const selectedTags = computed(() => {
return selectedTagIds.value.map(id =>
availableTags.value.find(t => t.id === id)
).filter(Boolean);
});
// --- Tag Drag-and-Drop State ---
const draggedTagIndex = ref(null);
const dragOverTagIndex = ref(null);
// Reorder selectedTagIds array
const reorderSelectedTags = (fromIndex, toIndex) => {
const tagIds = [...selectedTagIds.value];
const [removed] = tagIds.splice(fromIndex, 1);
tagIds.splice(toIndex, 0, removed);
selectedTagIds.value = tagIds;
applyTagDefaults(); // Re-apply defaults since first tag may have changed
};
// === MOUSE DRAG HANDLERS ===
const handleTagDragStart = (index, event) => {
draggedTagIndex.value = index;
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', index.toString());
};
const handleTagDragOver = (index, event) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
dragOverTagIndex.value = index;
};
const handleTagDrop = (targetIndex, event) => {
event.preventDefault();
if (draggedTagIndex.value !== null && draggedTagIndex.value !== targetIndex) {
reorderSelectedTags(draggedTagIndex.value, targetIndex);
}
draggedTagIndex.value = null;
dragOverTagIndex.value = null;
};
const handleTagDragEnd = () => {
draggedTagIndex.value = null;
dragOverTagIndex.value = null;
};
// === TOUCH HANDLERS (Mobile) ===
let touchStartIndex = null;
const handleTagTouchStart = (index, event) => {
touchStartIndex = index;
draggedTagIndex.value = index;
};
const handleTagTouchMove = (event) => {
if (touchStartIndex === null) return;
event.preventDefault();
const touch = event.touches[0];
const elementBelow = document.elementFromPoint(touch.clientX, touch.clientY);
const tagElement = elementBelow?.closest('[data-tag-index]');
if (tagElement) {
const targetIndex = parseInt(tagElement.dataset.tagIndex);
dragOverTagIndex.value = targetIndex;
}
};
const handleTagTouchEnd = () => {
if (touchStartIndex !== null && dragOverTagIndex.value !== null &&
touchStartIndex !== dragOverTagIndex.value) {
reorderSelectedTags(touchStartIndex, dragOverTagIndex.value);
}
touchStartIndex = null;
draggedTagIndex.value = null;
dragOverTagIndex.value = null;
};
// Handle drag events
const handleDragOver = (e) => {
e.preventDefault();
dragover.value = true;
};
const handleDragLeave = (e) => {
if (e.relatedTarget && e.currentTarget.contains(e.relatedTarget)) {
return;
}
dragover.value = false;
};
const handleDrop = (e) => {
e.preventDefault();
dragover.value = false;
addFilesToQueue(e.dataTransfer.files);
};
const handleFileSelect = (e) => {
addFilesToQueue(e.target.files);
e.target.value = null;
};
// Add files to the upload queue
const addFilesToQueue = (files) => {
let filesAdded = 0;
for (const file of files) {
const fileObject = file.file ? file.file : file;
const notes = file.notes || null;
const tags = file.tags || selectedTags.value || [];
const asrOptions = file.asrOptions || {
language: asrLanguage.value,
min_speakers: asrMinSpeakers.value,
max_speakers: asrMaxSpeakers.value
};
// Check if it's an audio file or video container with audio
const isAudioFile = fileObject && (
fileObject.type.startsWith('audio/') ||
fileObject.type === 'video/mp4' ||
fileObject.type === 'video/quicktime' ||
fileObject.type === 'video/x-msvideo' ||
fileObject.type === 'video/webm' ||
fileObject.name.toLowerCase().endsWith('.amr') ||
fileObject.name.toLowerCase().endsWith('.3gp') ||
fileObject.name.toLowerCase().endsWith('.3gpp') ||
fileObject.name.toLowerCase().endsWith('.mp4') ||
fileObject.name.toLowerCase().endsWith('.mov') ||
fileObject.name.toLowerCase().endsWith('.avi') ||
fileObject.name.toLowerCase().endsWith('.mkv') ||
fileObject.name.toLowerCase().endsWith('.webm') ||
fileObject.name.toLowerCase().endsWith('.weba')
);
if (isAudioFile) {
// Only check general file size limit
if (fileObject.size > maxFileSizeMB.value * 1024 * 1024) {
setGlobalError(t('upload.fileExceedsMaxSize', { name: fileObject.name, size: maxFileSizeMB.value }));
continue;
}
const clientId = `client-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
uploadQueue.value.push({
file: fileObject,
notes: notes,
tags: tags,
asrOptions: asrOptions,
status: 'queued',
recordingId: null,
clientId: clientId,
error: null,
willAutoSummarize: false // Server will tell us via SUMMARIZING status
});
filesAdded++;
} else if (fileObject) {
setGlobalError(t('upload.invalidFileType', { name: fileObject.name }));
}
}
if (filesAdded > 0) {
console.log(`Added ${filesAdded} file(s) to the queue.`);
}
};
// Remove a file from the queue before processing starts
const removeFromQueue = (clientId) => {
const index = uploadQueue.value.findIndex(item => item.clientId === clientId);
if (index !== -1 && (uploadQueue.value[index].status === 'queued' || uploadQueue.value[index].status === 'ready')) {
uploadQueue.value.splice(index, 1);
console.log(`Removed file from queue: ${clientId}`);
}
};
// Cancel a waiting file from the upload progress queue
const cancelWaitingFile = (clientId) => {
const index = uploadQueue.value.findIndex(item => item.clientId === clientId);
if (index !== -1 && uploadQueue.value[index].status === 'ready') {
uploadQueue.value.splice(index, 1);
console.log(`Cancelled waiting file: ${clientId}`);
showToast(t('upload.fileRemovedFromQueue'), 'fa-trash');
}
};
// Clear completed uploads from queue
const clearCompletedUploads = () => {
uploadQueue.value = uploadQueue.value.filter(item => !['completed', 'failed'].includes(item.status));
};
// Start processing all queued files
const startUpload = () => {
const pendingFiles = uploadQueue.value.filter(item => item.status === 'queued');
if (pendingFiles.length === 0) {
return;
}
// Show upload disclaimer if configured
if (uploadDisclaimer.value && uploadDisclaimer.value.trim() !== '') {
showUploadDisclaimerModal.value = true;
return;
}
// Update all queued files with current tags and ASR options
// AND change their status to 'ready' so they move to upload progress immediately
for (const item of uploadQueue.value) {
if (item.status === 'queued') {
if (!item.preserveOptions) {
// For file uploads: use current UI selection (user may have changed tags after dropping)
item.tags = [...selectedTags.value];
item.asrOptions = {
language: asrLanguage.value,
min_speakers: asrMinSpeakers.value,
max_speakers: asrMaxSpeakers.value,
hotwords: uploadHotwords.value,
initial_prompt: uploadInitialPrompt.value,
};
item.folder_id = selectedFolderId.value;
}
// Change status to 'ready' to remove from upload view but keep in queue
item.status = 'ready';
}
}
progressPopupMinimized.value = false;
progressPopupClosed.value = false;
startProcessingQueue();
};
// --- Parallel Upload System ---
// Concurrency limiter: configurable via MAX_CONCURRENT_UPLOADS env var (default 3)
let activeUploadCount = 0;
const pendingUploadQueue = []; // Functions waiting for a slot
const acquireUploadSlot = () => {
return new Promise(resolve => {
if (activeUploadCount < (maxConcurrentUploads?.value || 3)) {
activeUploadCount++;
resolve();
} else {
pendingUploadQueue.push(resolve);
}
});
};
const releaseUploadSlot = () => {
activeUploadCount--;
if (pendingUploadQueue.length > 0) {
activeUploadCount++;
const next = pendingUploadQueue.shift();
next();
}
// When all uploads are done, clear processing active flag
const stillUploading = uploadQueue.value.some(item =>
['uploading', 'ready'].includes(item.status)
);
if (!stillUploading) {
isProcessingActive.value = false;
}
};
const resetCurrentFileProcessingState = () => {
if (pollInterval.value) clearInterval(pollInterval.value);
pollInterval.value = null;
currentlyProcessingFile.value = null;
processingProgress.value = 0;
processingMessage.value = '';
};
/**
* Upload a single file to the server.
* Acquires a concurrency slot, uploads, then releases.
* Status updates are per-item (no global processingProgress).
*/
const uploadSingleFile = async (fileItem) => {
await acquireUploadSlot();
fileItem.status = 'uploading';
fileItem.progress = 5;
try {
const formData = new FormData();
formData.append('file', fileItem.file);
// Send file's lastModified timestamp for meeting_date
if (fileItem.file.lastModified) {
const lastModified = fileItem.file.lastModified;
formData.append('file_last_modified', lastModified.toString());
}
if (fileItem.notes) {
formData.append('notes', fileItem.notes);
}
// Add tags if selected
const tagsToUse = fileItem.tags || selectedTags.value || [];
tagsToUse.forEach((tag, index) => {
const tagId = tag.id || tag;
formData.append(`tag_ids[${index}]`, tagId);
});
// Add folder if selected
const folderToUse = fileItem.folder_id || selectedFolderId.value;
if (folderToUse) {
formData.append('folder_id', folderToUse);
}
// Add ASR options
const asrOpts = fileItem.asrOptions || {};
const language = asrOpts.language || uploadLanguage.value;
if (language) {
formData.append('language', language);
}
if (connectorSupportsDiarization.value) {
const minSpeakers = asrOpts.min_speakers || uploadMinSpeakers.value;
const maxSpeakers = asrOpts.max_speakers || uploadMaxSpeakers.value;
if (minSpeakers && minSpeakers !== '') {
formData.append('min_speakers', minSpeakers.toString());
}
if (maxSpeakers && maxSpeakers !== '') {
formData.append('max_speakers', maxSpeakers.toString());
}
}
// Add hotwords and initial prompt
const hotwords = asrOpts.hotwords || uploadHotwords.value;
const initialPrompt = asrOpts.initial_prompt || uploadInitialPrompt.value;
if (hotwords && hotwords.trim()) {
formData.append('hotwords', hotwords.trim());
}
if (initialPrompt && initialPrompt.trim()) {
formData.append('initial_prompt', initialPrompt.trim());
}
// Refresh CSRF token before upload (prevents stale token after sleep/idle)
let csrfToken;
if (window.csrfManager) {
try {
csrfToken = await window.csrfManager.refreshToken();
} catch (e) {
csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
}
} else {
csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
}
// Use XMLHttpRequest for per-file upload progress
const data = await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
// Map upload progress to 5-90% range
fileItem.progress = Math.round(5 + (e.loaded / e.total) * 85);
}
};
xhr.onload = () => {
const contentType = xhr.getResponseHeader('content-type') || '';
if (!contentType.includes('application/json')) {
const titleMatch = xhr.responseText.match(/<title>([^<]+)<\/title>/i);
const h1Match = xhr.responseText.match(/<h1>([^<]+)<\/h1>/i);
reject(new Error(titleMatch?.[1] || h1Match?.[1] ||
`Server error (${xhr.status}): Response was not JSON`));
return;
}
let parsed;
try {
parsed = JSON.parse(xhr.responseText);
} catch {
reject(new Error(`Invalid JSON response (${xhr.status})`));
return;
}
if (xhr.status === 202 && parsed.id) {
resolve(parsed);
} else if (!String(xhr.status).startsWith('2')) {
let errorMsg = parsed.error || `Upload failed with status ${xhr.status}`;
if (xhr.status === 413) errorMsg = parsed.error || `File too large. Max: ${parsed.max_size_mb?.toFixed(0) || maxFileSizeMB.value} MB.`;
reject(new Error(errorMsg));
} else {
reject(new Error('Unexpected success response from server after upload.'));
}
};
xhr.onerror = () => reject(new Error('Network error during upload'));
xhr.ontimeout = () => reject(new Error('Upload timed out'));
// Store abort controller on item for cancellation
fileItem._xhr = xhr;
xhr.open('POST', '/upload');
if (csrfToken) {
xhr.setRequestHeader('X-CSRFToken', csrfToken);
}
xhr.send(formData);
});
// Upload succeeded - recording is now on the server
console.log(`File ${fileItem.file.name} uploaded. Recording ID: ${data.id}. Server will process via job queue.`);
fileItem.status = 'pending';
fileItem.recordingId = data.id;
fileItem.progress = 100;
// Add to recordings list
recordings.value.unshift(data);
totalRecordings.value++;
// Clear recording session only after confirmed upload
if (fileItem.onUploadSuccess) {
await fileItem.onUploadSuccess();
}
// Handle duplicate warning
if (data.duplicate_warning) {
const warning = data.duplicate_warning;
const existingDate = warning.existing_created_at
? new Date(warning.existing_created_at).toLocaleDateString()
: '';
const existingName = warning.existing_title || 'Unknown';
showToast(
`⚠️ ${existingName} (${existingDate})`,
'fa-copy'
);
fileItem.duplicateWarning = warning;
}
} catch (error) {
console.error(`Upload Error for ${fileItem.file.name} (Client ID: ${fileItem.clientId}):`, error);
fileItem.status = 'failed';
fileItem.error = error.message;
fileItem.progress = 0;
// Show friendly error message
const friendlyErr = getFriendlyError(error.message, t);
setGlobalError(`${friendlyErr.title}: ${friendlyErr.guidance}`);
// Store failed upload in IndexedDB for background sync retry
try {
await FailedUploads.storeFailedUpload({
file: fileItem.file,
fileName: fileItem.file.name,
fileSize: fileItem.file.size,
clientId: fileItem.clientId,
notes: fileItem.notes,
tags: fileItem.tags,
asrOptions: fileItem.asrOptions,
error: error.message
});
if ('serviceWorker' in navigator && 'sync' in ServiceWorkerRegistration.prototype) {
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-uploads');
console.log('[Upload] Registered background sync for failed upload');
}
} catch (syncError) {
console.warn('[Upload] Failed to register background sync:', syncError);
}
} finally {
fileItem._xhr = null;
releaseUploadSlot();
}
};
/**
* Start uploading all ready files in parallel (with concurrency limit).
* Processing status is tracked via allJobs polling in app.modular.js.
*/
const startProcessingQueue = async () => {
const readyItems = uploadQueue.value.filter(item => item.status === 'ready');
if (readyItems.length === 0) {
console.log("No files ready to upload.");
return;
}
isProcessingActive.value = true;
console.log(`Starting parallel upload of ${readyItems.length} file(s) (max ${maxConcurrentUploads?.value || 3} concurrent)...`);
// Fire off all uploads concurrently (semaphore handles limiting)
const uploadPromises = readyItems.map(item => uploadSingleFile(item));
// Don't await - let them run in background. isProcessingActive is cleared by releaseUploadSlot.
Promise.allSettled(uploadPromises).then(() => {
console.log('All uploads settled.');
});
};
// Keep backward-compat aliases
const startStatusPolling = (fileItem, recordingId) => {
// No longer needed - allJobs polling handles status tracking
fileItem.recordingId = recordingId;
};
const pollProcessingStatus = () => {
// No-op: status tracking is now handled by allJobs polling in app.modular.js
};
// Tag selection helpers
const addTagToSelection = (tagId) => {
if (!selectedTagIds.value.includes(tagId)) {
selectedTagIds.value.push(tagId);
applyTagDefaults();
}
};
const removeTagFromSelection = (tagId) => {
const index = selectedTagIds.value.indexOf(tagId);
if (index > -1) {
selectedTagIds.value.splice(index, 1);
applyTagDefaults();
}
};
const applyTagDefaults = () => {
const selectedTagsObjects = selectedTagIds.value.map(tagId =>
availableTags.value.find(tag => tag.id == tagId)
).filter(Boolean);
const firstTag = selectedTagsObjects[0];
if (firstTag && connectorSupportsDiarization.value) {
if (firstTag.default_language) {
uploadLanguage.value = firstTag.default_language;
}
if (firstTag.default_min_speakers) {
uploadMinSpeakers.value = firstTag.default_min_speakers;
}
if (firstTag.default_max_speakers) {
uploadMaxSpeakers.value = firstTag.default_max_speakers;
}
}
// Apply hotwords/initial_prompt from first tag (works for all connectors)
if (firstTag) {
if (firstTag.default_hotwords) {
uploadHotwords.value = firstTag.default_hotwords;
}
if (firstTag.default_initial_prompt) {
uploadInitialPrompt.value = firstTag.default_initial_prompt;
}
}
};
// Computed property for filtered available tags in upload view
const filteredAvailableTagsForUpload = computed(() => {
const availableForSelection = availableTags.value.filter(tag => !selectedTagIds.value.includes(tag.id));
if (!uploadTagSearchFilter.value) return availableForSelection;
const filter = uploadTagSearchFilter.value.toLowerCase();
return availableForSelection.filter(tag =>
tag.name.toLowerCase().includes(filter)
);
});
// === INCOGNITO MODE FUNCTIONS ===
/**
* Upload and process a file in incognito mode.
* The file is processed synchronously and no data is saved to the database.
* Results are stored only in sessionStorage.
*/
const startIncognitoUpload = async () => {
const pendingFiles = uploadQueue.value.filter(item => item.status === 'queued');
if (pendingFiles.length === 0) {
return;
}
// Only process the first file for incognito mode
const fileItem = pendingFiles[0];
// Check if incognito mode state is available
if (!incognitoMode || !incognitoProcessing || !incognitoRecording) {
console.warn('[Incognito] Incognito state not available, falling back to normal upload');
startUpload();
return;
}
incognitoProcessing.value = true;
processingMessage.value = t('incognito.processingInProgress');
processingProgress.value = 10;
progressPopupMinimized.value = false;
progressPopupClosed.value = false;
try {
const formData = new FormData();
formData.append('file', fileItem.file);
// Add ASR options
const asrOpts = fileItem.asrOptions || {};
const language = asrOpts.language || uploadLanguage.value;
const minSpeakers = asrOpts.min_speakers || uploadMinSpeakers.value;
const maxSpeakers = asrOpts.max_speakers || uploadMaxSpeakers.value;
if (language) {
formData.append('language', language);
}
if (minSpeakers && minSpeakers !== '') {
formData.append('min_speakers', minSpeakers.toString());
}
if (maxSpeakers && maxSpeakers !== '') {
formData.append('max_speakers', maxSpeakers.toString());
}
const hotwords = asrOpts.hotwords || uploadHotwords.value;
const initialPrompt = asrOpts.initial_prompt || uploadInitialPrompt.value;
if (hotwords && hotwords.trim()) {
formData.append('hotwords', hotwords.trim());
}
if (initialPrompt && initialPrompt.trim()) {
formData.append('initial_prompt', initialPrompt.trim());
}
// Request auto-summarization
formData.append('auto_summarize', 'true');
processingMessage.value = t('incognito.uploadingFile');
processingProgress.value = 20;
console.log('[Incognito] Uploading file:', fileItem.file.name);
const response = await fetch('/api/recordings/incognito', {
method: 'POST',
body: formData
});
processingProgress.value = 50;
// Parse response
const contentType = response.headers.get('content-type') || '';
if (!contentType.includes('application/json')) {
const text = await response.text();
const titleMatch = text.match(/<title>([^<]+)<\/title>/i);
throw new Error(titleMatch?.[1] || `Server error (${response.status})`);
}
const data = await response.json();
if (!response.ok || data.error) {
throw new Error(data.error || `Processing failed with status ${response.status}`);
}
processingProgress.value = 80;
processingMessage.value = t('incognito.processingComplete');
// Store result in sessionStorage
const incognitoData = {
id: 'incognito',
incognito: true,
title: data.title || t('incognito.recordingTitle'),
transcription: data.transcription,
summary: data.summary,
summary_html: data.summary_html,
created_at: data.created_at,
original_filename: data.original_filename,
file_size: data.file_size,
audio_duration_seconds: data.audio_duration_seconds,
processing_time_seconds: data.processing_time_seconds,
status: 'COMPLETED'
};
IncognitoStorage.saveIncognitoRecording(incognitoData);
incognitoRecording.value = incognitoData;
// Remove the processed file from queue
const index = uploadQueue.value.findIndex(item => item.clientId === fileItem.clientId);
if (index !== -1) {
uploadQueue.value.splice(index, 1);
}
processingProgress.value = 100;
processingMessage.value = t('incognito.recordingReady');
// Auto-select the incognito recording and switch to detail view
selectedRecording.value = incognitoData;
currentView.value = 'detail';
// Show toast
showToast(t('incognito.recordingProcessed'), 'fa-user-secret');
console.log('[Incognito] Processing complete');
} catch (error) {
console.error('[Incognito] Processing failed:', error);
const friendlyErr = getFriendlyError(error.message, t);
setGlobalError(`${friendlyErr.title}: ${friendlyErr.guidance}`);
fileItem.status = 'failed';
fileItem.error = error.message;
} finally {
incognitoProcessing.value = false;
processingProgress.value = 0;
processingMessage.value = '';
}
};
/**
* Clear the incognito recording with confirmation
*/
const clearIncognitoRecordingWithConfirm = () => {
if (incognitoRecording && incognitoRecording.value) {
if (confirm(t('incognito.discardConfirm'))) {
IncognitoStorage.clearIncognitoRecording();
incognitoRecording.value = null;
// If the incognito recording was selected, clear selection
if (selectedRecording.value?.id === 'incognito') {
selectedRecording.value = null;
}
showToast(t('incognito.recordingDiscarded'), 'fa-trash');
}
}
};
/**
* Select the incognito recording for viewing
*/
const selectIncognitoRecording = () => {
if (incognitoRecording && incognitoRecording.value) {
selectedRecording.value = incognitoRecording.value;
currentView.value = 'detail';
}
};
/**
* Load incognito recording from sessionStorage on app init
*/
const loadIncognitoRecording = () => {
const stored = IncognitoStorage.getIncognitoRecording();
if (stored && incognitoRecording) {
incognitoRecording.value = stored;
console.log('[Incognito] Loaded recording from sessionStorage');
}
};
/**
* Check if there's an incognito recording (for navigation guards)
*/
const hasIncognitoRecording = () => {
return IncognitoStorage.hasIncognitoRecording();
};
return {
handleDragOver,
handleDragLeave,
handleDrop,
handleFileSelect,
addFilesToQueue,
removeFromQueue,
cancelWaitingFile,
clearCompletedUploads,
startUpload,
startProcessingQueue,
resetCurrentFileProcessingState,
startStatusPolling,
pollProcessingStatus,
addTagToSelection,
removeTagFromSelection,
applyTagDefaults,
filteredAvailableTagsForUpload,
// Tag drag-and-drop
draggedTagIndex,
dragOverTagIndex,
handleTagDragStart,
handleTagDragOver,
handleTagDrop,
handleTagDragEnd,
handleTagTouchStart,
handleTagTouchMove,
handleTagTouchEnd,
// Incognito mode
startIncognitoUpload,
clearIncognitoRecordingWithConfirm,
selectIncognitoRecording,
loadIncognitoRecording,
hasIncognitoRecording
};
}