diff --git a/src/components/ui/badge.jsx b/src/components/ui/badge.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/button.jsx b/src/components/ui/button.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/input.jsx b/src/components/ui/input.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/scroll-area.jsx b/src/components/ui/scroll-area.jsx
old mode 100644
new mode 100755
diff --git a/src/contexts/ThemeContext.jsx b/src/contexts/ThemeContext.jsx
new file mode 100755
index 00000000..f2a37647
--- /dev/null
+++ b/src/contexts/ThemeContext.jsx
@@ -0,0 +1,72 @@
+import React, { createContext, useContext, useState, useEffect } from 'react';
+
+const ThemeContext = createContext();
+
+export const useTheme = () => {
+ const context = useContext(ThemeContext);
+ if (!context) {
+ throw new Error('useTheme must be used within a ThemeProvider');
+ }
+ return context;
+};
+
+export const ThemeProvider = ({ children }) => {
+ // Check for saved theme preference or default to system preference
+ const [isDarkMode, setIsDarkMode] = useState(() => {
+ // Check localStorage first
+ const savedTheme = localStorage.getItem('theme');
+ if (savedTheme) {
+ return savedTheme === 'dark';
+ }
+
+ // Check system preference
+ if (window.matchMedia) {
+ return window.matchMedia('(prefers-color-scheme: dark)').matches;
+ }
+
+ return false;
+ });
+
+ // Update document class and localStorage when theme changes
+ useEffect(() => {
+ if (isDarkMode) {
+ document.documentElement.classList.add('dark');
+ localStorage.setItem('theme', 'dark');
+ } else {
+ document.documentElement.classList.remove('dark');
+ localStorage.setItem('theme', 'light');
+ }
+ }, [isDarkMode]);
+
+ // Listen for system theme changes
+ useEffect(() => {
+ if (!window.matchMedia) return;
+
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+ const handleChange = (e) => {
+ // Only update if user hasn't manually set a preference
+ const savedTheme = localStorage.getItem('theme');
+ if (!savedTheme) {
+ setIsDarkMode(e.matches);
+ }
+ };
+
+ mediaQuery.addEventListener('change', handleChange);
+ return () => mediaQuery.removeEventListener('change', handleChange);
+ }, []);
+
+ const toggleDarkMode = () => {
+ setIsDarkMode(prev => !prev);
+ };
+
+ const value = {
+ isDarkMode,
+ toggleDarkMode,
+ };
+
+ return (
+
+ {children}
+
+ );
+};
\ No newline at end of file
diff --git a/src/hooks/useAudioRecorder.js b/src/hooks/useAudioRecorder.js
new file mode 100755
index 00000000..c02fa345
--- /dev/null
+++ b/src/hooks/useAudioRecorder.js
@@ -0,0 +1,109 @@
+import { useState, useRef, useCallback } from 'react';
+
+export function useAudioRecorder() {
+ const [isRecording, setRecording] = useState(false);
+ const [audioBlob, setAudioBlob] = useState(null);
+ const [error, setError] = useState(null);
+ const mediaRecorderRef = useRef(null);
+ const streamRef = useRef(null);
+ const chunksRef = useRef([]);
+
+ const start = useCallback(async () => {
+ try {
+ setError(null);
+ setAudioBlob(null);
+ chunksRef.current = [];
+
+ // Request microphone access
+ const stream = await navigator.mediaDevices.getUserMedia({
+ audio: {
+ echoCancellation: true,
+ noiseSuppression: true,
+ sampleRate: 16000,
+ }
+ });
+
+ streamRef.current = stream;
+
+ // Determine supported MIME type
+ const mimeType = MediaRecorder.isTypeSupported('audio/webm')
+ ? 'audio/webm'
+ : 'audio/mp4';
+
+ // Create media recorder
+ const recorder = new MediaRecorder(stream, { mimeType });
+ mediaRecorderRef.current = recorder;
+
+ // Set up event handlers
+ recorder.ondataavailable = (e) => {
+ if (e.data.size > 0) {
+ chunksRef.current.push(e.data);
+ }
+ };
+
+ recorder.onstop = () => {
+ // Create blob from chunks
+ const blob = new Blob(chunksRef.current, { type: mimeType });
+ setAudioBlob(blob);
+
+ // Clean up stream
+ if (streamRef.current) {
+ streamRef.current.getTracks().forEach(track => track.stop());
+ streamRef.current = null;
+ }
+ };
+
+ recorder.onerror = (event) => {
+ console.error('MediaRecorder error:', event);
+ setError('Recording failed');
+ setRecording(false);
+ };
+
+ // Start recording
+ recorder.start();
+ setRecording(true);
+ console.log('Recording started');
+ } catch (err) {
+ console.error('Failed to start recording:', err);
+ setError(err.message || 'Failed to start recording');
+ setRecording(false);
+ }
+ }, []);
+
+ const stop = useCallback(() => {
+ console.log('Stop called, recorder state:', mediaRecorderRef.current?.state);
+
+ try {
+ if (mediaRecorderRef.current && mediaRecorderRef.current.state === 'recording') {
+ mediaRecorderRef.current.stop();
+ console.log('Recording stopped');
+ }
+ } catch (err) {
+ console.error('Error stopping recorder:', err);
+ }
+
+ // Always update state
+ setRecording(false);
+
+ // Clean up stream if still active
+ if (streamRef.current) {
+ streamRef.current.getTracks().forEach(track => track.stop());
+ streamRef.current = null;
+ }
+ }, []);
+
+ const reset = useCallback(() => {
+ setAudioBlob(null);
+ setError(null);
+ chunksRef.current = [];
+ }, []);
+
+ return {
+ isRecording,
+ audioBlob,
+ error,
+ start,
+ stop,
+ reset
+ };
+}
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
old mode 100644
new mode 100755
index 689130c8..68ed3a50
--- a/src/index.css
+++ b/src/index.css
@@ -2,50 +2,69 @@
@tailwind components;
@tailwind utilities;
+/* Global spinner animation - defined early to ensure it loads */
+@keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+@-webkit-keyframes spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(360deg);
+ }
+}
+
@layer base {
:root {
--background: 0 0% 100%;
- --foreground: 240 10% 3.9%;
+ --foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
- --card-foreground: 240 10% 3.9%;
+ --card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
- --popover-foreground: 240 10% 3.9%;
- --primary: 240 5.9% 10%;
- --primary-foreground: 0 0% 98%;
- --secondary: 240 4.8% 95.9%;
- --secondary-foreground: 240 5.9% 10%;
- --muted: 240 4.8% 95.9%;
- --muted-foreground: 240 3.8% 46.1%;
- --accent: 240 4.8% 95.9%;
- --accent-foreground: 240 5.9% 10%;
+ --popover-foreground: 222.2 84% 4.9%;
+ --primary: 221.2 83.2% 53.3%;
+ --primary-foreground: 210 40% 98%;
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
- --destructive-foreground: 0 0% 98%;
- --border: 240 5.9% 90%;
- --input: 240 5.9% 90%;
- --ring: 240 5.9% 10%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
}
.dark {
- --background: 240 10% 3.9%;
- --foreground: 0 0% 98%;
- --card: 240 10% 3.9%;
- --card-foreground: 0 0% 98%;
- --popover: 240 10% 3.9%;
- --popover-foreground: 0 0% 98%;
- --primary: 0 0% 98%;
- --primary-foreground: 240 5.9% 10%;
- --secondary: 240 3.7% 15.9%;
- --secondary-foreground: 0 0% 98%;
- --muted: 240 3.7% 15.9%;
- --muted-foreground: 240 5% 64.9%;
- --accent: 240 3.7% 15.9%;
- --accent-foreground: 0 0% 98%;
+ --background: 222.2 84% 4.9%;
+ --foreground: 210 40% 98%;
+ --card: 217.2 91.2% 8%;
+ --card-foreground: 210 40% 98%;
+ --popover: 217.2 91.2% 8%;
+ --popover-foreground: 210 40% 98%;
+ --primary: 217.2 91.2% 59.8%;
+ --primary-foreground: 222.2 47.4% 11.2%;
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
- --destructive-foreground: 0 0% 98%;
- --border: 240 3.7% 15.9%;
- --input: 240 3.7% 15.9%;
- --ring: 240 4.9% 83.9%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 212.7 26.8% 83.9%;
}
}
@@ -53,6 +72,7 @@
* {
@apply border-border;
box-sizing: border-box;
+ transition: none;
}
body {
@@ -67,9 +87,106 @@
margin: 0;
padding: 0;
}
+
+ /* Global transition defaults */
+ button,
+ a,
+ input,
+ textarea,
+ select,
+ [role="button"],
+ .transition-all {
+ transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
+ }
+
+ /* Color transitions for theme switching */
+ * {
+ transition: background-color 200ms ease-in-out,
+ border-color 200ms ease-in-out,
+ color 200ms ease-in-out;
+ }
+
+ /* Transform transitions */
+ .transition-transform {
+ transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
+ }
+
+ /* Opacity transitions */
+ .transition-opacity {
+ transition: opacity 200ms ease-in-out;
+ }
+
+ /* Scale transitions for interactions */
+ .transition-scale {
+ transition: transform 100ms cubic-bezier(0.4, 0, 0.2, 1);
+ }
+
+ /* Shadow transitions */
+ .transition-shadow {
+ transition: box-shadow 200ms ease-in-out;
+ }
+
+ /* Respect reduced motion preference */
+ @media (prefers-reduced-motion: reduce) {
+ *,
+ ::before,
+ ::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ scroll-behavior: auto !important;
+ }
+ }
}
@layer utilities {
+ /* Smooth hover transitions for interactive elements */
+ button:hover,
+ a:hover,
+ [role="button"]:hover {
+ transition-duration: 100ms;
+ }
+
+ /* Active state transitions */
+ button:active,
+ a:active,
+ [role="button"]:active {
+ transition-duration: 50ms;
+ }
+
+ /* Focus transitions */
+ button:focus-visible,
+ a:focus-visible,
+ input:focus-visible,
+ textarea:focus-visible,
+ select:focus-visible {
+ transition: outline-offset 150ms ease-out, box-shadow 150ms ease-out;
+ }
+
+ /* Sidebar transitions */
+ .sidebar-transition {
+ transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1),
+ opacity 300ms ease-in-out;
+ }
+
+ /* Modal and dropdown transitions */
+ .modal-transition {
+ transition: opacity 200ms ease-in-out,
+ transform 200ms cubic-bezier(0.4, 0, 0.2, 1);
+ }
+
+ /* Chat message transitions */
+ .message-transition {
+ transition: opacity 300ms ease-in-out,
+ transform 300ms cubic-bezier(0.4, 0, 0.2, 1);
+ }
+
+ /* Height transitions for expanding elements */
+ .height-transition {
+ transition: height 200ms ease-in-out,
+ max-height 200ms ease-in-out;
+ }
+
.scrollbar-thin {
scrollbar-width: thin;
scrollbar-color: hsl(var(--muted-foreground)) transparent;
@@ -77,6 +194,7 @@
.scrollbar-thin::-webkit-scrollbar {
width: 6px;
+ height: 6px;
}
.scrollbar-thin::-webkit-scrollbar-track {
@@ -91,6 +209,222 @@
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background-color: hsl(var(--muted-foreground) / 0.8);
}
+
+ /* Dark mode scrollbar styles */
+ .dark .scrollbar-thin {
+ scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
+ }
+
+ .dark .scrollbar-thin::-webkit-scrollbar-track {
+ background: rgba(31, 41, 55, 0.3);
+ }
+
+ .dark .scrollbar-thin::-webkit-scrollbar-thumb {
+ background-color: rgba(156, 163, 175, 0.5);
+ border-radius: 3px;
+ }
+
+ .dark .scrollbar-thin::-webkit-scrollbar-thumb:hover {
+ background-color: rgba(156, 163, 175, 0.7);
+ }
+
+ /* Global scrollbar styles for main content areas */
+ .dark::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+ }
+
+ .dark::-webkit-scrollbar-track {
+ background: rgba(31, 41, 55, 0.5);
+ }
+
+ .dark::-webkit-scrollbar-thumb {
+ background-color: rgba(107, 114, 128, 0.5);
+ border-radius: 4px;
+ }
+
+ .dark::-webkit-scrollbar-thumb:hover {
+ background-color: rgba(107, 114, 128, 0.7);
+ }
+
+ /* Firefox scrollbar styles */
+ .dark {
+ scrollbar-width: thin;
+ scrollbar-color: rgba(107, 114, 128, 0.5) rgba(31, 41, 55, 0.5);
+ }
+
+ /* Ensure checkbox styling is preserved */
+ input[type="checkbox"] {
+ @apply accent-blue-600;
+ opacity: 1;
+ }
+
+ input[type="checkbox"]:focus {
+ opacity: 1;
+ outline: 2px solid hsl(var(--ring));
+ outline-offset: 2px;
+ }
+
+ /* Fix checkbox appearance in dark mode */
+ .dark input[type="checkbox"] {
+ background-color: rgb(31 41 55); /* gray-800 */
+ border-color: rgb(75 85 99); /* gray-600 */
+ color: rgb(37 99 235); /* blue-600 */
+ color-scheme: dark;
+ }
+
+ .dark input[type="checkbox"]:checked {
+ background-color: rgb(37 99 235); /* blue-600 */
+ border-color: rgb(37 99 235); /* blue-600 */
+ }
+
+ .dark input[type="checkbox"]:focus {
+ --tw-ring-color: rgb(59 130 246); /* blue-500 */
+ --tw-ring-offset-color: rgb(31 41 55); /* gray-800 */
+ }
+
+ /* Fix radio button appearance in dark mode */
+ .dark input[type="radio"] {
+ background-color: rgb(31 41 55); /* gray-800 */
+ border-color: rgb(75 85 99); /* gray-600 */
+ color: rgb(37 99 235); /* blue-600 */
+ color-scheme: dark;
+ }
+
+ .dark input[type="radio"]:checked {
+ background-color: rgb(37 99 235); /* blue-600 */
+ border-color: rgb(37 99 235); /* blue-600 */
+ }
+
+ .dark input[type="radio"]:focus {
+ --tw-ring-color: rgb(59 130 246); /* blue-500 */
+ --tw-ring-offset-color: rgb(31 41 55); /* gray-800 */
+ }
+
+ /* Ensure textarea text is always visible in dark mode */
+ textarea {
+ color-scheme: light dark;
+ }
+
+ .dark textarea {
+ color: rgb(243 244 246) !important; /* gray-100 */
+ -webkit-text-fill-color: rgb(243 244 246) !important;
+ caret-color: rgb(243 244 246) !important;
+ }
+
+ /* Fix for focus state in dark mode */
+ .dark textarea:focus {
+ color: rgb(243 244 246) !important;
+ -webkit-text-fill-color: rgb(243 244 246) !important;
+ }
+
+ /* Fix for iOS/Safari dark mode textarea issues */
+ @supports (-webkit-touch-callout: none) {
+ .dark textarea {
+ background-color: transparent !important;
+ color: rgb(243 244 246) !important;
+ -webkit-text-fill-color: rgb(243 244 246) !important;
+ }
+
+ .dark textarea:focus {
+ background-color: transparent !important;
+ color: rgb(243 244 246) !important;
+ -webkit-text-fill-color: rgb(243 244 246) !important;
+ }
+ }
+
+ /* Ensure parent container doesn't override textarea styles */
+ .dark .bg-gray-800 textarea {
+ color: rgb(243 244 246) !important;
+ -webkit-text-fill-color: rgb(243 244 246) !important;
+ }
+
+ /* Fix focus-within container issues in dark mode */
+ .dark .focus-within\:ring-2:focus-within {
+ background-color: rgb(31 41 55) !important; /* gray-800 */
+ }
+
+ /* Ensure textarea remains transparent with visible text */
+ .dark textarea.bg-transparent {
+ background-color: transparent !important;
+ color: rgb(243 244 246) !important;
+ -webkit-text-fill-color: rgb(243 244 246) !important;
+ }
+
+ /* Fix placeholder text color to be properly gray */
+ textarea::placeholder {
+ color: rgb(156 163 175) !important; /* gray-400 */
+ opacity: 1 !important;
+ }
+
+ .dark textarea::placeholder {
+ color: rgb(75 85 99) !important; /* gray-600 - darker gray */
+ opacity: 1 !important;
+ }
+
+ /* More specific selector for chat input textarea */
+ .dark .bg-gray-800 textarea::placeholder,
+ .dark textarea.bg-transparent::placeholder {
+ color: rgb(75 85 99) !important; /* gray-600 - darker gray */
+ opacity: 1 !important;
+ -webkit-text-fill-color: rgb(75 85 99) !important;
+ }
+
+ /* Custom class for chat input placeholder */
+ .chat-input-placeholder::placeholder {
+ color: rgb(156 163 175) !important;
+ opacity: 1 !important;
+ }
+
+ .dark .chat-input-placeholder::placeholder {
+ color: rgb(75 85 99) !important;
+ opacity: 1 !important;
+ -webkit-text-fill-color: rgb(75 85 99) !important;
+ }
+
+ .chat-input-placeholder::-webkit-input-placeholder {
+ color: rgb(156 163 175) !important;
+ opacity: 1 !important;
+ }
+
+ .dark .chat-input-placeholder::-webkit-input-placeholder {
+ color: rgb(75 85 99) !important;
+ opacity: 1 !important;
+ -webkit-text-fill-color: rgb(75 85 99) !important;
+ }
+
+ /* WebKit specific placeholder styles */
+ textarea::-webkit-input-placeholder {
+ color: rgb(156 163 175) !important;
+ opacity: 1 !important;
+ }
+
+ .dark textarea::-webkit-input-placeholder {
+ color: rgb(75 85 99) !important; /* gray-600 - darker gray */
+ opacity: 1 !important;
+ }
+
+ /* Mozilla specific placeholder styles */
+ textarea::-moz-placeholder {
+ color: rgb(156 163 175) !important;
+ opacity: 1 !important;
+ }
+
+ .dark textarea::-moz-placeholder {
+ color: rgb(75 85 99) !important; /* gray-600 - darker gray */
+ opacity: 1 !important;
+ }
+
+ /* IE/Edge specific placeholder styles */
+ textarea:-ms-input-placeholder {
+ color: rgb(156 163 175) !important;
+ opacity: 1 !important;
+ }
+
+ .dark textarea:-ms-input-placeholder {
+ color: rgb(75 85 99) !important; /* gray-600 - darker gray */
+ opacity: 1 !important;
+ }
}
/* Mobile optimizations and components */
@@ -102,6 +436,12 @@
-webkit-tap-highlight-color: transparent;
}
+ /* Preserve checkbox visibility */
+ input[type="checkbox"] {
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
+ opacity: 1 !important;
+ }
+
button,
[role="button"],
.clickable,
@@ -132,6 +472,12 @@
@apply w-full sm:max-w-[95%];
}
+ /* Session name truncation on mobile */
+ .session-name-mobile {
+ @apply truncate;
+ max-width: calc(100vw - 120px); /* Account for sidebar padding and buttons */
+ }
+
/* Enable text selection on mobile for terminal */
.xterm,
.xterm .xterm-viewport {
@@ -182,6 +528,12 @@
-webkit-tap-highlight-color: transparent !important;
}
+ /* Preserve checkbox visibility on touch devices */
+ input[type="checkbox"] {
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1) !important;
+ opacity: 1 !important;
+ }
+
/* Only disable hover states for interactive elements, not containers */
button:hover,
[role="button"]:hover,
@@ -251,4 +603,137 @@
max-width: 100%;
box-sizing: border-box;
}
+}
+
+/* Hide markdown backticks in prose content */
+.prose code::before,
+.prose code::after {
+ content: "" !important;
+ display: none !important;
+}
+
+/* Custom spinner animation for mobile compatibility */
+@layer utilities {
+ @keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+ }
+
+ .animate-spin {
+ animation: spin 1s linear infinite;
+ }
+
+ /* Force hardware acceleration for smoother animation on mobile */
+ .loading-spinner {
+ animation: spin 1s linear infinite;
+ will-change: transform;
+ transform: translateZ(0);
+ -webkit-transform: translateZ(0);
+ backface-visibility: hidden;
+ -webkit-backface-visibility: hidden;
+ }
+
+ /* Improved textarea styling */
+ .chat-input-placeholder {
+ scrollbar-width: thin;
+ scrollbar-color: rgba(156, 163, 175, 0.3) transparent;
+ }
+
+ .chat-input-placeholder::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ .chat-input-placeholder::-webkit-scrollbar-track {
+ background: transparent;
+ margin: 8px 0;
+ }
+
+ .chat-input-placeholder::-webkit-scrollbar-thumb {
+ background-color: rgba(156, 163, 175, 0.3);
+ border-radius: 3px;
+ transition: background-color 0.2s;
+ }
+
+ .chat-input-placeholder::-webkit-scrollbar-thumb:hover {
+ background-color: rgba(156, 163, 175, 0.5);
+ }
+
+ .dark .chat-input-placeholder {
+ scrollbar-color: rgba(107, 114, 128, 0.3) transparent;
+ }
+
+ .dark .chat-input-placeholder::-webkit-scrollbar-thumb {
+ background-color: rgba(107, 114, 128, 0.3);
+ }
+
+ .dark .chat-input-placeholder::-webkit-scrollbar-thumb:hover {
+ background-color: rgba(107, 114, 128, 0.5);
+ }
+
+ /* Enhanced box shadow when textarea expands */
+ .chat-input-expanded {
+ box-shadow: 0 -5px 15px -3px rgba(0, 0, 0, 0.1), 0 -4px 6px -2px rgba(0, 0, 0, 0.05);
+ }
+
+ .dark .chat-input-expanded {
+ box-shadow: 0 -5px 15px -3px rgba(0, 0, 0, 0.3), 0 -4px 6px -2px rgba(0, 0, 0, 0.2);
+ }
+
+ /* Fix focus ring offset color in dark mode */
+ .dark [class*="ring-offset"] {
+ --tw-ring-offset-color: rgb(31 41 55); /* gray-800 */
+ }
+
+ /* Ensure buttons don't show white backgrounds in dark mode */
+ .dark button:focus {
+ --tw-ring-offset-color: rgb(31 41 55); /* gray-800 */
+ }
+
+ /* Fix mobile select dropdown styling */
+ @supports (-webkit-touch-callout: none) {
+ select {
+ font-size: 16px !important;
+ -webkit-appearance: none;
+ }
+ }
+
+ /* Improve select appearance in dark mode */
+ .dark select {
+ background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239CA3AF' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
+ background-repeat: no-repeat;
+ background-position: right 0.5rem center;
+ background-size: 1.5em 1.5em;
+ padding-right: 2.5rem;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ }
+
+ select {
+ background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236B7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
+ background-repeat: no-repeat;
+ background-position: right 0.5rem center;
+ background-size: 1.5em 1.5em;
+ padding-right: 2.5rem;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ }
+
+ /* Fix select option text in mobile */
+ select option {
+ font-size: 16px !important;
+ padding: 8px !important;
+ background-color: var(--background) !important;
+ color: var(--foreground) !important;
+ }
+
+ .dark select option {
+ background-color: rgb(31 41 55) !important;
+ color: rgb(243 244 246) !important;
+ }
}
\ No newline at end of file
diff --git a/src/lib/utils.js b/src/lib/utils.js
old mode 100644
new mode 100755
diff --git a/src/main.jsx b/src/main.jsx
old mode 100644
new mode 100755
diff --git a/src/utils/websocket.js b/src/utils/websocket.js
old mode 100644
new mode 100755
diff --git a/src/utils/whisper.js b/src/utils/whisper.js
new file mode 100755
index 00000000..a8dd445e
--- /dev/null
+++ b/src/utils/whisper.js
@@ -0,0 +1,38 @@
+export async function transcribeWithWhisper(audioBlob, onStatusChange) {
+ const formData = new FormData();
+ const fileName = `recording_${Date.now()}.webm`;
+ const file = new File([audioBlob], fileName, { type: audioBlob.type });
+
+ formData.append('audio', file);
+
+ const whisperMode = window.localStorage.getItem('whisperMode') || 'default';
+ formData.append('mode', whisperMode);
+
+ try {
+ // Start with transcribing state
+ if (onStatusChange) {
+ onStatusChange('transcribing');
+ }
+
+ const response = await fetch('/api/transcribe', {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.error ||
+ `Transcription error: ${response.status} ${response.statusText}`
+ );
+ }
+
+ const data = await response.json();
+ return data.text || '';
+ } catch (error) {
+ if (error.name === 'TypeError' && error.message.includes('fetch')) {
+ throw new Error('Cannot connect to server. Please ensure the backend is running.');
+ }
+ throw error;
+ }
+ }
\ No newline at end of file
diff --git a/tailwind.config.js b/tailwind.config.js
old mode 100644
new mode 100755
diff --git a/vite.config.js b/vite.config.js
old mode 100644
new mode 100755
index 0c1dad1a..4d3971b7
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,19 +1,25 @@
-import { defineConfig } from 'vite'
+import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
-export default defineConfig({
- plugins: [react()],
- server: {
- port: process.env.VITE_PORT || 3001,
- proxy: {
- '/api': `http://localhost:${process.env.PORT || 3002}`,
- '/ws': {
- target: `ws://localhost:${process.env.PORT || 3002}`,
- ws: true
+export default defineConfig(({ command, mode }) => {
+ // Load env file based on `mode` in the current working directory.
+ const env = loadEnv(mode, process.cwd(), '')
+
+
+ return {
+ plugins: [react()],
+ server: {
+ port: parseInt(env.VITE_PORT) || 3001,
+ proxy: {
+ '/api': `http://localhost:${env.PORT || 3002}`,
+ '/ws': {
+ target: `ws://localhost:${env.PORT || 3002}`,
+ ws: true
+ }
}
+ },
+ build: {
+ outDir: 'dist'
}
- },
- build: {
- outDir: 'dist'
}
})
\ No newline at end of file