Skip to content

Commit

Permalink
[Big rewrite] Transcription-first UX
Browse files Browse the repository at this point in the history
  • Loading branch information
addyosmani committed Oct 28, 2024
1 parent 568d4cd commit 4842605
Show file tree
Hide file tree
Showing 4 changed files with 330 additions and 783 deletions.
155 changes: 65 additions & 90 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import NoteList from './components/NoteList';
import NoteEditor from './components/NoteEditor';
import { useTranscriber } from "./hooks/useTranscriber";
import { AudioManager } from './components/AudioManager';

interface NoteVersion {
content: string;
Expand All @@ -23,14 +24,15 @@ function App() {
const [selectedNoteId, setSelectedNoteId] = useState<string | null>(null);
const [isLoaded, setIsLoaded] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [showNoteList, setShowNoteList] = useState(false);
const isCreatingNoteRef = useRef(false);

useEffect(() => {
const loadNotes = () => {
const storedNotes = localStorage.getItem('notes');
if (storedNotes) {
try {
const parsedNotes = JSON.parse(storedNotes);
// Migrate old notes to new format if needed
const migratedNotes = parsedNotes.map((note: any) => ({
...note,
tags: note.tags || [],
Expand All @@ -56,54 +58,28 @@ function App() {
saveNotes(newNotes);
}, [saveNotes]);

const handleCreateNote = useCallback(() => {
const handleCreateNote = useCallback((content: string = '') => {
if (isCreatingNoteRef.current) return null;

isCreatingNoteRef.current = true;
const newNote: Note = {
id: Date.now().toString(),
title: 'New Note',
content: '',
content,
tags: [],
versions: []
};
updateNotes([...notes, newNote]);
setSelectedNoteId(newNote.id);
setShowNoteList(true);
isCreatingNoteRef.current = false;
return newNote.id;
}, [notes, updateNotes]);

const startDictating = useCallback(async () => {
try {
// Request microphone permission
await navigator.mediaDevices.getUserMedia({ audio: true });

// Create a new note
const newNote: Note = {
id: Date.now().toString(),
title: 'New Dictation',
content: '',
tags: [],
versions: []
};
updateNotes([...notes, newNote]);
setSelectedNoteId(newNote.id);

// Wait a bit for the note editor to mount
setTimeout(() => {
// Find and click the Record tile button
const recordTile = document.querySelector('button:has(.h-7 svg path[d*="M12 18.75"])') as HTMLButtonElement;
if (recordTile) {
recordTile.click();
// Wait for the modal to appear and click Start Recording
setTimeout(() => {
const startRecordingButton = document.querySelector('button.bg-blue-500.hover\\:bg-blue-600') as HTMLButtonElement;
if (startRecordingButton) {
startRecordingButton.click();
}
}, 100);
}
}, 100);
} catch (error) {
console.error('Microphone permission denied:', error);
alert('Microphone access is required for dictation. Please grant permission and try again.');
}
}, [notes, updateNotes]);
const handleTranscriptionComplete = useCallback((text: string) => {
console.log('Transcription complete:', text);
handleCreateNote(text);
}, [handleCreateNote]);

const handleDeleteNote = useCallback((id: string) => {
const updatedNotes = notes.filter(note => note.id !== id);
Expand Down Expand Up @@ -172,10 +148,18 @@ function App() {
}

return (
<div className='flex flex-col min-h-screen'>
<header className='bg-slate-800 text-white p-4'>
<div className='flex flex-col min-h-screen bg-slate-50'>
<header className='bg-slate-800 text-white p-4 shadow-lg'>
<div className="flex justify-between items-center">
<h1 className='text-3xl font-bold'>SpeakEZ</h1>
<div className="flex items-center gap-4">
<h1 className='text-3xl font-bold'>SpeakEZ</h1>
<button
onClick={() => setShowNoteList(!showNoteList)}
className="px-3 py-1 text-sm bg-slate-700 hover:bg-slate-600 rounded-md transition-colors"
>
{showNoteList ? 'Hide Notes' : 'Show Notes'}
</button>
</div>
<div className="flex gap-2">
<button
onClick={() => {
Expand Down Expand Up @@ -211,7 +195,6 @@ function App() {
'title' in note &&
'content' in note
)) {
// Migrate imported notes if needed
const migratedNotes = importedNotes.map(note => ({
...note,
tags: note.tags || [],
Expand All @@ -236,55 +219,47 @@ function App() {
</div>
</header>
<main className='flex-grow flex'>
<aside className='w-1/4 bg-slate-100 p-4'>
<NoteList
notes={filteredNotes}
selectedNoteId={selectedNoteId}
onSelectNote={setSelectedNoteId}
onDeleteNote={handleDeleteNote}
onCreateNote={handleCreateNote}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
/>
</aside>
<section className='w-3/4 p-4'>
{selectedNoteId ? (
<NoteEditor
note={notes.find(note => note.id === selectedNoteId)!}
onUpdateNote={handleUpdateNote}
onSaveVersion={handleSaveVersion}
onRestoreVersion={handleRestoreVersion}
onUpdateTags={handleUpdateTags}
transcriber={transcriber}
hasMicrophonePermission={true}
{showNoteList && (
<aside className='w-72 bg-white border-r border-slate-200 p-4 overflow-y-auto'>
<NoteList
notes={filteredNotes}
selectedNoteId={selectedNoteId}
onSelectNote={setSelectedNoteId}
onDeleteNote={handleDeleteNote}
onCreateNote={() => handleCreateNote()}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
/>
) : (
<div className="text-center space-y-8">
<div className="text-gray-500 mb-8">
Select a note or create a new one to get started.
</aside>
)}
<section className={`flex-grow p-4 ${showNoteList ? 'w-[calc(100%-18rem)]' : 'w-full'}`}>
<div className="max-w-4xl mx-auto">
<div className="bg-white rounded-xl shadow-lg p-6 mb-6">
<h2 className="text-2xl font-semibold mb-4">Quick Record</h2>
<AudioManager
transcriber={transcriber}
onTranscriptionComplete={handleTranscriptionComplete}
/>
</div>

{selectedNoteId ? (
<div className="bg-white rounded-xl shadow-lg p-6">
<NoteEditor
note={notes.find(note => note.id === selectedNoteId)!}
onUpdateNote={handleUpdateNote}
onSaveVersion={handleSaveVersion}
onRestoreVersion={handleRestoreVersion}
onUpdateTags={handleUpdateTags}
transcriber={transcriber}
hasMicrophonePermission={true}
/>
</div>
<div className="flex flex-col items-center gap-4">
<button
onClick={handleCreateNote}
className="flex items-center justify-center gap-2 px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors w-64"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Create New Note
</button>
<button
onClick={startDictating}
className="flex items-center justify-center gap-2 px-6 py-3 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors w-64"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" />
</svg>
Start Dictating
</button>
) : (
<div className="text-center py-8 text-slate-500">
Select a note or start recording to begin
</div>
</div>
)}
)}
</div>
</section>
</main>
</div>
Expand Down
Loading

0 comments on commit 4842605

Please sign in to comment.