+
+ {/* Changes View */}
+ {activeView === 'changes' && (
+ <>
+ {/* Commit Message Input */}
+
+ >
+ )}
+
+ {/* File Selection Controls - Only show in changes view */}
+ {activeView === 'changes' && gitStatus && (
+
+
+ {/* File List - Changes View */}
+ {activeView === 'changes' && (
+
+ )}
+
+ {/* History View */}
+ {activeView === 'history' && (
+
+ )}
+
+ {/* New Branch Modal */}
+ {showNewBranchModal && (
+
+ );
+}
+
+export default GitPanel;
\ No newline at end of file
diff --git a/src/components/MainContent.jsx b/src/components/MainContent.jsx
index 9f5b581..677248b 100755
--- a/src/components/MainContent.jsx
+++ b/src/components/MainContent.jsx
@@ -11,11 +11,12 @@
* No session protection logic is implemented here - it's purely a props bridge.
*/
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
import ChatInterface from './ChatInterface';
import FileTree from './FileTree';
import CodeEditor from './CodeEditor';
import Shell from './Shell';
+import GitPanel from './GitPanel';
function MainContent({
selectedProject,
@@ -37,7 +38,8 @@ function MainContent({
onNavigateToSession, // Navigate to a specific session (for Claude CLI session duplication workaround)
onShowSettings, // Show tools settings panel
autoExpandTools, // Auto-expand tool accordions
- showRawParameters // Show raw parameters in tool accordions
+ showRawParameters, // Show raw parameters in tool accordions
+ autoScrollToBottom // Auto-scroll to bottom when new messages arrive
}) {
const [editingFile, setEditingFile] = useState(null);
@@ -73,8 +75,15 @@ function MainContent({
)}
-
-
+
Loading Claude Code UI
Setting up your workspace...
@@ -135,7 +144,7 @@ function MainContent({
e.preventDefault();
onMenuClick();
}}
- className="p-2.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 touch-manipulation active:scale-95 transition-transform duration-75"
+ className="p-2.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 touch-manipulation active:scale-95"
>
@@ -164,7 +173,7 @@ function MainContent({
) : (
- Project Files
+ {activeTab === 'files' ? 'Project Files' : activeTab === 'git' ? 'Source Control' : 'Project'}
{selectedProject.displayName}
@@ -179,7 +188,7 @@ function MainContent({
setActiveTab('chat')}
- className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
+ className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md ${
activeTab === 'chat'
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
@@ -222,6 +231,36 @@ function MainContent({
Files
+
setActiveTab('git')}
+ className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
+ activeTab === 'git'
+ ? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
+ : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
+ }`}
+ >
+
+
+
+
+ Source Control
+
+
+ {/*
setActiveTab('preview')}
+ className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
+ activeTab === 'preview'
+ ? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
+ : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
+ }`}
+ >
+
+
+
+
+ Preview
+
+ */}
@@ -245,7 +284,7 @@ function MainContent({
onShowSettings={onShowSettings}
autoExpandTools={autoExpandTools}
showRawParameters={showRawParameters}
-
+ autoScrollToBottom={autoScrollToBottom}
/>
@@ -258,6 +297,35 @@ function MainContent({
isActive={activeTab === 'shell'}
/>
+
+
+
+
+ {/* {
+ sendMessage({
+ type: 'server:start',
+ projectPath: selectedProject?.fullPath,
+ script: script
+ });
+ }}
+ onStopServer={() => {
+ sendMessage({
+ type: 'server:stop',
+ projectPath: selectedProject?.fullPath
+ });
+ }}
+ onScriptSelect={setCurrentScript}
+ currentScript={currentScript}
+ isMobile={isMobile}
+ serverLogs={serverLogs}
+ onClearLogs={() => setServerLogs([])}
+ /> */}
+
{/* Code Editor Modal */}
diff --git a/src/components/MicButton.jsx b/src/components/MicButton.jsx
new file mode 100755
index 0000000..a199e8b
--- /dev/null
+++ b/src/components/MicButton.jsx
@@ -0,0 +1,217 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { Mic, Loader2, Brain } from 'lucide-react';
+import { transcribeWithWhisper } from '../utils/whisper';
+
+export function MicButton({ onTranscript, className = '' }) {
+ const [state, setState] = useState('idle'); // idle, recording, transcribing, processing
+ const [error, setError] = useState(null);
+
+ const mediaRecorderRef = useRef(null);
+ const streamRef = useRef(null);
+ const chunksRef = useRef([]);
+ const lastTapRef = useRef(0);
+
+ // Version indicator to verify updates
+ console.log('MicButton v2.0 loaded');
+
+ // Start recording
+ const startRecording = async () => {
+ try {
+ console.log('Starting recording...');
+ setError(null);
+ chunksRef.current = [];
+
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+ streamRef.current = stream;
+
+ const mimeType = MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' : 'audio/mp4';
+ const recorder = new MediaRecorder(stream, { mimeType });
+ mediaRecorderRef.current = recorder;
+
+ recorder.ondataavailable = (e) => {
+ if (e.data.size > 0) {
+ chunksRef.current.push(e.data);
+ }
+ };
+
+ recorder.onstop = async () => {
+ console.log('Recording stopped, creating blob...');
+ const blob = new Blob(chunksRef.current, { type: mimeType });
+
+ // Clean up stream
+ if (streamRef.current) {
+ streamRef.current.getTracks().forEach(track => track.stop());
+ streamRef.current = null;
+ }
+
+ // Start transcribing
+ setState('transcribing');
+
+ // Check if we're in an enhancement mode
+ const whisperMode = window.localStorage.getItem('whisperMode') || 'default';
+ const isEnhancementMode = whisperMode === 'prompt' || whisperMode === 'vibe' || whisperMode === 'instructions' || whisperMode === 'architect';
+
+ // Set up a timer to switch to processing state for enhancement modes
+ let processingTimer;
+ if (isEnhancementMode) {
+ processingTimer = setTimeout(() => {
+ setState('processing');
+ }, 2000); // Switch to processing after 2 seconds
+ }
+
+ try {
+ const text = await transcribeWithWhisper(blob);
+ if (text && onTranscript) {
+ onTranscript(text);
+ }
+ } catch (err) {
+ console.error('Transcription error:', err);
+ setError(err.message);
+ } finally {
+ if (processingTimer) {
+ clearTimeout(processingTimer);
+ }
+ setState('idle');
+ }
+ };
+
+ recorder.start();
+ setState('recording');
+ console.log('Recording started successfully');
+ } catch (err) {
+ console.error('Failed to start recording:', err);
+ setError('Microphone access denied');
+ setState('idle');
+ }
+ };
+
+ // Stop recording
+ const stopRecording = () => {
+ console.log('Stopping recording...');
+ if (mediaRecorderRef.current && mediaRecorderRef.current.state === 'recording') {
+ mediaRecorderRef.current.stop();
+ // Don't set state here - let the onstop handler do it
+ } else {
+ // If recorder isn't in recording state, force cleanup
+ console.log('Recorder not in recording state, forcing cleanup');
+ if (streamRef.current) {
+ streamRef.current.getTracks().forEach(track => track.stop());
+ streamRef.current = null;
+ }
+ setState('idle');
+ }
+ };
+
+ // Handle button click
+ const handleClick = (e) => {
+ // Prevent double firing on mobile
+ if (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ // Debounce for mobile double-tap issue
+ const now = Date.now();
+ if (now - lastTapRef.current < 300) {
+ console.log('Ignoring rapid tap');
+ return;
+ }
+ lastTapRef.current = now;
+
+ console.log('Button clicked, current state:', state);
+
+ if (state === 'idle') {
+ startRecording();
+ } else if (state === 'recording') {
+ stopRecording();
+ }
+ // Do nothing if transcribing or processing
+ };
+
+ // Clean up on unmount
+ useEffect(() => {
+ return () => {
+ if (streamRef.current) {
+ streamRef.current.getTracks().forEach(track => track.stop());
+ }
+ };
+ }, []);
+
+ // Button appearance based on state
+ const getButtonAppearance = () => {
+ switch (state) {
+ case 'recording':
+ return {
+ icon:
,
+ className: 'bg-red-500 hover:bg-red-600 animate-pulse',
+ disabled: false
+ };
+ case 'transcribing':
+ return {
+ icon:
,
+ className: 'bg-blue-500 hover:bg-blue-600',
+ disabled: true
+ };
+ case 'processing':
+ return {
+ icon:
,
+ className: 'bg-purple-500 hover:bg-purple-600',
+ disabled: true
+ };
+ default: // idle
+ return {
+ icon:
,
+ className: 'bg-gray-700 hover:bg-gray-600',
+ disabled: false
+ };
+ }
+ };
+
+ const { icon, className: buttonClass, disabled } = getButtonAppearance();
+
+ return (
+
+
+ {icon}
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {state === 'recording' && (
+
+ )}
+
+ {state === 'processing' && (
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/MobileNav.jsx b/src/components/MobileNav.jsx
index 60df23b..185a9f4 100755
--- a/src/components/MobileNav.jsx
+++ b/src/components/MobileNav.jsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { MessageSquare, Folder, Terminal } from 'lucide-react';
+import { MessageSquare, Folder, Terminal, GitBranch, Globe } from 'lucide-react';
function MobileNav({ activeTab, setActiveTab, isInputFocused }) {
// Detect dark mode
@@ -19,6 +19,11 @@ function MobileNav({ activeTab, setActiveTab, isInputFocused }) {
id: 'files',
icon: Folder,
onClick: () => setActiveTab('files')
+ },
+ {
+ id: 'git',
+ icon: GitBranch,
+ onClick: () => setActiveTab('git')
}
];
@@ -52,7 +57,7 @@ function MobileNav({ activeTab, setActiveTab, isInputFocused }) {
e.preventDefault();
item.onClick();
}}
- className={`flex items-center justify-center p-2 rounded-lg transition-colors duration-75 min-h-[40px] min-w-[40px] relative touch-manipulation ${
+ className={`flex items-center justify-center p-2 rounded-lg min-h-[40px] min-w-[40px] relative touch-manipulation ${
isActive
? 'text-blue-600 dark:text-blue-400'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
diff --git a/src/components/QuickSettingsPanel.jsx b/src/components/QuickSettingsPanel.jsx
index f663acd..249a54a 100755
--- a/src/components/QuickSettingsPanel.jsx
+++ b/src/components/QuickSettingsPanel.jsx
@@ -4,14 +4,14 @@ import {
ChevronRight,
Maximize2,
Eye,
- EyeOff,
- Zap,
- Layout,
- Terminal,
- Code2,
Settings2,
Moon,
- Sun
+ Sun,
+ ArrowDown,
+ Mic,
+ Brain,
+ Sparkles,
+ FileText
} from 'lucide-react';
import DarkModeToggle from './DarkModeToggle';
import { useTheme } from '../contexts/ThemeContext';
@@ -23,9 +23,14 @@ const QuickSettingsPanel = ({
onAutoExpandChange,
showRawParameters,
onShowRawParametersChange,
+ autoScrollToBottom,
+ onAutoScrollChange,
isMobile
}) => {
const [localIsOpen, setLocalIsOpen] = useState(isOpen);
+ const [whisperMode, setWhisperMode] = useState(() => {
+ return localStorage.getItem('whisperMode') || 'default';
+ });
const { isDarkMode } = useTheme();
useEffect(() => {
@@ -44,7 +49,7 @@ const QuickSettingsPanel = ({
{/* Header */}
@@ -75,12 +80,12 @@ const QuickSettingsPanel = ({
{/* Settings Content */}
-
+
{/* Appearance Settings */}
Appearance
-
+
{isDarkMode ? : }
Dark Mode
@@ -93,7 +98,7 @@ const QuickSettingsPanel = ({
Tool Display
-
+
Auto-expand tools
@@ -102,11 +107,11 @@ const QuickSettingsPanel = ({
type="checkbox"
checked={autoExpandTools}
onChange={(e) => onAutoExpandChange(e.target.checked)}
- className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 accent-blue-600"
+ className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
/>
-
+
Show raw parameters
@@ -115,60 +120,114 @@ const QuickSettingsPanel = ({
type="checkbox"
checked={showRawParameters}
onChange={(e) => onShowRawParametersChange(e.target.checked)}
- className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 accent-blue-600"
+ className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
/>
-
-
-
- Syntax highlighting
-
-
- {/* Performance Settings */}
-
-
Performance
-
-
-
- Stream optimizations
-
-
-
-
- Compact mode
-
-
-
{/* View Options */}
View Options
-
-
- Terminal theme
-
-
-
-
- Hide timestamps
-
+
+
+
+ Auto-scroll to bottom
+
+ onAutoScrollChange(e.target.checked)}
+ className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
+ />
+
-
- {/* Footer */}
-
-
- Advanced Settings
-
+ {/* Whisper Dictation Settings */}
+
+
Whisper Dictation
+
+
+
+ {
+ setWhisperMode('default');
+ localStorage.setItem('whisperMode', 'default');
+ window.dispatchEvent(new Event('whisperModeChanged'));
+ }}
+ className="mt-0.5 h-4 w-4 border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
+ />
+
+
+
+ Default Mode
+
+
+ Direct transcription of your speech
+
+
+
+
+
+ {
+ setWhisperMode('prompt');
+ localStorage.setItem('whisperMode', 'prompt');
+ window.dispatchEvent(new Event('whisperModeChanged'));
+ }}
+ className="mt-0.5 h-4 w-4 border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
+ />
+
+
+
+ Prompt Enhancement
+
+
+ Transform rough ideas into clear, detailed AI prompts
+
+
+
+
+
+ {
+ setWhisperMode('vibe');
+ localStorage.setItem('whisperMode', 'vibe');
+ window.dispatchEvent(new Event('whisperModeChanged'));
+ }}
+ className="mt-0.5 h-4 w-4 border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
+ />
+
+
+
+ Vibe Mode
+
+
+ Format ideas as clear agent instructions with details
+
+
+
+
+
- {/* Backdrop for mobile */}
- {isMobile && localIsOpen && (
+ {/* Backdrop */}
+ {localIsOpen && (
)}
diff --git a/src/components/Shell.jsx b/src/components/Shell.jsx
index 3d9035f..9609aa0 100755
--- a/src/components/Shell.jsx
+++ b/src/components/Shell.jsx
@@ -493,11 +493,11 @@ function Shell({ selectedProject, selectedSession, isActive }) {
{/* Connect button when not connected */}
{isInitialized && !isConnected && !isConnecting && (
-
-
+
+
@@ -505,7 +505,7 @@ function Shell({ selectedProject, selectedSession, isActive }) {
Continue in Shell
-
+
{selectedSession ?
`Resume session: ${selectedSession.summary.slice(0, 50)}...` :
'Start a new Claude session'
@@ -517,13 +517,13 @@ function Shell({ selectedProject, selectedSession, isActive }) {
{/* Connecting state */}
{isConnecting && (
-
-
-
+
+
+
-
+
Starting Claude CLI in {selectedProject.displayName}
diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx
index fe5cd9d..9c00c3d 100755
--- a/src/components/Sidebar.jsx
+++ b/src/components/Sidebar.jsx
@@ -3,8 +3,9 @@ import { ScrollArea } from './ui/scroll-area';
import { Button } from './ui/button';
import { Badge } from './ui/badge';
import { Input } from './ui/input';
-import { FolderOpen, Folder, Plus, MessageSquare, Clock, ChevronDown, ChevronRight, Edit3, Check, X, Trash2, Settings, FolderPlus, RefreshCw } from 'lucide-react';
+import { FolderOpen, Folder, Plus, MessageSquare, Clock, ChevronDown, ChevronRight, Edit3, Check, X, Trash2, Settings, FolderPlus, RefreshCw, Sparkles, Edit2 } from 'lucide-react';
import { cn } from '../lib/utils';
+import ClaudeLogo from './ClaudeLogo';
// Move formatTimeAgo outside component to avoid recreation on every render
const formatTimeAgo = (dateString, currentTime) => {
@@ -56,6 +57,9 @@ function Sidebar({
const [initialSessionsLoaded, setInitialSessionsLoaded] = useState(new Set());
const [currentTime, setCurrentTime] = useState(new Date());
const [isRefreshing, setIsRefreshing] = useState(false);
+ const [editingSession, setEditingSession] = useState(null);
+ const [editingSessionName, setEditingSessionName] = useState('');
+ const [generatingSummary, setGeneratingSummary] = useState({});
// Touch handler to prevent double-tap issues on iPad
const handleTouchClick = (callback) => {
@@ -601,7 +605,7 @@ function Sidebar({
<>
{getAllSessions(project).length === 0 && (
{
e.stopPropagation();
deleteProject(project.name);
@@ -612,7 +616,7 @@ function Sidebar({
)}
{
e.stopPropagation();
startEditing(project);
@@ -639,7 +643,7 @@ function Sidebar({
{
@@ -781,14 +785,27 @@ function Sidebar({
No sessions yet
) : (
- getAllSessions(project).map((session) => (
+ getAllSessions(project).map((session) => {
+ // Calculate if session is active (within last 10 minutes)
+ const sessionDate = new Date(session.lastActivity);
+ const diffInMinutes = Math.floor((currentTime - sessionDate) / (1000 * 60));
+ const isActive = diffInMinutes < 10;
+
+ return (
+ {/* Active session indicator dot */}
+ {isActive && (
+
+ )}
{/* Mobile Session Item */}
{
onProjectSelect(project);
@@ -871,22 +888,99 @@ function Sidebar({
- {/* Desktop delete button */}
-
{
- e.stopPropagation();
- deleteSession(project.name, session.id);
- }}
- title="Delete session (Delete)"
- >
-
-
+ {/* Desktop hover buttons */}
+
+ {editingSession === session.id ? (
+ <>
+
setEditingSessionName(e.target.value)}
+ onKeyDown={(e) => {
+ e.stopPropagation();
+ if (e.key === 'Enter') {
+ updateSessionSummary(project.name, session.id, editingSessionName);
+ } else if (e.key === 'Escape') {
+ setEditingSession(null);
+ setEditingSessionName('');
+ }
+ }}
+ onClick={(e) => e.stopPropagation()}
+ className="w-32 px-2 py-1 text-xs border border-border rounded bg-background focus:outline-none focus:ring-1 focus:ring-primary"
+ autoFocus
+ />
+
{
+ e.stopPropagation();
+ updateSessionSummary(project.name, session.id, editingSessionName);
+ }}
+ title="Save"
+ >
+
+
+
{
+ e.stopPropagation();
+ setEditingSession(null);
+ setEditingSessionName('');
+ }}
+ title="Cancel"
+ >
+
+
+ >
+ ) : (
+ <>
+ {/* Generate summary button */}
+ {/*
{
+ e.stopPropagation();
+ generateSessionSummary(project.name, session.id);
+ }}
+ title="Generate AI summary for this session"
+ disabled={generatingSummary[`${project.name}-${session.id}`]}
+ >
+ {generatingSummary[`${project.name}-${session.id}`] ? (
+
+ ) : (
+
+ )}
+ */}
+ {/* Edit button */}
+
{
+ e.stopPropagation();
+ setEditingSession(session.id);
+ setEditingSessionName(session.summary || 'New Session');
+ }}
+ title="Manually edit session name"
+ >
+
+
+ {/* Delete button */}
+
{
+ e.stopPropagation();
+ deleteSession(project.name, session.id);
+ }}
+ title="Delete this session permanently"
+ >
+
+
+ >
+ )}
+
- ))
+ );
+ })
)}
-
+
{/* Show More Sessions Button */}
{getAllSessions(project).length > 0 && project.sessionMeta?.hasMore !== false && (
{
+ 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
index 29a9858..68ed3a5 100755
--- a/src/index.css
+++ b/src/index.css
@@ -2,6 +2,25 @@
@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%;
@@ -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/utils/whisper.js b/src/utils/whisper.js
new file mode 100755
index 0000000..a8dd445
--- /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