summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoryum <yum.food.vr@gmail.com>2025-05-29 17:23:09 -0700
committeryum <yum.food.vr@gmail.com>2025-05-29 17:23:09 -0700
commit82a5b3805b2a54faea501ee362419330664c277a (patch)
tree235ac6540d1a25e4fcf4107c1ea93f69e43b563b
parent1ede199387c072a85e8757a6aaec04d2c7cdeba4 (diff)
Begin roughing out STT UI
HEAVILY VIBE CODED!
-rw-r--r--ui/index.html189
-rw-r--r--ui/index.js155
-rw-r--r--ui/package.json14
-rw-r--r--ui/preload.js6
-rw-r--r--ui/renderer.js249
-rw-r--r--ui/src/components.css110
-rw-r--r--ui/tailwind.config.js5
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: {},