Initial release: DictIA v0.8.14-alpha (fork de Speakr, AGPL-3.0)
This commit is contained in:
119
static/js/utils/apiClient.js
Normal file
119
static/js/utils/apiClient.js
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* API client utilities for making HTTP requests
|
||||
*/
|
||||
|
||||
class APIError extends Error {
|
||||
constructor(message, status, data) {
|
||||
super(message);
|
||||
this.name = 'APIError';
|
||||
this.status = status;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely parse JSON response, handling HTML error pages gracefully
|
||||
*/
|
||||
async function safeJsonParse(response) {
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
|
||||
// If response is not JSON, extract useful error from HTML
|
||||
if (!contentType.includes('application/json')) {
|
||||
const text = await response.text();
|
||||
// Try to extract error message from HTML title or h1
|
||||
const titleMatch = text.match(/<title>([^<]+)<\/title>/i);
|
||||
const h1Match = text.match(/<h1>([^<]+)<\/h1>/i);
|
||||
const errorMsg = titleMatch?.[1] || h1Match?.[1] ||
|
||||
`Server returned non-JSON response (status ${response.status})`;
|
||||
throw new APIError(errorMsg, response.status, { htmlResponse: true });
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function apiRequest(url, options = {}) {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
|
||||
const defaultOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(csrfToken && { 'X-CSRFToken': csrfToken })
|
||||
}
|
||||
};
|
||||
|
||||
const mergedOptions = {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
headers: {
|
||||
...defaultOptions.headers,
|
||||
...options.headers
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, mergedOptions);
|
||||
const data = await safeJsonParse(response);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError(
|
||||
data.error || 'Request failed',
|
||||
response.status,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (error instanceof APIError) {
|
||||
throw error;
|
||||
}
|
||||
throw new APIError(error.message, 0, null);
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadFile(url, file, onProgress = null) {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
const formData = new FormData();
|
||||
formData.append('audio_file', file);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
if (onProgress) {
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const percentComplete = (e.loaded / e.total) * 100;
|
||||
onProgress(percentComplete);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
try {
|
||||
const data = JSON.parse(xhr.responseText);
|
||||
resolve(data);
|
||||
} catch (e) {
|
||||
reject(new Error('Invalid response format'));
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const error = JSON.parse(xhr.responseText);
|
||||
reject(new APIError(error.error || 'Upload failed', xhr.status, error));
|
||||
} catch (e) {
|
||||
reject(new APIError('Upload failed', xhr.status, null));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', () => {
|
||||
reject(new Error('Network error'));
|
||||
});
|
||||
|
||||
xhr.open('POST', url);
|
||||
if (csrfToken) {
|
||||
xhr.setRequestHeader('X-CSRFToken', csrfToken);
|
||||
}
|
||||
xhr.send(formData);
|
||||
});
|
||||
}
|
||||
61
static/js/utils/audioUtils.js
Normal file
61
static/js/utils/audioUtils.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Audio processing and visualization utilities
|
||||
*/
|
||||
|
||||
export function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
export function createAudioVisualizer(canvas, stream, color = '#3b82f6') {
|
||||
if (!canvas || !stream) return null;
|
||||
|
||||
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const analyser = audioContext.createAnalyser();
|
||||
const microphone = audioContext.createMediaStreamSource(stream);
|
||||
|
||||
analyser.fftSize = 256;
|
||||
const bufferLength = analyser.frequencyBinCount;
|
||||
const dataArray = new Uint8Array(bufferLength);
|
||||
|
||||
microphone.connect(analyser);
|
||||
|
||||
const canvasCtx = canvas.getContext('2d');
|
||||
const WIDTH = canvas.width;
|
||||
const HEIGHT = canvas.height;
|
||||
|
||||
function draw() {
|
||||
requestAnimationFrame(draw);
|
||||
analyser.getByteFrequencyData(dataArray);
|
||||
|
||||
canvasCtx.fillStyle = 'rgb(17, 24, 39)';
|
||||
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
|
||||
|
||||
const barWidth = (WIDTH / bufferLength) * 2.5;
|
||||
let barHeight;
|
||||
let x = 0;
|
||||
|
||||
for (let i = 0; i < bufferLength; i++) {
|
||||
barHeight = (dataArray[i] / 255) * HEIGHT;
|
||||
canvasCtx.fillStyle = color;
|
||||
canvasCtx.fillRect(x, HEIGHT - barHeight, barWidth, barHeight);
|
||||
x += barWidth + 1;
|
||||
}
|
||||
}
|
||||
|
||||
draw();
|
||||
|
||||
return { audioContext, analyser, stop: () => audioContext.close() };
|
||||
}
|
||||
|
||||
export function detectBrowser() {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
if (userAgent.indexOf('firefox') > -1) return 'firefox';
|
||||
if (userAgent.indexOf('chrome') > -1 && userAgent.indexOf('edge') === -1) return 'chrome';
|
||||
if (userAgent.indexOf('safari') > -1 && userAgent.indexOf('chrome') === -1) return 'safari';
|
||||
if (userAgent.indexOf('edge') > -1) return 'edge';
|
||||
return 'unknown';
|
||||
}
|
||||
83
static/js/utils/dateUtils.js
Normal file
83
static/js/utils/dateUtils.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Date utility functions for formatting and parsing dates
|
||||
*/
|
||||
|
||||
export function formatDateTime(dateString) {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now - date;
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) {
|
||||
return 'Today at ' + date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
|
||||
} else if (diffDays === 1) {
|
||||
return 'Yesterday at ' + date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
|
||||
} else if (diffDays < 7) {
|
||||
const dayName = date.toLocaleDateString('en-US', { weekday: 'long' });
|
||||
return dayName + ' at ' + date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
|
||||
} else {
|
||||
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
}
|
||||
}
|
||||
|
||||
export function formatTimeAgo(dateString) {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now - date;
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return 'Just now';
|
||||
if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`;
|
||||
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
|
||||
if (diffDays < 30) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
|
||||
|
||||
const diffMonths = Math.floor(diffDays / 30);
|
||||
if (diffMonths < 12) return `${diffMonths} month${diffMonths > 1 ? 's' : ''} ago`;
|
||||
|
||||
const diffYears = Math.floor(diffDays / 365);
|
||||
return `${diffYears} year${diffYears > 1 ? 's' : ''} ago`;
|
||||
}
|
||||
|
||||
export function formatDuration(seconds) {
|
||||
if (!seconds || seconds < 0) return '0:00';
|
||||
const hrs = Math.floor(seconds / 3600);
|
||||
const mins = Math.floor((seconds % 3600) / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
|
||||
if (hrs > 0) {
|
||||
return `${hrs}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export function parseDateRange(preset) {
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
|
||||
switch (preset) {
|
||||
case 'today':
|
||||
return { start: today, end: new Date() };
|
||||
case 'yesterday':
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
return { start: yesterday, end: today };
|
||||
case 'week':
|
||||
const weekStart = new Date(today);
|
||||
weekStart.setDate(weekStart.getDate() - 7);
|
||||
return { start: weekStart, end: new Date() };
|
||||
case 'month':
|
||||
const monthStart = new Date(today);
|
||||
monthStart.setMonth(monthStart.getMonth() - 1);
|
||||
return { start: monthStart, end: new Date() };
|
||||
case 'year':
|
||||
const yearStart = new Date(today);
|
||||
yearStart.setFullYear(yearStart.getFullYear() - 1);
|
||||
return { start: yearStart, end: new Date() };
|
||||
default:
|
||||
return { start: null, end: null };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user