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
This commit is contained in:
+71
-56
@@ -2,8 +2,10 @@
|
||||
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import Image from "next/image";
|
||||
import { useDoublePress } from "@/hooks/use-double-press";
|
||||
import { DictationOverlay } from "@/components/dictation-overlay";
|
||||
import { LiveDemo } from "@/components/live-demo";
|
||||
import {
|
||||
Mic,
|
||||
Terminal,
|
||||
@@ -30,9 +32,9 @@ import {
|
||||
Volume2,
|
||||
Sparkles,
|
||||
Heart,
|
||||
ExternalLink,
|
||||
} from "lucide-react";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { VocalinuxLogo } from "@/components/icons";
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
@@ -170,7 +172,14 @@ export default function HomePage() {
|
||||
<header className="fixed top-0 left-0 right-0 z-50 bg-background/80 backdrop-blur-md border-b border-border">
|
||||
<nav className="max-w-7xl mx-auto px-4 sm:px-6 py-3 flex items-center justify-between">
|
||||
<a href="/" className="flex items-center gap-2 group">
|
||||
<VocalinuxLogo className="h-7 w-7 sm:h-8 sm:w-8 text-primary transition-transform group-hover:scale-110" />
|
||||
<Image
|
||||
src="/vocalinux.svg"
|
||||
alt="Vocalinux Logo"
|
||||
width={32}
|
||||
height={32}
|
||||
className="h-7 w-7 sm:h-8 sm:w-8 transition-transform group-hover:scale-110"
|
||||
priority
|
||||
/>
|
||||
<span className="font-bold text-lg sm:text-xl">Vocalinux</span>
|
||||
<span className="hidden sm:inline-block text-xs bg-primary/10 text-primary px-2 py-0.5 rounded-full font-medium">
|
||||
v0.2.0 Alpha
|
||||
@@ -311,29 +320,67 @@ export default function HomePage() {
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* One-Click Install Box */}
|
||||
{/* One-Click Install Box - Redesigned */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
className="max-w-3xl mx-auto"
|
||||
className="max-w-4xl mx-auto"
|
||||
>
|
||||
<div className="bg-zinc-900 rounded-2xl p-6 shadow-2xl border border-zinc-800">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="h-5 w-5 text-green-400" />
|
||||
<span className="text-sm font-medium text-zinc-300">
|
||||
One-Click Install
|
||||
<div className="bg-gradient-to-br from-zinc-900 to-zinc-950 rounded-2xl p-6 sm:p-8 shadow-2xl border border-zinc-800/50 backdrop-blur">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center gap-6">
|
||||
{/* Left side - Instructions */}
|
||||
<div className="flex-shrink-0 lg:w-auto">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="flex items-center justify-center h-10 w-10 rounded-full bg-green-500/20">
|
||||
<Terminal className="h-5 w-5 text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white">Quick Install</h3>
|
||||
<p className="text-xs text-zinc-400">Copy & paste in terminal</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right side - Command */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="relative group">
|
||||
<div className="bg-zinc-950/80 rounded-xl border border-zinc-800 overflow-hidden">
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-zinc-800 bg-zinc-900/50">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-red-500/70" />
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-yellow-500/70" />
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-green-500/70" />
|
||||
</div>
|
||||
<CopyButton text={oneClickInstallCommand} />
|
||||
</div>
|
||||
<div className="p-4 overflow-x-auto">
|
||||
<code className="font-mono text-sm sm:text-base text-green-400 whitespace-nowrap flex items-center gap-2">
|
||||
<span className="text-zinc-500 select-none">$</span>
|
||||
<span>{oneClickInstallCommand}</span>
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom info */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 mt-6 pt-6 border-t border-zinc-800/50">
|
||||
<p className="text-sm text-zinc-400">
|
||||
<span className="text-zinc-500">Compatible:</span> Ubuntu 22.04+, Fedora, Debian, Arch, and most Linux distros
|
||||
</p>
|
||||
<div className="flex items-center gap-4 text-xs text-zinc-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<CheckCircle2 className="h-3.5 w-3.5 text-green-500" />
|
||||
No sudo required
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Zap className="h-3.5 w-3.5 text-yellow-500" />
|
||||
~5-10 min install
|
||||
</span>
|
||||
</div>
|
||||
<CopyButton text={oneClickInstallCommand} />
|
||||
</div>
|
||||
<div className="bg-zinc-950 rounded-lg p-4 font-mono text-sm sm:text-base text-green-400 overflow-x-auto">
|
||||
<span className="text-zinc-500">$</span> {oneClickInstallCommand}
|
||||
</div>
|
||||
<p className="text-xs text-zinc-500 mt-3 text-center">
|
||||
Works on Ubuntu 22.04+, Fedora, Debian, and most Linux distros. Takes ~5-10 min.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -365,54 +412,22 @@ export default function HomePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Demo Video Section */}
|
||||
{/* Demo Section - Live Interactive Demo */}
|
||||
<section id="demo" className="py-16 sm:py-24 px-4 sm:px-6 bg-zinc-50 dark:bg-zinc-900/50">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<FadeInSection>
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold mb-4">
|
||||
See It In Action
|
||||
Try It Yourself
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
Double-tap Ctrl, speak naturally, and watch your words appear instantly. It's that simple.
|
||||
Experience voice-to-text right here in your browser. Click the microphone and start speaking!
|
||||
</p>
|
||||
</div>
|
||||
</FadeInSection>
|
||||
|
||||
<FadeInSection delay={0.1}>
|
||||
<div className="relative aspect-video rounded-2xl overflow-hidden shadow-2xl bg-zinc-900">
|
||||
{/* Placeholder for demo video - using animated mockup */}
|
||||
<div className="absolute inset-0 flex flex-col">
|
||||
{/* Terminal-like header */}
|
||||
<div className="bg-zinc-800 px-4 py-3 flex items-center gap-2">
|
||||
<div className="flex gap-2">
|
||||
<div className="h-3 w-3 rounded-full bg-red-500" />
|
||||
<div className="h-3 w-3 rounded-full bg-yellow-500" />
|
||||
<div className="h-3 w-3 rounded-full bg-green-500" />
|
||||
</div>
|
||||
<span className="text-sm text-zinc-400 ml-4">Vocalinux Demo</span>
|
||||
</div>
|
||||
|
||||
{/* Demo content */}
|
||||
<div className="flex-1 flex items-center justify-center p-8">
|
||||
<div className="text-center">
|
||||
<motion.div
|
||||
animate={{ scale: [1, 1.1, 1] }}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
className="inline-flex items-center justify-center h-24 w-24 rounded-full bg-primary/20 mb-6"
|
||||
>
|
||||
<Mic className="h-12 w-12 text-primary" />
|
||||
</motion.div>
|
||||
<p className="text-zinc-300 text-lg mb-4">
|
||||
Press <kbd className="px-2 py-1 bg-zinc-700 rounded text-sm mx-1">Ctrl</kbd> twice to start dictating
|
||||
</p>
|
||||
<p className="text-zinc-500 text-sm">
|
||||
Try it right now! This page has the demo built-in.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<LiveDemo />
|
||||
</FadeInSection>
|
||||
|
||||
{/* Key demo points */}
|
||||
@@ -422,7 +437,7 @@ export default function HomePage() {
|
||||
{
|
||||
icon: <Keyboard className="h-6 w-6 text-primary" />,
|
||||
title: "Double-tap Ctrl",
|
||||
description: "Simple, universal activation that works in any app",
|
||||
description: "In the real app, just double-tap Ctrl to start dictating anywhere",
|
||||
},
|
||||
{
|
||||
icon: <Zap className="h-6 w-6 text-primary" />,
|
||||
@@ -529,7 +544,7 @@ export default function HomePage() {
|
||||
<div className="relative">
|
||||
<div className="bg-white dark:bg-zinc-800 rounded-2xl p-8 shadow-xl">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<VocalinuxLogo className="h-16 w-16 text-primary" />
|
||||
<Image src="/vocalinux.svg" alt="Vocalinux" width={64} height={64} className="h-16 w-16" />
|
||||
<div>
|
||||
<div className="text-2xl font-bold">Vocalinux</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
@@ -958,7 +973,7 @@ export default function HomePage() {
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-8 mb-12">
|
||||
<div className="md:col-span-2">
|
||||
<a href="/" className="flex items-center gap-2 mb-4">
|
||||
<VocalinuxLogo className="h-8 w-8 text-primary" />
|
||||
<Image src="/vocalinux.svg" alt="Vocalinux" width={32} height={32} className="h-8 w-8" />
|
||||
<span className="text-xl font-bold">Vocalinux</span>
|
||||
</a>
|
||||
<p className="text-zinc-400 mb-4 max-w-md">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,323 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Mic, MicOff, AlertCircle, Chrome, Globe, Volume2, Sparkles } 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 recognitionRef = useRef<SpeechRecognition | null>(null);
|
||||
|
||||
// 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();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const clearTranscript = useCallback(() => {
|
||||
setTranscript("");
|
||||
setInterimTranscript("");
|
||||
}, []);
|
||||
|
||||
// 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'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's address bar to enable microphone access.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Microphone button */}
|
||||
<div className="flex flex-col items-center mb-6">
|
||||
<motion.button
|
||||
onClick={isListening ? stopListening : startListening}
|
||||
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-white relative z-10" />
|
||||
) : (
|
||||
<Mic className="h-8 w-8 sm:h-10 sm:w-10 text-white relative z-10" />
|
||||
)}
|
||||
</motion.button>
|
||||
|
||||
<p className="text-sm text-zinc-400 mt-4">
|
||||
{isListening ? (
|
||||
<span className="flex items-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... Click to stop
|
||||
</span>
|
||||
) : (
|
||||
"Click the microphone to try voice recognition"
|
||||
)}
|
||||
</p>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -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)} />
|
||||
<VocalinuxLogo className={cn("h-8 w-8", iconClassName)} />
|
||||
<span className={cn("font-bold text-xl", textClassName)}>Vocalinux</span>
|
||||
</div>;
|
||||
}
|
||||
Reference in New Issue
Block a user