audex 1.0.7a3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. audex/__init__.py +9 -0
  2. audex/__main__.py +7 -0
  3. audex/cli/__init__.py +189 -0
  4. audex/cli/apis/__init__.py +12 -0
  5. audex/cli/apis/init/__init__.py +34 -0
  6. audex/cli/apis/init/gencfg.py +130 -0
  7. audex/cli/apis/init/setup.py +330 -0
  8. audex/cli/apis/init/vprgroup.py +125 -0
  9. audex/cli/apis/serve.py +141 -0
  10. audex/cli/args.py +356 -0
  11. audex/cli/exceptions.py +44 -0
  12. audex/cli/helper/__init__.py +0 -0
  13. audex/cli/helper/ansi.py +193 -0
  14. audex/cli/helper/display.py +288 -0
  15. audex/config/__init__.py +64 -0
  16. audex/config/core/__init__.py +30 -0
  17. audex/config/core/app.py +29 -0
  18. audex/config/core/audio.py +45 -0
  19. audex/config/core/logging.py +163 -0
  20. audex/config/core/session.py +11 -0
  21. audex/config/helper/__init__.py +1 -0
  22. audex/config/helper/client/__init__.py +1 -0
  23. audex/config/helper/client/http.py +28 -0
  24. audex/config/helper/client/websocket.py +21 -0
  25. audex/config/helper/provider/__init__.py +1 -0
  26. audex/config/helper/provider/dashscope.py +13 -0
  27. audex/config/helper/provider/unisound.py +18 -0
  28. audex/config/helper/provider/xfyun.py +23 -0
  29. audex/config/infrastructure/__init__.py +31 -0
  30. audex/config/infrastructure/cache.py +51 -0
  31. audex/config/infrastructure/database.py +48 -0
  32. audex/config/infrastructure/recorder.py +32 -0
  33. audex/config/infrastructure/store.py +19 -0
  34. audex/config/provider/__init__.py +18 -0
  35. audex/config/provider/transcription.py +109 -0
  36. audex/config/provider/vpr.py +99 -0
  37. audex/container.py +40 -0
  38. audex/entity/__init__.py +468 -0
  39. audex/entity/doctor.py +109 -0
  40. audex/entity/doctor.pyi +51 -0
  41. audex/entity/fields.py +401 -0
  42. audex/entity/segment.py +115 -0
  43. audex/entity/segment.pyi +38 -0
  44. audex/entity/session.py +133 -0
  45. audex/entity/session.pyi +47 -0
  46. audex/entity/utterance.py +142 -0
  47. audex/entity/utterance.pyi +48 -0
  48. audex/entity/vp.py +68 -0
  49. audex/entity/vp.pyi +35 -0
  50. audex/exceptions.py +157 -0
  51. audex/filters/__init__.py +692 -0
  52. audex/filters/generated/__init__.py +21 -0
  53. audex/filters/generated/doctor.py +987 -0
  54. audex/filters/generated/segment.py +723 -0
  55. audex/filters/generated/session.py +978 -0
  56. audex/filters/generated/utterance.py +939 -0
  57. audex/filters/generated/vp.py +815 -0
  58. audex/helper/__init__.py +1 -0
  59. audex/helper/hash.py +33 -0
  60. audex/helper/mixin.py +65 -0
  61. audex/helper/net.py +19 -0
  62. audex/helper/settings/__init__.py +830 -0
  63. audex/helper/settings/fields.py +317 -0
  64. audex/helper/stream.py +153 -0
  65. audex/injectors/__init__.py +1 -0
  66. audex/injectors/config.py +12 -0
  67. audex/injectors/lifespan.py +7 -0
  68. audex/lib/__init__.py +1 -0
  69. audex/lib/cache/__init__.py +383 -0
  70. audex/lib/cache/inmemory.py +513 -0
  71. audex/lib/database/__init__.py +83 -0
  72. audex/lib/database/sqlite.py +406 -0
  73. audex/lib/exporter.py +189 -0
  74. audex/lib/injectors/__init__.py +1 -0
  75. audex/lib/injectors/cache.py +25 -0
  76. audex/lib/injectors/container.py +47 -0
  77. audex/lib/injectors/exporter.py +26 -0
  78. audex/lib/injectors/recorder.py +33 -0
  79. audex/lib/injectors/server.py +17 -0
  80. audex/lib/injectors/session.py +18 -0
  81. audex/lib/injectors/sqlite.py +24 -0
  82. audex/lib/injectors/store.py +13 -0
  83. audex/lib/injectors/transcription.py +42 -0
  84. audex/lib/injectors/usb.py +12 -0
  85. audex/lib/injectors/vpr.py +65 -0
  86. audex/lib/injectors/wifi.py +7 -0
  87. audex/lib/recorder.py +844 -0
  88. audex/lib/repos/__init__.py +149 -0
  89. audex/lib/repos/container.py +23 -0
  90. audex/lib/repos/database/__init__.py +1 -0
  91. audex/lib/repos/database/sqlite.py +672 -0
  92. audex/lib/repos/decorators.py +74 -0
  93. audex/lib/repos/doctor.py +286 -0
  94. audex/lib/repos/segment.py +302 -0
  95. audex/lib/repos/session.py +285 -0
  96. audex/lib/repos/tables/__init__.py +70 -0
  97. audex/lib/repos/tables/doctor.py +137 -0
  98. audex/lib/repos/tables/segment.py +113 -0
  99. audex/lib/repos/tables/session.py +140 -0
  100. audex/lib/repos/tables/utterance.py +131 -0
  101. audex/lib/repos/tables/vp.py +102 -0
  102. audex/lib/repos/utterance.py +288 -0
  103. audex/lib/repos/vp.py +286 -0
  104. audex/lib/restful.py +251 -0
  105. audex/lib/server/__init__.py +97 -0
  106. audex/lib/server/auth.py +98 -0
  107. audex/lib/server/handlers.py +248 -0
  108. audex/lib/server/templates/index.html.j2 +226 -0
  109. audex/lib/server/templates/login.html.j2 +111 -0
  110. audex/lib/server/templates/static/script.js +68 -0
  111. audex/lib/server/templates/static/style.css +579 -0
  112. audex/lib/server/types.py +123 -0
  113. audex/lib/session.py +503 -0
  114. audex/lib/store/__init__.py +238 -0
  115. audex/lib/store/localfile.py +411 -0
  116. audex/lib/transcription/__init__.py +33 -0
  117. audex/lib/transcription/dashscope.py +525 -0
  118. audex/lib/transcription/events.py +62 -0
  119. audex/lib/usb.py +554 -0
  120. audex/lib/vpr/__init__.py +38 -0
  121. audex/lib/vpr/unisound/__init__.py +185 -0
  122. audex/lib/vpr/unisound/types.py +469 -0
  123. audex/lib/vpr/xfyun/__init__.py +483 -0
  124. audex/lib/vpr/xfyun/types.py +679 -0
  125. audex/lib/websocket/__init__.py +8 -0
  126. audex/lib/websocket/connection.py +485 -0
  127. audex/lib/websocket/pool.py +991 -0
  128. audex/lib/wifi.py +1146 -0
  129. audex/lifespan.py +75 -0
  130. audex/service/__init__.py +27 -0
  131. audex/service/decorators.py +73 -0
  132. audex/service/doctor/__init__.py +652 -0
  133. audex/service/doctor/const.py +36 -0
  134. audex/service/doctor/exceptions.py +96 -0
  135. audex/service/doctor/types.py +54 -0
  136. audex/service/export/__init__.py +236 -0
  137. audex/service/export/const.py +17 -0
  138. audex/service/export/exceptions.py +34 -0
  139. audex/service/export/types.py +21 -0
  140. audex/service/injectors/__init__.py +1 -0
  141. audex/service/injectors/container.py +53 -0
  142. audex/service/injectors/doctor.py +34 -0
  143. audex/service/injectors/export.py +27 -0
  144. audex/service/injectors/session.py +49 -0
  145. audex/service/session/__init__.py +754 -0
  146. audex/service/session/const.py +34 -0
  147. audex/service/session/exceptions.py +67 -0
  148. audex/service/session/types.py +91 -0
  149. audex/types.py +39 -0
  150. audex/utils.py +287 -0
  151. audex/valueobj/__init__.py +81 -0
  152. audex/valueobj/common/__init__.py +1 -0
  153. audex/valueobj/common/auth.py +84 -0
  154. audex/valueobj/common/email.py +16 -0
  155. audex/valueobj/common/ops.py +22 -0
  156. audex/valueobj/common/phone.py +84 -0
  157. audex/valueobj/common/version.py +72 -0
  158. audex/valueobj/session.py +19 -0
  159. audex/valueobj/utterance.py +15 -0
  160. audex/view/__init__.py +51 -0
  161. audex/view/container.py +17 -0
  162. audex/view/decorators.py +303 -0
  163. audex/view/pages/__init__.py +1 -0
  164. audex/view/pages/dashboard/__init__.py +286 -0
  165. audex/view/pages/dashboard/wifi.py +407 -0
  166. audex/view/pages/login.py +110 -0
  167. audex/view/pages/recording.py +348 -0
  168. audex/view/pages/register.py +202 -0
  169. audex/view/pages/sessions/__init__.py +196 -0
  170. audex/view/pages/sessions/details.py +224 -0
  171. audex/view/pages/sessions/export.py +443 -0
  172. audex/view/pages/settings.py +374 -0
  173. audex/view/pages/voiceprint/__init__.py +1 -0
  174. audex/view/pages/voiceprint/enroll.py +195 -0
  175. audex/view/pages/voiceprint/update.py +195 -0
  176. audex/view/static/css/dashboard.css +452 -0
  177. audex/view/static/css/glass.css +22 -0
  178. audex/view/static/css/global.css +541 -0
  179. audex/view/static/css/login.css +386 -0
  180. audex/view/static/css/recording.css +439 -0
  181. audex/view/static/css/register.css +293 -0
  182. audex/view/static/css/sessions/styles.css +501 -0
  183. audex/view/static/css/settings.css +186 -0
  184. audex/view/static/css/voiceprint/enroll.css +43 -0
  185. audex/view/static/css/voiceprint/styles.css +209 -0
  186. audex/view/static/css/voiceprint/update.css +44 -0
  187. audex/view/static/images/logo.svg +95 -0
  188. audex/view/static/js/recording.js +42 -0
  189. audex-1.0.7a3.dist-info/METADATA +361 -0
  190. audex-1.0.7a3.dist-info/RECORD +192 -0
  191. audex-1.0.7a3.dist-info/WHEEL +4 -0
  192. audex-1.0.7a3.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,68 @@
1
+ // Common utility functions
2
+
3
+ /**
4
+ * Make authenticated API request
5
+ */
6
+ async function apiRequest(url, options = {}) {
7
+ const response = await fetch(url, {
8
+ ...options,
9
+ credentials: 'same-origin',
10
+ });
11
+
12
+ // Handle 401 Unauthorized
13
+ if (response.status === 401) {
14
+ window.location.href = '/login';
15
+ throw new Error('Unauthorized');
16
+ }
17
+
18
+ return response;
19
+ }
20
+
21
+ /**
22
+ * Download file from blob
23
+ */
24
+ function downloadBlob(blob, filename) {
25
+ const url = window.URL. createObjectURL(blob);
26
+ const a = document.createElement('a');
27
+ a.href = url;
28
+ a.download = filename;
29
+ document.body.appendChild(a);
30
+ a.click();
31
+ window.URL.revokeObjectURL(url);
32
+ document.body.removeChild(a);
33
+ }
34
+
35
+ /**
36
+ * Show toast notification
37
+ */
38
+ function showToast(message, type = 'info') {
39
+ // Simple alert for now, can be enhanced
40
+ if (type === 'error') {
41
+ alert('错误: ' + message);
42
+ } else {
43
+ console.log(message);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Escape HTML to prevent XSS
49
+ */
50
+ function escapeHtml(text) {
51
+ const div = document. createElement('div');
52
+ div. textContent = text;
53
+ return div.innerHTML;
54
+ }
55
+
56
+ /**
57
+ * Format ISO date string to localized format
58
+ */
59
+ function formatDate(isoString) {
60
+ const date = new Date(isoString);
61
+ return date. toLocaleString('zh-CN', {
62
+ year: 'numeric',
63
+ month: '2-digit',
64
+ day: '2-digit',
65
+ hour: '2-digit',
66
+ minute: '2-digit'
67
+ });
68
+ }
@@ -0,0 +1,579 @@
1
+ /* ==================== CSS Variables ==================== */
2
+ :root {
3
+ --color-primary: #007aff;
4
+ --color-primary-hover: #0051d5;
5
+ --color-text: #1d1d1f;
6
+ --color-text-secondary: #86868b;
7
+ --color-text-light: #a1a1a6;
8
+ --color-border: #d2d2d7;
9
+ --color-bg: #ffffff;
10
+ --color-bg-secondary: #f5f5f7;
11
+ --color-success: #34c759;
12
+ --color-warning: #ff9500;
13
+ --color-error: #ff3b30;
14
+ --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.04);
15
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
16
+ --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12);
17
+ --shadow-card: 0 2px 8px rgba(0, 0, 0, 0.06);
18
+ --shadow-card-hover: 0 8px 20px rgba(0, 0, 0, 0.12);
19
+ --radius-full: 980px;
20
+ --transition: all 0.2s ease;
21
+ --font-mono: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace;
22
+ }
23
+
24
+ /* ==================== Reset & Base ==================== */
25
+ * {
26
+ margin: 0;
27
+ padding: 0;
28
+ box-sizing: border-box;
29
+ }
30
+
31
+ body {
32
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'SF Pro Display',
33
+ 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
34
+ background: var(--color-bg);
35
+ color: var(--color-text);
36
+ line-height: 1.5;
37
+ font-size: 14px;
38
+ -webkit-font-smoothing: antialiased;
39
+ -moz-osx-font-smoothing: grayscale;
40
+ }
41
+
42
+ /* ==================== Login Page ==================== */
43
+ .login-body {
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: center;
47
+ min-height: 100vh;
48
+ background: var(--color-bg);
49
+ }
50
+
51
+ .login-container {
52
+ width: 100%;
53
+ max-width: 380px;
54
+ padding: 20px;
55
+ }
56
+
57
+ .login-card {
58
+ background: var(--color-bg);
59
+ padding: 40px;
60
+ animation: slideUp 0.5s ease;
61
+ }
62
+
63
+ @keyframes slideUp {
64
+ from {
65
+ opacity: 0;
66
+ transform: translateY(20px);
67
+ }
68
+ to {
69
+ opacity: 1;
70
+ transform: translateY(0);
71
+ }
72
+ }
73
+
74
+ .login-header {
75
+ text-align: center;
76
+ margin-bottom: 32px;
77
+ }
78
+
79
+ .login-header h1 {
80
+ font-size: 24px;
81
+ font-weight: 600;
82
+ color: var(--color-text);
83
+ margin-bottom: 8px;
84
+ letter-spacing: -0.02em;
85
+ }
86
+
87
+ .login-header p {
88
+ font-size: 14px;
89
+ color: var(--color-text-secondary);
90
+ font-weight: 400;
91
+ }
92
+
93
+ .login-form {
94
+ display: flex;
95
+ flex-direction: column;
96
+ gap: 20px;
97
+ }
98
+
99
+ .form-group {
100
+ display: flex;
101
+ flex-direction: column;
102
+ gap: 8px;
103
+ }
104
+
105
+ .form-group label {
106
+ font-size: 13px;
107
+ font-weight: 500;
108
+ color: var(--color-text);
109
+ }
110
+
111
+ .form-group input {
112
+ padding: 12px 16px;
113
+ border: none;
114
+ border-radius: var(--radius-full);
115
+ font-size: 14px;
116
+ transition: var(--transition);
117
+ background: var(--color-bg-secondary);
118
+ font-family: inherit;
119
+ }
120
+
121
+ .form-group input::placeholder {
122
+ color: var(--color-text-light);
123
+ }
124
+
125
+ .form-group input:focus {
126
+ outline: none;
127
+ background: #ebebf0;
128
+ }
129
+
130
+ .error-message {
131
+ padding: 12px 16px;
132
+ background: rgba(255, 59, 48, 0.08);
133
+ color: var(--color-error);
134
+ border-radius: var(--radius-full);
135
+ font-size: 13px;
136
+ animation: shake 0.3s ease;
137
+ }
138
+
139
+ @keyframes shake {
140
+ 0%, 100% { transform: translateX(0); }
141
+ 10%, 30%, 50%, 70%, 90% { transform: translateX(-3px); }
142
+ 20%, 40%, 60%, 80% { transform: translateX(3px); }
143
+ }
144
+
145
+ /* ==================== Main Page ==================== */
146
+ .container {
147
+ max-width: 1600px;
148
+ margin: 0 auto;
149
+ padding: 48px 32px 32px;
150
+ animation: fadeIn 0.4s ease;
151
+ }
152
+
153
+ @keyframes fadeIn {
154
+ from { opacity: 0; }
155
+ to { opacity: 1; }
156
+ }
157
+
158
+ .page-header {
159
+ margin-bottom: 10px;
160
+ }
161
+
162
+ .header-content {
163
+ display: flex;
164
+ justify-content: space-between;
165
+ align-items: flex-start;
166
+ margin-bottom: 6px;
167
+ }
168
+
169
+ .header-left h1 {
170
+ font-size: 32px;
171
+ font-weight: 600;
172
+ color: var(--color-text);
173
+ letter-spacing: -0.02em;
174
+ line-height: 1.2;
175
+ }
176
+
177
+ .session-count {
178
+ font-size: 15px;
179
+ color: var(--color-text-secondary);
180
+ font-weight: 400;
181
+ margin-bottom: 32px;
182
+ }
183
+
184
+ .header-right {
185
+ display: flex;
186
+ align-items: center;
187
+ gap: 16px;
188
+ }
189
+
190
+ .doctor-name {
191
+ font-size: 14px;
192
+ color: var(--color-text-secondary);
193
+ font-weight: 400;
194
+ }
195
+
196
+ .actions-bar {
197
+ display: flex;
198
+ gap: 10px;
199
+ margin-bottom: 32px;
200
+ flex-wrap: wrap;
201
+ }
202
+
203
+ /* ==================== Buttons ==================== */
204
+ .btn {
205
+ padding: 8px 18px;
206
+ border-radius: var(--radius-full);
207
+ border: none;
208
+ font-size: 14px;
209
+ font-weight: 400;
210
+ cursor: pointer;
211
+ transition: var(--transition);
212
+ display: inline-flex;
213
+ align-items: center;
214
+ justify-content: center;
215
+ gap: 6px;
216
+ font-family: inherit;
217
+ }
218
+
219
+ .btn:disabled {
220
+ opacity: 0.4;
221
+ cursor: not-allowed;
222
+ }
223
+
224
+ .btn-primary {
225
+ background: var(--color-primary);
226
+ color: white;
227
+ }
228
+
229
+ .btn-primary:hover:not(:disabled) {
230
+ background: var(--color-primary-hover);
231
+ }
232
+
233
+ .btn-secondary {
234
+ background: var(--color-bg-secondary);
235
+ color: var(--color-text);
236
+ }
237
+
238
+ .btn-secondary:hover:not(:disabled) {
239
+ background: #e8e8ed;
240
+ }
241
+
242
+ .btn-ghost {
243
+ background: transparent;
244
+ color: var(--color-primary);
245
+ }
246
+
247
+ .btn-ghost:hover {
248
+ color: var(--color-primary-hover);
249
+ }
250
+
251
+ .btn-ghost:active {
252
+ color: #0040a8;
253
+ }
254
+
255
+ .btn-block {
256
+ width: 100%;
257
+ padding: 11px 18px;
258
+ }
259
+
260
+ /* ==================== Sessions Grid ==================== */
261
+ .sessions-grid {
262
+ display: grid;
263
+ grid-template-columns: repeat(4, 1fr);
264
+ gap: 16px;
265
+ }
266
+
267
+ .session-card {
268
+ background: var(--color-bg);
269
+ border-radius: 16px;
270
+ padding: 20px;
271
+ border: none;
272
+ box-shadow: var(--shadow-card);
273
+ transition: var(--transition);
274
+ animation: cardIn 0.4s ease;
275
+ }
276
+
277
+ @keyframes cardIn {
278
+ from {
279
+ opacity: 0;
280
+ transform: translateY(10px);
281
+ }
282
+ to {
283
+ opacity: 1;
284
+ transform: translateY(0);
285
+ }
286
+ }
287
+
288
+ .session-card:hover {
289
+ box-shadow: var(--shadow-card-hover);
290
+ }
291
+
292
+ .session-header {
293
+ display: flex;
294
+ align-items: start;
295
+ gap: 12px;
296
+ margin-bottom: 16px;
297
+ }
298
+
299
+ .checkbox-wrapper {
300
+ position: relative;
301
+ flex-shrink: 0;
302
+ margin-top: 2px;
303
+ }
304
+
305
+ .checkbox {
306
+ width: 18px;
307
+ height: 18px;
308
+ cursor: pointer;
309
+ opacity: 0;
310
+ position: absolute;
311
+ z-index: 2;
312
+ }
313
+
314
+ .checkbox-custom {
315
+ width: 18px;
316
+ height: 18px;
317
+ border: none;
318
+ border-radius: 50%;
319
+ display: flex;
320
+ align-items: center;
321
+ justify-content: center;
322
+ transition: var(--transition);
323
+ background: var(--color-bg-secondary);
324
+ position: relative;
325
+ }
326
+
327
+ .checkbox:checked + .checkbox-custom {
328
+ background: var(--color-primary);
329
+ }
330
+
331
+ .checkbox:checked + .checkbox-custom::after {
332
+ content: '✓';
333
+ color: white;
334
+ font-size: 11px;
335
+ font-weight: 600;
336
+ }
337
+
338
+ .checkbox:hover + .checkbox-custom {
339
+ background: #e8e8ed;
340
+ }
341
+
342
+ .checkbox:checked:hover + .checkbox-custom {
343
+ background: var(--color-primary-hover);
344
+ }
345
+
346
+ .session-info {
347
+ flex: 1;
348
+ min-width: 0;
349
+ }
350
+
351
+ .session-info h3 {
352
+ font-size: 16px;
353
+ font-weight: 600;
354
+ color: var(--color-text);
355
+ margin-bottom: 10px;
356
+ letter-spacing: -0.01em;
357
+ white-space: nowrap;
358
+ overflow: hidden;
359
+ text-overflow: ellipsis;
360
+ }
361
+
362
+ .session-meta {
363
+ font-size: 13px;
364
+ color: var(--color-text-secondary);
365
+ margin-bottom: 4px;
366
+ line-height: 1.4;
367
+ white-space: nowrap;
368
+ overflow: hidden;
369
+ text-overflow: ellipsis;
370
+ }
371
+
372
+ .status-badge {
373
+ display: inline-flex;
374
+ align-items: center;
375
+ gap: 5px;
376
+ padding: 4px 10px;
377
+ border-radius: 10px;
378
+ font-size: 11px;
379
+ font-weight: 400;
380
+ margin-top: 10px;
381
+ font-family: var(--font-mono);
382
+ letter-spacing: -0.01em;
383
+ }
384
+
385
+ .status-badge::before {
386
+ content: '';
387
+ width: 5px;
388
+ height: 5px;
389
+ border-radius: 50%;
390
+ }
391
+
392
+ .status-completed {
393
+ background: rgba(52, 199, 89, 0.12);
394
+ color: #248a3d;
395
+ }
396
+
397
+ .status-completed::before {
398
+ background: #248a3d;
399
+ }
400
+
401
+ .status-in-progress {
402
+ background: rgba(255, 149, 0, 0.12);
403
+ color: #c27200;
404
+ }
405
+
406
+ .status-in-progress::before {
407
+ background: #c27200;
408
+ }
409
+
410
+ .status-draft {
411
+ background: rgba(142, 142, 147, 0.12);
412
+ color: #6e6e73;
413
+ }
414
+
415
+ .status-draft::before {
416
+ background: #6e6e73;
417
+ }
418
+
419
+ .status-cancelled {
420
+ background: rgba(255, 59, 48, 0.12);
421
+ color: #c92a2a;
422
+ }
423
+
424
+ .status-cancelled::before {
425
+ background: #c92a2a;
426
+ }
427
+
428
+ .export-btn {
429
+ width: 100%;
430
+ margin-top: 16px;
431
+ padding: 10px;
432
+ background: var(--color-bg-secondary);
433
+ color: var(--color-text);
434
+ border: none;
435
+ border-radius: var(--radius-full);
436
+ font-size: 14px;
437
+ font-weight: 400;
438
+ cursor: pointer;
439
+ transition: var(--transition);
440
+ font-family: inherit;
441
+ }
442
+
443
+ .export-btn:hover {
444
+ background: var(--color-primary);
445
+ color: white;
446
+ }
447
+
448
+ /* ==================== States ==================== */
449
+ .loading,
450
+ .empty-state,
451
+ .error-state {
452
+ text-align: center;
453
+ padding: 80px 20px;
454
+ color: var(--color-text-secondary);
455
+ grid-column: 1 / -1;
456
+ }
457
+
458
+ .loading {
459
+ display: flex;
460
+ flex-direction: column;
461
+ align-items: center;
462
+ gap: 16px;
463
+ }
464
+
465
+ .spinner {
466
+ width: 32px;
467
+ height: 32px;
468
+ border: 2px solid var(--color-border);
469
+ border-top-color: var(--color-primary);
470
+ border-radius: 50%;
471
+ animation: spin 0.8s linear infinite;
472
+ }
473
+
474
+ @keyframes spin {
475
+ to { transform: rotate(360deg); }
476
+ }
477
+
478
+ .empty-state {
479
+ display: flex;
480
+ flex-direction: column;
481
+ align-items: center;
482
+ gap: 12px;
483
+ }
484
+
485
+ .empty-state p {
486
+ font-size: 15px;
487
+ color: var(--color-text-secondary);
488
+ }
489
+
490
+ .error-state {
491
+ color: var(--color-error);
492
+ }
493
+
494
+ /* ==================== Selected Count Badge ==================== */
495
+ .count-badge {
496
+ display: inline-flex;
497
+ align-items: center;
498
+ justify-content: center;
499
+ min-width: 20px;
500
+ height: 20px;
501
+ padding: 0 6px;
502
+ background: rgba(255, 255, 255, 0.3);
503
+ color: white;
504
+ border-radius: 10px;
505
+ font-size: 12px;
506
+ font-weight: 600;
507
+ }
508
+
509
+ /* ==================== Responsive ==================== */
510
+ @media (max-width: 1400px) {
511
+ .sessions-grid {
512
+ grid-template-columns: repeat(3, 1fr);
513
+ }
514
+ }
515
+
516
+ @media (max-width: 1024px) {
517
+ .container {
518
+ padding: 40px 24px 24px;
519
+ }
520
+
521
+ .header-left h1 {
522
+ font-size: 28px;
523
+ }
524
+
525
+ .sessions-grid {
526
+ grid-template-columns: repeat(2, 1fr);
527
+ }
528
+ }
529
+
530
+ @media (max-width: 768px) {
531
+ .container {
532
+ padding: 32px 20px 20px;
533
+ }
534
+
535
+ .header-content {
536
+ flex-direction: column;
537
+ gap: 12px;
538
+ }
539
+
540
+ .header-left h1 {
541
+ font-size: 24px;
542
+ }
543
+
544
+ .session-count {
545
+ font-size: 14px;
546
+ margin-bottom: 24px;
547
+ }
548
+
549
+ .sessions-grid {
550
+ grid-template-columns: 1fr;
551
+ }
552
+
553
+ .actions-bar {
554
+ flex-direction: column;
555
+ }
556
+
557
+ .btn {
558
+ width: 100%;
559
+ }
560
+ }
561
+
562
+ /* ==================== Scrollbar ==================== */
563
+ ::-webkit-scrollbar {
564
+ width: 8px;
565
+ height: 8px;
566
+ }
567
+
568
+ ::-webkit-scrollbar-track {
569
+ background: transparent;
570
+ }
571
+
572
+ ::-webkit-scrollbar-thumb {
573
+ background: var(--color-border);
574
+ border-radius: 4px;
575
+ }
576
+
577
+ ::-webkit-scrollbar-thumb:hover {
578
+ background: var(--color-text-light);
579
+ }