Skip to content

Commit 951150b

Browse files
authored
Merge pull request #1 from yoidea/components
コンポーネント分割
2 parents 79babd5 + 3ee56be commit 951150b

File tree

5 files changed

+187
-104
lines changed

5 files changed

+187
-104
lines changed

components/notice.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from "react";
2+
import Snackbar from "@material-ui/core/Snackbar";
3+
import MuiAlert from "@material-ui/lab/Alert";
4+
import { bool, node, string, func } from "prop-types";
5+
6+
const Notice = props => (
7+
<Snackbar open={props.open} autoHideDuration={6000} onClose={props.onClose}>
8+
<MuiAlert
9+
elevation={6}
10+
variant="filled"
11+
onClose={props.onClose}
12+
severity={props.severity}
13+
>
14+
{props.children}
15+
</MuiAlert>
16+
</Snackbar>
17+
);
18+
19+
Notice.propTypes = {
20+
open: bool,
21+
severity: string,
22+
onClose: func,
23+
children: node
24+
};
25+
26+
export default Notice;

components/tagField.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React from "react";
2+
import Chip from "@material-ui/core/Chip";
3+
import Autocomplete from "@material-ui/lab/Autocomplete";
4+
import TextField from "@material-ui/core/TextField";
5+
import { bool, func, array, string } from "prop-types";
6+
7+
const TagField = props => (
8+
<Autocomplete
9+
disabled={props.disabled}
10+
multiple
11+
id="tags-filled"
12+
options={props.options}
13+
freeSolo
14+
defaultValue={props.defaultValue}
15+
onChange={(event, values) => {
16+
props.onTagChange(values);
17+
}}
18+
renderTags={(value, getTagProps) =>
19+
value.map((option, index) => (
20+
<Chip variant="outlined" label={option} {...getTagProps({ index })} />
21+
))
22+
}
23+
renderInput={params => {
24+
return (
25+
<TextField
26+
{...params}
27+
variant="outlined"
28+
label={props.label}
29+
placeholder={props.placeholder}
30+
/>
31+
);
32+
}}
33+
/>
34+
);
35+
36+
TagField.propTypes = {
37+
options: array,
38+
defaultValue: array,
39+
disabled: bool,
40+
onTagChange: func,
41+
label: string,
42+
placeholder: string
43+
};
44+
45+
export default TagField;

components/transcriptField.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from "react";
2+
import { bool, string } from "prop-types";
3+
4+
const TranscriptField = props => (
5+
<p>
6+
{props.finalText}
7+
<span style={{ color: props.isMatch ? "#f00" : "#aaa" }}>
8+
{props.transcript}
9+
</span>
10+
</p>
11+
);
12+
13+
TranscriptField.propTypes = {
14+
isMatch: bool,
15+
finalText: string,
16+
transcript: string
17+
};
18+
19+
export default TranscriptField;

components/uploadButton.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React from "react";
2+
import IconButton from "@material-ui/core/IconButton";
3+
import LibraryMusicIcon from "@material-ui/icons/LibraryMusic";
4+
import { bool, func, string } from "prop-types";
5+
6+
const UploadButton = props => (
7+
<div>
8+
<input
9+
accept={`${props.fileType}/*`}
10+
id="file-input"
11+
multiple
12+
type="file"
13+
style={{ display: "none" }}
14+
onChange={event => {
15+
const file = event.target.files[0];
16+
if (!(file instanceof File)) return;
17+
if (file.type.indexOf(props.fileType) === -1) {
18+
props.onInvalidFileError();
19+
return;
20+
}
21+
props.onFileChange(file);
22+
}}
23+
/>
24+
<label htmlFor="file-input">
25+
<IconButton
26+
color="primary"
27+
size="large"
28+
disabled={props.disabled}
29+
aria-label="upload audio"
30+
component="span"
31+
>
32+
<LibraryMusicIcon />
33+
</IconButton>
34+
</label>
35+
</div>
36+
);
37+
38+
UploadButton.propTypes = {
39+
onFileChange: func,
40+
onInvalidFileError: func,
41+
fileType: string,
42+
disabled: bool
43+
};
44+
45+
export default UploadButton;

pages/index.js

Lines changed: 52 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,41 @@
11
import React, { useState, useEffect, useRef } from "react";
2-
import Link from "next/link";
32
import Head from "../components/head";
43
import Container from "@material-ui/core/Container";
5-
import Chip from "@material-ui/core/Chip";
6-
import Autocomplete from "@material-ui/lab/Autocomplete";
7-
import TextField from "@material-ui/core/TextField";
8-
import Snackbar from "@material-ui/core/Snackbar";
9-
import MuiAlert from "@material-ui/lab/Alert";
104
import Box from "@material-ui/core/Box";
115
import Grid from "@material-ui/core/Grid";
126
import Button from "@material-ui/core/Button";
13-
import IconButton from "@material-ui/core/IconButton";
14-
import LibraryMusicIcon from "@material-ui/icons/LibraryMusic";
7+
import Notice from "../components/notice";
8+
import UploadButton from "../components/uploadButton";
9+
import TagField from "../components/tagField";
10+
import TranscriptField from "../components/transcriptField";
1511

1612
const Home = () => {
13+
// 音声認識インスタンス
1714
const recognizerRef = useRef();
18-
const inputRef = useRef();
19-
const [finalText, setFinalText] = useState("");
20-
const [transcript, setTranscript] = useState("ボタンを押して検知開始");
21-
const initialTagValues = ["年収"];
22-
const [tagValues, setTagValues] = useState(initialTagValues);
23-
const [detecting, setDetecting] = useState(false);
24-
const candidates = ["年収", "自由", "成功"];
25-
const [alertOpen, setAlertOpen] = useState(false);
26-
const [fileLoaded, setFileLoaded] = useState(false);
27-
const [userMusic, setUserMusic] = useState(null);
28-
const [userMusicName, setUserMusicName] = useState("");
15+
// スナックバー表示
16+
const [alertOpen, setAlertOpen] = useState(false); // 自慢検知アラート
17+
const [fileLoaded, setFileLoaded] = useState(false); // ファイル読み込み完了
18+
// 音声認識
19+
const [detecting, setDetecting] = useState(false); // 音声認識ステータス
20+
const [finalText, setFinalText] = useState(""); // 確定された文章
21+
const [transcript, setTranscript] = useState("ボタンを押して検知開始"); // 認識中の文章
22+
// 単語検知
23+
const initialTagValues = ["年収"]; // デフォルト検知単語
24+
const candidates = ["年収", "自由", "成功"]; // 検知単語候補
25+
const [tagValues, setTagValues] = useState(initialTagValues); // 検知単語一覧
26+
// 効果音
27+
const [userMusic, setUserMusic] = useState(null); // ユーザー追加音
28+
const [userMusicName, setUserMusicName] = useState(""); // ファイル名
2929

3030
useEffect(() => {
31-
const music = new Audio("/static/warning01.mp3");
31+
const music = new Audio("/static/warning01.mp3"); // デフォルト音
32+
// NOTE: Web Speech APIが使えるブラウザか判定
33+
// https://developer.mozilla.org/ja/docs/Web/API/Web_Speech_API
3234
if (!window.SpeechRecognition && !window.webkitSpeechRecognition) {
3335
alert("お使いのブラウザには未対応です");
3436
return;
3537
}
38+
// NOTE: 将来的にwebkit prefixが取れる可能性があるため
3639
const SpeechRecognition =
3740
window.SpeechRecognition || window.webkitSpeechRecognition;
3841
recognizerRef.current = new SpeechRecognition();
@@ -49,12 +52,15 @@ const Home = () => {
4952
[...event.results].slice(event.resultIndex).forEach(result => {
5053
const transcript = result[0].transcript;
5154
if (result.isFinal) {
55+
// 音声認識が完了して文章が確定
5256
setFinalText(prevState => {
5357
return prevState + transcript;
5458
});
5559
setTranscript("");
5660
} else {
61+
// 音声認識の途中経過
5762
if (tagValues.some(value => transcript.includes(value))) {
63+
// NOTE: ユーザーが効果音を追加しなければデフォルトを鳴らす
5864
(userMusic || music).play();
5965
setAlertOpen(true);
6066
}
@@ -66,124 +72,66 @@ const Home = () => {
6672

6773
return (
6874
<div>
69-
<Head title="Home" />
70-
<Snackbar
75+
<Head title="自慢ディテクター" />
76+
<Notice
7177
open={alertOpen}
72-
autoHideDuration={6000}
78+
severity="error"
7379
onClose={() => {
7480
setAlertOpen(false);
7581
}}
7682
>
77-
<MuiAlert
78-
elevation={6}
79-
variant="filled"
80-
onClose={() => {
81-
setAlertOpen(false);
82-
}}
83-
severity="error"
84-
>
85-
自慢を検知しました
86-
</MuiAlert>
87-
</Snackbar>
88-
<Snackbar
83+
自慢を検知しました
84+
</Notice>
85+
<Notice
8986
open={fileLoaded}
90-
autoHideDuration={6000}
87+
severity="success"
9188
onClose={() => {
9289
setFileLoaded(false);
9390
}}
9491
>
95-
<MuiAlert
96-
elevation={6}
97-
variant="filled"
98-
onClose={() => {
99-
setFileLoaded(false);
100-
}}
101-
severity="success"
102-
>
103-
{userMusicName}を読み込みました
104-
</MuiAlert>
105-
</Snackbar>
92+
{userMusicName}を読み込みました
93+
</Notice>
10694
<Container>
10795
<Grid container alignItems="center" justify="center">
10896
<Grid item>
109-
<img src="/static/logo.png" height="200px" />
97+
<img src="/static/logo.png" height="200px" alt="自慢ディテクター" />
11098
</Grid>
11199
</Grid>
112100
<Box fontSize={25}>
113-
<p>
114-
{finalText}
115-
<span style={{ color: alertOpen ? "#f00" : "#aaa" }}>
116-
{transcript}
117-
</span>
118-
</p>
119-
<div id="result-div"></div>
101+
<TranscriptField
102+
finalText={finalText}
103+
transcript={transcript}
104+
isMatch={alertOpen}
105+
/>
120106
</Box>
121107
<Grid container spacing={2}>
122108
<Grid item xs={11}>
123-
<Autocomplete
109+
<TagField
124110
disabled={detecting}
125-
multiple
126-
id="tags-filled"
127111
options={candidates}
128-
freeSolo
129112
defaultValue={initialTagValues}
130-
onChange={(event, values) => {
113+
label="反応する単語"
114+
placeholder="単語を追加 +"
115+
onTagChange={values => {
131116
setTagValues(values);
132117
}}
133-
renderTags={(value, getTagProps) =>
134-
value.map((option, index) => (
135-
<Chip
136-
variant="outlined"
137-
label={option}
138-
{...getTagProps({ index })}
139-
/>
140-
))
141-
}
142-
renderInput={params => {
143-
return (
144-
<TextField
145-
{...params}
146-
variant="outlined"
147-
label="反応する単語"
148-
placeholder="単語を追加 +"
149-
/>
150-
);
151-
}}
152118
/>
153119
</Grid>
154120
<Grid item>
155-
<input
156-
ref={inputRef}
157-
accept="audio/*"
158-
id="file-input"
159-
multiple
160-
type="file"
161-
style={{ display: "none" }}
162-
onChange={event => {
163-
const file = event.target.files[0];
164-
if (!(file instanceof File)) return;
165-
if (file.type.indexOf("audio") === -1) {
166-
alert("オーディオファイルを選択してください");
167-
return;
168-
}
121+
<UploadButton
122+
disabled={detecting}
123+
fileType="audio"
124+
onFileChange={file => {
169125
const src = window.URL.createObjectURL(file);
170126
const audio = new Audio(src);
171127
setUserMusic(audio);
172128
setUserMusicName(file.name);
173129
setFileLoaded(true);
174130
}}
131+
onInvalidFileError={() => {
132+
alert("オーディオファイルを選択してください");
133+
}}
175134
/>
176-
<label htmlFor="file-input">
177-
<IconButton
178-
color="primary"
179-
size="large"
180-
disabled={detecting}
181-
aria-label="upload audio"
182-
component="span"
183-
>
184-
<LibraryMusicIcon />
185-
</IconButton>
186-
</label>
187135
</Grid>
188136
</Grid>
189137
<Box m={2}>

0 commit comments

Comments
 (0)