Initial release: DictIA v0.8.14-alpha (fork de Speakr, AGPL-3.0)

This commit is contained in:
InnovA AI
2026-03-16 21:47:37 +00:00
commit 42772a31ed
365 changed files with 103572 additions and 0 deletions

View File

@@ -0,0 +1,659 @@
/**
* Modal management composable
* Handles opening, closing, and saving modal dialogs
*/
export function useModals(state, utils) {
const {
showEditModal, showDeleteModal, showEditTagsModal,
showReprocessModal, showResetModal, showShareModal,
showSharesListModal, showTextEditorModal, showAsrEditorModal,
showEditSpeakersModal, showAddSpeakerModal, showEditTextModal,
showShareDeleteModal, showUnifiedShareModal, showColorSchemeModal,
showSystemAudioHelpModal, editingRecording, recordingToDelete, recordingToReset,
selectedRecording, recordings, selectedNewTagId, tagSearchFilter,
availableTags, currentView, totalRecordings, toasts, uploadQueue, allJobs,
// DateTime picker state
showDateTimePicker, pickerMonth, pickerYear, pickerHour, pickerMinute,
pickerAmPm, pickerSelectedDate, dateTimePickerTarget, dateTimePickerCallback
} = state;
const { showToast, setGlobalError } = utils;
const { computed } = Vue;
// =========================================
// Edit Recording Modal
// =========================================
const openEditModal = (recording) => {
editingRecording.value = { ...recording };
showEditModal.value = true;
};
const cancelEdit = () => {
showEditModal.value = false;
editingRecording.value = null;
};
const saveEdit = async () => {
if (!editingRecording.value) return;
try {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const response = await fetch(`/api/recordings/${editingRecording.value.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
title: editingRecording.value.title,
participants: editingRecording.value.participants,
meeting_date: editingRecording.value.meeting_date,
notes: editingRecording.value.notes
})
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || 'Failed to save changes');
// Update local data
const index = recordings.value.findIndex(r => r.id === editingRecording.value.id);
if (index !== -1) {
recordings.value[index] = { ...recordings.value[index], ...editingRecording.value };
}
if (selectedRecording.value && selectedRecording.value.id === editingRecording.value.id) {
selectedRecording.value = { ...selectedRecording.value, ...editingRecording.value };
}
showToast('Recording updated!', 'fa-check-circle');
showEditModal.value = false;
editingRecording.value = null;
} catch (error) {
setGlobalError(`Failed to save changes: ${error.message}`);
}
};
// =========================================
// Delete Recording Modal
// =========================================
const confirmDelete = (recording) => {
recordingToDelete.value = recording;
showDeleteModal.value = true;
};
const cancelDelete = () => {
showDeleteModal.value = false;
recordingToDelete.value = null;
};
const deleteRecording = async () => {
if (!recordingToDelete.value) return;
const deletedId = recordingToDelete.value.id;
try {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const response = await fetch(`/recording/${deletedId}`, {
method: 'DELETE',
headers: { 'X-CSRFToken': csrfToken }
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || 'Failed to delete recording');
// Remove from recordings list
recordings.value = recordings.value.filter(r => r.id !== deletedId);
totalRecordings.value--;
// Remove from upload queue if present (frontend tracking)
if (uploadQueue?.value) {
uploadQueue.value = uploadQueue.value.filter(item => item.recordingId !== deletedId);
}
// Remove from backend job queue if present (backend processing tracking)
// This is critical - without this, deleted recordings remain in processing queue
if (allJobs?.value) {
allJobs.value = allJobs.value.filter(job => job.recording_id !== deletedId);
}
// Clear selected recording if it's the one being deleted
if (selectedRecording.value?.id === deletedId) {
selectedRecording.value = null;
currentView.value = 'upload';
}
showToast('Recording deleted.', 'fa-trash');
showDeleteModal.value = false;
recordingToDelete.value = null;
} catch (error) {
setGlobalError(`Failed to delete recording: ${error.message}`);
}
};
// =========================================
// Archive Recording
// =========================================
const archiveRecording = async (recording) => {
if (!recording) return;
try {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const response = await fetch(`/api/recordings/${recording.id}/archive`, {
method: 'POST',
headers: { 'X-CSRFToken': csrfToken }
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || 'Failed to archive recording');
recording.is_archived = true;
recording.audio_deleted_at = data.audio_deleted_at;
// Update in recordings list
const index = recordings.value.findIndex(r => r.id === recording.id);
if (index !== -1) {
recordings.value[index].is_archived = true;
recordings.value[index].audio_deleted_at = data.audio_deleted_at;
}
showToast('Recording archived (audio deleted)', 'fa-archive');
} catch (error) {
setGlobalError(`Failed to archive recording: ${error.message}`);
}
};
// =========================================
// Edit Tags Modal
// =========================================
const openEditTagsModal = () => {
selectedNewTagId.value = '';
tagSearchFilter.value = '';
showEditTagsModal.value = true;
};
const closeEditTagsModal = () => {
showEditTagsModal.value = false;
};
const addTagToRecording = async (tagId) => {
if (!selectedRecording.value || !tagId) return;
try {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const response = await fetch(`/api/recordings/${selectedRecording.value.id}/tags`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({ tag_id: tagId })
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || 'Failed to add tag');
// Find the tag object
const tag = availableTags.value.find(t => t.id === tagId);
if (tag) {
if (!selectedRecording.value.tags) {
selectedRecording.value.tags = [];
}
selectedRecording.value.tags.push(tag);
}
// Update in recordings list
const index = recordings.value.findIndex(r => r.id === selectedRecording.value.id);
if (index !== -1 && tag) {
if (!recordings.value[index].tags) {
recordings.value[index].tags = [];
}
recordings.value[index].tags.push(tag);
}
selectedNewTagId.value = '';
showToast('Tag added!', 'fa-tag');
} catch (error) {
setGlobalError(`Failed to add tag: ${error.message}`);
}
};
const removeTagFromRecording = async (tagId) => {
if (!selectedRecording.value || !tagId) return;
try {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const response = await fetch(`/api/recordings/${selectedRecording.value.id}/tags/${tagId}`, {
method: 'DELETE',
headers: { 'X-CSRFToken': csrfToken }
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || 'Failed to remove tag');
// Remove from selected recording
if (selectedRecording.value.tags) {
selectedRecording.value.tags = selectedRecording.value.tags.filter(t => t.id !== tagId);
}
// Update in recordings list
const index = recordings.value.findIndex(r => r.id === selectedRecording.value.id);
if (index !== -1 && recordings.value[index].tags) {
recordings.value[index].tags = recordings.value[index].tags.filter(t => t.id !== tagId);
}
showToast('Tag removed!', 'fa-tag');
} catch (error) {
setGlobalError(`Failed to remove tag: ${error.message}`);
}
};
// =========================================
// Reset Modal
// =========================================
const openResetModal = (recording) => {
recordingToReset.value = recording;
showResetModal.value = true;
};
const cancelReset = () => {
showResetModal.value = false;
recordingToReset.value = null;
};
const resetRecording = async () => {
if (!recordingToReset.value) return;
try {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const response = await fetch(`/recording/${recordingToReset.value.id}/reset_status`, {
method: 'POST',
headers: { 'X-CSRFToken': csrfToken }
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || 'Failed to reset recording');
// Update recording status
const index = recordings.value.findIndex(r => r.id === recordingToReset.value.id);
if (index !== -1) {
recordings.value[index].status = 'PENDING';
recordings.value[index].transcription = '';
recordings.value[index].summary = '';
}
if (selectedRecording.value?.id === recordingToReset.value.id) {
selectedRecording.value.status = 'PENDING';
selectedRecording.value.transcription = '';
selectedRecording.value.summary = '';
}
showToast('Recording reset for reprocessing.', 'fa-redo');
showResetModal.value = false;
recordingToReset.value = null;
} catch (error) {
setGlobalError(`Failed to reset recording: ${error.message}`);
}
};
// =========================================
// System Audio Help Modal
// =========================================
const openSystemAudioHelpModal = () => {
showSystemAudioHelpModal.value = true;
};
const closeSystemAudioHelpModal = () => {
showSystemAudioHelpModal.value = false;
};
// =========================================
// Toast Management
// =========================================
const dismissToast = (id) => {
toasts.value = toasts.value.filter(t => t.id !== id);
};
// Aliases for template compatibility
const editRecording = openEditModal;
const editRecordingTags = openEditTagsModal;
// =========================================
// DateTime Picker
// =========================================
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'];
const dayNames = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
// Generate available years (10 years before and after current year)
const availableYears = computed(() => {
const currentYear = new Date().getFullYear();
const years = [];
for (let y = currentYear - 10; y <= currentYear + 10; y++) {
years.push(y);
}
return years;
});
// Generate hours for 12-hour format
const hours12 = computed(() => {
const hours = [];
for (let h = 1; h <= 12; h++) {
hours.push({ value: h, label: h.toString() });
}
return hours;
});
// Generate minutes
const minutes = computed(() => {
const mins = [];
for (let m = 0; m < 60; m++) {
mins.push(m);
}
return mins;
});
// Generate calendar days for current month view
const calendarDays = computed(() => {
const days = [];
const year = pickerYear.value;
const month = pickerMonth.value;
// First day of the month
const firstDay = new Date(year, month, 1);
const startingDay = firstDay.getDay();
// Last day of the month
const lastDay = new Date(year, month + 1, 0);
const totalDays = lastDay.getDate();
// Previous month days to fill the grid
const prevMonthLastDay = new Date(year, month, 0).getDate();
for (let i = startingDay - 1; i >= 0; i--) {
days.push({
day: prevMonthLastDay - i,
date: new Date(year, month - 1, prevMonthLastDay - i),
inMonth: false,
isToday: false,
isSelected: false
});
}
// Current month days
const today = new Date();
for (let d = 1; d <= totalDays; d++) {
const date = new Date(year, month, d);
const isToday = date.toDateString() === today.toDateString();
const isSelected = pickerSelectedDate.value &&
date.toDateString() === pickerSelectedDate.value.toDateString();
days.push({
day: d,
date: date,
inMonth: true,
isToday: isToday,
isSelected: isSelected
});
}
// Next month days to fill the grid (6 rows * 7 days = 42 total)
const remainingDays = 42 - days.length;
for (let d = 1; d <= remainingDays; d++) {
days.push({
day: d,
date: new Date(year, month + 1, d),
inMonth: false,
isToday: false,
isSelected: false
});
}
return days;
});
const openDateTimePicker = (target, currentValue, callback) => {
dateTimePickerTarget.value = target;
dateTimePickerCallback.value = callback;
// Parse current value if exists
if (currentValue) {
const date = new Date(currentValue);
if (!isNaN(date.getTime())) {
pickerSelectedDate.value = date;
pickerMonth.value = date.getMonth();
pickerYear.value = date.getFullYear();
let hours = date.getHours();
const ampm = hours >= 12 ? 'PM' : 'AM';
hours = hours % 12;
hours = hours === 0 ? 12 : hours;
pickerHour.value = hours;
pickerMinute.value = date.getMinutes();
pickerAmPm.value = ampm;
} else {
setPickerToNow();
}
} else {
setPickerToNow();
}
showDateTimePicker.value = true;
};
const setPickerToNow = () => {
const now = new Date();
pickerSelectedDate.value = now;
pickerMonth.value = now.getMonth();
pickerYear.value = now.getFullYear();
let hours = now.getHours();
const ampm = hours >= 12 ? 'PM' : 'AM';
hours = hours % 12;
hours = hours === 0 ? 12 : hours;
pickerHour.value = hours;
pickerMinute.value = now.getMinutes();
pickerAmPm.value = ampm;
};
const closeDateTimePicker = () => {
showDateTimePicker.value = false;
dateTimePickerTarget.value = null;
dateTimePickerCallback.value = null;
};
const prevMonth = () => {
if (pickerMonth.value === 0) {
pickerMonth.value = 11;
pickerYear.value--;
} else {
pickerMonth.value--;
}
};
const nextMonth = () => {
if (pickerMonth.value === 11) {
pickerMonth.value = 0;
pickerYear.value++;
} else {
pickerMonth.value++;
}
};
const updatePickerView = () => {
// Called when month/year dropdowns change
// The computed calendarDays will automatically update
};
const selectDate = (date) => {
pickerSelectedDate.value = date;
};
const setToNow = () => {
setPickerToNow();
};
const setToToday = () => {
const today = new Date();
pickerSelectedDate.value = today;
pickerMonth.value = today.getMonth();
pickerYear.value = today.getFullYear();
// Keep the current time
};
const clearDateTime = () => {
pickerSelectedDate.value = null;
const now = new Date();
pickerMonth.value = now.getMonth();
pickerYear.value = now.getFullYear();
pickerHour.value = 12;
pickerMinute.value = 0;
pickerAmPm.value = 'PM';
};
const formatPickerPreview = () => {
if (!pickerSelectedDate.value) return '';
const date = pickerSelectedDate.value;
const monthName = monthNames[date.getMonth()];
const day = date.getDate();
const year = date.getFullYear();
const hour = pickerHour.value;
const minute = pickerMinute.value.toString().padStart(2, '0');
const ampm = pickerAmPm.value;
return `${monthName} ${day}, ${year} at ${hour}:${minute} ${ampm}`;
};
const applyDateTime = () => {
if (!pickerSelectedDate.value) {
// If no date selected, just close
closeDateTimePicker();
return;
}
// Build the full datetime
const date = new Date(pickerSelectedDate.value);
let hours = pickerHour.value;
// Convert 12-hour to 24-hour
if (pickerAmPm.value === 'AM') {
hours = hours === 12 ? 0 : hours;
} else {
hours = hours === 12 ? 12 : hours + 12;
}
date.setHours(hours);
date.setMinutes(pickerMinute.value);
date.setSeconds(0);
date.setMilliseconds(0);
// Format as ISO string for storage (YYYY-MM-DDTHH:mm:ss)
const isoString = date.toISOString().slice(0, 19);
// Call the callback with the result
if (dateTimePickerCallback.value) {
dateTimePickerCallback.value(isoString, date);
}
closeDateTimePicker();
};
// Helper to open datetime picker for meeting date
const openMeetingDatePicker = () => {
if (!selectedRecording.value) return;
openDateTimePicker(
'meeting_date',
selectedRecording.value.meeting_date,
(isoString) => {
selectedRecording.value.meeting_date = isoString;
// Auto-save the change
saveInlineMeetingDate();
}
);
};
// Save meeting date inline (similar to other inline edits)
const saveInlineMeetingDate = async () => {
if (!selectedRecording.value) return;
const fullPayload = {
id: selectedRecording.value.id,
title: selectedRecording.value.title,
participants: selectedRecording.value.participants,
notes: selectedRecording.value.notes,
summary: selectedRecording.value.summary,
meeting_date: selectedRecording.value.meeting_date
};
try {
const csrfTokenValue = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const response = await fetch('/save', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfTokenValue
},
body: JSON.stringify(fullPayload)
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || 'Failed to save meeting date');
showToast('Meeting date updated!', 'fa-calendar-check');
} catch (error) {
showToast(`Failed to save: ${error.message}`, 'fa-exclamation-circle', 3000, 'error');
}
};
return {
// Edit modal
openEditModal,
editRecording,
cancelEdit,
saveEdit,
// Delete modal
confirmDelete,
cancelDelete,
deleteRecording,
// Archive
archiveRecording,
// Tags modal
openEditTagsModal,
editRecordingTags,
closeEditTagsModal,
addTagToRecording,
removeTagFromRecording,
// Reset modal
openResetModal,
cancelReset,
resetRecording,
// System audio help
openSystemAudioHelpModal,
closeSystemAudioHelpModal,
// Toast
dismissToast,
// DateTime picker
monthNames,
dayNames,
availableYears,
hours12,
minutes,
calendarDays,
openDateTimePicker,
closeDateTimePicker,
prevMonth,
nextMonth,
updatePickerView,
selectDate,
setToNow,
setToToday,
clearDateTime,
formatPickerPreview,
applyDateTime,
openMeetingDatePicker
};
}