Website Beta v2: UI Polish and Improvements (#56)

* feat(web): Complete website overhaul for beta release

Major updates to make the website beta-ready:

## Content & Design
- Completely rewritten homepage with modern, catchy design
- Prominent one-click installation command in hero section
- Clear value proposition: 'Voice Dictation for Linux, Finally Done Right'
- Trust indicators (100% Offline, Privacy First, Open Source, X11/Wayland)
- Interactive demo section encouraging users to try double-tap Ctrl
- Feature cards highlighting key benefits
- Detailed installation section with both full and quick install options
- Voice commands reference section
- Speech engines comparison (Whisper AI vs VOSK)
- Comprehensive FAQ section
- Modern CTA section and footer

## SEO Improvements
- Added comprehensive meta tags with proper descriptions
- Open Graph and Twitter Card meta tags with proper images
- JSON-LD structured data for SoftwareApplication schema
- Added robots.txt for search engine crawling
- Added sitemap.xml for better indexing
- Added site.webmanifest for PWA support
- Proper canonical URL and meta keywords
- Author attribution and GPL-3.0 license info

## Technical
- Removed unused creatr-devtools dependency
- Real GitHub stars fetching from API
- Improved responsive design
- Better accessibility with proper aria labels
- Clean, maintainable component structure

The website now properly reflects the v0.2.0-alpha release features
and provides a clear path for users to install and use Vocalinux.

* feat(web): Add live demo, improve logo and install UX

Changes:
- Use actual app logo SVG image for consistent branding
- Added live browser-based speech recognition demo using Web Speech API
  - Allows users to try voice-to-text directly in their browser
  - Clear indication that the demo uses cloud-based Web Speech API
  - Prominent call-to-action to install Vocalinux for offline experience
- Redesigned one-click install box with better visual hierarchy
  - Left-aligned layout with clear instructions
  - Terminal-style command display with copy button
  - Added compatibility info and install time estimates
- Improved footer and feature section with consistent logo usage

* feat(web): Add double-tap Ctrl to trigger live demo

- Added keyboard detection for double-tap Ctrl (same as the real app)
- Shows visual hint 'Tap Ctrl again to start!' on first Ctrl press
- Instructions updated to highlight Ctrl+Ctrl as primary activation
- Click still works as alternative method
- Double-tap Ctrl also stops recording when active

* fix(web): Remove duplicate DictationOverlay that conflicted with LiveDemo

The old DictationOverlay was still responding to double-tap Ctrl alongside
the new LiveDemo component, causing two overlays to appear simultaneously.
Removed the page-level useDoublePress hook and DictationOverlay since the
LiveDemo now handles double-tap Ctrl detection internally.

* fix(web): Improve Quick Install section layout and readability

- Changed to stacked vertical layout for cleaner appearance
- Command now wraps with break-all instead of horizontal scroll
- Simplified compatibility text to fit better
- Better spacing and padding throughout
- Responsive bottom info section

* fix(web): Make Quick Install box wider for better text fit

Changed max-w-4xl to max-w-5xl and added horizontal padding so the
curl command doesn't wrap as much on medium-sized screens.

* Website beta v2: UI polish and improvements

- Reorder nav links to match page section order (Demo, Features, Install)
- Split install commands into 3 lines for better readability
- Fix button contrast in dark mode (Install Now, Install Vocalinux)
- Improve live demo mic icon and Ctrl text readability
- Add gradient/border styling to version tag in header
- Hide scrollbars on code blocks globally
- Add favicon using vocalinux.svg
- Add copy button to uninstall command
- Left-align Quick Install header text
- Update footer author link to X/Twitter profile
This commit is contained in:
Jatin K Malik
2026-01-19 17:34:24 -08:00
committed by GitHub
parent 2dcf0ef17c
commit 99b6fb6062
14 changed files with 7456 additions and 896 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 MiB

+8
View File
@@ -0,0 +1,8 @@
# Robots.txt for Vocalinux
# https://vocalinux.com
User-agent: *
Allow: /
# Sitemap location
Sitemap: https://vocalinux.com/sitemap.xml
+26
View File
@@ -0,0 +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"
}
]
}
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://vocalinux.com/</loc>
<lastmod>2026-01-19</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
</urlset>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
<!-- Placeholder for microphone off icon -->
<circle cx="64" cy="64" r="60" fill="#e74c3c" />
<rect x="44" y="40" width="40" height="60" rx="20" fill="#7f8c8d" />
<line x1="40" y1="88" x2="88" y2="40" stroke="#e74c3c" stroke-width="8" />
<!-- This is a placeholder. Replace with actual custom design. -->
</svg>

After

Width:  |  Height:  |  Size: 465 B

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
<!-- Placeholder for microphone processing icon -->
<circle cx="64" cy="64" r="60" fill="#f39c12" />
<rect x="44" y="40" width="40" height="60" rx="20" fill="#2c3e50" />
<circle cx="64" cy="40" r="8" fill="#f39c12">
<animate attributeName="cy" values="40;50;40" dur="1s" repeatCount="indefinite" />
</circle>
<circle cx="48" cy="60" r="8" fill="#f39c12">
<animate attributeName="cy" values="60;70;60" dur="1s" repeatCount="indefinite" />
</circle>
<circle cx="80" cy="60" r="8" fill="#f39c12">
<animate attributeName="cy" values="60;70;60" dur="1s" repeatCount="indefinite" />
</circle>
<!-- This is a placeholder. Replace with actual custom design. -->
</svg>

After

Width:  |  Height:  |  Size: 836 B

+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
<!-- Placeholder for microphone on/active icon -->
<circle cx="64" cy="64" r="60" fill="#2ecc71" />
<rect x="44" y="40" width="40" height="60" rx="20" fill="#2c3e50" />
<path d="M 40,78 Q 64,88 88,78" stroke="#2ecc71" stroke-width="4" fill="none" />
<path d="M 40,88 Q 64,98 88,88" stroke="#2ecc71" stroke-width="4" fill="none" />
<!-- This is a placeholder. Replace with actual custom design. -->
</svg>

After

Width:  |  Height:  |  Size: 560 B

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 308 KiB

+109 -42
View File
@@ -3,44 +3,54 @@ import React from "react";
import { GeistSans } from "geist/font/sans";
import { type Metadata } from "next";
import { ThemeProvider } from "@/components/theme-provider";
import { DevtoolsProvider } from 'creatr-devtools';
export const viewport = {
width: "device-width",
initialScale: 1,
maximumScale: 1
maximumScale: 5,
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#ffffff" },
{ media: "(prefers-color-scheme: dark)", color: "#18181b" },
],
};
export const metadata: Metadata = {
metadataBase: new URL("https://vocalinux.com"),
title: {
default: "Vocalinux - Open-Source Voice Dictation for Linux",
default: "Vocalinux - Voice Dictation for Linux, Finally Done Right",
template: "%s | Vocalinux"
},
description: "Open-source offline voice dictation for Linux using Whisper models. Privacy-focused with no cloud dependencies.",
description: "Free, open-source voice dictation for Linux. 100% offline with Whisper AI. Double-tap Ctrl to start. Works everywhere - X11 & Wayland. One-click install.",
applicationName: "Vocalinux",
keywords: ["voice dictation", "linux", "speech recognition", "whisper", "offline", "privacy", "x11", "wayland", "open-source"],
authors: [{
name: "Vocalinux Team"
}],
creator: "Vocalinux Team",
publisher: "Vocalinux Team",
keywords: [
"voice dictation linux",
"speech to text linux",
"linux voice typing",
"whisper linux",
"vosk linux",
"offline voice dictation",
"privacy voice recognition",
"speech recognition linux",
"dictation software ubuntu",
"voice typing fedora",
"wayland voice dictation",
"x11 speech to text",
"open source dictation",
"free speech recognition",
"linux accessibility",
"hands free typing linux",
"voice commands linux",
"GTK voice dictation"
],
authors: [{ name: "Jatin K Malik", url: "https://github.com/jatinkrmalik" }],
creator: "Jatin K Malik",
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"
}],
apple: [{
url: "/apple-touch-icon.png",
sizes: "180x180",
type: "image/png"
}]
icon: [
{ url: "/vocalinux.svg", type: "image/svg+xml" },
],
apple: [{ url: "/vocalinux.svg", type: "image/svg+xml" }],
shortcut: "/vocalinux.svg"
},
manifest: "/site.webmanifest",
appleWebApp: {
@@ -54,34 +64,91 @@ export const metadata: Metadata = {
openGraph: {
type: "website",
locale: "en_US",
url: "https://github.com/jatinkrmalik/vocalinux",
url: "https://vocalinux.com",
siteName: "Vocalinux",
title: "Vocalinux - Open-Source Voice Dictation for Linux",
description: "Open-source offline voice dictation for Linux using Whisper models. Works with X11 and Wayland.",
title: "Vocalinux - Voice Dictation for Linux, Finally Done Right",
description: "Free, open-source voice dictation for Linux. 100% offline & private with Whisper AI and VOSK. Double-tap Ctrl to start dictating in any app.",
images: [{
url: "https://picsum.photos/200",
width: 1280,
height: 640,
alt: "Vocalinux Open-Source Voice Dictation"
url: "/og-image.png",
width: 1200,
height: 630,
alt: "Vocalinux - Voice Dictation for Linux"
}]
},
twitter: {
card: "summary_large_image",
title: "Vocalinux - Open-Source Voice Dictation for Linux",
description: "Open-source offline voice dictation for Linux using Whisper models. Works with X11 and Wayland.",
images: ["https://picsum.photos/200"]
}
title: "Vocalinux - Voice Dictation for Linux, Finally Done Right",
description: "Free, open-source voice dictation for Linux. 100% offline & private. Double-tap Ctrl to start dictating in any app.",
images: ["/og-image.png"],
creator: "@jatinkrmalik"
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
alternates: {
canonical: "https://vocalinux.com",
},
category: "Technology",
};
export default function RootLayout({
children
}: Readonly<{
children: React.ReactNode;
}>) {
return <html lang="en" className={`${GeistSans.variable} scroll-smooth`} suppressHydrationWarning>
const jsonLd = {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Vocalinux",
"applicationCategory": "UtilitiesApplication",
"operatingSystem": "Linux",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"description": "Free, open-source voice dictation for Linux. 100% offline with Whisper AI and VOSK. Works with X11 and Wayland.",
"softwareVersion": "0.2.0-alpha",
"author": {
"@type": "Person",
"name": "Jatin K Malik",
"url": "https://github.com/jatinkrmalik"
},
"url": "https://vocalinux.com",
"downloadUrl": "https://github.com/jatinkrmalik/vocalinux",
"screenshot": "https://vocalinux.com/og-image.png",
"featureList": [
"100% offline voice recognition",
"Privacy-focused - no cloud dependencies",
"Works with all Linux applications",
"X11 and Wayland support",
"Double-tap Ctrl activation",
"Whisper AI and VOSK engines",
"Voice commands for punctuation",
"System tray integration"
]
};
return (
<html lang="en" className={`${GeistSans.variable} scroll-smooth`} suppressHydrationWarning>
<head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
</head>
<body>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
</body>
</html>;
}
</html>
);
}
+962 -846
View File
File diff suppressed because it is too large Load Diff
+23 -4
View File
@@ -1,11 +1,30 @@
"use client";
import React from "react";
export const VocalinuxLogo: React.FC<React.SVGProps<SVGSVGElement>> = props => {
return <svg width="36" height="36" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
import Image from "next/image";
// Logo component using the actual application icon
export const VocalinuxLogo: React.FC<{ className?: string }> = ({ className = "" }) => {
return (
<Image
src="/vocalinux.svg"
alt="Vocalinux Logo"
width={36}
height={36}
className={className}
priority
/>
);
};
// Alternative: Inline SVG version for when color customization is needed
export const VocalinuxLogoInline: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
return (
<svg width="36" height="36" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M243.33 120.92c-58.29 1.8-107.88 51.39-107.88 110.12v100.24c0 20.87 5.16 39.95 14.02 57.07l-44.12 44.12c-3.27 3.27-3.27 8.58 0 11.86 1.64 1.64 3.79 2.46 5.93 2.46 2.14 0 4.29-.82 5.93-2.46l43.39-43.39a107.388 107.388 0 0074.73 30.13h.01c20.94 0 40.83-6.05 57.99-16.52 23.86-14.53 42.22-37.58 49.7-64.48.8-2.89-1.18-5.8-4.17-6.14-2.99-.33-5.57 1.79-5.75 4.8-2.91 48.11-43.05 85.82-91.5 85.82h-.76c-50.83-1.57-93.56-42.09-93.56-93.67V231.04c0-51.11 41.57-93.67 92.68-93.67 46.36 0 86.43 35.37 93.54 80.62.41 2.64 2.69 4.56 5.38 4.5 2.67-.06 4.84-2.1 5.13-4.75 7.65-71.11-46.43-98.62-99.69-96.82z" fill="currentColor" />
<path d="M299.33 256.96c0-2.36-1.91-4.27-4.27-4.27s-4.27 1.91-4.27 4.27v62.09c0 2.36 1.91 4.27 4.27 4.27s4.27-1.91 4.27-4.27v-62.09z" fill="currentColor" />
<path d="M361.42 164.01v-17.06c0-2.36-1.91-4.27-4.27-4.27s-4.27 1.91-4.27 4.27v17.06c0 2.36 1.91 4.27 4.27 4.27s4.27-1.91 4.27-4.27zM336.62 164.01v-17.06c0-2.36-1.91-4.27-4.27-4.27s-4.27 1.91-4.27 4.27v17.06c0 2.36 1.91 4.27 4.27 4.27s4.27-1.91 4.27-4.27zM311.82 164.01v-17.06c0-2.36-1.91-4.27-4.27-4.27s-4.27 1.91-4.27 4.27v17.06c0 2.36 1.91 4.27 4.27 4.27s4.27-1.91 4.27-4.27z" fill="currentColor" />
<path d="M404.61 164.01c0-28.19-11.08-38.24-11.08-38.24-5.54-5.54-12.75-8.31-20-8.31s-14.46 2.77-20 8.31l-7.65 7.65-7.65-7.65c-11.04-11.04-28.96-11.04-40 0l-7.65 7.65-7.65-7.65c-11.04-11.04-28.96-11.04-40 0-11.08 11.08-11.08 28.92 0 40l47.65 47.57v44.62c0 14.15 11.51 25.65 25.65 25.65 14.15 0 25.65-11.51 25.65-25.65v-44.62l47.65-47.57c5.54-5.54 8.31-12.75 8.31-20s-2.77-14.46-8.31-20c-1.67-1.67-1.67-4.38 0-6.04 10.88 4.95 20.73 13.84 20.73 24.27zm-80.82 84.97v19.07c0 9.44-7.68 17.11-17.11 17.11s-17.11-7.67-17.11-17.11v-19.07l17.11 17.11 17.11-17.11zm24.27-48.27l-41.38 41.38L265.3 200.7c-7.37-7.37-7.37-19.32 0-26.69 7.37-7.37 19.32-7.37 26.69 0l14.69 14.69 14.69-14.69c3.56-3.56 8.3-5.52 13.34-5.52s9.78 1.96 13.35 5.52a4.233 4.233 0 006.04 0c3.56-3.56 8.3-5.52 13.34-5.52s9.78 1.96 13.34 5.52c7.37 7.37 7.37 19.32 0 26.69z" fill="currentColor" />
</svg>;
};
</svg>
);
};
+387
View File
@@ -0,0 +1,387 @@
"use client";
import React, { useState, useEffect, useRef, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Mic, MicOff, AlertCircle, Chrome, Globe, Volume2, Sparkles, Keyboard } from "lucide-react";
// Define types for Web Speech API
interface SpeechRecognitionEvent {
results: SpeechRecognitionResultList;
resultIndex: number;
}
interface SpeechRecognitionErrorEvent {
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;
}
declare global {
interface Window {
SpeechRecognition?: new () => SpeechRecognition;
webkitSpeechRecognition?: new () => SpeechRecognition;
}
}
export function LiveDemo() {
const [isSupported, setIsSupported] = useState<boolean | null>(null);
const [isListening, setIsListening] = useState(false);
const [transcript, setTranscript] = useState("");
const [interimTranscript, setInterimTranscript] = useState("");
const [error, setError] = useState<string | null>(null);
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
const [lastCtrlPress, setLastCtrlPress] = useState<number | null>(null);
const [showCtrlHint, setShowCtrlHint] = useState(false);
const recognitionRef = useRef<SpeechRecognition | null>(null);
const isListeningRef = useRef(false);
// 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);
}, []);
// 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";
recognition.onstart = () => {
setIsListening(true);
setError(null);
setHasPermission(true);
};
recognition.onend = () => {
setIsListening(false);
};
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;
}
}
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]);
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("");
}, []);
// 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 (
<div className="bg-zinc-900 rounded-2xl p-6 sm:p-8 border border-zinc-800">
<div className="text-center">
<div className="inline-flex items-center justify-center h-16 w-16 rounded-full bg-yellow-500/10 mb-4">
<AlertCircle className="h-8 w-8 text-yellow-500" />
</div>
<h3 className="text-xl font-semibold text-white mb-2">
Browser Demo Not Available
</h3>
<p className="text-zinc-400 mb-6 max-w-md mx-auto">
Your browser doesn&apos;t support the Web Speech API. Try opening this page in:
</p>
<div className="flex justify-center gap-4 mb-6">
<div className="flex items-center gap-2 text-zinc-300">
<Chrome className="h-5 w-5" />
<span>Chrome</span>
</div>
<div className="flex items-center gap-2 text-zinc-300">
<Globe className="h-5 w-5" />
<span>Edge</span>
</div>
</div>
<p className="text-sm text-zinc-500">
Or better yet — <a href="#install" className="text-primary hover:underline">install Vocalinux</a> for
the full offline experience with Whisper AI!
</p>
</div>
</div>
);
}
// Loading state
if (isSupported === null) {
return (
<div className="bg-zinc-900 rounded-2xl p-8 border border-zinc-800 animate-pulse">
<div className="h-48 flex items-center justify-center">
<div className="text-zinc-500">Checking browser support...</div>
</div>
</div>
);
}
return (
<div className="bg-gradient-to-br from-zinc-900 to-zinc-950 rounded-2xl overflow-hidden border border-zinc-800">
{/* Header */}
<div className="px-4 sm:px-6 py-3 bg-zinc-800/50 border-b border-zinc-800 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex gap-1.5">
<div className="h-3 w-3 rounded-full bg-red-500/70" />
<div className="h-3 w-3 rounded-full bg-yellow-500/70" />
<div className="h-3 w-3 rounded-full bg-green-500/70" />
</div>
<span className="text-sm text-zinc-400 font-medium">
Live Browser Demo
</span>
<span className="text-xs bg-primary/20 text-primary px-2 py-0.5 rounded-full">
Web Speech API
</span>
</div>
{transcript && (
<button
onClick={clearTranscript}
className="text-xs text-zinc-500 hover:text-zinc-300 transition-colors"
>
Clear
</button>
)}
</div>
{/* Main content */}
<div className="p-6 sm:p-8">
{/* Error message */}
<AnimatePresence>
{error && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="mb-6 p-4 bg-red-500/10 border border-red-500/20 rounded-lg flex items-start gap-3"
>
<AlertCircle className="h-5 w-5 text-red-400 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm text-red-400">{error}</p>
{hasPermission === false && (
<p className="text-xs text-red-400/70 mt-1">
Click the lock icon in your browser&apos;s address bar to enable microphone access.
</p>
)}
</div>
</motion.div>
)}
</AnimatePresence>
{/* Microphone button */}
<div className="flex flex-col items-center mb-6">
{/* Double-tap Ctrl hint */}
<AnimatePresence>
{showCtrlHint && !isListening && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="mb-4 px-4 py-2 bg-primary/20 rounded-full text-sm text-primary flex items-center gap-2"
>
<Keyboard className="h-4 w-4" />
Tap Ctrl again to start!
</motion.div>
)}
</AnimatePresence>
<motion.button
onClick={toggleListening}
className={`relative h-20 w-20 sm:h-24 sm:w-24 rounded-full flex items-center justify-center transition-all ${isListening
? "bg-red-500 hover:bg-red-600 shadow-lg shadow-red-500/30"
: "bg-primary hover:bg-primary/90 shadow-lg shadow-primary/30"
}`}
whileTap={{ scale: 0.95 }}
aria-label={isListening ? "Stop listening" : "Start listening"}
>
{/* Pulse animation when listening */}
{isListening && (
<>
<motion.div
className="absolute inset-0 rounded-full bg-red-500"
animate={{ scale: [1, 1.2], opacity: [0.5, 0] }}
transition={{ duration: 1.5, repeat: Infinity }}
/>
<motion.div
className="absolute inset-0 rounded-full bg-red-500"
animate={{ scale: [1, 1.4], opacity: [0.3, 0] }}
transition={{ duration: 1.5, repeat: Infinity, delay: 0.5 }}
/>
</>
)}
{isListening ? (
<MicOff className="h-8 w-8 sm:h-10 sm:w-10 text-zinc-900 relative z-10" />
) : (
<Mic className="h-8 w-8 sm:h-10 sm:w-10 text-zinc-900 relative z-10" />
)}
</motion.button>
<div className="text-center mt-4">
{isListening ? (
<p className="text-sm text-zinc-300">
<span className="flex items-center justify-center gap-2">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-red-500"></span>
</span>
Listening... Double-tap <kbd className="px-2 py-1 bg-zinc-800 border border-zinc-600 rounded text-xs mx-1 text-zinc-200 font-mono">Ctrl</kbd> or click to stop
</span>
</p>
) : (
<div className="space-y-1">
<p className="text-sm text-zinc-200">
Double-tap <kbd className="px-2 py-1 bg-zinc-800 border border-zinc-600 rounded text-xs mx-1 text-zinc-200 font-mono">Ctrl</kbd> to start
</p>
<p className="text-xs text-zinc-400">or click the microphone</p>
</div>
)}
</div>
</div>
{/* Transcript area */}
<div className="bg-zinc-950/50 rounded-xl p-4 sm:p-6 min-h-[120px] border border-zinc-800/50">
{transcript || interimTranscript ? (
<div className="text-zinc-200 leading-relaxed">
<span>{transcript}</span>
{interimTranscript && (
<span className="text-zinc-500 italic">{interimTranscript}</span>
)}
</div>
) : (
<div className="text-zinc-500 text-center py-4">
<Volume2 className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p>Your transcribed speech will appear here</p>
</div>
)}
</div>
{/* Info banner */}
<div className="mt-6 flex items-start gap-3 p-4 bg-primary/5 rounded-lg border border-primary/10">
<Sparkles className="h-5 w-5 text-primary flex-shrink-0 mt-0.5" />
<div className="text-sm">
<p className="text-zinc-300">
<strong>This is just a preview!</strong> The browser demo uses Web Speech API (cloud-based).
</p>
<p className="text-zinc-500 mt-1">
Vocalinux runs <strong>100% offline</strong> using Whisper AI — no cloud, no data sharing,
complete privacy. <a href="#install" className="text-primary hover:underline">Install it</a> to
experience the real deal!
</p>
</div>
</div>
</div>
</div>
);
}
+4 -4
View File
@@ -12,7 +12,7 @@ export function Logo({
textClassName
}: LogoProps) {
return <div className={cn("flex items-center gap-2", className)}>
<VocalinuxLogo className={cn("h-8 w-8 text-primary", iconClassName)} />
<span className={cn("font-bold text-xl", textClassName)}>Vocalinux</span>
</div>;
}
<VocalinuxLogo className={cn("h-8 w-8", iconClassName)} />
<span className={cn("font-bold text-xl", textClassName)}>Vocalinux</span>
</div>;
}
+22
View File
@@ -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;
}