diff --git a/web/public/site.webmanifest b/web/public/site.webmanifest index b435bfc..4c8d27b 100644 --- a/web/public/site.webmanifest +++ b/web/public/site.webmanifest @@ -1,26 +1,26 @@ { - "name": "Vocalinux", - "short_name": "Vocalinux", - "description": "Voice dictation for Linux, finally done right. 100% offline and privacy-focused.", - "start_url": "/", - "display": "standalone", - "background_color": "#ffffff", - "theme_color": "#7c3aed", - "icons": [ - { - "src": "/favicon-16x16.png", - "sizes": "16x16", - "type": "image/png" - }, - { - "src": "/favicon-32x32.png", - "sizes": "32x32", - "type": "image/png" - }, - { - "src": "/apple-touch-icon.png", - "sizes": "180x180", - "type": "image/png" - } - ] + "name": "Vocalinux", + "short_name": "Vocalinux", + "description": "Voice dictation for Linux, finally done right. 100% offline and privacy-focused.", + "start_url": "/", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#7c3aed", + "icons": [ + { + "src": "/favicon-16x16.png", + "sizes": "16x16", + "type": "image/png" + }, + { + "src": "/favicon-32x32.png", + "sizes": "32x32", + "type": "image/png" + }, + { + "src": "/apple-touch-icon.png", + "sizes": "180x180", + "type": "image/png" + } + ] } diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index 1fbf863..cbf694c 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -47,11 +47,10 @@ export const metadata: Metadata = { publisher: "Vocalinux", icons: { icon: [ - { url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" }, - { url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" }, - { url: "/favicon.ico", sizes: "48x48", type: "image/x-icon" } + { url: "/vocalinux.svg", type: "image/svg+xml" }, ], - apple: [{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" }] + apple: [{ url: "/vocalinux.svg", type: "image/svg+xml" }], + shortcut: "/vocalinux.svg" }, manifest: "/site.webmanifest", appleWebApp: { @@ -152,4 +151,4 @@ export default function RootLayout({ ); -} \ No newline at end of file +} diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 6c64f99..fbc9d0e 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -37,10 +37,14 @@ import SyntaxHighlighter from "react-syntax-highlighter"; import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs"; import { useInView } from "react-intersection-observer"; -// The one-liner install command -const oneClickInstallCommand = `curl -fsSL https://raw.githubusercontent.com/jatinkrmalik/vocalinux/main/install.sh | bash`; +// The one-liner install command (split into three lines for display) +const oneClickInstallCommand = `curl \\ + -fsSL https://raw.githubusercontent.com/jatinkrmalik/vocalinux/main/install.sh \\ + | bash`; -const oneClickInstallNoWhisper = `curl -fsSL https://raw.githubusercontent.com/jatinkrmalik/vocalinux/main/install.sh | bash -s -- --no-whisper`; +const oneClickInstallNoWhisper = `curl \\ + -fsSL https://raw.githubusercontent.com/jatinkrmalik/vocalinux/main/install.sh \\ + | bash -s -- --no-whisper`; const FeatureCard = ({ icon, @@ -147,8 +151,8 @@ export default function HomePage() { }, []); const navLinks = [ - { href: "#features", label: "Features" }, { href: "#demo", label: "Demo" }, + { href: "#features", label: "Features" }, { href: "#install", label: "Install" }, { href: "#voice-commands", label: "Commands" }, { href: "#faq", label: "FAQ" }, @@ -169,7 +173,7 @@ export default function HomePage() { priority /> Vocalinux - + v0.2.0 Alpha @@ -293,7 +297,7 @@ export default function HomePage() {
Install Now — It's Free @@ -313,17 +317,17 @@ export default function HomePage() { initial={{ opacity: 0, y: 30 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.6, delay: 0.2 }} - className="w-full max-w-5xl mx-auto px-4" + className="w-full max-w-6xl mx-auto px-4" >
{/* Header */}
-
+
-
-

Quick Install

-

Copy & paste in your terminal

+
+

Quick Install

+

Copy & paste in your terminal

@@ -338,10 +342,9 @@ export default function HomePage() {
-
- $ - {oneClickInstallCommand} -
+
+$ {oneClickInstallCommand}
+                    
@@ -394,7 +397,7 @@ export default function HomePage() { {/* Demo Section - Live Interactive Demo */}
-
+

@@ -574,25 +577,21 @@ export default function HomePage() {
-
+

Recommended: Full Install

Includes Whisper AI for best accuracy (~5-10 min)

+
-
- - {oneClickInstallCommand} - -
- -
-
+ + {oneClickInstallCommand} +

{/* Alternative install */} @@ -601,25 +600,21 @@ export default function HomePage() {
-
+

Quick Install (VOSK only)

Faster install, lighter on resources (~2-3 min)

+
-
- - {oneClickInstallNoWhisper} - -
- -
-
+ + {oneClickInstallNoWhisper} +
{/* What the installer does */} @@ -676,16 +671,20 @@ export default function HomePage() {
-

- - Uninstall -

+
+

+ + Uninstall +

+ +

Clean removal in one command:

- - curl -fsSL https://raw.githubusercontent.com/jatinkrmalik/vocalinux/main/uninstall.sh | bash - +
+{`curl -fsSL \\
+  https://raw.githubusercontent.com/jatinkrmalik/vocalinux/main/uninstall.sh | bash`}
+                
@@ -928,7 +927,7 @@ export default function HomePage() {
Install Vocalinux @@ -1052,7 +1051,7 @@ export default function HomePage() {

Made with by{" "} = ({ className = "" }) => { return ( - Vocalinux Logo> = (pro ); -}; \ No newline at end of file +}; diff --git a/web/src/components/live-demo.tsx b/web/src/components/live-demo.tsx index 4f6c9ae..f9cb9ac 100644 --- a/web/src/components/live-demo.tsx +++ b/web/src/components/live-demo.tsx @@ -6,383 +6,382 @@ import { Mic, MicOff, AlertCircle, Chrome, Globe, Volume2, Sparkles, Keyboard } // Define types for Web Speech API interface SpeechRecognitionEvent { - results: SpeechRecognitionResultList; - resultIndex: number; + results: SpeechRecognitionResultList; + resultIndex: number; } interface SpeechRecognitionErrorEvent { - error: string; - message?: string; + error: string; + message?: string; } interface SpeechRecognition extends EventTarget { - continuous: boolean; - interimResults: boolean; - lang: string; - start(): void; - stop(): void; - abort(): void; - onstart: ((this: SpeechRecognition, ev: Event) => void) | null; - onend: ((this: SpeechRecognition, ev: Event) => void) | null; - onresult: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => void) | null; - onerror: ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => void) | null; + continuous: boolean; + interimResults: boolean; + lang: string; + start(): void; + stop(): void; + abort(): void; + onstart: ((this: SpeechRecognition, ev: Event) => void) | null; + onend: ((this: SpeechRecognition, ev: Event) => void) | null; + onresult: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => void) | null; + onerror: ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => void) | null; } declare global { - interface Window { - SpeechRecognition?: new () => SpeechRecognition; - webkitSpeechRecognition?: new () => SpeechRecognition; - } + interface Window { + SpeechRecognition?: new () => SpeechRecognition; + webkitSpeechRecognition?: new () => SpeechRecognition; + } } export function LiveDemo() { - const [isSupported, setIsSupported] = useState(null); - const [isListening, setIsListening] = useState(false); - const [transcript, setTranscript] = useState(""); - const [interimTranscript, setInterimTranscript] = useState(""); - const [error, setError] = useState(null); - const [hasPermission, setHasPermission] = useState(null); - const [lastCtrlPress, setLastCtrlPress] = useState(null); - const [showCtrlHint, setShowCtrlHint] = useState(false); - const recognitionRef = useRef(null); - const isListeningRef = useRef(false); + const [isSupported, setIsSupported] = useState(null); + const [isListening, setIsListening] = useState(false); + const [transcript, setTranscript] = useState(""); + const [interimTranscript, setInterimTranscript] = useState(""); + const [error, setError] = useState(null); + const [hasPermission, setHasPermission] = useState(null); + const [lastCtrlPress, setLastCtrlPress] = useState(null); + const [showCtrlHint, setShowCtrlHint] = useState(false); + const recognitionRef = useRef(null); + const isListeningRef = useRef(false); - // Keep ref in sync with state for use in event handlers - useEffect(() => { - isListeningRef.current = isListening; - }, [isListening]); + // Keep ref in sync with state for use in event handlers + useEffect(() => { + isListeningRef.current = isListening; + }, [isListening]); - // Check for browser support - useEffect(() => { - const SpeechRecognitionAPI = window.SpeechRecognition || window.webkitSpeechRecognition; - setIsSupported(!!SpeechRecognitionAPI); - }, []); + // Check for browser support + useEffect(() => { + const SpeechRecognitionAPI = window.SpeechRecognition || window.webkitSpeechRecognition; + setIsSupported(!!SpeechRecognitionAPI); + }, []); - // Initialize recognition - const initRecognition = useCallback(() => { - const SpeechRecognitionAPI = window.SpeechRecognition || window.webkitSpeechRecognition; - if (!SpeechRecognitionAPI) return null; + // Initialize recognition + const initRecognition = useCallback(() => { + const SpeechRecognitionAPI = window.SpeechRecognition || window.webkitSpeechRecognition; + if (!SpeechRecognitionAPI) return null; - const recognition = new SpeechRecognitionAPI(); - recognition.continuous = true; - recognition.interimResults = true; - recognition.lang = "en-US"; + const recognition = new SpeechRecognitionAPI(); + recognition.continuous = true; + recognition.interimResults = true; + recognition.lang = "en-US"; - recognition.onstart = () => { - setIsListening(true); - setError(null); - setHasPermission(true); - }; + recognition.onstart = () => { + setIsListening(true); + setError(null); + setHasPermission(true); + }; - recognition.onend = () => { - setIsListening(false); - }; + recognition.onend = () => { + setIsListening(false); + }; - recognition.onresult = (event: SpeechRecognitionEvent) => { - let interim = ""; - let final = ""; + recognition.onresult = (event: SpeechRecognitionEvent) => { + let interim = ""; + let final = ""; - for (let i = event.resultIndex; i < event.results.length; i++) { - const transcript = event.results[i][0].transcript; - if (event.results[i].isFinal) { - final += transcript; - } else { - interim += transcript; + for (let i = event.resultIndex; i < event.results.length; i++) { + const transcript = event.results[i][0].transcript; + if (event.results[i].isFinal) { + final += transcript; + } else { + interim += transcript; + } + } + + if (final) { + setTranscript((prev) => prev + final + " "); + setInterimTranscript(""); + } else { + setInterimTranscript(interim); + } + }; + + recognition.onerror = (event: SpeechRecognitionErrorEvent) => { + if (event.error === "not-allowed") { + setHasPermission(false); + setError("Microphone access denied. Please allow microphone permissions."); + } else if (event.error === "no-speech") { + // Ignore no-speech errors + } else { + setError(`Error: ${event.error}`); + } + setIsListening(false); + }; + + return recognition; + }, []); + + const startListening = useCallback(() => { + if (recognitionRef.current) { + recognitionRef.current.abort(); } - } + recognitionRef.current = initRecognition(); + if (recognitionRef.current) { + setTranscript(""); + setInterimTranscript(""); + try { + recognitionRef.current.start(); + } catch (e) { + setError("Could not start recognition. Please try again."); + } + } + }, [initRecognition]); - if (final) { - setTranscript((prev) => prev + final + " "); + const stopListening = useCallback(() => { + if (recognitionRef.current) { + recognitionRef.current.stop(); + } + }, []); + + // Toggle listening state + const toggleListening = useCallback(() => { + if (isListeningRef.current) { + stopListening(); + } else { + startListening(); + } + }, [startListening, stopListening]); + + const clearTranscript = useCallback(() => { + setTranscript(""); setInterimTranscript(""); - } else { - setInterimTranscript(interim); - } - }; + }, []); - recognition.onerror = (event: SpeechRecognitionErrorEvent) => { - if (event.error === "not-allowed") { - setHasPermission(false); - setError("Microphone access denied. Please allow microphone permissions."); - } else if (event.error === "no-speech") { - // Ignore no-speech errors - } else { - setError(`Error: ${event.error}`); - } - setIsListening(false); - }; + // Double-tap Ctrl detection + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key !== "Control") return; - return recognition; - }, []); + const now = Date.now(); + const DOUBLE_TAP_DELAY = 400; - const startListening = useCallback(() => { - if (recognitionRef.current) { - recognitionRef.current.abort(); - } - recognitionRef.current = initRecognition(); - if (recognitionRef.current) { - setTranscript(""); - setInterimTranscript(""); - try { - recognitionRef.current.start(); - } catch (e) { - setError("Could not start recognition. Please try again."); - } - } - }, [initRecognition]); + // Show hint on first Ctrl press + setShowCtrlHint(true); + setTimeout(() => setShowCtrlHint(false), 600); - const stopListening = useCallback(() => { - if (recognitionRef.current) { - recognitionRef.current.stop(); - } - }, []); + if (lastCtrlPress && now - lastCtrlPress <= DOUBLE_TAP_DELAY) { + // Double-tap detected! + toggleListening(); + setLastCtrlPress(null); + setShowCtrlHint(false); + } else { + setLastCtrlPress(now); + } + }; - // Toggle listening state - const toggleListening = useCallback(() => { - if (isListeningRef.current) { - stopListening(); - } else { - startListening(); - } - }, [startListening, stopListening]); + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [lastCtrlPress, toggleListening]); - const clearTranscript = useCallback(() => { - setTranscript(""); - setInterimTranscript(""); - }, []); + // Cleanup on unmount + useEffect(() => { + return () => { + if (recognitionRef.current) { + recognitionRef.current.abort(); + } + }; + }, []); - // Double-tap Ctrl detection - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key !== "Control") return; - - const now = Date.now(); - const DOUBLE_TAP_DELAY = 400; - - // Show hint on first Ctrl press - setShowCtrlHint(true); - setTimeout(() => setShowCtrlHint(false), 600); - - if (lastCtrlPress && now - lastCtrlPress <= DOUBLE_TAP_DELAY) { - // Double-tap detected! - toggleListening(); - setLastCtrlPress(null); - setShowCtrlHint(false); - } else { - setLastCtrlPress(now); - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [lastCtrlPress, toggleListening]); - - // Cleanup on unmount - useEffect(() => { - return () => { - if (recognitionRef.current) { - recognitionRef.current.abort(); - } - }; - }, []); - - // Not supported fallback - if (isSupported === false) { - return ( -

-
-
- -
-

- Browser Demo Not Available -

-

- Your browser doesn't support the Web Speech API. Try opening this page in: -

-
-
- - Chrome + // Not supported fallback + if (isSupported === false) { + return ( + -
- - Edge + ); + } + + // Loading state + if (isSupported === null) { + return ( +
+
+
Checking browser support...
+
-
-

- Or better yet — install Vocalinux for - the full offline experience with Whisper AI! -

-
-
- ); - } + ); + } - // Loading state - if (isSupported === null) { return ( -
-
-
Checking browser support...
-
-
- ); - } - - return ( -
- {/* Header */} -
-
-
-
-
-
-
- - Live Browser Demo - - - Web Speech API - -
- {transcript && ( - - )} -
- - {/* Main content */} -
- {/* Error message */} - - {error && ( - - -
-

{error}

- {hasPermission === false && ( -

- Click the lock icon in your browser's address bar to enable microphone access. -

+
+ {/* Header */} +
+
+
+
+
+
+
+ + Live Browser Demo + + + Web Speech API + +
+ {transcript && ( + )} -
- - )} - - - {/* Microphone button */} -
- {/* Double-tap Ctrl hint */} - - {showCtrlHint && !isListening && ( - - - Tap Ctrl again to start! - - )} - - - - {/* Pulse animation when listening */} - {isListening && ( - <> - - - - )} - {isListening ? ( - - ) : ( - - )} - - -
- {isListening ? ( -

- - - - - - Listening... Double-tap Ctrl or click to stop - -

- ) : ( -
-

- Double-tap Ctrl to start -

-

or click the microphone

-
- )} -
-
- - {/* Transcript area */} -
- {transcript || interimTranscript ? ( -
- {transcript} - {interimTranscript && ( - {interimTranscript} - )}
- ) : ( -
- -

Your transcribed speech will appear here

-
- )} -
- {/* Info banner */} -
- -
-

- This is just a preview! The browser demo uses Web Speech API (cloud-based). -

-

- Vocalinux runs 100% offline using Whisper AI — no cloud, no data sharing, - complete privacy. Install it to - experience the real deal! -

-
+ {/* Main content */} +
+ {/* Error message */} + + {error && ( + + +
+

{error}

+ {hasPermission === false && ( +

+ Click the lock icon in your browser's address bar to enable microphone access. +

+ )} +
+
+ )} +
+ + {/* Microphone button */} +
+ {/* Double-tap Ctrl hint */} + + {showCtrlHint && !isListening && ( + + + Tap Ctrl again to start! + + )} + + + + {/* Pulse animation when listening */} + {isListening && ( + <> + + + + )} + {isListening ? ( + + ) : ( + + )} + + +
+ {isListening ? ( +

+ + + + + + Listening... Double-tap Ctrl or click to stop + +

+ ) : ( +
+

+ Double-tap Ctrl to start +

+

or click the microphone

+
+ )} +
+
+ + {/* Transcript area */} +
+ {transcript || interimTranscript ? ( +
+ {transcript} + {interimTranscript && ( + {interimTranscript} + )} +
+ ) : ( +
+ +

Your transcribed speech will appear here

+
+ )} +
+ + {/* Info banner */} +
+ +
+

+ This is just a preview! The browser demo uses Web Speech API (cloud-based). +

+

+ Vocalinux runs 100% offline using Whisper AI — no cloud, no data sharing, + complete privacy. Install it to + experience the real deal! +

+
+
+
-
-
- ); + ); } diff --git a/web/src/components/logo.tsx b/web/src/components/logo.tsx index 5d2210b..487d3e4 100644 --- a/web/src/components/logo.tsx +++ b/web/src/components/logo.tsx @@ -12,7 +12,7 @@ export function Logo({ textClassName }: LogoProps) { return
- - Vocalinux -
; -} \ No newline at end of file + + Vocalinux +
; +} diff --git a/web/src/styles/globals.css b/web/src/styles/globals.css index d47d129..a761093 100644 --- a/web/src/styles/globals.css +++ b/web/src/styles/globals.css @@ -87,6 +87,28 @@ } } +/* Hide scrollbars but keep scroll functionality */ +@layer utilities { + .scrollbar-hide { + -ms-overflow-style: none; + scrollbar-width: none; + } + .scrollbar-hide::-webkit-scrollbar { + display: none; + } +} + +/* Hide scrollbars on syntax highlighter code blocks */ +pre[class*="language-"], +code { + -ms-overflow-style: none; + scrollbar-width: none; +} +pre[class*="language-"]::-webkit-scrollbar, +code::-webkit-scrollbar { + display: none; +} + nextjs-portal { display: none; }