mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-12 10:09:39 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5e3bd0633 | ||
|
|
27f34db777 |
@@ -21,9 +21,9 @@ async function saveProjectConfig(config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate better display name from path
|
// Generate better display name from path
|
||||||
async function generateDisplayName(projectName) {
|
async function generateDisplayName(projectName, actualProjectDir = null) {
|
||||||
// Convert "-home-user-projects-myapp" to a readable format
|
// Use actual project directory if provided, otherwise decode from project name
|
||||||
let projectPath = projectName.replace(/-/g, '/');
|
let projectPath = actualProjectDir || projectName.replace(/-/g, '/');
|
||||||
|
|
||||||
// Try to read package.json from the project path
|
// Try to read package.json from the project path
|
||||||
try {
|
try {
|
||||||
@@ -54,6 +54,91 @@ async function generateDisplayName(projectName) {
|
|||||||
return projectPath;
|
return projectPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract the actual project directory from JSONL sessions
|
||||||
|
async function extractProjectDirectory(projectName) {
|
||||||
|
const projectDir = path.join(process.env.HOME, '.claude', 'projects', projectName);
|
||||||
|
const cwdCounts = new Map();
|
||||||
|
let latestTimestamp = 0;
|
||||||
|
let latestCwd = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const files = await fs.readdir(projectDir);
|
||||||
|
const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
|
||||||
|
|
||||||
|
if (jsonlFiles.length === 0) {
|
||||||
|
// Fall back to decoded project name if no sessions
|
||||||
|
return projectName.replace(/-/g, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process all JSONL files to collect cwd values
|
||||||
|
for (const file of jsonlFiles) {
|
||||||
|
const jsonlFile = path.join(projectDir, file);
|
||||||
|
const fileStream = require('fs').createReadStream(jsonlFile);
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: fileStream,
|
||||||
|
crlfDelay: Infinity
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const line of rl) {
|
||||||
|
if (line.trim()) {
|
||||||
|
try {
|
||||||
|
const entry = JSON.parse(line);
|
||||||
|
|
||||||
|
if (entry.cwd) {
|
||||||
|
// Count occurrences of each cwd
|
||||||
|
cwdCounts.set(entry.cwd, (cwdCounts.get(entry.cwd) || 0) + 1);
|
||||||
|
|
||||||
|
// Track the most recent cwd
|
||||||
|
const timestamp = new Date(entry.timestamp || 0).getTime();
|
||||||
|
if (timestamp > latestTimestamp) {
|
||||||
|
latestTimestamp = timestamp;
|
||||||
|
latestCwd = entry.cwd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
// Skip malformed lines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the best cwd to use
|
||||||
|
if (cwdCounts.size === 0) {
|
||||||
|
// No cwd found, fall back to decoded project name
|
||||||
|
return projectName.replace(/-/g, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cwdCounts.size === 1) {
|
||||||
|
// Only one cwd, use it
|
||||||
|
return Array.from(cwdCounts.keys())[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple cwd values - prefer the most recent one if it has reasonable usage
|
||||||
|
const mostRecentCount = cwdCounts.get(latestCwd) || 0;
|
||||||
|
const maxCount = Math.max(...cwdCounts.values());
|
||||||
|
|
||||||
|
// Use most recent if it has at least 25% of the max count
|
||||||
|
if (mostRecentCount >= maxCount * 0.25) {
|
||||||
|
return latestCwd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise use the most frequently used cwd
|
||||||
|
for (const [cwd, count] of cwdCounts.entries()) {
|
||||||
|
if (count === maxCount) {
|
||||||
|
return cwd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback (shouldn't reach here)
|
||||||
|
return latestCwd || projectName.replace(/-/g, '/');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error extracting project directory for ${projectName}:`, error);
|
||||||
|
// Fall back to decoded project name
|
||||||
|
return projectName.replace(/-/g, '/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function getProjects() {
|
async function getProjects() {
|
||||||
const claudeDir = path.join(process.env.HOME, '.claude', 'projects');
|
const claudeDir = path.join(process.env.HOME, '.claude', 'projects');
|
||||||
const config = await loadProjectConfig();
|
const config = await loadProjectConfig();
|
||||||
@@ -69,14 +154,17 @@ async function getProjects() {
|
|||||||
existingProjects.add(entry.name);
|
existingProjects.add(entry.name);
|
||||||
const projectPath = path.join(claudeDir, entry.name);
|
const projectPath = path.join(claudeDir, entry.name);
|
||||||
|
|
||||||
|
// Extract actual project directory from JSONL sessions
|
||||||
|
const actualProjectDir = await extractProjectDirectory(entry.name);
|
||||||
|
|
||||||
// Get display name from config or generate one
|
// Get display name from config or generate one
|
||||||
const customName = config[entry.name]?.displayName;
|
const customName = config[entry.name]?.displayName;
|
||||||
const autoDisplayName = await generateDisplayName(entry.name);
|
const autoDisplayName = await generateDisplayName(entry.name, actualProjectDir);
|
||||||
const fullPath = entry.name.replace(/-/g, '/');
|
const fullPath = actualProjectDir;
|
||||||
|
|
||||||
const project = {
|
const project = {
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
path: projectPath,
|
path: actualProjectDir,
|
||||||
displayName: customName || autoDisplayName,
|
displayName: customName || autoDisplayName,
|
||||||
fullPath: fullPath,
|
fullPath: fullPath,
|
||||||
isCustomName: !!customName,
|
isCustomName: !!customName,
|
||||||
@@ -105,13 +193,23 @@ async function getProjects() {
|
|||||||
// Add manually configured projects that don't exist as folders yet
|
// Add manually configured projects that don't exist as folders yet
|
||||||
for (const [projectName, projectConfig] of Object.entries(config)) {
|
for (const [projectName, projectConfig] of Object.entries(config)) {
|
||||||
if (!existingProjects.has(projectName) && projectConfig.manuallyAdded) {
|
if (!existingProjects.has(projectName) && projectConfig.manuallyAdded) {
|
||||||
const fullPath = projectName.replace(/-/g, '/');
|
// Use the original path if available, otherwise extract from potential sessions
|
||||||
|
let actualProjectDir = projectConfig.originalPath;
|
||||||
|
|
||||||
|
if (!actualProjectDir) {
|
||||||
|
try {
|
||||||
|
actualProjectDir = await extractProjectDirectory(projectName);
|
||||||
|
} catch (error) {
|
||||||
|
// Fall back to decoded project name
|
||||||
|
actualProjectDir = projectName.replace(/-/g, '/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const project = {
|
const project = {
|
||||||
name: projectName,
|
name: projectName,
|
||||||
path: null, // No physical path yet
|
path: actualProjectDir,
|
||||||
displayName: projectConfig.displayName || await generateDisplayName(projectName),
|
displayName: projectConfig.displayName || await generateDisplayName(projectName, actualProjectDir),
|
||||||
fullPath: fullPath,
|
fullPath: actualProjectDir,
|
||||||
isCustomName: !!projectConfig.displayName,
|
isCustomName: !!projectConfig.displayName,
|
||||||
isManuallyAdded: true,
|
isManuallyAdded: true,
|
||||||
sessions: []
|
sessions: []
|
||||||
@@ -463,9 +561,9 @@ async function addProjectManually(projectPath, displayName = null) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
name: projectName,
|
name: projectName,
|
||||||
path: null,
|
path: absolutePath,
|
||||||
fullPath: absolutePath,
|
fullPath: absolutePath,
|
||||||
displayName: displayName || await generateDisplayName(projectName),
|
displayName: displayName || await generateDisplayName(projectName, absolutePath),
|
||||||
isManuallyAdded: true,
|
isManuallyAdded: true,
|
||||||
sessions: []
|
sessions: []
|
||||||
};
|
};
|
||||||
@@ -483,5 +581,6 @@ module.exports = {
|
|||||||
deleteProject,
|
deleteProject,
|
||||||
addProjectManually,
|
addProjectManually,
|
||||||
loadProjectConfig,
|
loadProjectConfig,
|
||||||
saveProjectConfig
|
saveProjectConfig,
|
||||||
|
extractProjectDirectory
|
||||||
};
|
};
|
||||||
@@ -1112,15 +1112,15 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
const isNearBottom = useCallback(() => {
|
const isNearBottom = useCallback(() => {
|
||||||
if (!scrollContainerRef.current) return false;
|
if (!scrollContainerRef.current) return false;
|
||||||
const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current;
|
const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current;
|
||||||
// Consider "near bottom" if within 100px of the bottom
|
// Consider "near bottom" if within 50px of the bottom
|
||||||
return scrollHeight - scrollTop - clientHeight < 100;
|
return scrollHeight - scrollTop - clientHeight < 50;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Handle scroll events to detect when user manually scrolls up
|
// Handle scroll events to detect when user manually scrolls up
|
||||||
const handleScroll = useCallback(() => {
|
const handleScroll = useCallback(() => {
|
||||||
if (scrollContainerRef.current) {
|
if (scrollContainerRef.current) {
|
||||||
const wasNearBottom = isNearBottom();
|
const nearBottom = isNearBottom();
|
||||||
setIsUserScrolledUp(!wasNearBottom);
|
setIsUserScrolledUp(!nearBottom);
|
||||||
}
|
}
|
||||||
}, [isNearBottom]);
|
}, [isNearBottom]);
|
||||||
|
|
||||||
@@ -1540,13 +1540,12 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only auto-scroll to bottom when new messages arrive if:
|
// Auto-scroll to bottom when new messages arrive
|
||||||
// 1. Auto-scroll is enabled in settings
|
|
||||||
// 2. User hasn't manually scrolled up
|
|
||||||
if (scrollContainerRef.current && chatMessages.length > 0) {
|
if (scrollContainerRef.current && chatMessages.length > 0) {
|
||||||
if (autoScrollToBottom) {
|
if (autoScrollToBottom) {
|
||||||
|
// If auto-scroll is enabled, always scroll to bottom unless user has manually scrolled up
|
||||||
if (!isUserScrolledUp) {
|
if (!isUserScrolledUp) {
|
||||||
setTimeout(() => scrollToBottom(), 0);
|
setTimeout(() => scrollToBottom(), 50); // Small delay to ensure DOM is updated
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// When auto-scroll is disabled, preserve the visual position
|
// When auto-scroll is disabled, preserve the visual position
|
||||||
@@ -1564,12 +1563,15 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
}
|
}
|
||||||
}, [chatMessages.length, isUserScrolledUp, scrollToBottom, autoScrollToBottom]);
|
}, [chatMessages.length, isUserScrolledUp, scrollToBottom, autoScrollToBottom]);
|
||||||
|
|
||||||
// Scroll to bottom when component mounts with existing messages
|
// Scroll to bottom when component mounts with existing messages or when messages first load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (scrollContainerRef.current && chatMessages.length > 0 && autoScrollToBottom) {
|
if (scrollContainerRef.current && chatMessages.length > 0) {
|
||||||
setTimeout(() => scrollToBottom(), 100); // Small delay to ensure rendering
|
// Always scroll to bottom when messages first load (user expects to see latest)
|
||||||
|
// Also reset scroll state
|
||||||
|
setIsUserScrolledUp(false);
|
||||||
|
setTimeout(() => scrollToBottom(), 200); // Longer delay to ensure full rendering
|
||||||
}
|
}
|
||||||
}, [scrollToBottom, autoScrollToBottom]);
|
}, [chatMessages.length > 0, scrollToBottom]); // Trigger when messages first appear
|
||||||
|
|
||||||
// Add scroll event listener to detect user scrolling
|
// Add scroll event listener to detect user scrolling
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1636,8 +1638,9 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
can_interrupt: true
|
can_interrupt: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// Always scroll to bottom when user sends a message (they're actively participating)
|
// Always scroll to bottom when user sends a message and reset scroll state
|
||||||
setTimeout(() => scrollToBottom(), 0);
|
setIsUserScrolledUp(false); // Reset scroll state so auto-scroll works for Claude's response
|
||||||
|
setTimeout(() => scrollToBottom(), 100); // Longer delay to ensure message is rendered
|
||||||
|
|
||||||
// Session Protection: Mark session as active to prevent automatic project updates during conversation
|
// Session Protection: Mark session as active to prevent automatic project updates during conversation
|
||||||
// This is crucial for maintaining chat state integrity. We handle two cases:
|
// This is crucial for maintaining chat state integrity. We handle two cases:
|
||||||
@@ -1882,12 +1885,13 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Floating scroll to bottom button */}
|
{/* Floating scroll to bottom button - positioned outside scrollable container */}
|
||||||
{isUserScrolledUp && chatMessages.length > 0 && (
|
{isUserScrolledUp && chatMessages.length > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={scrollToBottom}
|
onClick={scrollToBottom}
|
||||||
className="absolute bottom-4 right-4 w-10 h-10 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-lg flex items-center justify-center transition-all duration-200 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:ring-offset-gray-800 z-10"
|
className="fixed bottom-20 sm:bottom-24 right-4 sm:right-6 w-12 h-12 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-lg flex items-center justify-center transition-all duration-200 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:ring-offset-gray-800 z-50"
|
||||||
title="Scroll to bottom"
|
title="Scroll to bottom"
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -1895,7 +1899,6 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Input Area - Fixed Bottom */}
|
{/* Input Area - Fixed Bottom */}
|
||||||
<div className={`p-2 sm:p-4 md:p-6 flex-shrink-0 ${
|
<div className={`p-2 sm:p-4 md:p-6 flex-shrink-0 ${
|
||||||
@@ -1977,8 +1980,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{/* Mic button */}
|
{/* Mic button - HIDDEN */}
|
||||||
<div className="absolute right-16 sm:right-16 top-1/2 transform -translate-y-1/2">
|
<div className="absolute right-16 sm:right-16 top-1/2 transform -translate-y-1/2" style={{ display: 'none' }}>
|
||||||
<MicButton
|
<MicButton
|
||||||
onTranscript={handleTranscript}
|
onTranscript={handleTranscript}
|
||||||
className="w-10 h-10 sm:w-10 sm:h-10"
|
className="w-10 h-10 sm:w-10 sm:h-10"
|
||||||
|
|||||||
@@ -568,6 +568,7 @@ function GitPanel({ selectedProject, isMobile }) {
|
|||||||
<Sparkles className="w-4 h-4" />
|
<Sparkles className="w-4 h-4" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
<div style={{ display: 'none' }}>
|
||||||
<MicButton
|
<MicButton
|
||||||
onTranscript={(transcript) => setCommitMessage(transcript)}
|
onTranscript={(transcript) => setCommitMessage(transcript)}
|
||||||
mode="default"
|
mode="default"
|
||||||
@@ -575,6 +576,7 @@ function GitPanel({ selectedProject, isMobile }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex items-center justify-between mt-2">
|
<div className="flex items-center justify-between mt-2">
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-xs text-gray-500">
|
||||||
{selectedFiles.size} file{selectedFiles.size !== 1 ? 's' : ''} selected
|
{selectedFiles.size} file{selectedFiles.size !== 1 ? 's' : ''} selected
|
||||||
|
|||||||
@@ -5,13 +5,35 @@ import { transcribeWithWhisper } from '../utils/whisper';
|
|||||||
export function MicButton({ onTranscript, className = '' }) {
|
export function MicButton({ onTranscript, className = '' }) {
|
||||||
const [state, setState] = useState('idle'); // idle, recording, transcribing, processing
|
const [state, setState] = useState('idle'); // idle, recording, transcribing, processing
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
const [isSupported, setIsSupported] = useState(true);
|
||||||
|
|
||||||
const mediaRecorderRef = useRef(null);
|
const mediaRecorderRef = useRef(null);
|
||||||
const streamRef = useRef(null);
|
const streamRef = useRef(null);
|
||||||
const chunksRef = useRef([]);
|
const chunksRef = useRef([]);
|
||||||
const lastTapRef = useRef(0);
|
const lastTapRef = useRef(0);
|
||||||
|
|
||||||
// Version indicator to verify updates
|
// Check microphone support on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const checkSupport = () => {
|
||||||
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||||
|
setIsSupported(false);
|
||||||
|
setError('Microphone not supported. Please use HTTPS or a modern browser.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional check for secure context
|
||||||
|
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
|
||||||
|
setIsSupported(false);
|
||||||
|
setError('Microphone requires HTTPS. Please use a secure connection.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSupported(true);
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
checkSupport();
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Start recording
|
// Start recording
|
||||||
const startRecording = async () => {
|
const startRecording = async () => {
|
||||||
@@ -20,6 +42,11 @@ export function MicButton({ onTranscript, className = '' }) {
|
|||||||
setError(null);
|
setError(null);
|
||||||
chunksRef.current = [];
|
chunksRef.current = [];
|
||||||
|
|
||||||
|
// Check if getUserMedia is available
|
||||||
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||||
|
throw new Error('Microphone access not available. Please use HTTPS or a supported browser.');
|
||||||
|
}
|
||||||
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
streamRef.current = stream;
|
streamRef.current = stream;
|
||||||
|
|
||||||
@@ -79,7 +106,23 @@ export function MicButton({ onTranscript, className = '' }) {
|
|||||||
console.log('Recording started successfully');
|
console.log('Recording started successfully');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to start recording:', err);
|
console.error('Failed to start recording:', err);
|
||||||
setError('Microphone access denied');
|
|
||||||
|
// Provide specific error messages based on error type
|
||||||
|
let errorMessage = 'Microphone access failed';
|
||||||
|
|
||||||
|
if (err.name === 'NotAllowedError') {
|
||||||
|
errorMessage = 'Microphone access denied. Please allow microphone permissions.';
|
||||||
|
} else if (err.name === 'NotFoundError') {
|
||||||
|
errorMessage = 'No microphone found. Please check your audio devices.';
|
||||||
|
} else if (err.name === 'NotSupportedError') {
|
||||||
|
errorMessage = 'Microphone not supported by this browser.';
|
||||||
|
} else if (err.name === 'NotReadableError') {
|
||||||
|
errorMessage = 'Microphone is being used by another application.';
|
||||||
|
} else if (err.message.includes('HTTPS')) {
|
||||||
|
errorMessage = err.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(errorMessage);
|
||||||
setState('idle');
|
setState('idle');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -109,6 +152,11 @@ export function MicButton({ onTranscript, className = '' }) {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't proceed if microphone is not supported
|
||||||
|
if (!isSupported) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Debounce for mobile double-tap issue
|
// Debounce for mobile double-tap issue
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastTapRef.current < 300) {
|
if (now - lastTapRef.current < 300) {
|
||||||
@@ -138,6 +186,14 @@ export function MicButton({ onTranscript, className = '' }) {
|
|||||||
|
|
||||||
// Button appearance based on state
|
// Button appearance based on state
|
||||||
const getButtonAppearance = () => {
|
const getButtonAppearance = () => {
|
||||||
|
if (!isSupported) {
|
||||||
|
return {
|
||||||
|
icon: <Mic className="w-5 h-5" />,
|
||||||
|
className: 'bg-gray-400 cursor-not-allowed',
|
||||||
|
disabled: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case 'recording':
|
case 'recording':
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -142,8 +142,8 @@ const QuickSettingsPanel = ({
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Whisper Dictation Settings */}
|
{/* Whisper Dictation Settings - HIDDEN */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2" style={{ display: 'none' }}>
|
||||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Whisper Dictation</h4>
|
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Whisper Dictation</h4>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user