diff options
| author | yum <yum.food.vr@gmail.com> | 2025-05-29 17:23:09 -0700 |
|---|---|---|
| committer | yum <yum.food.vr@gmail.com> | 2025-05-29 17:23:09 -0700 |
| commit | 82a5b3805b2a54faea501ee362419330664c277a (patch) | |
| tree | 235ac6540d1a25e4fcf4107c1ea93f69e43b563b /ui | |
| parent | 1ede199387c072a85e8757a6aaec04d2c7cdeba4 (diff) | |
Begin roughing out STT UI
HEAVILY VIBE CODED!
Diffstat (limited to 'ui')
| -rw-r--r-- | ui/index.html | 189 | ||||
| -rw-r--r-- | ui/index.js | 155 | ||||
| -rw-r--r-- | ui/package.json | 14 | ||||
| -rw-r--r-- | ui/preload.js | 6 | ||||
| -rw-r--r-- | ui/renderer.js | 249 | ||||
| -rw-r--r-- | ui/src/components.css | 110 | ||||
| -rw-r--r-- | ui/tailwind.config.js | 5 |
7 files changed, 708 insertions, 20 deletions
diff --git a/ui/index.html b/ui/index.html index 240e6ca..14cc354 100644 --- a/ui/index.html +++ b/ui/index.html @@ -3,18 +3,185 @@ <head> <meta charset="UTF-8"> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"> - <title>Hello World!</title> - <link rel="stylesheet" href="build/output.css"> </head> -<body> - <div class="container mx-auto mt-10 p-5 bg-gray-100 rounded shadow-lg"> - <h1 class="text-4xl font-bold text-blue-600 text-center"> - Hello World! - </h1> - <p class="text-center text-gray-700 mt-2"> - Welcome to your Electron app with Tailwind CSS! - </p> + <title>TaSTT</title> + <link rel="stylesheet" href="build/output.css"> +</head> +<body class="bg-gray-100"> + <div class="container-fluid px-6 py-6 h-screen flex flex-col"> + <h1 class="text-3xl font-bold text-gray-800 mb-8">TaSTT</h1> + + <div class="flex flex-1 gap-6 overflow-hidden"> + <!-- Left panel: configuration form --> + <div class="w-1/2 overflow-y-auto"> + <form id="config-form" class="space-y-6 pr-3"> + <!-- Basic settings (Always Visible) --> + <section class="config-section"> + <div class="grid grid-cols-2 gap-4"> + <div> + <label for="model" class="form-label">Model</label> + <select id="model" class="form-input"> + <option value="tiny">tiny</option> + <option value="base">base</option> + <option value="small">small</option> + <option value="medium">medium</option> + <option value="large">large</option> + <option value="turbo">turbo</option> + </select> + </div> + <div> + <label for="language" class="form-label">Language</label> + <select id="language" class="form-input"> + <option value="english">English</option> + <option value="spanish">Spanish</option> + <option value="french">French</option> + <option value="german">German</option> + <option value="italian">Italian</option> + <option value="portuguese">Portuguese</option> + <option value="russian">Russian</option> + <option value="chinese">Chinese</option> + <option value="japanese">Japanese</option> + <option value="korean">Korean</option> + </select> + </div> + <div class="col-span-2"> + <label for="microphone" class="form-label">Microphone</label> + <select id="microphone" class="form-input"> + <option value="">Loading microphones...</option> + </select> + </div> + </div> + </section> + + <!-- Advanced settings toggle --> + <button type="button" id="toggle-advanced" class="flex items-center gap-2 text-gray-600 hover:text-gray-800 font-medium"> + <svg id="chevron" class="w-5 h-5 transform transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/> + </svg> + Advanced Settings + </button> + + <!-- Advanced settings (initially hidden) --> + <div id="advanced-settings" class="hidden space-y-6"> + <!-- Compute Settings --> + <section class="config-section"> + <h2 class="section-title">Compute Settings</h2> + <div class="grid grid-cols-2 gap-4"> + <div> + <label for="compute_type" class="form-label">Compute Type</label> + <select id="compute_type" class="form-input"> + <option value="int8">int8</option> + <option value="float16">float16</option> + <option value="float32">float32</option> + </select> + </div> + <div> + <label for="gpu_idx" class="form-label">GPU Index</label> + <input type="number" id="gpu_idx" min="0" value="0" class="form-input"> + </div> + <div class="col-span-2"> + <label for="use_cpu" class="checkbox-label"> + <input type="checkbox" id="use_cpu" class="mr-2"> + <span class="checkbox-text">Use CPU</span> + </label> + </div> + </div> + </section> + + <!-- Audio Settings --> + <section class="config-section"> + <h2 class="section-title">Audio Settings</h2> + <div class="grid grid-cols-2 gap-4"> + <div> + <label for="max_speech_duration_s" class="form-label">Max Speech Duration (seconds)</label> + <input type="number" id="max_speech_duration_s" min="1" value="10" class="form-input"> + </div> + <div> + <label for="min_silence_duration_ms" class="form-label">Min Silence Duration (ms)</label> + <input type="number" id="min_silence_duration_ms" min="0" value="250" class="form-input"> + </div> + <div> + <label for="reset_after_silence_s" class="form-label">Reset After Silence (seconds)</label> + <input type="number" id="reset_after_silence_s" min="1" value="15" class="form-input"> + </div> + </div> + </section> + + <!-- Performance Settings --> + <section class="config-section"> + <h2 class="section-title">Performance Settings</h2> + <div> + <label for="transcription_loop_delay_ms" class="form-label">Transcription Loop Delay (ms)</label> + <input type="number" id="transcription_loop_delay_ms" min="0" value="100" class="form-input"> + </div> + </section> + + <!-- Debug/Preview Settings --> + <section class="config-section"> + <h2 class="section-title">Debug/Preview Settings</h2> + <div class="space-y-3"> + <label for="enable_debug_mode" class="checkbox-label"> + <input type="checkbox" id="enable_debug_mode" class="mr-2"> + <span class="checkbox-text">Enable Debug Mode</span> + </label> + <label for="enable_previews" class="checkbox-label"> + <input type="checkbox" id="enable_previews" checked class="mr-2"> + <span class="checkbox-text">Enable Previews</span> + </label> + </div> + </section> + + <!-- Display Settings --> + <section class="config-section"> + <h2 class="section-title">Display Settings</h2> + <div class="grid grid-cols-2 gap-4"> + <div> + <label for="block_width" class="form-label">Block Width</label> + <input type="number" id="block_width" min="1" value="2" class="form-input"> + </div> + <div> + <label for="num_blocks" class="form-label">Number of Blocks</label> + <input type="number" id="num_blocks" min="1" value="40" class="form-input"> + </div> + <div> + <label for="rows" class="form-label">Rows</label> + <input type="number" id="rows" min="1" value="10" class="form-input"> + </div> + <div> + <label for="cols" class="form-label">Columns</label> + <input type="number" id="cols" min="1" value="24" class="form-input"> + </div> + </div> + </section> + </div> + + <!-- Action Buttons --> + <div class="flex justify-between pb-6"> + <button type="button" id="setup-venv" class="btn btn-blue"> + Set up virtual environment + </button> + </div> + </form> + + <!-- Status Message --> + <div id="status-message" class="mt-6 p-4 rounded-md hidden"></div> + </div> + + <!-- Right Panel: Python Console --> + <div class="w-1/2 flex flex-col bg-gray-900 rounded-lg overflow-hidden"> + <div class="bg-gray-800 px-4 py-2 flex justify-between items-center"> + <h2 class="text-white font-semibold">Python Output</h2> + <button id="clear-console" class="text-gray-400 hover:text-white text-sm"> + Clear + </button> + </div> + <div id="python-console" class="flex-1 overflow-y-auto p-4 font-mono text-sm"> + <div id="console-content" class="text-gray-300 whitespace-pre-wrap"></div> + </div> + </div> + </div> </div> - </body> + <script src="renderer.js"></script> +</body> </html> diff --git a/ui/index.js b/ui/index.js index 9751fb2..0a7fdf9 100644 --- a/ui/index.js +++ b/ui/index.js @@ -1,10 +1,71 @@ const { app, BrowserWindow, ipcMain } = require('electron'); const path = require('node:path'); +const fs = require('node:fs').promises; +const yaml = require('js-yaml'); +const { spawn } = require('child_process'); + +let mainWindow; + +// Helper function to get the correct Python executable from venv +function getVenvPython() { + const venvPath = path.join(__dirname, '..', 'venv'); + const isWindows = process.platform === 'win32'; + const pythonExecutable = isWindows ? 'python.exe' : 'python'; + const pythonPath = path.join(venvPath, isWindows ? 'Scripts' : 'bin', pythonExecutable); + return pythonPath; +} + +// Helper function to send Python output to renderer +function sendPythonOutput(message, type = 'stdout') { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('python-output', { message, type }); + } +} + +// Helper function to execute Python commands using venv +function executePythonCommand(args, options = {}) { + return new Promise((resolve, reject) => { + const pythonPath = getVenvPython(); + const commandStr = `${path.basename(pythonPath)} ${args.join(' ')}`; + sendPythonOutput(`> ${commandStr}`, 'info'); + + const pythonProcess = spawn(pythonPath, args, options); + + let stdout = ''; + let stderr = ''; + + pythonProcess.stdout.on('data', (data) => { + const text = data.toString(); + stdout += text; + sendPythonOutput(text.trimEnd(), 'stdout'); + }); + + pythonProcess.stderr.on('data', (data) => { + const text = data.toString(); + stderr += text; + sendPythonOutput(text.trimEnd(), 'stderr'); + }); + + pythonProcess.on('error', (error) => { + sendPythonOutput(`Failed to start Python process: ${error.message}`, 'stderr'); + reject({ error: error.message, stdout, stderr }); + }); + + pythonProcess.on('close', (code) => { + if (code !== 0) { + sendPythonOutput(`Process exited with code ${code}`, 'stderr'); + reject({ code, stdout, stderr }); + } else { + resolve({ stdout, stderr }); + } + }); + }); +} function createWindow () { - const mainWindow = new BrowserWindow({ - width: 800, - height: 600, + mainWindow = new BrowserWindow({ + width: 1000, + height: 800, webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, @@ -15,6 +76,94 @@ function createWindow () { mainWindow.loadFile('index.html'); } +// Path to config.yaml (one level up from ui directory) +const configPath = path.join(__dirname, '..', 'config.yaml'); + +// IPC handlers +ipcMain.handle('load-config', async () => { + try { + const fileContent = await fs.readFile(configPath, 'utf8'); + return yaml.load(fileContent); + } catch (error) { + console.error('Error loading config:', error); + throw error; + } +}); + +ipcMain.handle('save-config', async (event, config) => { + try { + const yamlContent = yaml.dump(config, { lineWidth: -1 }); + await fs.writeFile(configPath, yamlContent, 'utf8'); + return { success: true }; + } catch (error) { + console.error('Error saving config:', error); + throw error; + } +}); + +ipcMain.handle('restart-app', () => { + app.relaunch(); + app.exit(); +}); + +ipcMain.handle('install-requirements', async (event) => { + const requirementsPath = path.join(__dirname, '..', 'app', 'requirements.txt'); + + try { + // Check if requirements.txt exists + await fs.access(requirementsPath); + + const result = await executePythonCommand(['-m', 'pip', 'install', '-r', requirementsPath]); + + return { success: true, message: 'Requirements installed successfully' }; + } catch (error) { + console.error('Error installing requirements:', error); + if (error.code === 'ENOENT') { + throw new Error('requirements.txt not found'); + } + throw new Error(`Installation failed: ${error.stderr || error.error || 'Unknown error'}`); + } +}); + +ipcMain.handle('get-microphones', async () => { + const pythonScript = ` +import pyaudio +import json +import sys + +try: + p = pyaudio.PyAudio() + info = p.get_host_api_info_by_index(0) + numdevices = info.get('deviceCount') + + microphones = [] + for i in range(0, numdevices): + device_info = p.get_device_info_by_host_api_device_index(0, i) + if device_info.get('maxInputChannels') > 0: + microphones.append({ + 'index': i, + 'name': device_info.get('name'), + 'defaultSampleRate': device_info.get('defaultSampleRate') + }) + + print(json.dumps(microphones)) + p.terminate() +except Exception as e: + print(json.dumps({'error': str(e)}), file=sys.stderr) + sys.exit(1) +`; + + try { + const result = await executePythonCommand(['-c', pythonScript]); + const microphones = JSON.parse(result.stdout.trim()); + console.log('Successfully retrieved microphones:', microphones); + return microphones; + } catch (error) { + console.error('Failed to get microphones:', error); + throw new Error(`Failed to get microphones: ${error.stderr || error.error || 'Unknown error'}`); + } +}); + app.whenReady().then(() => { createWindow(); diff --git a/ui/package.json b/ui/package.json index 1c56341..fee2d67 100644 --- a/ui/package.json +++ b/ui/package.json @@ -5,20 +5,26 @@ "main": "index.js", "scripts": { "start": "npm run build:css && electron .", - "build:css": "tailwindcss -i ./src/input.css -o ./build/output.css", - "watch:css": "tailwindcss -i ./src/input.css -o ./build/output.css --watch", - "dev": "npm run watch:css & electron .", + "build:css": "tailwindcss -i ./src/components.css -o ./build/output.css", + "watch:css": "tailwindcss -i ./src/components.css -o ./build/output.css --watch", + "dev": "concurrently \"npm run watch:css\" \"electron .\"", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "yum_food", "license": "MIT", + "dependencies": { + "js-yaml": "^4.1.0" + }, "devDependencies": { + "@vitejs/plugin-vue": "^5.2.4", "autoprefixer": "^10.4.21", "concurrently": "^9.1.2", "cross-env": "^7.0.3", "electron": "^36.3.2", "postcss": "^8.5.4", - "tailwindcss": "^3.4.17" + "tailwindcss": "^3.4.17", + "vite": "^6.3.5", + "vue": "^3.5.16" } } diff --git a/ui/preload.js b/ui/preload.js index 9f87d19..108bffe 100644 --- a/ui/preload.js +++ b/ui/preload.js @@ -1,6 +1,12 @@ const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('electronAPI', { + loadConfig: () => ipcRenderer.invoke('load-config'), + saveConfig: (config) => ipcRenderer.invoke('save-config', config), + restartApp: () => ipcRenderer.invoke('restart-app'), + getMicrophones: () => ipcRenderer.invoke('get-microphones'), + installRequirements: () => ipcRenderer.invoke('install-requirements'), + onPythonOutput: (callback) => ipcRenderer.on('python-output', (event, data) => callback(data)) }); console.log('Preload script loaded.'); diff --git a/ui/renderer.js b/ui/renderer.js new file mode 100644 index 0000000..83c652c --- /dev/null +++ b/ui/renderer.js @@ -0,0 +1,249 @@ +// Handle status messages +function showStatus(message, type = 'info') { + const statusEl = document.getElementById('status-message'); + statusEl.textContent = message; + statusEl.classList.remove('hidden', 'bg-green-100', 'bg-red-100', 'bg-blue-100', 'text-green-800', 'text-red-800', 'text-blue-800'); + + if (type === 'success') { + statusEl.classList.add('bg-green-100', 'text-green-800'); + } else if (type === 'error') { + statusEl.classList.add('bg-red-100', 'text-red-800'); + } else { + statusEl.classList.add('bg-blue-100', 'text-blue-800'); + } + + // Also log to console + appendToConsole(message, type === 'error' ? 'stderr' : 'info'); + + setTimeout(() => { + statusEl.classList.add('hidden'); + }, 5000); +} + +// Get form values +function getFormValues() { + return { + compute_type: document.getElementById('compute_type').value, + enable_debug_mode: document.getElementById('enable_debug_mode').checked ? 1 : 0, + enable_previews: document.getElementById('enable_previews').checked ? 1 : 0, + language: document.getElementById('language').value, + gpu_idx: parseInt(document.getElementById('gpu_idx').value), + max_speech_duration_s: parseInt(document.getElementById('max_speech_duration_s').value), + min_silence_duration_ms: parseInt(document.getElementById('min_silence_duration_ms').value), + microphone: document.getElementById('microphone').value, + model: document.getElementById('model').value, + reset_after_silence_s: parseInt(document.getElementById('reset_after_silence_s').value), + transcription_loop_delay_ms: parseInt(document.getElementById('transcription_loop_delay_ms').value), + use_cpu: document.getElementById('use_cpu').checked ? 1 : 0, + block_width: parseInt(document.getElementById('block_width').value), + num_blocks: parseInt(document.getElementById('num_blocks').value), + rows: parseInt(document.getElementById('rows').value), + cols: parseInt(document.getElementById('cols').value) + }; +} + +// Add a flag to prevent auto-save during programmatic updates +let isSettingValues = false; + +// Set form values +function setFormValues(config) { + isSettingValues = true; // Disable auto-save temporarily + + document.getElementById('compute_type').value = config.compute_type || 'int8'; + document.getElementById('enable_debug_mode').checked = config.enable_debug_mode === 1; + document.getElementById('enable_previews').checked = config.enable_previews === 1; + document.getElementById('language').value = config.language || 'english'; + document.getElementById('gpu_idx').value = config.gpu_idx || 0; + document.getElementById('max_speech_duration_s').value = config.max_speech_duration_s || 10; + document.getElementById('min_silence_duration_ms').value = config.min_silence_duration_ms || 250; + document.getElementById('microphone').value = config.microphone || 'motu'; + document.getElementById('model').value = config.model || 'turbo'; + document.getElementById('reset_after_silence_s').value = config.reset_after_silence_s || 15; + document.getElementById('transcription_loop_delay_ms').value = config.transcription_loop_delay_ms || 100; + document.getElementById('use_cpu').checked = config.use_cpu === 1; + document.getElementById('block_width').value = config.block_width || 2; + document.getElementById('num_blocks').value = config.num_blocks || 40; + document.getElementById('rows').value = config.rows || 10; + document.getElementById('cols').value = config.cols || 24; + + isSettingValues = false; // Re-enable auto-save +} + +// Toggle advanced settings +document.getElementById('toggle-advanced').addEventListener('click', () => { + const advancedSettings = document.getElementById('advanced-settings'); + const chevron = document.getElementById('chevron'); + + if (advancedSettings.classList.contains('hidden')) { + advancedSettings.classList.remove('hidden'); + chevron.classList.add('rotate-90'); + } else { + advancedSettings.classList.add('hidden'); + chevron.classList.remove('rotate-90'); + } +}); + +// Simplify button handlers by extracting common patterns +async function handleAsyncAction(actionName, actionFn) { + try { + const result = await actionFn(); + if (result && result.message) { + showStatus(result.message, 'success'); + } + return result; + } catch (error) { + showStatus(`${actionName} failed: ${error.message}`, 'error'); + throw error; + } +} + +// Auto-save functionality with debouncing +let saveTimeout; +const SAVE_DELAY = 500; // milliseconds + +async function autoSaveConfig() { + if (isSettingValues) return; // Don't save during programmatic updates + + clearTimeout(saveTimeout); + saveTimeout = setTimeout(async () => { + try { + const config = getFormValues(); + await window.electronAPI.saveConfig(config); + showStatus('Configuration saved', 'success'); + } catch (error) { + showStatus(`Failed to save configuration: ${error.message}`, 'error'); + } + }, SAVE_DELAY); +} + +// Add event listeners to all form inputs for auto-save +function setupAutoSave() { + // Get all form inputs + const form = document.getElementById('config-form'); + const inputs = form.querySelectorAll('input, select'); + + // Add change listener to each input + inputs.forEach(input => { + if (input.type === 'checkbox') { + input.addEventListener('change', autoSaveConfig); + } else if (input.type === 'number' || input.type === 'text') { + input.addEventListener('input', autoSaveConfig); + } else if (input.tagName === 'SELECT') { + input.addEventListener('change', autoSaveConfig); + } + }); +} + +// Update the setup-venv handler +document.getElementById('setup-venv').addEventListener('click', async () => { + const setupButton = document.getElementById('setup-venv'); + setupButton.disabled = true; + setupButton.classList.add('opacity-50', 'cursor-not-allowed'); + + try { + await handleAsyncAction('Install requirements', async () => { + return await window.electronAPI.installRequirements(); + }); + // Reload microphones after successful installation + await loadMicrophones(); + } finally { + setupButton.disabled = false; + setupButton.classList.remove('opacity-50', 'cursor-not-allowed'); + } +}); + +// Simplified microphone loading +async function loadMicrophones() { + const microphoneSelect = document.getElementById('microphone'); + + try { + appendToConsole('Loading available microphones...', 'info'); + const microphones = await window.electronAPI.getMicrophones(); + + microphoneSelect.innerHTML = ''; + + if (microphones.length === 0) { + microphoneSelect.innerHTML = '<option value="" disabled>No microphones found</option>'; + appendToConsole('No microphones found', 'stderr'); + return; + } + + appendToConsole(`Found ${microphones.length} microphone(s)`, 'info'); + microphones.forEach(mic => { + const option = document.createElement('option'); + option.value = mic.index.toString(); + option.textContent = mic.name; + microphoneSelect.appendChild(option); + appendToConsole(` - ${mic.name} (Device ${mic.index})`, 'stdout'); + }); + + // Restore previously selected microphone if possible + try { + const config = await window.electronAPI.loadConfig(); + if (config.microphone) { + microphoneSelect.value = config.microphone; + } + } catch (error) { + // Ignore config load errors here + } + + } catch (error) { + appendToConsole(`Failed to load microphones: ${error.message}`, 'stderr'); + microphoneSelect.innerHTML = '<option value="" disabled>Error loading microphones</option>'; + } +} + +// Update window load to include auto-save setup +window.addEventListener('load', async () => { + appendToConsole('TaSTT Configuration UI initialized', 'info'); + + // Load config first + try { + const config = await window.electronAPI.loadConfig(); + setFormValues(config); + appendToConsole('Configuration loaded', 'info'); + } catch (error) { + appendToConsole(`Failed to load configuration: ${error.message}`, 'stderr'); + } + + // Load microphones + await loadMicrophones(); + + // Set up auto-save after everything is loaded + setupAutoSave(); +}); + +// Console management +const consoleContent = document.getElementById('console-content'); + +function appendToConsole(message, type = 'stdout') { + const timestamp = new Date().toLocaleTimeString(); + const timestampSpan = document.createElement('span'); + timestampSpan.className = 'console-timestamp'; + timestampSpan.textContent = `[${timestamp}] `; + + const messageSpan = document.createElement('span'); + messageSpan.className = `console-${type}`; + messageSpan.textContent = message; + + const lineDiv = document.createElement('div'); + lineDiv.appendChild(timestampSpan); + lineDiv.appendChild(messageSpan); + + consoleContent.appendChild(lineDiv); + + // Auto-scroll to bottom + const pythonConsole = document.getElementById('python-console'); + pythonConsole.scrollTop = pythonConsole.scrollHeight; +} + +// Clear console button +document.getElementById('clear-console').addEventListener('click', () => { + consoleContent.innerHTML = ''; + appendToConsole('Console cleared', 'info'); +}); + +// Listen for Python output +window.electronAPI.onPythonOutput((data) => { + appendToConsole(data.message, data.type); +});
\ No newline at end of file diff --git a/ui/src/components.css b/ui/src/components.css new file mode 100644 index 0000000..be046ea --- /dev/null +++ b/ui/src/components.css @@ -0,0 +1,110 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer components { + .config-section { + @apply bg-white rounded-lg shadow-md p-6; + } + + .section-title { + @apply text-xl font-semibold text-gray-700 mb-4; + } + + .form-label { + @apply block text-sm font-medium text-gray-700 mb-2; + } + + .form-input { + @apply w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm; + } + + .checkbox-label { + @apply flex items-center cursor-pointer hover:bg-gray-50 p-2 rounded; + } + + .checkbox-text { + @apply text-sm text-gray-700; + } + + .btn { + @apply px-4 py-2 font-medium text-sm rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2; + } + + .btn-blue { + @apply bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500; + } + + .btn-green { + @apply bg-green-600 text-white hover:bg-green-700 focus:ring-green-500; + } + + .btn-gray { + @apply bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500; + } +} + +/* Console styling */ +#python-console { + background-color: #1a1a1a; + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + line-height: 1.4; +} + +#console-content { + word-wrap: break-word; +} + +/* Console text colors */ +.console-stdout { + color: #a8cc8c; +} + +.console-stderr { + color: #e88388; +} + +.console-info { + color: #66c2cd; +} + +.console-timestamp { + color: #6c7986; + font-size: 0.875rem; +} + +/* Ensure full height layout */ +html, body { + height: 100%; + margin: 0; + padding: 0; +} + +.container-fluid { + max-width: 100%; + height: 100vh; +} + +/* Scrollbar styling for console */ +#python-console::-webkit-scrollbar { + width: 8px; +} + +#python-console::-webkit-scrollbar-track { + background: #2a2a2a; +} + +#python-console::-webkit-scrollbar-thumb { + background: #4a4a4a; + border-radius: 4px; +} + +#python-console::-webkit-scrollbar-thumb:hover { + background: #5a5a5a; +} + +/* Ensure buttons have proper disabled states */ +button:disabled { + cursor: not-allowed; + opacity: 0.5; +} diff --git a/ui/tailwind.config.js b/ui/tailwind.config.js index fa93053..804b7f0 100644 --- a/ui/tailwind.config.js +++ b/ui/tailwind.config.js @@ -1,8 +1,9 @@ /** @type {import('tailwindcss').Config} */ module.exports = { content: [ - "./index.html", - "./src/**/*.{html,js}", + "./*.html", + "./*.js", + "./src/**/*.{html,js}" ], theme: { extend: {}, |
