Skip to content

Commit

Permalink
Created a voice recording app with perplexity-like UI
Browse files Browse the repository at this point in the history
  • Loading branch information
ansh committed Aug 30, 2024
1 parent 0f40527 commit 9d50d71
Show file tree
Hide file tree
Showing 8 changed files with 302 additions and 35 deletions.
26 changes: 25 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"ai": "^3.3.20",
"date-fns": "^3.6.0",
"firebase": "^10.13.0",
"framer-motion": "^11.3.31",
"lucide-react": "^0.436.0",
"next": "14.2.7",
"react": "^18",
Expand Down
56 changes: 56 additions & 0 deletions src/app/all-notes/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use client";

import { useState, useEffect } from 'react';
import { getDocuments } from '../../lib/firebaseUtils';
import Link from 'next/link';
import { ArrowLeft } from 'lucide-react';

interface Note {
id: string;
text: string;
timestamp: string;
}

export default function AllNotes() {
const [notes, setNotes] = useState<Note[]>([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
async function fetchNotes() {
try {
const fetchedNotes = await getDocuments('notes');
setNotes(fetchedNotes as Note[]);
setLoading(false);
} catch (error) {
console.error('Error fetching notes:', error);
setLoading(false);
}
}

fetchNotes();
}, []);

return (
<div className="min-h-screen bg-[#1c1c1e] text-white font-sans p-4">
<Link href="/" className="flex items-center text-orange-500 mb-6">
<ArrowLeft className="mr-2" />
Back to Recording
</Link>
<h1 className="text-3xl font-bold mb-6">All Notes</h1>
{loading ? (
<p>Loading notes...</p>
) : (
<div className="space-y-4">
{notes.map((note) => (
<div key={note.id} className="bg-gray-800 p-4 rounded-lg">
<p className="text-sm text-gray-400 mb-2">
{new Date(note.timestamp).toLocaleString()}
</p>
<p>{note.text}</p>
</div>
))}
</div>
)}
</div>
);
}
5 changes: 4 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { AuthProvider } from "../contexts/AuthContext";
import { DeepgramContextProvider } from "../contexts/DeepgramContext";
import "./globals.css";

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<AuthProvider>{children}</AuthProvider>
<AuthProvider>
<DeepgramContextProvider>{children}</DeepgramContextProvider>
</AuthProvider>
</body>
</html>
);
Expand Down
158 changes: 126 additions & 32 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,135 @@
import Link from "next/link";
"use client";

import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { MicIcon, StopCircleIcon, Loader2 } from 'lucide-react';
import { useDeepgram } from '../contexts/DeepgramContext';
import { addDocument } from '../lib/firebaseUtils';
import { useRouter } from 'next/navigation';

export default function Home() {
const [showText, setShowText] = useState(false);
const [lines, setLines] = useState<number[]>(Array(30).fill(0));
const [currentTime, setCurrentTime] = useState<string>('');
const [isLoading, setIsLoading] = useState(false);
const { connectToDeepgram, disconnectFromDeepgram, connectionState, realtimeTranscript } = useDeepgram();
const router = useRouter();

const isRecording = connectionState === 1;

useEffect(() => {
const timer = setInterval(() => {
setCurrentTime(new Date().toLocaleTimeString());
}, 1000);

return () => clearInterval(timer);
}, []);

useEffect(() => {
if (isRecording) {
const interval = setInterval(() => {
setLines(prev => prev.map(() => Math.random()));
}, 500);
return () => clearInterval(interval);
} else {
setLines(Array(30).fill(0));
}
}, [isRecording]);

const handleRecordToggle = async () => {
if (isRecording) {
disconnectFromDeepgram();
setShowText(true);
setIsLoading(true);

if (realtimeTranscript) {
try {
await addDocument('notes', {
text: realtimeTranscript,
timestamp: new Date().toISOString()
});
console.log('Note saved successfully');

// Wait for a short delay to show the loading indicator
await new Promise(resolve => setTimeout(resolve, 1500));

// Navigate to the All Notes page
router.push('/all-notes');
} catch (error) {
console.error('Error saving note:', error);
setIsLoading(false);
}
}
} else {
connectToDeepgram();
setShowText(false);
}
};

return (
<main className="flex min-h-screen flex-col items-center justify-between p-8">
<div>
<h2 className="text-2xl font-semibold text-center border p-4 font-mono rounded-md">
Get started by choosing a template path from the /paths/ folder.
</h2>
</div>
<div>
<h1 className="text-6xl font-bold text-center">Make anything you imagine 🪄</h1>
<h2 className="text-2xl text-center font-light text-gray-500 pt-4">
This whole page will be replaced when you run your template path.
</h2>
</div>
<div className="w-full grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="border rounded-lg p-6 hover:bg-gray-100 transition-colors">
<h3 className="text-xl font-semibold">AI Chat App</h3>
<p className="mt-2 text-sm text-gray-600">
An intelligent conversational app powered by AI models, featuring real-time responses
and seamless integration with Next.js and various AI providers.
</p>
<div className="flex flex-col h-screen bg-[#1c1c1e] text-white font-sans">
{/* Status Bar */}
<div className="flex justify-between items-center px-4 py-2 text-sm">
<span>{currentTime}</span>
<div className="flex space-x-1">
<div className="w-4 h-4 rounded-full bg-white"></div>
<div className="w-4 h-4 rounded-full bg-white"></div>
<div className="w-4 h-4 rounded-full bg-white"></div>
</div>
<div className="border rounded-lg p-6 hover:bg-gray-100 transition-colors">
<h3 className="text-xl font-semibold">AI Image Generation App</h3>
<p className="mt-2 text-sm text-gray-600">
Create images from text prompts using AI, powered by the Replicate API and Next.js.
</p>
</div>

{/* Main Content */}
<div className="flex-1 flex flex-col justify-between p-4">
<AnimatePresence>
{(showText || isRecording || isLoading) && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="flex-1 flex items-center justify-center p-4"
>
{isLoading ? (
<div className="flex flex-col items-center">
<Loader2 className="w-10 h-10 animate-spin mb-4" />
<p className="text-lg">Saving your note...</p>
</div>
) : (
<p className="text-lg text-center">
{realtimeTranscript || "Start speaking to see real-time transcription."}
</p>
)}
</motion.div>
)}
</AnimatePresence>

{/* Animated Lines */}
<div className="h-40 flex justify-center items-end space-x-0.5 mb-20">
{lines.map((height, index) => (
<motion.div
key={index}
className="w-1 bg-white rounded-full"
initial={{ height: 0 }}
animate={{ height: `${height * 100}%` }}
transition={{ duration: 0.5, ease: "easeInOut" }}
/>
))}
</div>
<div className="border rounded-lg p-6 hover:bg-gray-100 transition-colors">
<h3 className="text-xl font-semibold">Social Media App</h3>
<p className="mt-2 text-sm text-gray-600">
A feature-rich social platform with user profiles, posts, and interactions using
Firebase and Next.js.
</p>

{/* Control Buttons */}
<div className="flex justify-center space-x-4 mb-8">
<button
onClick={handleRecordToggle}
className="w-20 h-20 rounded-2xl bg-orange-500 hover:bg-orange-600 focus:outline-none flex items-center justify-center"
disabled={isLoading}
>
{isRecording ? (
<StopCircleIcon size={32} />
) : (
<MicIcon size={32} />
)}
</button>
</div>
</div>
</main>
</div>
);
}
37 changes: 37 additions & 0 deletions src/components/NotesList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client';

import { useState, useEffect } from 'react';
import { getDocuments } from '../lib/firebaseUtils';

interface Note {
id: string;
text: string;
timestamp: string;
}

export default function NotesList() {
const [notes, setNotes] = useState<Note[]>([]);

useEffect(() => {
const fetchNotes = async () => {
const notesData = await getDocuments('notes');
setNotes(notesData.docs.map(doc => ({ id: doc.id, ...doc.data() } as Note)));
};

fetchNotes();
}, []);

return (
<div className="w-full max-w-md mt-8">
<h2 className="text-2xl font-bold mb-4">Your Notes</h2>
<ul className="space-y-4">
{notes.map((note) => (
<li key={note.id} className="bg-white shadow rounded-lg p-4">
<p className="text-sm text-gray-600">{new Date(note.timestamp).toLocaleString()}</p>
<p className="mt-2">{note.text}</p>
</li>
))}
</ul>
</div>
);
}
46 changes: 46 additions & 0 deletions src/components/VoiceRecorder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use client';

import { useState } from 'react';
import { useDeepgram } from '../contexts/DeepgramContext';
import { addDocument } from '../lib/firebaseUtils';

export default function VoiceRecorder() {
const [isRecording, setIsRecording] = useState(false);
const { connectToDeepgram, disconnectFromDeepgram, connectionState, realtimeTranscript } = useDeepgram();

const handleStartRecording = async () => {
await connectToDeepgram();
setIsRecording(true);
};

const handleStopRecording = async () => {
disconnectFromDeepgram();
setIsRecording(false);

// Save the note to Firebase
if (realtimeTranscript) {
await addDocument('notes', {
text: realtimeTranscript,
timestamp: new Date().toISOString(),
});
}
};

return (
<div className="w-full max-w-md">
<button
onClick={isRecording ? handleStopRecording : handleStartRecording}
className={`w-full py-2 px-4 rounded-full ${
isRecording ? 'bg-red-500 hover:bg-red-600' : 'bg-blue-500 hover:bg-blue-600'
} text-white font-bold`}
>
{isRecording ? 'Stop Recording' : 'Start Recording'}
</button>
{isRecording && (
<div className="mt-4 p-4 bg-gray-100 rounded-lg">
<p className="text-sm text-gray-600">{realtimeTranscript}</p>
</div>
)}
</div>
);
}
8 changes: 7 additions & 1 deletion src/lib/firebaseUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ export const logoutUser = () => signOut(auth);
export const addDocument = (collectionName: string, data: any) =>
addDoc(collection(db, collectionName), data);

export const getDocuments = (collectionName: string) => getDocs(collection(db, collectionName));
export const getDocuments = async (collectionName: string) => {
const querySnapshot = await getDocs(collection(db, collectionName));
return querySnapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
};

export const updateDocument = (collectionName: string, id: string, data: any) =>
updateDoc(doc(db, collectionName, id), data);
Expand Down

0 comments on commit 9d50d71

Please sign in to comment.