Skip to content

Commit 4b3329a

Browse files
docs(examples): add Voice Search example (#538)
1 parent d8c2cb8 commit 4b3329a

10 files changed

Lines changed: 519 additions & 1 deletion

File tree

.codesandbox/ci.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"/examples/react-renderer",
1616
"/examples/recently-viewed-items",
1717
"/examples/starter-algolia",
18-
"/examples/starter"
18+
"/examples/starter",
19+
"/examples/voice-search"
1920
],
2021
"node": "14"
2122
}

examples/voice-search/README.md

Whitespace-only changes.

examples/voice-search/app.tsx

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/** @jsx h */
2+
import { autocomplete, getAlgoliaHits } from '@algolia/autocomplete-js';
3+
import { Hit } from '@algolia/client-search';
4+
import algoliasearch from 'algoliasearch';
5+
import { h } from 'preact';
6+
7+
import '@algolia/autocomplete-theme-classic';
8+
import { createVoiceSearchPlugin } from './voiceSearchPlugin';
9+
10+
const appId = 'latency';
11+
const apiKey = '6be0576ff61c053d5f9a3225e2a90f76';
12+
const searchClient = algoliasearch(appId, apiKey);
13+
14+
type AutocompleteItem = Hit<{
15+
brand: string;
16+
categories: string[];
17+
description: string;
18+
image: string;
19+
name: string;
20+
price: number;
21+
rating: number;
22+
type: string;
23+
url: string;
24+
}>;
25+
26+
const voiceSearchPlugin = createVoiceSearchPlugin({});
27+
28+
autocomplete<AutocompleteItem>({
29+
container: '#autocomplete',
30+
placeholder: 'Search',
31+
detachedMediaQuery: 'none',
32+
plugins: [voiceSearchPlugin],
33+
getSources({ query }) {
34+
return [
35+
{
36+
sourceId: 'products',
37+
getItems() {
38+
return getAlgoliaHits<AutocompleteItem>({
39+
searchClient,
40+
queries: [
41+
{
42+
indexName: 'instant_search',
43+
query,
44+
},
45+
],
46+
});
47+
},
48+
templates: {
49+
item({ item, components }) {
50+
return (
51+
<div className="aa-ItemWrapper">
52+
<div className="aa-ItemContent">
53+
<div className="aa-ItemIcon aa-ItemIcon--picture aa-ItemIcon--alignTop">
54+
<img
55+
src={item.image}
56+
alt={item.name}
57+
width="40"
58+
height="40"
59+
/>
60+
</div>
61+
62+
<div className="aa-ItemContentBody">
63+
<div className="aa-ItemContentTitle">
64+
<components.Highlight hit={item} attribute="name" />
65+
</div>
66+
<div className="aa-ItemContentDescription">
67+
By <strong>{item.brand}</strong> in{' '}
68+
<strong>{item.categories[0]}</strong>
69+
</div>
70+
</div>
71+
</div>
72+
</div>
73+
);
74+
},
75+
noResults() {
76+
return 'No products matching.';
77+
},
78+
},
79+
},
80+
];
81+
},
82+
});

examples/voice-search/env.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as preact from 'preact';
2+
3+
// Parcel picks the `source` field of the monorepo packages and thus doesn't
4+
// apply the Babel config. We therefore need to manually override the constants
5+
// in the app, as well as the React pragmas.
6+
// See https://twitter.com/devongovett/status/1134231234605830144
7+
(global as any).__DEV__ = process.env.NODE_ENV !== 'production';
8+
(global as any).__TEST__ = false;
9+
(global as any).h = preact.h;
10+
(global as any).React = preact;

examples/voice-search/favicon.png

228 KB
Loading

examples/voice-search/index.html

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<html lang="en">
2+
<head>
3+
<meta charset="UTF-8" />
4+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
5+
6+
<link rel="shortcut icon" href="favicon.png" type="image/x-icon" />
7+
<link rel="stylesheet" href="style.css" />
8+
9+
<title>Voice Search | Autocomplete</title>
10+
</head>
11+
12+
<body>
13+
<div class="container">
14+
<div id="autocomplete"></div>
15+
</div>
16+
17+
<script src="env.ts"></script>
18+
<script src="app.tsx"></script>
19+
</body>
20+
</html>

examples/voice-search/package.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "@algolia/autocomplete-example-voice-search",
3+
"description": "Autocomplete example with Voice Search",
4+
"version": "1.0.0-alpha.45",
5+
"private": true,
6+
"license": "MIT",
7+
"main": "index.html",
8+
"scripts": {
9+
"build": "parcel build index.html",
10+
"start": "parcel index.html"
11+
},
12+
"dependencies": {
13+
"@algolia/autocomplete-js": "1.0.0-alpha.45",
14+
"@algolia/autocomplete-theme-classic": "1.0.0-alpha.45",
15+
"algoliasearch": "4.8.6",
16+
"preact": "10.5.13"
17+
},
18+
"devDependencies": {
19+
"@algolia/client-search": "4.8.6",
20+
"parcel": "2.0.0-beta.2"
21+
},
22+
"keywords": [
23+
"algolia",
24+
"autocomplete",
25+
"javascript"
26+
]
27+
}

examples/voice-search/style.css

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
* {
2+
box-sizing: border-box;
3+
}
4+
5+
body {
6+
background-color: rgb(244, 244, 249);
7+
color: rgb(65, 65, 65);
8+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
9+
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
10+
sans-serif;
11+
-webkit-font-smoothing: antialiased;
12+
-moz-osx-font-smoothing: grayscale;
13+
padding: 1rem;
14+
}
15+
16+
.container {
17+
margin: 0 auto;
18+
max-width: 640px;
19+
width: 100%;
20+
}
21+
22+
.aa-VoiceSearch {
23+
border-left: 1px solid rgba(var(--aa-muted-color-rgb), 0.5);
24+
display: flex;
25+
height: 100%;
26+
}
27+
28+
.aa-VoiceSearchButton {
29+
align-items: center;
30+
background: none;
31+
border: 0;
32+
color: rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha));
33+
cursor: pointer;
34+
display: flex;
35+
flex-shrink: 0;
36+
padding: 0 calc(var(--aa-spacing) * 0.83333 - 0.5px);
37+
}
38+
39+
.aa-VoiceSearchButton:hover svg,
40+
.aa-VoiceSearchButton:focus svg {
41+
color: rgba(var(--aa-text-color-rgb), var(--aa-text-color-alpha));
42+
}
43+
44+
@media (hover: none) and (pointer: coarse) {
45+
.aa-VoiceSearchButton:hover svg {
46+
color: inherit;
47+
}
48+
}
49+
50+
.aa-VoiceSearchButton svg {
51+
color: rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha));
52+
margin: 0;
53+
margin: calc(var(--aa-spacing) / 3);
54+
stroke-width: var(--aa-icon-stroke-width);
55+
width: var(--aa-action-icon-size);
56+
}
57+
58+
.aa-VoiceSearchOverlay {
59+
align-items: center;
60+
background: #fff;
61+
bottom: 0;
62+
display: flex;
63+
font-size: 2rem;
64+
justify-content: center;
65+
left: 0;
66+
position: fixed;
67+
right: 0;
68+
top: 0;
69+
z-index: var(--aa-base-z-index);
70+
}
71+
72+
.aa-VoiceSearchWrapper {
73+
align-items: center;
74+
color: rgba(var(--aa-muted-color-rgb), 1);
75+
display: flex;
76+
justify-content: space-between;
77+
max-width: 480px;
78+
width: 100%;
79+
}
80+
81+
.aa-VoiceSearchListeningButton {
82+
background: none;
83+
border: 1px solid rgba(var(--aa-muted-color-rgb), 0.5);
84+
border-radius: 50%;
85+
color: red;
86+
cursor: pointer;
87+
padding: 2rem;
88+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
export type VoiceSearchStatus =
2+
| 'INITIAL'
3+
| 'REQUESTING_PERMISSION'
4+
| 'LISTENING'
5+
| 'RECOGNIZING'
6+
| 'ERROR';
7+
8+
type VoiceSearchState = {
9+
status: VoiceSearchStatus;
10+
transcript: string;
11+
errorCode: SpeechRecognitionErrorCode | null;
12+
};
13+
14+
type CreateVoiceSearchParams = {
15+
language?: string;
16+
onStateChange(state: VoiceSearchState): void;
17+
onTranscript(transcript: string): void;
18+
};
19+
20+
type VoiceSearchApi = {
21+
isBrowserSupported(): boolean;
22+
start(): void;
23+
stop(): void;
24+
};
25+
26+
function createState(state: Partial<VoiceSearchState>): VoiceSearchState {
27+
return {
28+
status: 'INITIAL',
29+
transcript: '',
30+
errorCode: null,
31+
...state,
32+
};
33+
}
34+
35+
export function createVoiceSearch({
36+
language,
37+
onTranscript,
38+
onStateChange,
39+
}: CreateVoiceSearchParams): VoiceSearchApi {
40+
const SpeechRecognitionAPI: new () => SpeechRecognition =
41+
(window as any).webkitSpeechRecognition ||
42+
(window as any).SpeechRecognition;
43+
let state: VoiceSearchState = createState({});
44+
let recognition: SpeechRecognition | undefined;
45+
46+
function isBrowserSupported() {
47+
return Boolean(SpeechRecognitionAPI);
48+
}
49+
50+
function setState(newState: Partial<VoiceSearchState>) {
51+
state = { ...state, ...newState };
52+
onStateChange(state);
53+
}
54+
55+
function onStart() {
56+
setState({ status: 'LISTENING' });
57+
}
58+
59+
function onError(event: SpeechRecognitionErrorEvent) {
60+
setState({ status: 'ERROR', errorCode: event.error });
61+
}
62+
63+
function onResult(event: SpeechRecognitionEvent) {
64+
setState({
65+
status: 'RECOGNIZING',
66+
transcript:
67+
(event.results[0] &&
68+
event.results[0][0] &&
69+
event.results[0][0].transcript) ||
70+
'',
71+
});
72+
}
73+
74+
function onEnd() {
75+
if (!state.errorCode && state.transcript) {
76+
onTranscript(state.transcript);
77+
}
78+
79+
if (state.status !== 'ERROR') {
80+
setState(createState({ status: 'INITIAL' }));
81+
}
82+
}
83+
84+
function start() {
85+
recognition = new SpeechRecognitionAPI();
86+
if (!recognition) {
87+
return;
88+
}
89+
90+
setState(createState({ status: 'REQUESTING_PERMISSION' }));
91+
recognition.interimResults = true;
92+
if (language) {
93+
recognition.lang = language;
94+
}
95+
recognition.addEventListener('start', onStart);
96+
recognition.addEventListener('error', onError);
97+
recognition.addEventListener('result', onResult);
98+
recognition.addEventListener('end', onEnd);
99+
recognition.start();
100+
}
101+
102+
function stop() {
103+
if (!recognition) {
104+
return;
105+
}
106+
107+
recognition.stop();
108+
recognition.removeEventListener('start', onStart);
109+
recognition.removeEventListener('error', onError);
110+
recognition.removeEventListener('result', onResult);
111+
recognition.removeEventListener('end', onEnd);
112+
recognition = undefined;
113+
114+
setState(createState({ status: 'INITIAL' }));
115+
}
116+
117+
return {
118+
isBrowserSupported,
119+
start,
120+
stop,
121+
};
122+
}

0 commit comments

Comments
 (0)