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,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);
});
}

View 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';
}

View 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 };
}
}