mizzzuno commited on
Commit
dedbe0a
·
verified ·
1 Parent(s): 891dc6c
Files changed (1) hide show
  1. templates/index.html +157 -79
templates/index.html CHANGED
@@ -6,55 +6,95 @@
6
  <title>Voice Recorder Interface</title>
7
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
8
  <script src="https://cdn.tailwindcss.com"></script>
 
 
 
 
9
  </head>
10
- <body class="flex flex-col items-center justify-center h-screen bg-gray-900 text-white">
11
- <!-- メンバーを登録ボタン -->
12
- <div class="flex items-center mb-5">
13
- <button
14
- id="registerButton"
15
- onclick="showUserRegister()"
16
- class="px-4 py-2 bg-blue-600 rounded-md hover:bg-blue-700 transition"
17
- >
18
- メンバーを登録
19
- </button>
20
- </div>
21
-
22
- <!-- チャート表示部 -->
23
- <div class="chart w-72 h-72 mb-5">
24
- <canvas id="speechChart"></canvas>
25
- </div>
26
-
27
- <!-- 録音フォーム -->
28
- <form
29
- id="recordForm"
30
- action="/submit"
31
- method="POST"
32
- class="flex items-center space-x-2 w-full sm:w-auto"
33
- onsubmit="event.preventDefault();"
34
  >
35
-
36
- <!-- 録音ボタン -->
37
- <button type="button" class="record-button" id="recordButton" onclick="toggleRecording()">
38
- <div class="record-icon" id="recordIcon"></div>
39
- </button>
40
- </form>
41
-
42
- <!-- 結果ボタン -->
43
- <div class="flex mt-5">
44
- <button
45
- id="historyButton"
46
- onclick="showTalkdetail()"
47
- class="result-button px-4 py-2 mx-2 bg-green-600 rounded-md hover:bg-green-700 transition"
48
- >
49
- 会話履歴を表示
50
- </button>
51
- <button
52
- id="feedbackButton"
53
- onclick="showResults()"
54
- class="result-button px-4 py-2 mx-2 bg-blue-600 rounded-md hover:bg-blue-700 transition"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  >
56
- フィードバック画面を表示
57
- </button>
 
 
 
 
 
 
 
 
58
  </div>
59
 
60
  <style>
@@ -83,6 +123,31 @@
83
  height: 40px;
84
  border-radius: 10%;
85
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  </style>
87
 
88
  <script>
@@ -93,12 +158,18 @@
93
  let count_voice = 0;
94
  let before_rate = [];
95
  const RECORDING_INTERVAL_MS = 1000; // 1秒
96
-
97
  // メンバーとチャートの初期化
98
  let members = [];
99
  let voiceData = [];
100
- let baseMemberColors = ["#4caf50", "#007bff", "#ffc107", "#dc3545", "#28a745", "#9c27b0", "#ff9800"];
101
-
 
 
 
 
 
 
 
102
  // Chart.js の初期化
103
  const ctx = document.getElementById("speechChart").getContext("2d");
104
  const speechChart = new Chart(ctx, {
@@ -123,7 +194,6 @@
123
  },
124
  },
125
  });
126
-
127
  // サーバーからメンバー情報を取得してチャートを更新する関数
128
  async function updateChartFrom() {
129
  try {
@@ -132,7 +202,6 @@
132
  throw new Error(`HTTP error! status: ${response.status}`);
133
  }
134
  const data = await response.json();
135
-
136
  if (!data || !data.members || !Array.isArray(data.members)) {
137
  console.error("Invalid member data received:", data);
138
  members = ["member1"];
@@ -140,13 +209,11 @@
140
  updateChart();
141
  return;
142
  }
143
-
144
  members = data.members;
145
  voiceData = [];
146
  for (let i = 0; i < members.length; i++) {
147
  voiceData.push(100 / members.length);
148
  }
149
-
150
  updateChart();
151
  } catch (error) {
152
  console.error("Failed to fetch member data:", error);
@@ -155,7 +222,6 @@
155
  updateChart();
156
  }
157
  }
158
-
159
  function updateChart() {
160
  // 一人モードの場合は、ユーザーとグレー(無音)の比率をチャートに表示
161
  if (members.length === 1) {
@@ -165,40 +231,37 @@
165
  } else {
166
  // 複数メンバーの場合は通常通りの処理
167
  speechChart.data.labels = members;
168
- speechChart.data.datasets[0].backgroundColor = getMemberColors(members.length);
 
 
169
  }
170
-
171
  speechChart.data.datasets[0].data = voiceData;
172
  speechChart.update();
173
  }
174
-
175
  // ページ読み込み時にチャート情報を更新
176
  updateChartFrom();
177
-
178
  // 録音ボタンの録音開始/停止処理
179
  async function toggleRecording() {
180
  const recordButton = document.getElementById("recordButton");
181
-
182
  if (!isRecording) {
183
  // 録音開始
184
  isRecording = true;
185
  recordButton.classList.add("recording");
186
  try {
187
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
 
 
188
  mediaRecorder = new MediaRecorder(stream);
189
  audioChunks = [];
190
-
191
  mediaRecorder.ondataavailable = (event) => {
192
  if (event.data.size > 0) {
193
  audioChunks.push(event.data);
194
  }
195
  };
196
-
197
  mediaRecorder.onstop = () => {
198
  sendAudioChunks([...audioChunks]);
199
  audioChunks = [];
200
  };
201
-
202
  mediaRecorder.start();
203
  // 5秒ごとに録音を停止して送信するインターバルを設定
204
  recordingInterval = setInterval(() => {
@@ -224,7 +287,6 @@
224
  before_rate = [];
225
  }
226
  }
227
-
228
  function sendAudioChunks(chunks) {
229
  const audioBlob = new Blob(chunks, { type: "audio/wav" });
230
  const reader = new FileReader();
@@ -255,7 +317,6 @@
255
  };
256
  reader.readAsDataURL(audioBlob);
257
  }
258
-
259
  function getMemberColors(memberCount) {
260
  // 一人モードの場合は特別な処理をしない(updateChartで処理するため)
261
  if (memberCount <= 1) {
@@ -268,7 +329,6 @@
268
  return colors;
269
  }
270
  }
271
-
272
  function updateChartData(newRate) {
273
  // 一人モードの時の処理
274
  if (members.length === 1) {
@@ -277,12 +337,12 @@
277
  before_rate = [newRate];
278
  } else {
279
  // 一人モードでは、過去のデータと現在のデータを加重平均する
280
- let tmp_rate = (newRate + before_rate[0] * count_voice) / (count_voice + 1);
 
281
  speechChart.data.datasets[0].data = [tmp_rate, 100 - tmp_rate];
282
  before_rate = [tmp_rate];
283
  }
284
  count_voice++;
285
-
286
  // 一人モードでは常に緑色とグレーの組み合わせを使用
287
  speechChart.data.labels = [members[0], "無音"];
288
  speechChart.data.datasets[0].backgroundColor = ["#4caf50", "#757575"];
@@ -300,25 +360,21 @@
300
  );
301
  return;
302
  }
303
-
304
  let averagedRates = new Array(newRate.length);
305
-
306
  for (let i = 0; i < newRate.length; i++) {
307
  let tmp_rate;
308
-
309
  if (count_voice === 0) {
310
  // 初回はそのまま
311
  tmp_rate = newRate[i];
312
  } else {
313
  // 2回目以降は、過去の平均値と現在の値を加重平均する
314
- tmp_rate = (newRate[i] + before_rate[i] * count_voice) / (count_voice + 1);
 
315
  }
316
  averagedRates[i] = tmp_rate;
317
  }
318
-
319
  // before_rateを更新
320
  before_rate = averagedRates;
321
-
322
  //グラフに反映
323
  speechChart.data.datasets[0].data = averagedRates;
324
  count_voice++;
@@ -326,22 +382,44 @@
326
  members.length
327
  );
328
  }
329
-
330
  speechChart.update();
331
  }
332
 
333
- function showTalkdetail() {
334
- window.location.href = "talk_detail";
 
 
 
335
  }
336
 
337
- function showResults() {
338
- window.location.href = "feedback";
 
 
 
 
 
 
 
 
 
 
 
 
339
  }
340
 
341
  function showUserRegister() {
342
  fetch("/reset");
343
  window.location.href = "userregister";
344
  }
 
 
 
 
 
 
 
 
345
  </script>
346
  </body>
347
  </html>
 
6
  <title>Voice Recorder Interface</title>
7
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
8
  <script src="https://cdn.tailwindcss.com"></script>
9
+ <link
10
+ rel="stylesheet"
11
+ href="https://use.fontawesome.com/releases/v5.10.0/css/all.css"
12
+ />
13
  </head>
14
+ <body
15
+ class="flex items-center justify-center h-screen bg-gray-900 text-white"
16
+ onclick="closeMenu(event)"
17
+ >
18
+ <!-- 全体を囲む枠 -->
19
+ <div
20
+ class="border-4 border-gray-400 p-8 rounded-lg w-full sm:w-auto relative"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  >
22
+ <!-- タイトル -->
23
+ <div class="text-center text-4xl font-bold mb-5">JustTalk</div>
24
+
25
+ <!-- ハンバーガーメニュー -->
26
+ <div class="absolute top-4 right-4">
27
+ <button
28
+ id="menuButton"
29
+ class="text-white text-2xl focus:outline-none"
30
+ onclick="toggleMenu(event)"
31
+ >
32
+ <i class="fas fa-bars"></i>
33
+ </button>
34
+
35
+ <!-- メニューの本体 -->
36
+ <div
37
+ id="menu"
38
+ class="absolute top-0 right-0 h-full w-64 bg-gray-800 text-white transform translate-x-full transition-transform duration-300 ease-in-out opacity-0 visibility-hidden"
39
+ >
40
+ <div class="px-4 py-2 text-lg font-semibold">メニュー</div>
41
+ <button
42
+ onclick="showUserRegister()"
43
+ class="block px-4 py-2 text-sm hover:bg-gray-700"
44
+ >
45
+ メンバーを追加
46
+ </button>
47
+ <button
48
+ onclick="showTalkdetail()"
49
+ class="block px-4 py-2 text-sm hover:bg-gray-700"
50
+ >
51
+ 会話履歴を表示
52
+ </button>
53
+ <button
54
+ onclick="showResults()"
55
+ class="block px-4 py-2 text-sm hover:bg-gray-700"
56
+ >
57
+ フィードバックを表示
58
+ </button>
59
+ <!-- 追加したリセットボタン -->
60
+ <button
61
+ onclick="resetAction()"
62
+ class="block px-4 py-2 text-sm hover:bg-gray-700"
63
+ >
64
+ リセット
65
+ </button>
66
+ <button
67
+ onclick="toggleMenu(event)"
68
+ class="block px-4 py-2 text-sm hover:bg-gray-700"
69
+ >
70
+ 閉じる
71
+ </button>
72
+ </div>
73
+ </div>
74
+
75
+ <!-- チャート表示部 -->
76
+ <div class="chart w-72 h-72 mb-5">
77
+ <canvas id="speechChart"></canvas>
78
+ </div>
79
+
80
+ <!-- 録音フォーム -->
81
+ <form
82
+ id="recordForm"
83
+ action="/submit"
84
+ method="POST"
85
+ class="flex items-center justify-center space-x-2 w-full sm:w-auto"
86
+ onsubmit="event.preventDefault();"
87
  >
88
+ <!-- 録音ボタン -->
89
+ <button
90
+ type="button"
91
+ class="record-button"
92
+ id="recordButton"
93
+ onclick="toggleRecording()"
94
+ >
95
+ <div class="record-icon" id="recordIcon"></div>
96
+ </button>
97
+ </form>
98
  </div>
99
 
100
  <style>
 
123
  height: 40px;
124
  border-radius: 10%;
125
  }
126
+ .icon i {
127
+ font-size: 20px;
128
+ }
129
+
130
+ /* ハンバーガーメニュー */
131
+ #menu {
132
+ position: absolute;
133
+ top: 0;
134
+ right: 0;
135
+ z-index: 10;
136
+ transform: translateX(100%); /* 初期状態で画面外 */
137
+ visibility: hidden; /* 非表示 */
138
+ opacity: 0; /* 非表示 */
139
+ background-color: rgba(31, 41, 55, 1); /* 不透明な背景色 */
140
+ transition: transform 0.3s ease-in-out, visibility 0s 0.3s,
141
+ opacity 0.3s ease-in-out; /* スライド後に表示 */
142
+ }
143
+
144
+ #menu.open {
145
+ transform: translateX(0); /* メニューが開く */
146
+ visibility: visible; /* 表示 */
147
+ opacity: 1; /* 完全に表示 */
148
+ transition: transform 0.3s ease-in-out, visibility 0s 0s,
149
+ opacity 0.3s ease-in-out; /* すぐに表示 */
150
+ }
151
  </style>
152
 
153
  <script>
 
158
  let count_voice = 0;
159
  let before_rate = [];
160
  const RECORDING_INTERVAL_MS = 1000; // 1秒
 
161
  // メンバーとチャートの初期化
162
  let members = [];
163
  let voiceData = [];
164
+ let baseMemberColors = [
165
+ "#4caf50",
166
+ "#007bff",
167
+ "#ffc107",
168
+ "#dc3545",
169
+ "#28a745",
170
+ "#9c27b0",
171
+ "#ff9800",
172
+ ];
173
  // Chart.js の初期化
174
  const ctx = document.getElementById("speechChart").getContext("2d");
175
  const speechChart = new Chart(ctx, {
 
194
  },
195
  },
196
  });
 
197
  // サーバーからメンバー情報を取得してチャートを更新する関数
198
  async function updateChartFrom() {
199
  try {
 
202
  throw new Error(`HTTP error! status: ${response.status}`);
203
  }
204
  const data = await response.json();
 
205
  if (!data || !data.members || !Array.isArray(data.members)) {
206
  console.error("Invalid member data received:", data);
207
  members = ["member1"];
 
209
  updateChart();
210
  return;
211
  }
 
212
  members = data.members;
213
  voiceData = [];
214
  for (let i = 0; i < members.length; i++) {
215
  voiceData.push(100 / members.length);
216
  }
 
217
  updateChart();
218
  } catch (error) {
219
  console.error("Failed to fetch member data:", error);
 
222
  updateChart();
223
  }
224
  }
 
225
  function updateChart() {
226
  // 一人モードの場合は、ユーザーとグレー(無音)の比率をチャートに表示
227
  if (members.length === 1) {
 
231
  } else {
232
  // 複数メンバーの場合は通常通りの処理
233
  speechChart.data.labels = members;
234
+ speechChart.data.datasets[0].backgroundColor = getMemberColors(
235
+ members.length
236
+ );
237
  }
 
238
  speechChart.data.datasets[0].data = voiceData;
239
  speechChart.update();
240
  }
 
241
  // ページ読み込み時にチャート情報を更新
242
  updateChartFrom();
 
243
  // 録音ボタンの録音開始/停止処理
244
  async function toggleRecording() {
245
  const recordButton = document.getElementById("recordButton");
 
246
  if (!isRecording) {
247
  // 録音開始
248
  isRecording = true;
249
  recordButton.classList.add("recording");
250
  try {
251
+ const stream = await navigator.mediaDevices.getUserMedia({
252
+ audio: true,
253
+ });
254
  mediaRecorder = new MediaRecorder(stream);
255
  audioChunks = [];
 
256
  mediaRecorder.ondataavailable = (event) => {
257
  if (event.data.size > 0) {
258
  audioChunks.push(event.data);
259
  }
260
  };
 
261
  mediaRecorder.onstop = () => {
262
  sendAudioChunks([...audioChunks]);
263
  audioChunks = [];
264
  };
 
265
  mediaRecorder.start();
266
  // 5秒ごとに録音を停止して送信するインターバルを設定
267
  recordingInterval = setInterval(() => {
 
287
  before_rate = [];
288
  }
289
  }
 
290
  function sendAudioChunks(chunks) {
291
  const audioBlob = new Blob(chunks, { type: "audio/wav" });
292
  const reader = new FileReader();
 
317
  };
318
  reader.readAsDataURL(audioBlob);
319
  }
 
320
  function getMemberColors(memberCount) {
321
  // 一人モードの場合は特別な処理をしない(updateChartで処理するため)
322
  if (memberCount <= 1) {
 
329
  return colors;
330
  }
331
  }
 
332
  function updateChartData(newRate) {
333
  // 一人モードの時の処理
334
  if (members.length === 1) {
 
337
  before_rate = [newRate];
338
  } else {
339
  // 一人モードでは、過去のデータと現在のデータを加重平均する
340
+ let tmp_rate =
341
+ (newRate + before_rate[0] * count_voice) / (count_voice + 1);
342
  speechChart.data.datasets[0].data = [tmp_rate, 100 - tmp_rate];
343
  before_rate = [tmp_rate];
344
  }
345
  count_voice++;
 
346
  // 一人モードでは常に緑色とグレーの組み合わせを使用
347
  speechChart.data.labels = [members[0], "無音"];
348
  speechChart.data.datasets[0].backgroundColor = ["#4caf50", "#757575"];
 
360
  );
361
  return;
362
  }
 
363
  let averagedRates = new Array(newRate.length);
 
364
  for (let i = 0; i < newRate.length; i++) {
365
  let tmp_rate;
 
366
  if (count_voice === 0) {
367
  // 初回はそのまま
368
  tmp_rate = newRate[i];
369
  } else {
370
  // 2回目以降は、過去の平均値と現在の値を加重平均する
371
+ tmp_rate =
372
+ (newRate[i] + before_rate[i] * count_voice) / (count_voice + 1);
373
  }
374
  averagedRates[i] = tmp_rate;
375
  }
 
376
  // before_rateを更新
377
  before_rate = averagedRates;
 
378
  //グラフに反映
379
  speechChart.data.datasets[0].data = averagedRates;
380
  count_voice++;
 
382
  members.length
383
  );
384
  }
 
385
  speechChart.update();
386
  }
387
 
388
+ // メニューの表示・非表示
389
+ function toggleMenu(event) {
390
+ event.stopPropagation();
391
+ const menu = document.getElementById("menu");
392
+ menu.classList.toggle("open");
393
  }
394
 
395
+ function closeMenu(event) {
396
+ const menu = document.getElementById("menu");
397
+ if (
398
+ menu.classList.contains("open") &&
399
+ !menu.contains(event.target) &&
400
+ !event.target.closest("#menuButton")
401
+ ) {
402
+ menu.classList.remove("open");
403
+ }
404
+ }
405
+
406
+ // リセットボタンの処理
407
+ function resetAction() {
408
+ alert("リセットが実行されました!");
409
  }
410
 
411
  function showUserRegister() {
412
  fetch("/reset");
413
  window.location.href = "userregister";
414
  }
415
+
416
+ function showTalkdetail() {
417
+ window.location.href = "talk_detail";
418
+ }
419
+
420
+ function showResults() {
421
+ window.location.href = "feedback";
422
+ }
423
  </script>
424
  </body>
425
  </html>