XiaoBai1221 commited on
Commit
d96697e
·
1 Parent(s): 6d3e420

✨ 優化用戶體驗與界面設計

Browse files

🔧 功能優化:
- 修復 STT 文字未送入意圖分析的問題
- 優化輸入框寬度,提升視覺平衡性

🎨 界面優化:
- 縮小輸入框容器寬度 (680px → 520px)
- 調整內距與圓角,增強精緻感
- 優化手機版響應式設計

🌸 動畫體驗:
- 新增登入頁到聊天室的流暢過渡動畫
- 實現蓮花綻放載入動畫
- 添加頁面淡入淡出效果
- 優化視覺連貫性與用戶體驗

📱 響應式設計:
- 完善手機版動畫適配
- 優化觸控友好性
- 提升整體視覺一致性

app.py CHANGED
@@ -1331,6 +1331,7 @@ async def websocket_endpoint_with_jwt(websocket: WebSocket, token: str = Query(N
1331
  "message": str(response),
1332
  "timestamp": time.time()
1333
  })
 
1334
 
1335
  except Exception as e:
1336
  logger.error(f"語音對話流程失敗: {e}")
 
1331
  "message": str(response),
1332
  "timestamp": time.time()
1333
  })
1334
+ await _process_voice_chat()
1335
 
1336
  except Exception as e:
1337
  logger.error(f"語音對話流程失敗: {e}")
static/frontend/index.html CHANGED
@@ -19,6 +19,19 @@
19
  color: #1A1A1A;
20
  overflow: hidden;
21
  -webkit-font-smoothing: antialiased;
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  }
23
 
24
  /* === 控制面板 === */
@@ -118,6 +131,17 @@
118
  align-items: center;
119
  justify-content: center;
120
  background: #F5F1ED;
 
 
 
 
 
 
 
 
 
 
 
121
  }
122
 
123
  /* === 動態漸變背景(移除光暈,改用極淺色情緒底色)=== */
@@ -206,6 +230,19 @@
206
  gap: clamp(36px, 6vh, 72px);
207
  width: min(88vw, 640px);
208
  padding: clamp(12px, 3vh, 28px) 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  }
210
 
211
  /* === 波形與麥克風容器 === */
@@ -216,6 +253,19 @@
216
  display: flex;
217
  align-items: center;
218
  justify-content: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  }
220
 
221
  /* Canvas 波形 */
@@ -979,9 +1029,9 @@
979
  .voice-transcript-wrapper {
980
  position: relative;
981
  width: 100%;
982
- max-width: 680px; /* 限制最大寬度( 100% 改為固定上限)*/
983
  margin: 0 auto; /* 置中對齊 */
984
- padding: 0 20px; /* 左右留白 */
985
  display: flex;
986
  justify-content: center;
987
  }
@@ -989,18 +1039,18 @@
989
  .voice-transcript {
990
  width: 100%; /* 填滿 wrapper(受 max-width 限制)*/
991
  flex-shrink: 0;
992
- padding: 20px 28px; /* 減少內距( 24px 32px)*/
993
  background: rgba(255, 255, 255, 0.96);
994
  border: 1.5px solid rgba(0, 0, 0, 0.06); /* 更細的邊框 */
995
- border-radius: 14px; /* 稍微減少圓角( 16px)*/
996
  backdrop-filter: blur(24px) saturate(180%);
997
- font-size: 18px; /* 小字體( 20px)*/
998
  font-weight: 400;
999
  color: #1A1A1A;
1000
  text-align: center;
1001
  line-height: 1.5; /* 稍微緊湊(原 1.6)*/
1002
- min-height: 72px; /* 減少最小高度( 80px)*/
1003
- max-height: 140px; /* 減少最大高度( 160px)*/
1004
  display: flex;
1005
  align-items: center;
1006
  justify-content: center;
@@ -1031,7 +1081,7 @@
1031
  background: rgba(255, 255, 255, 0.98);
1032
  white-space: pre-wrap;
1033
  word-break: break-word;
1034
- max-height: 140px; /* 與基礎樣式保持一致 */
1035
  }
1036
 
1037
  .voice-transcript.text-input-mode:focus {
@@ -1077,15 +1127,15 @@
1077
  @media (max-width: 768px) {
1078
  .voice-transcript-wrapper {
1079
  max-width: 100%; /* 手機版全寬 */
1080
- padding: 0 16px; /* 減少左右留白 */
1081
  }
1082
 
1083
  .voice-transcript {
1084
  font-size: 16px; /* 手機版縮小字體 */
1085
- padding: 16px 20px; /* 減少內距 */
1086
- min-height: 64px; /* 減少最小高度 */
1087
- max-height: 120px; /* 減少最大高度 */
1088
- border-radius: 12px; /* 減少圓角 */
1089
  }
1090
 
1091
  .input-mode-hint {
 
19
  color: #1A1A1A;
20
  overflow: hidden;
21
  -webkit-font-smoothing: antialiased;
22
+ /* 頁面載入淡入動畫 */
23
+ animation: pageEnter 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards;
24
+ }
25
+
26
+ @keyframes pageEnter {
27
+ from {
28
+ opacity: 0;
29
+ transform: scale(1.02);
30
+ }
31
+ to {
32
+ opacity: 1;
33
+ transform: scale(1);
34
+ }
35
  }
36
 
37
  /* === 控制面板 === */
 
131
  align-items: center;
132
  justify-content: center;
133
  background: #F5F1ED;
134
+ /* 初始載入動畫 */
135
+ animation: overlayEnter 0.8s cubic-bezier(0.4, 0, 0.2, 1) 0.2s backwards;
136
+ }
137
+
138
+ @keyframes overlayEnter {
139
+ from {
140
+ opacity: 0;
141
+ }
142
+ to {
143
+ opacity: 1;
144
+ }
145
  }
146
 
147
  /* === 動態漸變背景(移除光暈,改用極淺色情緒底色)=== */
 
230
  gap: clamp(36px, 6vh, 72px);
231
  width: min(88vw, 640px);
232
  padding: clamp(12px, 3vh, 28px) 0;
233
+ /* 元素依序淡入動畫 */
234
+ animation: containerEnter 0.8s cubic-bezier(0.4, 0, 0.2, 1) 0.4s backwards;
235
+ }
236
+
237
+ @keyframes containerEnter {
238
+ from {
239
+ opacity: 0;
240
+ transform: translateY(20px);
241
+ }
242
+ to {
243
+ opacity: 1;
244
+ transform: translateY(0);
245
+ }
246
  }
247
 
248
  /* === 波形與麥克風容器 === */
 
253
  display: flex;
254
  align-items: center;
255
  justify-content: center;
256
+ /* 蓮花綻放入場動畫 */
257
+ animation: lotusBloom 1.2s cubic-bezier(0.34, 1.56, 0.64, 1) 0.6s backwards;
258
+ }
259
+
260
+ @keyframes lotusBloom {
261
+ from {
262
+ opacity: 0;
263
+ transform: scale(0.8) rotate(-10deg);
264
+ }
265
+ to {
266
+ opacity: 1;
267
+ transform: scale(1) rotate(0deg);
268
+ }
269
  }
270
 
271
  /* Canvas 波形 */
 
1029
  .voice-transcript-wrapper {
1030
  position: relative;
1031
  width: 100%;
1032
+ max-width: 520px; /* 優化:縮小最大寬度(680px 520px,減少約 24%)*/
1033
  margin: 0 auto; /* 置中對齊 */
1034
+ padding: 0 16px; /* 優化:減少左右留白(20px → 16px)*/
1035
  display: flex;
1036
  justify-content: center;
1037
  }
 
1039
  .voice-transcript {
1040
  width: 100%; /* 填滿 wrapper(受 max-width 限制)*/
1041
  flex-shrink: 0;
1042
+ padding: 18px 24px; /* 優化:進一步減少內距(20px 28px → 18px 24px)*/
1043
  background: rgba(255, 255, 255, 0.96);
1044
  border: 1.5px solid rgba(0, 0, 0, 0.06); /* 更細的邊框 */
1045
+ border-radius: 12px; /* 優化:減少圓角(14px → 12px)*/
1046
  backdrop-filter: blur(24px) saturate(180%);
1047
+ font-size: 17px; /* 優化:稍微縮小字體(18px → 17px)*/
1048
  font-weight: 400;
1049
  color: #1A1A1A;
1050
  text-align: center;
1051
  line-height: 1.5; /* 稍微緊湊(原 1.6)*/
1052
+ min-height: 68px; /* 優化:減少最小高度(72px → 68px)*/
1053
+ max-height: 130px; /* 優化:減少最大高度(140px → 130px)*/
1054
  display: flex;
1055
  align-items: center;
1056
  justify-content: center;
 
1081
  background: rgba(255, 255, 255, 0.98);
1082
  white-space: pre-wrap;
1083
  word-break: break-word;
1084
+ max-height: 130px; /* 優化:與基礎樣式保持一致 */
1085
  }
1086
 
1087
  .voice-transcript.text-input-mode:focus {
 
1127
  @media (max-width: 768px) {
1128
  .voice-transcript-wrapper {
1129
  max-width: 100%; /* 手機版全寬 */
1130
+ padding: 0 14px; /* 優化:減少左右留白(16px → 14px)*/
1131
  }
1132
 
1133
  .voice-transcript {
1134
  font-size: 16px; /* 手機版縮小字體 */
1135
+ padding: 15px 18px; /* 優化:減少內距(16px 20px → 15px 18px)*/
1136
+ min-height: 62px; /* 優化:減少最小高度(64px → 62px)*/
1137
+ max-height: 115px; /* 優化:減少最大高度(120px → 115px)*/
1138
+ border-radius: 11px; /* 優化:減少圓角(12px → 11px)*/
1139
  }
1140
 
1141
  .input-mode-hint {
static/frontend/js/login.js CHANGED
@@ -9,6 +9,27 @@
9
  }
10
  })();
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  // ========== Google OAuth PKCE 登入流程 ==========
13
 
14
  /**
@@ -168,8 +189,8 @@ async function handleOAuthCallback() {
168
  console.log('✅ 登入成功!導向聊天室...');
169
  console.log('🔑 JWT Token 已存儲,長度:', data.access_token.length);
170
 
171
- // 導向聊天室
172
- window.location.href = '/static/index.html';
173
  } else {
174
  throw new Error('Token 交換失敗');
175
  }
@@ -338,11 +359,20 @@ const iosPermissionManager = new IOSPermissionManager();
338
 
339
  // ========== 頁面初始化 ==========
340
 
 
 
 
 
 
 
 
 
341
  // 檢查是否已登入
342
  const token = localStorage.getItem('jwt_token');
343
  if (token && !window.location.search.includes('code=')) {
344
- // 已登入,直接導向聊天室
345
- window.location.href = '/static/index.html';
 
346
  }
347
 
348
  // 檢查是否為 OAuth callback
@@ -458,10 +488,8 @@ class VoiceLoginManager {
458
  try { this.ws && this.ws.readyState === WebSocket.OPEN && this.ws.close(1000, 'voice login done'); } catch(_) {}
459
  this.cleanup();
460
 
461
- // 快速跳轉到聊天室(縮短等待體感更順)
462
- setTimeout(() => {
463
- window.location.href = '/static/index.html';
464
- }, 800);
465
 
466
  } else {
467
  console.error('❌ 語音登入失敗:', data.error);
 
9
  }
10
  })();
11
 
12
+ /**
13
+ * 平滑跳轉到聊天室(帶過渡動畫)
14
+ */
15
+ function smoothTransitionToChatRoom(delay = 800) {
16
+ console.log('🌸 開始平滑過渡到聊天室...');
17
+
18
+ // 顯示載入覆蓋層
19
+ const loadingOverlay = document.getElementById('loadingOverlay');
20
+ if (loadingOverlay) {
21
+ loadingOverlay.classList.add('active');
22
+ }
23
+
24
+ // 觸發頁面淡出動畫
25
+ document.body.classList.add('page-transitioning');
26
+
27
+ // 延遲跳轉,讓動畫完成
28
+ setTimeout(() => {
29
+ window.location.href = '/static/index.html';
30
+ }, delay);
31
+ }
32
+
33
  // ========== Google OAuth PKCE 登入流程 ==========
34
 
35
  /**
 
189
  console.log('✅ 登入成功!導向聊天室...');
190
  console.log('🔑 JWT Token 已存儲,長度:', data.access_token.length);
191
 
192
+ // 使用平滑過渡
193
+ smoothTransitionToChatRoom(600);
194
  } else {
195
  throw new Error('Token 交換失敗');
196
  }
 
359
 
360
  // ========== 頁面初始化 ==========
361
 
362
+ // 頁面載入時的淡入動畫
363
+ window.addEventListener('load', () => {
364
+ // 移除淡入動畫類別(避免重複觸發)
365
+ setTimeout(() => {
366
+ document.body.classList.remove('page-entering');
367
+ }, 500);
368
+ });
369
+
370
  // 檢查是否已登入
371
  const token = localStorage.getItem('jwt_token');
372
  if (token && !window.location.search.includes('code=')) {
373
+ // 已登入,使用平滑過渡導向聊天室
374
+ console.log('✅ 已登入,自動跳轉到聊天室');
375
+ smoothTransitionToChatRoom(400);
376
  }
377
 
378
  // 檢查是否為 OAuth callback
 
488
  try { this.ws && this.ws.readyState === WebSocket.OPEN && this.ws.close(1000, 'voice login done'); } catch(_) {}
489
  this.cleanup();
490
 
491
+ // 使用平滑過渡到聊天室
492
+ smoothTransitionToChatRoom(800);
 
 
493
 
494
  } else {
495
  console.error('❌ 語音登入失敗:', data.error);
static/frontend/login.html CHANGED
@@ -222,9 +222,211 @@
222
  .voice-status.active {
223
  display: block;
224
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  </style>
226
  </head>
227
- <body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  <!-- 背景裝飾鬱金香 -->
229
  <div class="tulip-decoration top-left">
230
  <svg viewBox="0 0 180 240" fill="none" xmlns="http://www.w3.org/2000/svg" width="80" height="100">
 
222
  .voice-status.active {
223
  display: block;
224
  }
225
+
226
+ /* === 頁面過渡動畫 === */
227
+ @keyframes fadeOut {
228
+ from {
229
+ opacity: 1;
230
+ transform: scale(1);
231
+ }
232
+ to {
233
+ opacity: 0;
234
+ transform: scale(0.98);
235
+ }
236
+ }
237
+
238
+ @keyframes fadeIn {
239
+ from {
240
+ opacity: 0;
241
+ transform: scale(1.02);
242
+ }
243
+ to {
244
+ opacity: 1;
245
+ transform: scale(1);
246
+ }
247
+ }
248
+
249
+ /* 頁面淡出狀態 */
250
+ body.page-transitioning {
251
+ animation: fadeOut 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
252
+ }
253
+
254
+ /* 頁面淡入狀態 */
255
+ body.page-entering {
256
+ animation: fadeIn 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards;
257
+ }
258
+
259
+ /* 載入覆蓋層 */
260
+ .loading-overlay {
261
+ position: fixed;
262
+ inset: 0;
263
+ background: linear-gradient(135deg, #FFF8E7 0%, #F5F1ED 100%);
264
+ display: flex;
265
+ flex-direction: column;
266
+ align-items: center;
267
+ justify-content: center;
268
+ z-index: 9999;
269
+ opacity: 0;
270
+ visibility: hidden;
271
+ transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),
272
+ visibility 0.3s cubic-bezier(0.4, 0, 0.2, 1);
273
+ }
274
+
275
+ .loading-overlay.active {
276
+ opacity: 1;
277
+ visibility: visible;
278
+ }
279
+
280
+ /* 蓮花載入動畫 */
281
+ .loading-lotus {
282
+ width: 80px;
283
+ height: 80px;
284
+ position: relative;
285
+ margin-bottom: 2rem;
286
+ }
287
+
288
+ .lotus-petal {
289
+ position: absolute;
290
+ width: 24px;
291
+ height: 40px;
292
+ background: linear-gradient(180deg,
293
+ rgba(255, 255, 255, 0.95) 0%,
294
+ rgba(249, 250, 251, 0.9) 100%
295
+ );
296
+ border: 1px solid rgba(0, 0, 0, 0.08);
297
+ border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
298
+ top: 50%;
299
+ left: 50%;
300
+ transform-origin: 50% 100%;
301
+ animation: petalPulse 1.6s ease-in-out infinite;
302
+ }
303
+
304
+ .lotus-petal:nth-child(1) {
305
+ transform: translate(-50%, -50%) rotate(0deg);
306
+ animation-delay: 0s;
307
+ }
308
+
309
+ .lotus-petal:nth-child(2) {
310
+ transform: translate(-50%, -50%) rotate(45deg);
311
+ animation-delay: 0.2s;
312
+ }
313
+
314
+ .lotus-petal:nth-child(3) {
315
+ transform: translate(-50%, -50%) rotate(90deg);
316
+ animation-delay: 0.4s;
317
+ }
318
+
319
+ .lotus-petal:nth-child(4) {
320
+ transform: translate(-50%, -50%) rotate(135deg);
321
+ animation-delay: 0.6s;
322
+ }
323
+
324
+ .lotus-petal:nth-child(5) {
325
+ transform: translate(-50%, -50%) rotate(180deg);
326
+ animation-delay: 0.8s;
327
+ }
328
+
329
+ .lotus-petal:nth-child(6) {
330
+ transform: translate(-50%, -50%) rotate(225deg);
331
+ animation-delay: 1.0s;
332
+ }
333
+
334
+ .lotus-petal:nth-child(7) {
335
+ transform: translate(-50%, -50%) rotate(270deg);
336
+ animation-delay: 1.2s;
337
+ }
338
+
339
+ .lotus-petal:nth-child(8) {
340
+ transform: translate(-50%, -50%) rotate(315deg);
341
+ animation-delay: 1.4s;
342
+ }
343
+
344
+ /* 花蕊 */
345
+ .lotus-core {
346
+ position: absolute;
347
+ top: 50%;
348
+ left: 50%;
349
+ transform: translate(-50%, -50%);
350
+ width: 16px;
351
+ height: 16px;
352
+ border-radius: 50%;
353
+ background: radial-gradient(circle at 30% 30%,
354
+ #FEF9E7 0%,
355
+ #FCD34D 50%,
356
+ #F59E0B 100%
357
+ );
358
+ box-shadow:
359
+ 0 2px 8px rgba(245, 158, 11, 0.4),
360
+ inset 0 1px 4px rgba(255, 255, 255, 0.6);
361
+ animation: coreBreath 1.6s ease-in-out infinite;
362
+ }
363
+
364
+ @keyframes petalPulse {
365
+ 0%, 100% {
366
+ opacity: 0.7;
367
+ transform: translate(-50%, -50%) rotate(var(--rotate, 0deg)) translateY(-5px);
368
+ }
369
+ 50% {
370
+ opacity: 1;
371
+ transform: translate(-50%, -50%) rotate(var(--rotate, 0deg)) translateY(-8px);
372
+ }
373
+ }
374
+
375
+ @keyframes coreBreath {
376
+ 0%, 100% {
377
+ transform: translate(-50%, -50%) scale(1);
378
+ box-shadow:
379
+ 0 2px 8px rgba(245, 158, 11, 0.4),
380
+ inset 0 1px 4px rgba(255, 255, 255, 0.6);
381
+ }
382
+ 50% {
383
+ transform: translate(-50%, -50%) scale(1.1);
384
+ box-shadow:
385
+ 0 3px 12px rgba(245, 158, 11, 0.6),
386
+ inset 0 1px 6px rgba(255, 255, 255, 0.8);
387
+ }
388
+ }
389
+
390
+ /* 載入文字 */
391
+ .loading-text {
392
+ font-family: 'Playfair Display', Georgia, serif;
393
+ font-size: 1.5rem;
394
+ color: #2C2C2C;
395
+ letter-spacing: 0.05em;
396
+ margin-bottom: 0.5rem;
397
+ animation: textFade 1.6s ease-in-out infinite;
398
+ }
399
+
400
+ .loading-subtext {
401
+ font-size: 0.875rem;
402
+ color: rgba(0, 0, 0, 0.5);
403
+ letter-spacing: 0.1em;
404
+ }
405
+
406
+ @keyframes textFade {
407
+ 0%, 100% { opacity: 0.6; }
408
+ 50% { opacity: 1; }
409
+ }
410
  </style>
411
  </head>
412
+ <body class="page-entering">
413
+ <!-- 載入覆蓋層 -->
414
+ <div class="loading-overlay" id="loadingOverlay">
415
+ <div class="loading-lotus">
416
+ <div class="lotus-petal" style="--rotate: 0deg;"></div>
417
+ <div class="lotus-petal" style="--rotate: 45deg;"></div>
418
+ <div class="lotus-petal" style="--rotate: 90deg;"></div>
419
+ <div class="lotus-petal" style="--rotate: 135deg;"></div>
420
+ <div class="lotus-petal" style="--rotate: 180deg;"></div>
421
+ <div class="lotus-petal" style="--rotate: 225deg;"></div>
422
+ <div class="lotus-petal" style="--rotate: 270deg;"></div>
423
+ <div class="lotus-petal" style="--rotate: 315deg;"></div>
424
+ <div class="lotus-core"></div>
425
+ </div>
426
+ <div class="loading-text">Bloom Ware</div>
427
+ <div class="loading-subtext">正在進入...</div>
428
+ </div>
429
+
430
  <!-- 背景裝飾鬱金香 -->
431
  <div class="tulip-decoration top-left">
432
  <svg viewBox="0 0 180 240" fill="none" xmlns="http://www.w3.org/2000/svg" width="80" height="100">