improve uiux

This commit is contained in:
ALIHAN DIKEL
2025-03-26 02:32:25 +03:00
parent e5dda3db71
commit 4b6e3a4e1f
4 changed files with 145 additions and 19 deletions

View File

@@ -28,7 +28,7 @@ Simple app for uploading MP3/WAV files with real-time WebSocket updates.
```
main.py # FastAPI backend
templates/index.html # Frontend template
index.html # Frontend template
static/favicon.ico # Site icon
uploads/ # Where files are stored
create_favicon.py # Creates the favicon

View File

@@ -5,7 +5,7 @@ from pathlib import Path
BASE_DIR = Path(__file__).parent
UPLOAD_DIR = BASE_DIR / "uploads"
STATIC_DIR = BASE_DIR / "static"
TEMPLATES_DIR = BASE_DIR / "templates"
TEMPLATES_DIR = BASE_DIR #/ "templates"
TRANSCRIPT_DIR = BASE_DIR / "transcripts"
# Configuration

View File

@@ -4,7 +4,7 @@
<!-- Head content remains the same -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transcriptor Agent</title>
<title>Transcriptor</title>
<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
<style>
/* Existing styles */
@@ -169,10 +169,34 @@
color: #777;
font-size: 0.9em;
}
/* Process All button styling */
.process-all-btn {
background-color: #5bc0de;
margin-top: 10px;
font-size: 14px;
}
.download-all-btn {
background-color: #337ab7;
margin-top: 10px;
font-size: 14px;
margin-left: 10px;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.dashboard-actions {
display: flex;
}
.disabled-btn {
opacity: 0.6;
cursor: not-allowed;
}
</style>
</head>
<body>
<h1>Transcriptor Agent</h1>
<h1>Transcriptor</h1>
<div class="upload-form">
<form id="uploadForm">
@@ -194,7 +218,13 @@
<!-- File List Dashboard -->
<div class="file-dashboard">
<h2>Uploaded Files <span id="connectionStatus" class="connection-status connecting">Connecting...</span></h2>
<div class="dashboard-header">
<h2>Uploaded Files <span id="connectionStatus" class="connection-status connecting">Connecting...</span></h2>
<div class="dashboard-actions">
<button id="processAllBtn" class="process-all-btn">Process All Pending</button>
<button id="downloadAllBtn" class="download-all-btn disabled-btn">Download All Transcripts</button>
</div>
</div>
<div id="fileList" class="file-list">
<p>Loading files...</p>
</div>
@@ -270,6 +300,12 @@
document.getElementById(progressId).textContent = 'Uploaded successfully';
document.getElementById(progressId).style.color = '#3c763d';
successCount++;
// Force refresh file list after upload
if (socket && socket.readyState === WebSocket.OPEN) {
console.log("Requesting file list update after upload");
socket.send('getFiles');
}
}
} else {
document.getElementById(progressId).textContent = `Error: ${result.detail}`;
@@ -335,6 +371,7 @@
socket.addEventListener('message', (event) => {
try {
const data = JSON.parse(event.data);
console.log("WebSocket received data:", data); // Debug what's coming from server
updateFileList(data.files);
} catch (error) {
console.error('Error parsing WebSocket message:', error);
@@ -360,13 +397,19 @@
function updateFileList(files) {
const fileListElement = document.getElementById('fileList');
if (!files || files.length === 0) {
// Filter out .gitkeep files
const filteredFiles = files ? files.filter(file => file.name !== '.gitkeep') : [];
// Update download button state
updateDownloadButtonState(filteredFiles);
if (!filteredFiles || filteredFiles.length === 0) {
fileListElement.innerHTML = '<p>No files uploaded yet.</p>';
return;
}
let html = '';
files.forEach(file => {
filteredFiles.forEach(file => {
// Format the file size
const fileSizeKB = Math.round(file.size / 1024);
let fileSizeStr = fileSizeKB + ' KB';
@@ -383,10 +426,11 @@
const statusClass = `status-${file.status || 'pending'}`;
// Process button (only show if not completed/processing)
const processButton = file.status === 'completed' || file.status === 'processing'
const processButton = file.status === 'completed' || file.status === 'processing' || file.status === 'queued'
? ''
: `<button class="process-btn" onclick="triggerProcessing('${file.name}')">Process</button>`;
// Transcript indicator
const transcriptInfo = file.has_transcript
? '<span class="transcript-available">✓ Transcript</span>'
@@ -426,19 +470,32 @@
}
}
// New function to process multiple files at once
async function processAllSelected() {
const selectedFiles = document.querySelectorAll('.file-item input[type="checkbox"]:checked');
if (selectedFiles.length === 0) {
showResult('Please select at least one file to process', false);
// Function to process all pending files
async function processAllPending() {
// Get all files with pending or failed status
const allFiles = Array.from(document.querySelectorAll('.file-item'));
const pendingFiles = allFiles
.filter(item => {
const statusBadge = item.querySelector('.status-badge');
return statusBadge &&
(statusBadge.classList.contains('status-pending') ||
statusBadge.classList.contains('status-failed'));
})
.map(item => {
// Extract filename from the file-name div
return item.querySelector('.file-name').textContent;
});
if (pendingFiles.length === 0) {
showResult('No pending files to process', false);
return;
}
// Process each file
let successCount = 0;
let failCount = 0;
for (const checkbox of selectedFiles) {
const filename = checkbox.getAttribute('data-filename');
for (const filename of pendingFiles) {
try {
const response = await fetch(`/process/${filename}`, {
method: 'POST'
@@ -457,6 +514,37 @@
showResult(`Processing started for ${successCount} file(s). ${failCount > 0 ? failCount + ' file(s) failed.' : ''}`, failCount === 0);
}
// Function to download all transcripts
async function downloadAllTranscripts() {
const downloadBtn = document.getElementById('downloadAllBtn');
// Check if button is disabled
if (downloadBtn.classList.contains('disabled-btn')) {
return;
}
// Trigger file download
window.location.href = '/download-transcripts';
}
// Function to update the download button state
function updateDownloadButtonState(files) {
const downloadBtn = document.getElementById('downloadAllBtn');
// Check if any files have transcripts
const hasTranscripts = files ? files.some(file => file.has_transcript) : false;
if (hasTranscripts) {
downloadBtn.classList.remove('disabled-btn');
} else {
downloadBtn.classList.add('disabled-btn');
}
}
// Add event listeners for both buttons
document.getElementById('processAllBtn').addEventListener('click', processAllPending);
document.getElementById('downloadAllBtn').addEventListener('click', downloadAllTranscripts);
// Initialize WebSocket connection when page loads
document.addEventListener('DOMContentLoaded', connectWebSocket);
</script>

View File

@@ -5,10 +5,12 @@ from contextlib import asynccontextmanager
from typing import List
from fastapi import FastAPI, UploadFile, File, HTTPException, Request, WebSocket, WebSocketDisconnect, Form
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from loguru import logger
import io
import zipfile
from config import UPLOAD_DIR, ALLOWED_EXTENSIONS, STATIC_DIR, TEMPLATES_DIR, TRANSCRIPT_DIR
from worker import FileStatus, AudioProcessor
@@ -68,7 +70,6 @@ async def upload_file(file: UploadFile = File(...)):
shutil.copyfileobj(file.file, buffer)
# Broadcast file list update to all connected clients
import asyncio
asyncio.create_task(broadcast_file_list())
return {"filename": file.filename, "saved_path": str(file_path), "status": "success"}
@@ -195,7 +196,7 @@ def get_file_list():
"""Helper function to get file list with metadata and status"""
files = []
for file_path in UPLOAD_DIR.iterdir():
if file_path.is_file():
if file_path.is_file() and file_path.name != '.gitkeep': # Skip .gitkeep file
filename = file_path.name
file_stats = file_path.stat()
@@ -243,6 +244,39 @@ async def process_file(filename: str):
return {"filename": filename, "status": "processing_started"}
@app.get("/download-transcripts")
async def download_transcripts():
"""
API endpoint to download all transcript files as a single ZIP
"""
# Check if there are any transcripts
transcript_files = list(TRANSCRIPT_DIR.glob("*.txt"))
if not transcript_files:
raise HTTPException(status_code=404, detail="No transcripts available")
# Create a ZIP file in memory
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
for transcript_path in transcript_files:
# Add each transcript to the ZIP file
zip_file.write(
transcript_path,
arcname=transcript_path.name
)
# Reset buffer position
zip_buffer.seek(0)
# Create a streaming response with the ZIP file
return StreamingResponse(
zip_buffer,
media_type="application/zip",
headers={
"Content-Disposition": f"attachment; filename=transcripts.zip"
}
)
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
"""WebSocket endpoint for real-time file updates"""
@@ -267,6 +301,10 @@ async def broadcast_file_list():
if active_connections:
file_list = get_file_list()
for connection in active_connections:
await connection.send_text(json.dumps({"files": file_list}))
try:
await connection.send_text(json.dumps({"files": file_list}))
except Exception as e:
logger.error(f"Error broadcasting to a client: {e}")
# Run with: uvicorn src.main:app --reload