Skip to content

Commit 8bd2672

Browse files
feat: add interactive StackBlitz playground with demo for both hooks
1 parent a5c85e6 commit 8bd2672

File tree

8 files changed

+388
-0
lines changed

8 files changed

+388
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
55
[![npm version](https://img.shields.io/npm/v/@syntropy-labs/react-web-speech.svg)](https://www.npmjs.com/package/@syntropy-labs/react-web-speech)
66
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7+
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz_small.svg)](https://stackblitz.com/github/SyntropyLabs/react-web-speech/tree/main/sandbox?file=src%2FApp.tsx)
78

89
## Features
910

sandbox/index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>React Web Speech Demo</title>
7+
</head>
8+
<body>
9+
<div id="root"></div>
10+
<script type="module" src="/src/main.tsx"></script>
11+
</body>
12+
</html>

sandbox/package.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "react-web-speech-playground",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "tsc -b && vite build",
9+
"preview": "vite preview"
10+
},
11+
"dependencies": {
12+
"@syntropy-labs/react-web-speech": "^0.1.1",
13+
"react": "^18.2.0",
14+
"react-dom": "^18.2.0"
15+
},
16+
"devDependencies": {
17+
"@types/react": "^18.2.0",
18+
"@types/react-dom": "^18.2.0",
19+
"@vitejs/plugin-react": "^4.0.0",
20+
"typescript": "^5.0.0",
21+
"vite": "^5.0.0"
22+
}
23+
}

sandbox/src/App.tsx

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { useState, useRef } from 'react'
2+
import { useSpeechInput, useSpeechInputWithCursor } from '@syntropy-labs/react-web-speech'
3+
4+
function BasicDemo() {
5+
const {
6+
transcript,
7+
interimTranscript,
8+
isListening,
9+
isSupported,
10+
permissionState,
11+
toggle,
12+
clear,
13+
error,
14+
} = useSpeechInput({
15+
continuous: true,
16+
interimResults: true,
17+
silenceTimeout: 3000,
18+
})
19+
20+
if (!isSupported) {
21+
return (
22+
<div className="card error">
23+
<h2>❌ Not Supported</h2>
24+
<p>Web Speech API is not supported in this browser.</p>
25+
<p>Please try Chrome, Edge, or Safari.</p>
26+
</div>
27+
)
28+
}
29+
30+
return (
31+
<div className="card">
32+
<h2>🎙️ Basic Speech Demo</h2>
33+
34+
<div className="status">
35+
<span className={`badge ${permissionState}`}>Permission: {permissionState}</span>
36+
<span className={`badge ${isListening ? 'listening' : 'idle'}`}>
37+
{isListening ? '🔴 Listening...' : '⚪ Idle'}
38+
</span>
39+
</div>
40+
41+
<div className="transcript-box">
42+
<p className="transcript">
43+
{transcript}
44+
<span className="interim">{interimTranscript}</span>
45+
</p>
46+
</div>
47+
48+
{error && <div className="error-message">⚠️ {error.message}</div>}
49+
50+
<div className="buttons">
51+
<button onClick={toggle} className={isListening ? 'stop' : 'start'}>
52+
{isListening ? '⏹️ Stop' : '▶️ Start'}
53+
</button>
54+
<button onClick={clear} className="secondary">
55+
🗑️ Clear
56+
</button>
57+
</div>
58+
</div>
59+
)
60+
}
61+
62+
function CursorDemo() {
63+
const [text, setText] = useState('')
64+
const inputRef = useRef<HTMLInputElement>(null)
65+
66+
const { isListening, toggle, isSupported } = useSpeechInputWithCursor({
67+
inputRef,
68+
value: text,
69+
onChange: setText,
70+
appendSpace: true,
71+
silenceTimeout: 2000,
72+
})
73+
74+
if (!isSupported) return null
75+
76+
return (
77+
<div className="card">
78+
<h2>📝 Cursor-Aware Demo</h2>
79+
<p className="description">
80+
Click in the input, position your cursor, then speak. Text will be inserted at the cursor
81+
position!
82+
</p>
83+
84+
<div className="input-group">
85+
<input
86+
ref={inputRef}
87+
type="text"
88+
value={text}
89+
onChange={(e) => setText(e.target.value)}
90+
placeholder="Click here and speak..."
91+
className={isListening ? 'active' : ''}
92+
/>
93+
<button onClick={toggle} className={isListening ? 'stop' : 'start'}>
94+
{isListening ? '⏹️' : '🎙️'}
95+
</button>
96+
</div>
97+
</div>
98+
)
99+
}
100+
101+
export default function App() {
102+
return (
103+
<div className="app">
104+
<header>
105+
<h1>@syntropy-labs/react-web-speech</h1>
106+
<p>React hooks for the Web Speech API</p>
107+
<a
108+
href="https://github.com/SyntropyLabs/react-web-speech"
109+
target="_blank"
110+
rel="noopener noreferrer"
111+
>
112+
GitHub →
113+
</a>
114+
</header>
115+
116+
<main>
117+
<BasicDemo />
118+
<CursorDemo />
119+
</main>
120+
121+
<footer>
122+
<p>
123+
📦 <code>npm install @syntropy-labs/react-web-speech</code>
124+
</p>
125+
</footer>
126+
</div>
127+
)
128+
}

sandbox/src/main.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { StrictMode } from 'react'
2+
import { createRoot } from 'react-dom/client'
3+
import App from './App'
4+
import './styles.css'
5+
6+
createRoot(document.getElementById('root')!).render(
7+
<StrictMode>
8+
<App />
9+
</StrictMode>
10+
)

sandbox/src/styles.css

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
* {
2+
box-sizing: border-box;
3+
margin: 0;
4+
padding: 0;
5+
}
6+
7+
body {
8+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
9+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
10+
min-height: 100vh;
11+
color: #fff;
12+
}
13+
14+
.app {
15+
max-width: 600px;
16+
margin: 0 auto;
17+
padding: 2rem 1rem;
18+
}
19+
20+
header {
21+
text-align: center;
22+
margin-bottom: 2rem;
23+
}
24+
25+
header h1 {
26+
font-size: 1.5rem;
27+
margin-bottom: 0.5rem;
28+
background: linear-gradient(90deg, #00d4ff, #7b2ff7);
29+
-webkit-background-clip: text;
30+
-webkit-text-fill-color: transparent;
31+
background-clip: text;
32+
}
33+
34+
header p {
35+
color: #888;
36+
margin-bottom: 0.5rem;
37+
}
38+
39+
header a {
40+
color: #00d4ff;
41+
text-decoration: none;
42+
}
43+
44+
header a:hover {
45+
text-decoration: underline;
46+
}
47+
48+
.card {
49+
background: rgba(255, 255, 255, 0.05);
50+
border-radius: 1rem;
51+
padding: 1.5rem;
52+
margin-bottom: 1.5rem;
53+
border: 1px solid rgba(255, 255, 255, 0.1);
54+
}
55+
56+
.card h2 {
57+
font-size: 1.2rem;
58+
margin-bottom: 1rem;
59+
}
60+
61+
.card.error {
62+
border-color: #ff4757;
63+
}
64+
65+
.status {
66+
display: flex;
67+
gap: 0.5rem;
68+
margin-bottom: 1rem;
69+
flex-wrap: wrap;
70+
}
71+
72+
.badge {
73+
padding: 0.25rem 0.75rem;
74+
border-radius: 999px;
75+
font-size: 0.75rem;
76+
background: rgba(255, 255, 255, 0.1);
77+
}
78+
79+
.badge.granted { background: rgba(0, 255, 136, 0.2); color: #00ff88; }
80+
.badge.denied { background: rgba(255, 71, 87, 0.2); color: #ff4757; }
81+
.badge.prompt { background: rgba(255, 193, 7, 0.2); color: #ffc107; }
82+
.badge.listening { background: rgba(255, 71, 87, 0.2); color: #ff4757; }
83+
.badge.idle { background: rgba(255, 255, 255, 0.1); }
84+
85+
.transcript-box {
86+
background: rgba(0, 0, 0, 0.3);
87+
border-radius: 0.5rem;
88+
padding: 1rem;
89+
min-height: 100px;
90+
margin-bottom: 1rem;
91+
}
92+
93+
.transcript {
94+
color: #fff;
95+
line-height: 1.6;
96+
}
97+
98+
.interim {
99+
color: #888;
100+
font-style: italic;
101+
}
102+
103+
.error-message {
104+
background: rgba(255, 71, 87, 0.2);
105+
color: #ff4757;
106+
padding: 0.75rem;
107+
border-radius: 0.5rem;
108+
margin-bottom: 1rem;
109+
font-size: 0.875rem;
110+
}
111+
112+
.buttons {
113+
display: flex;
114+
gap: 0.5rem;
115+
}
116+
117+
button {
118+
padding: 0.75rem 1.5rem;
119+
border: none;
120+
border-radius: 0.5rem;
121+
cursor: pointer;
122+
font-size: 1rem;
123+
transition: all 0.2s;
124+
}
125+
126+
button.start {
127+
background: linear-gradient(90deg, #00d4ff, #7b2ff7);
128+
color: #fff;
129+
}
130+
131+
button.stop {
132+
background: #ff4757;
133+
color: #fff;
134+
}
135+
136+
button.secondary {
137+
background: rgba(255, 255, 255, 0.1);
138+
color: #fff;
139+
}
140+
141+
button:hover {
142+
transform: translateY(-2px);
143+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
144+
}
145+
146+
.description {
147+
color: #888;
148+
font-size: 0.875rem;
149+
margin-bottom: 1rem;
150+
}
151+
152+
.input-group {
153+
display: flex;
154+
gap: 0.5rem;
155+
}
156+
157+
.input-group input {
158+
flex: 1;
159+
padding: 0.75rem 1rem;
160+
border: 2px solid rgba(255, 255, 255, 0.1);
161+
border-radius: 0.5rem;
162+
background: rgba(0, 0, 0, 0.3);
163+
color: #fff;
164+
font-size: 1rem;
165+
outline: none;
166+
transition: border-color 0.2s;
167+
}
168+
169+
.input-group input:focus {
170+
border-color: #00d4ff;
171+
}
172+
173+
.input-group input.active {
174+
border-color: #ff4757;
175+
}
176+
177+
footer {
178+
text-align: center;
179+
margin-top: 2rem;
180+
color: #888;
181+
}
182+
183+
footer code {
184+
background: rgba(255, 255, 255, 0.1);
185+
padding: 0.25rem 0.5rem;
186+
border-radius: 0.25rem;
187+
font-size: 0.875rem;
188+
}

sandbox/tsconfig.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"useDefineForClassFields": true,
5+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
6+
"module": "ESNext",
7+
"skipLibCheck": true,
8+
"moduleResolution": "bundler",
9+
"allowImportingTsExtensions": true,
10+
"resolveJsonModule": true,
11+
"isolatedModules": true,
12+
"noEmit": true,
13+
"jsx": "react-jsx",
14+
"strict": true,
15+
"noUnusedLocals": true,
16+
"noUnusedParameters": true,
17+
"noFallthroughCasesInSwitch": true
18+
},
19+
"include": ["src"]
20+
}

0 commit comments

Comments
 (0)