@jtalk22/slack-mcp 3.1.0 → 3.2.0

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 (65) hide show
  1. package/README.md +45 -13
  2. package/docs/SETUP.md +64 -29
  3. package/docs/TROUBLESHOOTING.md +28 -0
  4. package/lib/handlers.js +156 -0
  5. package/lib/slack-client.js +11 -3
  6. package/lib/token-store.js +6 -5
  7. package/lib/tools.js +131 -0
  8. package/package.json +15 -8
  9. package/public/index.html +10 -6
  10. package/public/share.html +6 -5
  11. package/scripts/setup-wizard.js +1 -1
  12. package/server.json +8 -2
  13. package/src/server-http.js +16 -1
  14. package/src/server.js +31 -7
  15. package/src/web-server.js +117 -4
  16. package/docs/CLOUDFLARE-BROWSER-TOOLKIT.md +0 -67
  17. package/docs/COMMUNICATION-STYLE.md +0 -66
  18. package/docs/COMPATIBILITY.md +0 -19
  19. package/docs/DEPLOYMENT-MODES.md +0 -55
  20. package/docs/HN-LAUNCH.md +0 -72
  21. package/docs/INDEX.md +0 -41
  22. package/docs/INSTALL-PROOF.md +0 -18
  23. package/docs/LAUNCH-COPY-v3.0.0.md +0 -101
  24. package/docs/LAUNCH-MATRIX.md +0 -22
  25. package/docs/LAUNCH-OPS.md +0 -71
  26. package/docs/RELEASE-HEALTH.md +0 -77
  27. package/docs/SUPPORT-BOUNDARIES.md +0 -49
  28. package/docs/USE_CASE_RECIPES.md +0 -69
  29. package/docs/WEB-API.md +0 -303
  30. package/docs/images/demo-channel-messages.png +0 -0
  31. package/docs/images/demo-channels.png +0 -0
  32. package/docs/images/demo-claude-mobile-360x800.png +0 -0
  33. package/docs/images/demo-claude-mobile-390x844.png +0 -0
  34. package/docs/images/demo-claude-mobile-poster.png +0 -0
  35. package/docs/images/demo-main-mobile-360x800.png +0 -0
  36. package/docs/images/demo-main-mobile-390x844.png +0 -0
  37. package/docs/images/demo-main.png +0 -0
  38. package/docs/images/demo-messages.png +0 -0
  39. package/docs/images/demo-poster.png +0 -0
  40. package/docs/images/demo-sidebar.png +0 -0
  41. package/docs/images/diagram-oauth-comparison.svg +0 -80
  42. package/docs/images/diagram-session-flow.svg +0 -105
  43. package/docs/images/social-preview-v3.png +0 -0
  44. package/docs/images/web-api-mobile-360x800.png +0 -0
  45. package/docs/images/web-api-mobile-390x844.png +0 -0
  46. package/public/demo-claude.html +0 -1974
  47. package/public/demo-video.html +0 -244
  48. package/public/demo.html +0 -1196
  49. package/scripts/build-mobile-demo.js +0 -168
  50. package/scripts/build-release-health-delta.js +0 -201
  51. package/scripts/build-social-preview.js +0 -189
  52. package/scripts/capture-screenshots.js +0 -152
  53. package/scripts/check-owner-attribution.sh +0 -131
  54. package/scripts/check-public-language.sh +0 -26
  55. package/scripts/check-version-parity.js +0 -218
  56. package/scripts/cloudflare-browser-tool.js +0 -237
  57. package/scripts/collect-release-health.js +0 -162
  58. package/scripts/impact-push-v3.js +0 -781
  59. package/scripts/record-demo.js +0 -163
  60. package/scripts/release-preflight.js +0 -247
  61. package/scripts/setup-git-hooks.sh +0 -15
  62. package/scripts/update-github-social-preview.js +0 -208
  63. package/scripts/verify-core.js +0 -159
  64. package/scripts/verify-install-flow.js +0 -193
  65. package/scripts/verify-web.js +0 -273
@@ -1,1974 +0,0 @@
1
- <!--
2
- Slack MCP Server - Claude Desktop Demo
3
- Author: @jtalk22
4
- Repository: https://github.com/jtalk22/slack-mcp-server
5
- License: MIT
6
- Created: January 2026
7
- -->
8
- <!DOCTYPE html>
9
- <html lang="en">
10
- <head>
11
- <meta charset="UTF-8">
12
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
13
- <meta name="author" content="@jtalk22">
14
- <title>Slack MCP Server - Claude Desktop Demo</title>
15
- <meta name="description" content="Session-based Slack access for Claude with your existing workspace permissions. Demo workflows for DMs, channels, search, and threads.">
16
-
17
- <!-- Open Graph -->
18
- <meta property="og:title" content="Slack MCP Server - Session-Based Slack Access Demo">
19
- <meta property="og:description" content="Session-based Slack access for Claude with your existing workspace permissions. Search, thread, and messaging workflows.">
20
- <meta property="og:type" content="website">
21
- <meta property="og:url" content="https://jtalk22.github.io/slack-mcp-server/public/demo-claude.html">
22
- <meta property="og:image" content="https://jtalk22.github.io/slack-mcp-server/docs/images/social-preview-v3.png">
23
-
24
- <!-- Twitter Card -->
25
- <meta name="twitter:card" content="summary_large_image">
26
- <meta name="twitter:title" content="Slack MCP Server - Session-Based Slack Access Demo">
27
- <meta name="twitter:description" content="Session-based Slack access for Claude with your existing workspace permissions. Search, thread, and messaging workflows.">
28
- <meta name="twitter:image" content="https://jtalk22.github.io/slack-mcp-server/docs/images/social-preview-v3.png">
29
-
30
- <!-- Theme -->
31
- <meta name="theme-color" content="#1a1a1a">
32
- <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>💬</text></svg>">
33
- <style>
34
- @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=Space+Grotesk:wght@500;600;700&display=swap');
35
-
36
- /* ═══════════════════════════════════════════════════════════════
37
- Claude Desktop Color Palette (Dark Mode)
38
- ═══════════════════════════════════════════════════════════════ */
39
- :root {
40
- --font-heading: "Space Grotesk", "Avenir Next", "Segoe UI", sans-serif;
41
- --font-body: "IBM Plex Sans", "Inter", "Segoe UI", sans-serif;
42
-
43
- /* Window chrome */
44
- --window-bg: #1a1a1a;
45
- --window-chrome: #2d2d2d;
46
- --window-border: #3a3a3a;
47
- --traffic-red: #ff5f57;
48
- --traffic-yellow: #febc2e;
49
- --traffic-green: #28c840;
50
-
51
- /* Messages */
52
- --user-bubble-bg: #3b3b3b;
53
- --claude-bubble-bg: #2a2a2a;
54
- --claude-orange: #da7756;
55
-
56
- /* Tool calls */
57
- --tool-box-bg: #1f1f1f;
58
- --tool-box-border: #3a3a3a;
59
- --tool-header-bg: #252525;
60
- --tool-name-color: #a0a0a0;
61
-
62
- /* Text */
63
- --text-primary: #ffffff;
64
- --text-secondary: #b0b0b0;
65
- --text-muted: #666666;
66
-
67
- /* Accents */
68
- --link-color: #6eb5ff;
69
- --code-bg: #2d2d2d;
70
- --code-text: #e6e6e6;
71
- --success-color: #28c840;
72
- --warning-color: #febc2e;
73
-
74
- /* Brand DNA (subliminal) */
75
- --text-warm: #E8E4DF;
76
-
77
- /* Shadows */
78
- --shadow-lg: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
79
- }
80
-
81
- /* ═══════════════════════════════════════════════════════════════
82
- Typography (SF Pro / System)
83
- ═══════════════════════════════════════════════════════════════ */
84
- * {
85
- margin: 0;
86
- padding: 0;
87
- box-sizing: border-box;
88
- }
89
-
90
- body {
91
- font-family: var(--font-body);
92
- font-size: 15px;
93
- line-height: 1.5;
94
- -webkit-font-smoothing: antialiased;
95
- -moz-osx-font-smoothing: grayscale;
96
- background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
97
- min-height: 100vh;
98
- display: flex;
99
- flex-direction: column;
100
- align-items: center;
101
- justify-content: center;
102
- padding: 20px;
103
- color: var(--text-primary);
104
- }
105
-
106
- .mono {
107
- font-family: "SF Mono", "Menlo", "Monaco", monospace;
108
- }
109
-
110
- /* ═══════════════════════════════════════════════════════════════
111
- Page Header
112
- ═══════════════════════════════════════════════════════════════ */
113
- .page-header {
114
- text-align: center;
115
- margin-bottom: 24px;
116
- }
117
-
118
- .page-header h1 {
119
- font-size: 28px;
120
- font-weight: 600;
121
- margin-bottom: 8px;
122
- display: flex;
123
- align-items: center;
124
- justify-content: center;
125
- gap: 12px;
126
- font-family: var(--font-heading);
127
- letter-spacing: -0.02em;
128
- }
129
-
130
- .page-header p {
131
- color: var(--text-secondary);
132
- font-size: 16px;
133
- }
134
- .cta-strip {
135
- width: 100%;
136
- max-width: 960px;
137
- margin-bottom: 14px;
138
- background: rgba(15, 52, 96, 0.72);
139
- border: 1px solid rgba(255, 255, 255, 0.15);
140
- border-radius: 12px;
141
- padding: 10px 12px;
142
- display: flex;
143
- justify-content: space-between;
144
- align-items: center;
145
- gap: 10px;
146
- flex-wrap: wrap;
147
- font-size: 13px;
148
- }
149
- .cta-strip .links {
150
- display: flex;
151
- gap: 8px;
152
- flex-wrap: wrap;
153
- }
154
- .cta-strip .links a {
155
- color: #d8efff;
156
- text-decoration: none;
157
- border: 1px solid rgba(255, 255, 255, 0.25);
158
- border-radius: 999px;
159
- padding: 4px 8px;
160
- }
161
- .cta-strip .links a:hover {
162
- background: rgba(255, 255, 255, 0.08);
163
- }
164
- .cta-strip .note {
165
- color: rgba(255, 255, 255, 0.78);
166
- }
167
- .cta-strip .note a {
168
- color: #9ee7ff;
169
- text-decoration: underline;
170
- }
171
-
172
- .badge {
173
- display: inline-flex;
174
- align-items: center;
175
- gap: 6px;
176
- background: rgba(218, 119, 86, 0.2);
177
- color: var(--claude-orange);
178
- padding: 4px 10px;
179
- border-radius: 12px;
180
- font-size: 12px;
181
- font-weight: 500;
182
- }
183
-
184
- /* ═══════════════════════════════════════════════════════════════
185
- Scenario Selector
186
- ═══════════════════════════════════════════════════════════════ */
187
- .scenario-bar {
188
- display: flex;
189
- gap: 12px;
190
- margin-bottom: 24px;
191
- flex-wrap: wrap;
192
- justify-content: center;
193
- width: min(100%, 960px);
194
- }
195
-
196
- .scenario-btn {
197
- display: flex;
198
- align-items: center;
199
- gap: 8px;
200
- padding: 12px 20px;
201
- background: rgba(255, 255, 255, 0.05);
202
- border: 1px solid rgba(255, 255, 255, 0.1);
203
- border-radius: 12px;
204
- color: var(--text-secondary);
205
- cursor: pointer;
206
- transition: all 0.2s ease;
207
- font-size: 14px;
208
- font-weight: 500;
209
- }
210
-
211
- .scenario-btn:hover {
212
- background: rgba(255, 255, 255, 0.1);
213
- border-color: rgba(255, 255, 255, 0.2);
214
- color: var(--text-primary);
215
- transform: translateY(-2px);
216
- }
217
-
218
- .scenario-btn.active {
219
- background: rgba(218, 119, 86, 0.2);
220
- border-color: var(--claude-orange);
221
- color: var(--claude-orange);
222
- }
223
-
224
- .scenario-btn.playing {
225
- animation: pulse-border 1.5s ease-in-out infinite;
226
- }
227
-
228
- @keyframes pulse-border {
229
- 0%, 100% { box-shadow: 0 0 0 0 rgba(218, 119, 86, 0.4); }
230
- 50% { box-shadow: 0 0 0 4px rgba(218, 119, 86, 0); }
231
- }
232
-
233
- .scenario-btn .icon {
234
- font-size: 18px;
235
- }
236
-
237
- .replay-btn {
238
- display: flex;
239
- align-items: center;
240
- gap: 6px;
241
- padding: 10px 16px;
242
- background: rgba(110, 181, 255, 0.1);
243
- border: 1px solid rgba(110, 181, 255, 0.3);
244
- border-radius: 12px;
245
- color: var(--link-color);
246
- cursor: pointer;
247
- transition: all 0.2s ease;
248
- font-size: 13px;
249
- font-weight: 500;
250
- }
251
-
252
- .replay-btn:hover {
253
- background: rgba(110, 181, 255, 0.2);
254
- transform: translateY(-2px);
255
- }
256
-
257
- .replay-btn:disabled {
258
- opacity: 0.5;
259
- cursor: not-allowed;
260
- transform: none;
261
- }
262
-
263
- .controls-bar {
264
- display: flex;
265
- gap: 12px;
266
- margin-bottom: 16px;
267
- align-items: center;
268
- flex-wrap: wrap;
269
- justify-content: center;
270
- width: min(100%, 920px);
271
- }
272
-
273
- .speed-control {
274
- display: flex;
275
- align-items: center;
276
- gap: 8px;
277
- color: var(--text-muted);
278
- font-size: 12px;
279
- }
280
-
281
- .speed-control select {
282
- background: rgba(255, 255, 255, 0.05);
283
- border: 1px solid rgba(255, 255, 255, 0.1);
284
- border-radius: 6px;
285
- color: var(--text-secondary);
286
- padding: 6px 10px;
287
- font-size: 12px;
288
- cursor: pointer;
289
- }
290
-
291
- .speed-control select:focus {
292
- outline: none;
293
- border-color: var(--link-color);
294
- }
295
-
296
- .progress-indicator {
297
- display: flex;
298
- align-items: center;
299
- gap: 8px;
300
- color: var(--text-muted);
301
- font-size: 12px;
302
- }
303
-
304
- .progress-bar {
305
- width: 80px;
306
- height: 4px;
307
- background: rgba(255, 255, 255, 0.1);
308
- border-radius: 2px;
309
- overflow: hidden;
310
- }
311
-
312
- .progress-fill {
313
- height: 100%;
314
- background: var(--success-color);
315
- border-radius: 2px;
316
- transition: width 0.3s ease;
317
- }
318
-
319
- .share-btn {
320
- display: flex;
321
- align-items: center;
322
- gap: 6px;
323
- padding: 8px 14px;
324
- background: rgba(255, 255, 255, 0.05);
325
- border: 1px solid rgba(255, 255, 255, 0.1);
326
- border-radius: 8px;
327
- color: var(--text-secondary);
328
- cursor: pointer;
329
- transition: all 0.2s ease;
330
- font-size: 12px;
331
- font-weight: 500;
332
- }
333
-
334
- .share-btn:hover {
335
- background: rgba(255, 255, 255, 0.1);
336
- color: var(--text-primary);
337
- }
338
-
339
- .share-btn.copied {
340
- background: rgba(40, 200, 64, 0.15);
341
- border-color: var(--success-color);
342
- color: var(--success-color);
343
- }
344
-
345
- .share-btn .share-icon {
346
- font-size: 14px;
347
- }
348
-
349
- /* ═══════════════════════════════════════════════════════════════
350
- Claude Desktop Window Frame
351
- ═══════════════════════════════════════════════════════════════ */
352
- .claude-window {
353
- width: 100%;
354
- max-width: 800px;
355
- background: var(--window-bg);
356
- border-radius: 12px;
357
- box-shadow: var(--shadow-lg);
358
- overflow: hidden;
359
- border: 1px solid var(--window-border);
360
- position: relative;
361
- }
362
-
363
- .window-chrome {
364
- height: 52px;
365
- background: var(--window-chrome);
366
- display: flex;
367
- align-items: center;
368
- padding: 0 16px;
369
- border-bottom: 1px solid var(--window-border);
370
- }
371
-
372
- .traffic-lights {
373
- display: flex;
374
- gap: 8px;
375
- }
376
-
377
- .traffic-light {
378
- width: 12px;
379
- height: 12px;
380
- border-radius: 50%;
381
- }
382
-
383
- .traffic-light.red { background: var(--traffic-red); }
384
- .traffic-light.yellow { background: var(--traffic-yellow); }
385
- .traffic-light.green { background: var(--traffic-green); }
386
-
387
- .window-title {
388
- flex: 1;
389
- text-align: center;
390
- font-size: 13px;
391
- color: var(--text-secondary);
392
- font-weight: 500;
393
- }
394
-
395
- .window-controls {
396
- width: 52px;
397
- }
398
-
399
- /* ═══════════════════════════════════════════════════════════════
400
- Chat Container
401
- ═══════════════════════════════════════════════════════════════ */
402
- .chat-container {
403
- height: 520px;
404
- overflow-y: auto;
405
- padding: 24px;
406
- display: flex;
407
- flex-direction: column;
408
- gap: 20px;
409
- scroll-behavior: smooth;
410
- }
411
-
412
- /* Loading Skeleton */
413
- .loading-skeleton {
414
- display: flex;
415
- flex-direction: column;
416
- gap: 16px;
417
- }
418
-
419
- .skeleton-message {
420
- display: flex;
421
- gap: 12px;
422
- align-items: flex-start;
423
- }
424
-
425
- .skeleton-avatar {
426
- width: 28px;
427
- height: 28px;
428
- border-radius: 50%;
429
- background: linear-gradient(90deg, var(--window-border) 25%, #4a4a4a 50%, var(--window-border) 75%);
430
- background-size: 200% 100%;
431
- animation: skeleton-shimmer 1.5s infinite;
432
- }
433
-
434
- .skeleton-lines {
435
- flex: 1;
436
- display: flex;
437
- flex-direction: column;
438
- gap: 8px;
439
- }
440
-
441
- .skeleton-line {
442
- height: 16px;
443
- border-radius: 4px;
444
- background: linear-gradient(90deg, var(--window-border) 25%, #4a4a4a 50%, var(--window-border) 75%);
445
- background-size: 200% 100%;
446
- animation: skeleton-shimmer 1.5s infinite;
447
- }
448
-
449
- @keyframes skeleton-shimmer {
450
- 0% { background-position: 200% 0; }
451
- 100% { background-position: -200% 0; }
452
- }
453
-
454
- .chat-container::-webkit-scrollbar {
455
- width: 8px;
456
- }
457
-
458
- .chat-container::-webkit-scrollbar-track {
459
- background: transparent;
460
- }
461
-
462
- .chat-container::-webkit-scrollbar-thumb {
463
- background: var(--window-border);
464
- border-radius: 4px;
465
- }
466
-
467
- /* ═══════════════════════════════════════════════════════════════
468
- Message Bubbles
469
- ═══════════════════════════════════════════════════════════════ */
470
- .message {
471
- max-width: 100%;
472
- animation: message-appear 0.3s ease-out;
473
- }
474
-
475
- @keyframes message-appear {
476
- from {
477
- opacity: 0;
478
- transform: translateY(10px);
479
- }
480
- to {
481
- opacity: 1;
482
- transform: translateY(0);
483
- }
484
- }
485
-
486
- .message-header {
487
- display: flex;
488
- align-items: center;
489
- gap: 10px;
490
- margin-bottom: 8px;
491
- }
492
-
493
- .message-avatar {
494
- width: 28px;
495
- height: 28px;
496
- border-radius: 50%;
497
- display: flex;
498
- align-items: center;
499
- justify-content: center;
500
- font-size: 14px;
501
- }
502
-
503
- .message.user .message-avatar {
504
- background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
505
- color: white;
506
- font-weight: 600;
507
- font-size: 12px;
508
- }
509
-
510
- .message.claude .message-avatar {
511
- background: linear-gradient(135deg, var(--claude-orange) 0%, #c56644 100%);
512
- color: white;
513
- font-weight: 600;
514
- font-size: 12px;
515
- }
516
-
517
- .message.claude .message-avatar svg {
518
- width: 16px;
519
- height: 16px;
520
- }
521
-
522
- .message-sender {
523
- font-weight: 600;
524
- font-size: 14px;
525
- }
526
-
527
- .message.user .message-sender { color: #a5b4fc; }
528
- .message.claude .message-sender { color: var(--claude-orange); }
529
-
530
- .message-time {
531
- color: var(--text-muted);
532
- font-size: 12px;
533
- }
534
-
535
- .message-content {
536
- padding: 16px;
537
- border-radius: 12px;
538
- font-size: 15px;
539
- line-height: 1.6;
540
- }
541
-
542
- .message.user .message-content {
543
- background: var(--user-bubble-bg);
544
- }
545
-
546
- .message.claude .message-content {
547
- background: var(--claude-bubble-bg);
548
- }
549
-
550
- /* ═══════════════════════════════════════════════════════════════
551
- Tool Call Box
552
- ═══════════════════════════════════════════════════════════════ */
553
- .tool-call {
554
- background: var(--tool-box-bg);
555
- border: 1px solid var(--tool-box-border);
556
- border-radius: 8px;
557
- margin: 12px 0;
558
- overflow: hidden;
559
- }
560
-
561
- .tool-header {
562
- display: flex;
563
- align-items: center;
564
- gap: 10px;
565
- padding: 12px 16px;
566
- background: var(--tool-header-bg);
567
- cursor: pointer;
568
- transition: background 0.2s;
569
- }
570
-
571
- .tool-header:hover {
572
- background: #2a2a2a;
573
- }
574
-
575
- .tool-icon {
576
- font-size: 16px;
577
- }
578
-
579
- .tool-name {
580
- font-family: "SF Mono", monospace;
581
- font-size: 13px;
582
- color: var(--link-color);
583
- font-weight: 500;
584
- }
585
-
586
- .tool-status {
587
- margin-left: auto;
588
- display: inline-flex;
589
- align-items: center;
590
- gap: 6px;
591
- font-size: 12px;
592
- color: var(--text-muted);
593
- }
594
-
595
- .tool-status.running {
596
- color: var(--warning-color);
597
- }
598
-
599
- .tool-status.running::before {
600
- content: '';
601
- display: inline-block;
602
- width: 12px;
603
- height: 12px;
604
- border: 2px solid var(--warning-color);
605
- border-top-color: transparent;
606
- border-radius: 50%;
607
- animation: spin 0.8s linear infinite;
608
- margin-right: 6px;
609
- }
610
-
611
- @keyframes spin {
612
- to { transform: rotate(360deg); }
613
- }
614
-
615
- .tool-status.success {
616
- color: var(--success-color);
617
- }
618
-
619
- .tool-chevron {
620
- color: var(--text-muted);
621
- transition: transform 0.3s ease;
622
- font-size: 10px;
623
- }
624
-
625
- .tool-call.expanded .tool-chevron {
626
- transform: rotate(180deg);
627
- }
628
-
629
- .tool-body {
630
- max-height: 0;
631
- overflow: hidden;
632
- transition: max-height 0.3s ease-out;
633
- }
634
-
635
- .tool-call.expanded .tool-body {
636
- max-height: 400px;
637
- }
638
-
639
- .tool-section {
640
- padding: 12px 16px;
641
- border-top: 1px solid var(--tool-box-border);
642
- }
643
-
644
- .tool-section-label {
645
- font-size: 11px;
646
- text-transform: uppercase;
647
- letter-spacing: 0.5px;
648
- color: var(--text-muted);
649
- margin-bottom: 8px;
650
- }
651
-
652
- .tool-params {
653
- font-family: "SF Mono", monospace;
654
- font-size: 13px;
655
- color: var(--code-text);
656
- background: var(--code-bg);
657
- padding: 12px;
658
- border-radius: 6px;
659
- white-space: pre-wrap;
660
- }
661
-
662
- .tool-result {
663
- font-size: 13px;
664
- color: var(--text-secondary);
665
- }
666
-
667
- .tool-result .result-item {
668
- padding: 8px 12px;
669
- margin: 0 -12px;
670
- border-bottom: 1px solid var(--tool-box-border);
671
- border-left: 2px solid transparent;
672
- border-radius: 4px;
673
- transition: all 0.15s ease;
674
- cursor: default;
675
- }
676
-
677
- .tool-result .result-item:hover {
678
- background: rgba(255, 255, 255, 0.03);
679
- border-left-color: var(--claude-orange);
680
- }
681
-
682
- .tool-result .result-item:last-child {
683
- border-bottom: none;
684
- }
685
-
686
- .result-channel {
687
- color: var(--link-color);
688
- font-weight: 500;
689
- }
690
-
691
- .result-user {
692
- color: #f0abfc;
693
- font-weight: 500;
694
- }
695
-
696
- .result-time {
697
- color: var(--text-muted);
698
- font-size: 12px;
699
- }
700
-
701
- /* ═══════════════════════════════════════════════════════════════
702
- Typing Indicator
703
- ═══════════════════════════════════════════════════════════════ */
704
- .typing-indicator {
705
- display: flex;
706
- align-items: center;
707
- gap: 8px;
708
- padding: 12px 16px;
709
- background: var(--claude-bubble-bg);
710
- border-radius: 12px;
711
- width: fit-content;
712
- }
713
-
714
- .typing-dots {
715
- display: flex;
716
- gap: 4px;
717
- }
718
-
719
- .typing-dot {
720
- width: 8px;
721
- height: 8px;
722
- background: var(--text-muted);
723
- border-radius: 50%;
724
- animation: typing-bounce 1.4s ease-in-out infinite;
725
- }
726
-
727
- .typing-dot:nth-child(2) { animation-delay: 0.16s; }
728
- .typing-dot:nth-child(3) { animation-delay: 0.32s; }
729
-
730
- @keyframes typing-bounce {
731
- 0%, 80%, 100% { transform: translateY(0); opacity: 0.4; }
732
- 40% { transform: translateY(-4px); opacity: 1; }
733
- }
734
-
735
- .typing-cursor {
736
- display: inline-block;
737
- width: 2px;
738
- height: 1em;
739
- background: var(--claude-orange);
740
- margin-left: 1px;
741
- animation: cursor-blink 0.8s ease-in-out infinite;
742
- vertical-align: text-bottom;
743
- }
744
-
745
- @keyframes cursor-blink {
746
- 0%, 50% { opacity: 1; }
747
- 51%, 100% { opacity: 0; }
748
- }
749
-
750
- /* ═══════════════════════════════════════════════════════════════
751
- Input Bar
752
- ═══════════════════════════════════════════════════════════════ */
753
- .input-bar {
754
- display: flex;
755
- align-items: center;
756
- padding: 16px 20px;
757
- background: var(--window-chrome);
758
- border-top: 1px solid var(--window-border);
759
- gap: 12px;
760
- }
761
-
762
- .input-field {
763
- flex: 1;
764
- background: var(--window-bg);
765
- border: 1px solid var(--window-border);
766
- border-radius: 24px;
767
- padding: 12px 20px;
768
- color: var(--text-secondary);
769
- font-size: 14px;
770
- }
771
-
772
- .tools-button {
773
- display: flex;
774
- align-items: center;
775
- gap: 6px;
776
- padding: 10px 16px;
777
- background: rgba(218, 119, 86, 0.15);
778
- border: 1px solid rgba(218, 119, 86, 0.3);
779
- border-radius: 20px;
780
- color: var(--claude-orange);
781
- cursor: pointer;
782
- transition: all 0.2s;
783
- font-size: 13px;
784
- font-weight: 500;
785
- position: relative;
786
- }
787
-
788
- .tools-button:hover {
789
- background: rgba(218, 119, 86, 0.25);
790
- }
791
-
792
- .tools-button .icon {
793
- font-size: 16px;
794
- }
795
-
796
- /* ═══════════════════════════════════════════════════════════════
797
- Tools Dropdown
798
- ═══════════════════════════════════════════════════════════════ */
799
- .tools-dropdown {
800
- position: absolute;
801
- bottom: calc(100% + 8px);
802
- right: 0;
803
- width: 320px;
804
- background: var(--window-bg);
805
- border: 1px solid var(--window-border);
806
- border-radius: 12px;
807
- box-shadow: var(--shadow-lg);
808
- opacity: 0;
809
- visibility: hidden;
810
- transform: translateY(10px);
811
- transition: all 0.2s ease;
812
- z-index: 100;
813
- }
814
-
815
- .tools-button:hover .tools-dropdown,
816
- .tools-dropdown:hover {
817
- opacity: 1;
818
- visibility: visible;
819
- transform: translateY(0);
820
- }
821
-
822
- .dropdown-header {
823
- padding: 12px 16px;
824
- border-bottom: 1px solid var(--window-border);
825
- font-weight: 600;
826
- font-size: 14px;
827
- display: flex;
828
- align-items: center;
829
- gap: 8px;
830
- }
831
-
832
- .dropdown-list {
833
- max-height: 300px;
834
- overflow-y: auto;
835
- }
836
-
837
- .dropdown-item {
838
- padding: 10px 16px;
839
- border-bottom: 1px solid var(--tool-box-border);
840
- cursor: default;
841
- }
842
-
843
- .dropdown-item:last-child {
844
- border-bottom: none;
845
- }
846
-
847
- .dropdown-item:hover {
848
- background: rgba(255, 255, 255, 0.03);
849
- }
850
-
851
- .dropdown-item-name {
852
- font-family: "SF Mono", monospace;
853
- font-size: 13px;
854
- color: var(--link-color);
855
- margin-bottom: 2px;
856
- }
857
-
858
- .dropdown-item-desc {
859
- font-size: 12px;
860
- color: var(--text-muted);
861
- }
862
-
863
- /* ═══════════════════════════════════════════════════════════════
864
- Code Inline
865
- ═══════════════════════════════════════════════════════════════ */
866
- code {
867
- font-family: "SF Mono", monospace;
868
- background: var(--code-bg);
869
- padding: 2px 6px;
870
- border-radius: 4px;
871
- font-size: 13px;
872
- }
873
-
874
- strong {
875
- font-weight: 600;
876
- }
877
-
878
- em {
879
- font-style: italic;
880
- color: var(--text-secondary);
881
- }
882
-
883
- /* ═══════════════════════════════════════════════════════════════
884
- Footer
885
- ═══════════════════════════════════════════════════════════════ */
886
- .page-footer {
887
- margin-top: 24px;
888
- text-align: center;
889
- color: var(--text-muted);
890
- font-size: 13px;
891
- }
892
-
893
- .page-footer a {
894
- color: var(--link-color);
895
- text-decoration: none;
896
- }
897
-
898
- .page-footer a:hover {
899
- text-decoration: underline;
900
- }
901
-
902
- kbd {
903
- display: inline-block;
904
- padding: 2px 6px;
905
- font-family: "SF Mono", monospace;
906
- font-size: 10px;
907
- background: rgba(255, 255, 255, 0.1);
908
- border: 1px solid rgba(255, 255, 255, 0.2);
909
- border-radius: 4px;
910
- color: var(--text-secondary);
911
- }
912
-
913
- /* Fullscreen / Presentation Mode */
914
- body.fullscreen-mode {
915
- padding: 0;
916
- justify-content: center;
917
- }
918
-
919
- body.fullscreen-mode .page-header,
920
- body.fullscreen-mode .scenario-bar,
921
- body.fullscreen-mode .controls-bar,
922
- body.fullscreen-mode .page-footer {
923
- display: none !important;
924
- }
925
-
926
- body.fullscreen-mode .cta-strip,
927
- body.fullscreen-mode .scenario-caption {
928
- display: none !important;
929
- }
930
-
931
- body.fullscreen-mode .claude-window {
932
- max-width: 100%;
933
- height: 100vh;
934
- border-radius: 0;
935
- border: none;
936
- }
937
-
938
- body.fullscreen-mode .chat-container {
939
- height: calc(100vh - 52px - 68px);
940
- }
941
-
942
- .fullscreen-hint {
943
- position: fixed;
944
- bottom: 20px;
945
- right: 20px;
946
- background: rgba(0, 0, 0, 0.8);
947
- color: var(--text-secondary);
948
- padding: 8px 12px;
949
- border-radius: 6px;
950
- font-size: 12px;
951
- opacity: 0;
952
- transition: opacity 0.3s;
953
- pointer-events: none;
954
- }
955
-
956
- body.fullscreen-mode .fullscreen-hint {
957
- opacity: 1;
958
- }
959
-
960
- /* ═══════════════════════════════════════════════════════════════
961
- Accessibility
962
- ═══════════════════════════════════════════════════════════════ */
963
- @media (prefers-reduced-motion: reduce) {
964
- *, *::before, *::after {
965
- animation-duration: 0.01ms !important;
966
- animation-iteration-count: 1 !important;
967
- transition-duration: 0.01ms !important;
968
- }
969
- }
970
-
971
- .scenario-btn:focus-visible,
972
- .replay-btn:focus-visible,
973
- .tools-button:focus-visible,
974
- .speed-control select:focus-visible {
975
- outline: 2px solid var(--link-color);
976
- outline-offset: 2px;
977
- }
978
-
979
- .tool-header:focus-visible {
980
- outline: 2px solid var(--link-color);
981
- outline-offset: -2px;
982
- }
983
-
984
- /* ═══════════════════════════════════════════════════════════════
985
- Responsive
986
- ═══════════════════════════════════════════════════════════════ */
987
- @media (max-width: 900px) {
988
- body {
989
- padding: 14px;
990
- }
991
-
992
- .page-header {
993
- margin-bottom: 16px;
994
- }
995
-
996
- .page-header h1 {
997
- font-size: 24px;
998
- }
999
-
1000
- .scenario-btn {
1001
- padding: 10px 14px;
1002
- }
1003
-
1004
- .controls-bar {
1005
- gap: 10px;
1006
- }
1007
-
1008
- .claude-window {
1009
- max-width: 100%;
1010
- }
1011
- }
1012
-
1013
- @media (max-width: 600px) {
1014
- body {
1015
- padding: 12px;
1016
- }
1017
-
1018
- .cta-strip {
1019
- padding: 10px;
1020
- gap: 8px;
1021
- }
1022
-
1023
- .cta-strip .links {
1024
- width: 100%;
1025
- }
1026
-
1027
- .cta-strip .note {
1028
- font-size: 12px;
1029
- }
1030
-
1031
- .page-header h1 {
1032
- font-size: 22px;
1033
- flex-wrap: wrap;
1034
- gap: 8px;
1035
- }
1036
-
1037
- .page-header p {
1038
- font-size: 14px;
1039
- }
1040
-
1041
- .scenario-bar {
1042
- gap: 8px;
1043
- margin-bottom: 14px;
1044
- }
1045
-
1046
- .scenario-btn {
1047
- padding: 9px 12px;
1048
- font-size: 13px;
1049
- border-radius: 10px;
1050
- }
1051
-
1052
- .scenario-btn .label {
1053
- display: none;
1054
- }
1055
-
1056
- .controls-bar {
1057
- gap: 8px;
1058
- margin-bottom: 12px;
1059
- }
1060
-
1061
- .replay-btn,
1062
- .share-btn {
1063
- padding: 8px 12px;
1064
- }
1065
-
1066
- .speed-control {
1067
- width: 100%;
1068
- justify-content: space-between;
1069
- }
1070
-
1071
- .chat-container {
1072
- height: min(55vh, 450px);
1073
- padding: 14px;
1074
- }
1075
-
1076
- .tools-dropdown {
1077
- width: 280px;
1078
- right: auto;
1079
- left: 50%;
1080
- transform: translateX(-50%) translateY(0);
1081
- }
1082
-
1083
- .tools-button:hover .tools-dropdown,
1084
- .tools-dropdown:hover {
1085
- transform: translateX(-50%) translateY(0);
1086
- }
1087
-
1088
- .scenario-caption {
1089
- top: 56px;
1090
- font-size: 12px;
1091
- padding: 7px 14px;
1092
- }
1093
- }
1094
-
1095
- /* ═══════════════════════════════════════════════════════════════
1096
- Production Polish - Title, Captions, Transitions, Closing
1097
- ═══════════════════════════════════════════════════════════════ */
1098
-
1099
- /* Title Card */
1100
- .title-card {
1101
- position: absolute;
1102
- inset: 0;
1103
- top: 32px; /* Below window chrome */
1104
- display: none;
1105
- flex-direction: column;
1106
- align-items: center;
1107
- justify-content: center;
1108
- background: var(--window-bg);
1109
- z-index: 100;
1110
- opacity: 0;
1111
- transition: opacity 0.5s ease;
1112
- }
1113
-
1114
- .title-card.visible {
1115
- display: flex;
1116
- opacity: 1;
1117
- }
1118
-
1119
- .title-card h1 {
1120
- color: var(--text-warm);
1121
- letter-spacing: 0.08em;
1122
- font-weight: 300;
1123
- font-size: 28px;
1124
- margin: 16px 0 8px;
1125
- }
1126
-
1127
- .title-card .title-logo { font-size: 48px; }
1128
- .title-card .title-tagline { color: var(--text-secondary); font-size: 16px; }
1129
- .title-card .title-version { color: var(--text-muted); font-size: 13px; margin-top: 24px; }
1130
-
1131
- /* Scenario Caption Overlay */
1132
- .scenario-caption {
1133
- position: absolute;
1134
- top: 60px;
1135
- left: 50%;
1136
- transform: translateX(-50%);
1137
- background: rgba(0, 0, 0, 0.75);
1138
- color: var(--text-primary);
1139
- padding: 8px 20px;
1140
- border-radius: 20px;
1141
- font-size: 13px;
1142
- font-weight: 500;
1143
- opacity: 0;
1144
- transition: opacity 0.3s ease;
1145
- z-index: 50;
1146
- pointer-events: none;
1147
- max-width: calc(100% - 24px);
1148
- white-space: nowrap;
1149
- overflow: hidden;
1150
- text-overflow: ellipsis;
1151
- }
1152
-
1153
- .scenario-caption.visible {
1154
- opacity: 1;
1155
- }
1156
-
1157
- /* Smooth Transitions */
1158
- .chat-container {
1159
- transition: opacity 0.3s ease;
1160
- }
1161
-
1162
- .chat-container.fading {
1163
- opacity: 0;
1164
- }
1165
-
1166
- /* Closing Card */
1167
- .closing-card {
1168
- position: absolute;
1169
- inset: 0;
1170
- top: 32px;
1171
- display: none;
1172
- flex-direction: column;
1173
- align-items: center;
1174
- justify-content: center;
1175
- background: var(--window-bg);
1176
- z-index: 100;
1177
- opacity: 0;
1178
- transition: opacity 0.5s ease;
1179
- }
1180
-
1181
- .closing-card.visible {
1182
- display: flex;
1183
- opacity: 1;
1184
- }
1185
-
1186
- .closing-check { font-size: 48px; margin-bottom: 16px; }
1187
-
1188
- .closing-card h2 {
1189
- color: var(--text-warm);
1190
- font-weight: 400;
1191
- font-size: 24px;
1192
- margin-bottom: 8px;
1193
- }
1194
-
1195
- .closing-cta {
1196
- color: var(--text-secondary);
1197
- margin: 8px 0 24px;
1198
- font-size: 15px;
1199
- }
1200
-
1201
- .closing-links code {
1202
- background: var(--code-bg);
1203
- color: var(--code-text);
1204
- padding: 12px 24px;
1205
- border-radius: 8px;
1206
- font-size: 14px;
1207
- font-family: "SF Mono", "Menlo", monospace;
1208
- }
1209
-
1210
- .closing-github {
1211
- margin-top: 24px;
1212
- color: var(--link-color);
1213
- font-size: 14px;
1214
- }
1215
-
1216
- /* ê Easter Egg - The Rêvasser Wink */
1217
- .easter-egg {
1218
- position: absolute;
1219
- bottom: 16px;
1220
- right: 20px;
1221
- color: var(--text-warm);
1222
- opacity: 0.15;
1223
- font-size: 14px;
1224
- font-weight: 300;
1225
- font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
1226
- }
1227
-
1228
- .closing-card.visible .easter-egg {
1229
- animation: egg-wink 8s ease 2s forwards;
1230
- }
1231
-
1232
- @keyframes egg-wink {
1233
- 0%, 100% { opacity: 0.15; }
1234
- 10% { opacity: 0.35; }
1235
- 20% { opacity: 0.15; }
1236
- }
1237
- </style>
1238
- </head>
1239
- <body>
1240
- <div class="cta-strip">
1241
- <div class="links">
1242
- <a href="https://www.npmjs.com/package/@jtalk22/slack-mcp" target="_blank" rel="noopener noreferrer">Install</a>
1243
- <a href="https://github.com/jtalk22/slack-mcp-server/blob/main/docs/SETUP.md" target="_blank" rel="noopener noreferrer">Setup Guide</a>
1244
- <a href="https://github.com/jtalk22/slack-mcp-server#30-second-compatibility-check" target="_blank" rel="noopener noreferrer">30-Second Check</a>
1245
- </div>
1246
- <div class="note">
1247
- Free local-first path with team rollout references in <a href="https://github.com/jtalk22/slack-mcp-server/blob/main/docs/DEPLOYMENT-MODES.md" target="_blank" rel="noopener noreferrer">Deployment Modes</a>.
1248
- </div>
1249
- </div>
1250
- <header class="page-header">
1251
- <h1>
1252
- <span>Slack MCP Server</span>
1253
- <span class="badge">🔧 MCP Demo v3.0.0</span>
1254
- </h1>
1255
- <p>See how Claude uses MCP tools to access your Slack workspace</p>
1256
- </header>
1257
-
1258
- <div class="scenario-bar" role="tablist" aria-label="Demo scenarios">
1259
- <button class="scenario-btn active" data-scenario="search" onclick="runScenario('search')" role="tab" aria-selected="true" aria-label="Search DMs scenario">
1260
- <span class="icon" aria-hidden="true">🔍</span>
1261
- <span class="label">Search DMs</span>
1262
- </button>
1263
- <button class="scenario-btn" data-scenario="thread" onclick="runScenario('thread')" role="tab" aria-selected="false" aria-label="Get Thread scenario">
1264
- <span class="icon" aria-hidden="true">📜</span>
1265
- <span class="label">Get Thread</span>
1266
- </button>
1267
- <button class="scenario-btn" data-scenario="list" onclick="runScenario('list')" role="tab" aria-selected="false" aria-label="List DMs scenario">
1268
- <span class="icon" aria-hidden="true">💬</span>
1269
- <span class="label">List DMs</span>
1270
- </button>
1271
- <button class="scenario-btn" data-scenario="send" onclick="runScenario('send')" role="tab" aria-selected="false" aria-label="Send Message scenario">
1272
- <span class="icon" aria-hidden="true">✉️</span>
1273
- <span class="label">Send Message</span>
1274
- </button>
1275
- <button class="scenario-btn" data-scenario="multi" onclick="runScenario('multi')" role="tab" aria-selected="false" aria-label="Multi-Tool scenario">
1276
- <span class="icon" aria-hidden="true">⚡</span>
1277
- <span class="label">Multi-Tool</span>
1278
- </button>
1279
- </div>
1280
-
1281
- <div class="controls-bar" role="toolbar" aria-label="Demo controls">
1282
- <button class="replay-btn" id="replayBtn" onclick="replayScenario()" aria-label="Replay current scenario">
1283
- <span aria-hidden="true">↻</span>
1284
- <span>Replay</span>
1285
- </button>
1286
- <button class="replay-btn" id="autoPlayBtn" onclick="autoPlayAll()" style="background: rgba(40, 200, 64, 0.1); border-color: rgba(40, 200, 64, 0.3); color: var(--success-color);" aria-label="Auto-play all scenarios">
1287
- <span aria-hidden="true">▶</span>
1288
- <span>Auto-Play All</span>
1289
- </button>
1290
- <div class="speed-control">
1291
- <label>Speed:</label>
1292
- <select id="speedSelect" onchange="updateSpeed(this.value)">
1293
- <option value="0.5">0.5x (Slow - Video)</option>
1294
- <option value="1" selected>1x (Normal)</option>
1295
- <option value="1.5">1.5x (Fast)</option>
1296
- <option value="2">2x (Fastest)</option>
1297
- </select>
1298
- </div>
1299
- <div class="progress-indicator" id="progressIndicator" style="display: none;">
1300
- <span class="progress-text"></span>
1301
- <div class="progress-bar"><div class="progress-fill"></div></div>
1302
- </div>
1303
- <button class="share-btn" onclick="copyShareLink()" aria-label="Copy link to share">
1304
- <span class="share-icon">🔗</span>
1305
- <span class="share-text">Share</span>
1306
- </button>
1307
- </div>
1308
-
1309
- <div class="claude-window">
1310
- <div class="window-chrome">
1311
- <div class="traffic-lights">
1312
- <div class="traffic-light red"></div>
1313
- <div class="traffic-light yellow"></div>
1314
- <div class="traffic-light green"></div>
1315
- </div>
1316
- <div class="window-title">Claude</div>
1317
- <div class="window-controls"></div>
1318
- </div>
1319
-
1320
- <!-- Title Card (auto-play only) -->
1321
- <div class="title-card" id="titleCard">
1322
- <div class="title-logo">💬</div>
1323
- <h1>Slack MCP Server</h1>
1324
- <p class="title-tagline">Full Slack access for Claude Desktop</p>
1325
- <p class="title-version">v3.0.0 • @jtalk22</p>
1326
- </div>
1327
-
1328
- <!-- Scenario Caption Overlay -->
1329
- <div class="scenario-caption" id="scenarioCaption"></div>
1330
-
1331
- <!-- Closing Card (auto-play only) -->
1332
- <div class="closing-card" id="closingCard">
1333
- <div class="closing-check">✅</div>
1334
- <h2>Demo Complete</h2>
1335
- <p class="closing-cta">Session-based Slack access aligned to your existing workspace permissions.</p>
1336
- <div class="closing-links">
1337
- <code>npx -y @jtalk22/slack-mcp --setup</code>
1338
- </div>
1339
- <p class="closing-github">github.com/jtalk22/slack-mcp-server</p>
1340
- <span class="easter-egg">ê</span>
1341
- </div>
1342
-
1343
- <div class="chat-container" id="chatContainer" role="log" aria-label="Chat demonstration" aria-live="polite">
1344
- <!-- Loading skeleton shown before first scenario -->
1345
- <div class="loading-skeleton" id="loadingSkeleton">
1346
- <div class="skeleton-message">
1347
- <div class="skeleton-avatar"></div>
1348
- <div class="skeleton-lines">
1349
- <div class="skeleton-line" style="width: 60%"></div>
1350
- <div class="skeleton-line" style="width: 80%"></div>
1351
- </div>
1352
- </div>
1353
- </div>
1354
- </div>
1355
-
1356
- <div class="input-bar">
1357
- <div class="input-field">Message Claude...</div>
1358
- <div class="tools-button">
1359
- <span class="icon">🔨</span>
1360
- <span>11 tools</span>
1361
- <div class="tools-dropdown">
1362
- <div class="dropdown-header">
1363
- <span>🔨</span> Available Slack Tools
1364
- </div>
1365
- <div class="dropdown-list">
1366
- <div class="dropdown-item">
1367
- <div class="dropdown-item-name">slack_search_messages</div>
1368
- <div class="dropdown-item-desc">Search across your workspace</div>
1369
- </div>
1370
- <div class="dropdown-item">
1371
- <div class="dropdown-item-name">slack_list_conversations</div>
1372
- <div class="dropdown-item-desc">List DMs and channels</div>
1373
- </div>
1374
- <div class="dropdown-item">
1375
- <div class="dropdown-item-name">slack_conversations_history</div>
1376
- <div class="dropdown-item-desc">Get messages from a conversation</div>
1377
- </div>
1378
- <div class="dropdown-item">
1379
- <div class="dropdown-item-name">slack_get_thread</div>
1380
- <div class="dropdown-item-desc">Get all replies in a thread</div>
1381
- </div>
1382
- <div class="dropdown-item">
1383
- <div class="dropdown-item-name">slack_send_message</div>
1384
- <div class="dropdown-item-desc">Send a message to any channel</div>
1385
- </div>
1386
- <div class="dropdown-item">
1387
- <div class="dropdown-item-name">slack_get_full_conversation</div>
1388
- <div class="dropdown-item-desc">Export full history with threads</div>
1389
- </div>
1390
- <div class="dropdown-item">
1391
- <div class="dropdown-item-name">slack_users_info</div>
1392
- <div class="dropdown-item-desc">Get user details</div>
1393
- </div>
1394
- <div class="dropdown-item">
1395
- <div class="dropdown-item-name">slack_list_users</div>
1396
- <div class="dropdown-item-desc">List workspace users</div>
1397
- </div>
1398
- <div class="dropdown-item">
1399
- <div class="dropdown-item-name">slack_health_check</div>
1400
- <div class="dropdown-item-desc">Verify token validity</div>
1401
- </div>
1402
- <div class="dropdown-item">
1403
- <div class="dropdown-item-name">slack_token_status</div>
1404
- <div class="dropdown-item-desc">Check token health</div>
1405
- </div>
1406
- <div class="dropdown-item">
1407
- <div class="dropdown-item-name">slack_refresh_tokens</div>
1408
- <div class="dropdown-item-desc">Auto-extract fresh tokens</div>
1409
- </div>
1410
- </div>
1411
- </div>
1412
- </div>
1413
- </div>
1414
- </div>
1415
-
1416
- <div class="fullscreen-hint">Press <kbd>F</kbd> or <kbd>Esc</kbd> to exit</div>
1417
-
1418
- <footer class="page-footer">
1419
- <p>
1420
- Made by <a href="https://github.com/jtalk22" target="_blank">@jtalk22</a> ·
1421
- <a href="https://github.com/jtalk22/slack-mcp-server" target="_blank">GitHub</a> ·
1422
- <a href="https://www.npmjs.com/package/@jtalk22/slack-mcp" target="_blank">npm</a> ·
1423
- <a href="demo.html">Web UI Demo</a>
1424
- </p>
1425
- <p style="margin-top: 8px; font-size: 11px; color: var(--text-muted);">
1426
- Keyboard: <kbd>1-5</kbd> scenarios · <kbd>R</kbd> replay · <kbd>A</kbd> auto-play · <kbd>F</kbd> fullscreen · <kbd>Esc</kbd> exit
1427
- </p>
1428
- <p style="margin-top: 12px; font-size: 11px; color: var(--text-muted);">
1429
- © 2026 James Lambert · MIT License
1430
- </p>
1431
- </footer>
1432
-
1433
- <script>
1434
- // ═══════════════════════════════════════════════════════════════
1435
- // Scenario Data
1436
- // ═══════════════════════════════════════════════════════════════
1437
- const scenarios = {
1438
- search: {
1439
- userMessage: "Find all messages about the API key from last week",
1440
- claudeIntro: "I'll search your Slack workspace for messages about API keys from the past week.",
1441
- toolCall: {
1442
- name: "slack_search_messages",
1443
- params: {
1444
- query: "API key",
1445
- count: 20
1446
- },
1447
- result: [
1448
- { channel: "#engineering", user: "Sarah Chen", time: "Jan 15, 2:34 PM", text: "Here's the staging API key: <code>sk-abc123-staging-xyz</code>" },
1449
- { channel: "DM with Mike", user: "Mike Johnson", time: "Jan 12, 10:15 AM", text: "Don't forget to rotate the API key before the deploy" },
1450
- { channel: "#alerts", user: "DevOps Bot", time: "Jan 10, 9:00 AM", text: "API key rotation reminder: 3 days until expiry" }
1451
- ]
1452
- },
1453
- claudeSummary: "I found <strong>3 messages</strong> mentioning API keys from last week:\n\n1. <strong>Sarah Chen</strong> shared the staging API key in #engineering on Jan 15th\n2. <strong>Mike Johnson</strong> reminded you about key rotation in a DM on Jan 12th\n3. <strong>DevOps Bot</strong> posted an expiry reminder in #alerts on Jan 10th\n\nWould you like me to get more details on any of these?"
1454
- },
1455
-
1456
- thread: {
1457
- userMessage: "What was decided in the Q4 launch thread?",
1458
- claudeIntro: "Let me find that thread and get the full context of the discussion.",
1459
- toolCall: {
1460
- name: "slack_get_thread",
1461
- params: {
1462
- channel: "C04N8JXKZ7M",
1463
- thread_ts: "1704729600.847293"
1464
- },
1465
- result: [
1466
- { user: "Alex Kim", time: "Jan 8, 3:00 PM", text: "Kicking off Q4 launch planning. Key dates?" },
1467
- { user: "Sarah Chen", time: "Jan 8, 3:15 PM", text: "Marketing wants the 15th for press embargo lift" },
1468
- { user: "Mike Johnson", time: "Jan 8, 3:22 PM", text: "Engineering can hit the 12th for feature freeze" },
1469
- { user: "Jordan Lee", time: "Jan 8, 4:00 PM", text: "Let's lock in: Feature freeze Jan 12, Launch Jan 18" },
1470
- { user: "Alex Kim", time: "Jan 8, 4:05 PM", text: "✅ Approved. I'll update the roadmap" }
1471
- ]
1472
- },
1473
- claudeSummary: "The Q4 launch thread concluded with these <strong>key decisions</strong>:\n\n• <strong>Feature freeze:</strong> January 12th\n• <strong>Press embargo lift:</strong> January 15th \n• <strong>Launch date:</strong> January 18th\n\n<strong>Jordan Lee</strong> proposed the final timeline and <strong>Alex Kim</strong> approved it. Would you like me to pull any related threads or documents?"
1474
- },
1475
-
1476
- list: {
1477
- userMessage: "Who has messaged me recently?",
1478
- claudeIntro: "I'll check your recent direct messages to see who's reached out.",
1479
- toolCall: {
1480
- name: "slack_list_conversations",
1481
- params: {
1482
- types: "im",
1483
- limit: 10,
1484
- discover_dms: true
1485
- },
1486
- result: [
1487
- { name: "Sarah Chen", id: "D04AXQK8N2P", lastMessage: "2 hours ago", preview: "Thanks for the review!" },
1488
- { name: "Mike Johnson", id: "D04BYR7M3Q1", lastMessage: "Yesterday", preview: "Can we sync on the API changes?" },
1489
- { name: "Alex Kim", id: "D04CZS6L4R2", lastMessage: "2 days ago", preview: "Q4 roadmap is updated" },
1490
- { name: "Jordan Lee", id: "D04DWT5K5S3", lastMessage: "3 days ago", preview: "Meeting notes attached" },
1491
- { name: "DevOps Bot", id: "D04EXU4J6T4", lastMessage: "4 days ago", preview: "Deploy completed successfully" }
1492
- ]
1493
- },
1494
- claudeSummary: "You have recent DMs from <strong>5 people</strong>:\n\n• <strong>Sarah Chen</strong> — 2 hours ago: \"Thanks for the review!\"\n• <strong>Mike Johnson</strong> — Yesterday: Asking about API changes sync\n• <strong>Alex Kim</strong> — 2 days ago: Q4 roadmap update\n• <strong>Jordan Lee</strong> — 3 days ago: Meeting notes\n• <strong>DevOps Bot</strong> — 4 days ago: Deploy notification\n\nWant me to pull the full conversation with any of them?"
1495
- },
1496
-
1497
- send: {
1498
- userMessage: "Send a quick update to #general that the deploy is complete",
1499
- claudeIntro: "I'll post that update to #general for you.",
1500
- toolCall: {
1501
- name: "slack_send_message",
1502
- params: {
1503
- channel_id: "C04GENERAL1",
1504
- text: "✅ Deploy complete! All services are healthy and running on v2.4.1."
1505
- },
1506
- result: {
1507
- success: true,
1508
- channel: "#general",
1509
- timestamp: "1705612847.293847",
1510
- permalink: "https://acme-corp.slack.com/archives/C04GENERAL1/p1705612847293847"
1511
- }
1512
- },
1513
- claudeSummary: "Done! I've posted your update to <strong>#general</strong>:\n\n<em>\"✅ Deploy complete! All services are healthy and running on v2.4.1.\"</em>\n\nThe message was sent at <strong>2:34 PM</strong>. Would you like me to add any follow-up or tag specific people?"
1514
- },
1515
-
1516
- multi: {
1517
- userMessage: "Find the latest deploy thread and summarize it to #updates",
1518
- claudeIntro: "I'll search for the deploy thread, then post a summary to #updates.",
1519
- isMultiTool: true,
1520
- toolCalls: [
1521
- {
1522
- name: "slack_search_messages",
1523
- params: {
1524
- query: "deploy thread",
1525
- count: 5
1526
- },
1527
- result: [
1528
- { channel: "#engineering", user: "DevOps Bot", time: "Today, 11:42 AM", text: "🚀 Deploy v2.4.1 starting..." },
1529
- { channel: "#engineering", user: "Sarah Chen", time: "Today, 11:45 AM", text: "All tests passing, proceeding with rollout" }
1530
- ]
1531
- },
1532
- {
1533
- name: "slack_send_message",
1534
- params: {
1535
- channel_id: "C04UPDATES2K",
1536
- text: "📋 Deploy Summary (v2.4.1):\n• Started: 11:42 AM\n• Status: Complete\n• Tests: All passing"
1537
- },
1538
- result: {
1539
- success: true,
1540
- channel: "#updates",
1541
- timestamp: "1705614000.123456"
1542
- }
1543
- }
1544
- ],
1545
- claudeSummary: "Done! I found the deploy thread from this morning and posted a summary to <strong>#updates</strong>:\n\n<em>\"📋 Deploy Summary (v2.4.1): Started 11:42 AM, Status: Complete, Tests: All passing\"</em>\n\nThe deploy was led by <strong>Sarah Chen</strong> and completed successfully."
1546
- }
1547
- };
1548
-
1549
- // ═══════════════════════════════════════════════════════════════
1550
- // Animation Helpers
1551
- // ═══════════════════════════════════════════════════════════════
1552
- let speedMultiplier = 1;
1553
- let currentScenario = 'search';
1554
-
1555
- const sleep = ms => new Promise(r => setTimeout(r, ms / speedMultiplier));
1556
-
1557
- function updateSpeed(value) {
1558
- speedMultiplier = parseFloat(value);
1559
- }
1560
-
1561
- function replayScenario() {
1562
- runScenario(currentScenario);
1563
- }
1564
-
1565
- async function typeText(element, text, speed = 25) {
1566
- // Convert \n\n to proper line breaks for final display
1567
- const formattedText = text.replace(/\n\n/g, '<br><br>').replace(/\n/g, '<br>');
1568
-
1569
- // For short text or fast speed, just fade in
1570
- if (speedMultiplier >= 1.5 || text.length < 20) {
1571
- element.style.opacity = '0';
1572
- element.innerHTML = formattedText;
1573
- element.style.transition = 'opacity 0.3s ease-out';
1574
- await sleep(50);
1575
- element.style.opacity = '1';
1576
- return;
1577
- }
1578
-
1579
- // Character-by-character typing with cursor
1580
- element.innerHTML = '<span class="typing-cursor"></span>';
1581
- const cursor = element.querySelector('.typing-cursor');
1582
-
1583
- // Parse HTML and type text content while preserving tags
1584
- const tempDiv = document.createElement('div');
1585
- tempDiv.innerHTML = formattedText;
1586
- const plainText = tempDiv.textContent;
1587
-
1588
- let currentIndex = 0;
1589
- const textSpan = document.createElement('span');
1590
- element.insertBefore(textSpan, cursor);
1591
-
1592
- for (const char of plainText) {
1593
- textSpan.textContent += char;
1594
- await sleep(speed);
1595
- }
1596
-
1597
- // Replace with formatted HTML and remove cursor
1598
- await sleep(100);
1599
- element.innerHTML = formattedText;
1600
- }
1601
-
1602
- // Claude's sparkle icon SVG
1603
- const claudeIcon = `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L9.5 9.5L2 12L9.5 14.5L12 22L14.5 14.5L22 12L14.5 9.5L12 2Z"/></svg>`;
1604
-
1605
- function createMessage(type, sender, time) {
1606
- const msg = document.createElement('div');
1607
- msg.className = `message ${type}`;
1608
- const avatar = type === 'user' ? 'Y' : claudeIcon;
1609
- msg.innerHTML = `
1610
- <div class="message-header">
1611
- <div class="message-avatar">${avatar}</div>
1612
- <span class="message-sender">${sender}</span>
1613
- <span class="message-time">${time}</span>
1614
- </div>
1615
- <div class="message-content"></div>
1616
- `;
1617
- return msg;
1618
- }
1619
-
1620
- function createTypingIndicator() {
1621
- const indicator = document.createElement('div');
1622
- indicator.className = 'message claude';
1623
- indicator.id = 'typingIndicator';
1624
- indicator.innerHTML = `
1625
- <div class="message-header">
1626
- <div class="message-avatar">${claudeIcon}</div>
1627
- <span class="message-sender">Claude</span>
1628
- </div>
1629
- <div class="typing-indicator">
1630
- <div class="typing-dots">
1631
- <div class="typing-dot"></div>
1632
- <div class="typing-dot"></div>
1633
- <div class="typing-dot"></div>
1634
- </div>
1635
- </div>
1636
- `;
1637
- return indicator;
1638
- }
1639
-
1640
- function createToolCall(tool, isRunning = true) {
1641
- const toolEl = document.createElement('div');
1642
- toolEl.className = 'tool-call';
1643
-
1644
- let resultHtml = '';
1645
- if (Array.isArray(tool.result)) {
1646
- resultHtml = tool.result.map(item => {
1647
- if (item.channel) {
1648
- return `<div class="result-item">
1649
- <span class="result-channel">${item.channel}</span> ·
1650
- <span class="result-user">${item.user}</span> ·
1651
- <span class="result-time">${item.time}</span><br>
1652
- <em>"${item.text}"</em>
1653
- </div>`;
1654
- } else if (item.name) {
1655
- return `<div class="result-item">
1656
- <span class="result-user">${item.name}</span> ·
1657
- <span class="result-time">${item.lastMessage}</span><br>
1658
- <em>"${item.preview}"</em>
1659
- </div>`;
1660
- } else {
1661
- return `<div class="result-item">
1662
- <span class="result-user">${item.user}</span> ·
1663
- <span class="result-time">${item.time}</span><br>
1664
- "${item.text}"
1665
- </div>`;
1666
- }
1667
- }).join('');
1668
- } else if (tool.result.success) {
1669
- resultHtml = `<div class="result-item">
1670
- ✅ Message sent to <span class="result-channel">${tool.result.channel}</span>
1671
- </div>`;
1672
- }
1673
-
1674
- const statusClass = isRunning ? 'running' : 'success';
1675
- const statusText = isRunning ? 'Running...' : 'Complete';
1676
-
1677
- toolEl.innerHTML = `
1678
- <div class="tool-header" onclick="this.parentElement.classList.toggle('expanded')">
1679
- <span class="tool-icon">🔧</span>
1680
- <span class="tool-name">${tool.name}</span>
1681
- <span class="tool-status ${statusClass}">${statusText}</span>
1682
- <span class="tool-chevron">▼</span>
1683
- </div>
1684
- <div class="tool-body">
1685
- <div class="tool-section">
1686
- <div class="tool-section-label">Input</div>
1687
- <div class="tool-params">${JSON.stringify(tool.params, null, 2)}</div>
1688
- </div>
1689
- <div class="tool-section">
1690
- <div class="tool-section-label">Output</div>
1691
- <div class="tool-result">${resultHtml}</div>
1692
- </div>
1693
- </div>
1694
- `;
1695
- return toolEl;
1696
- }
1697
-
1698
- function updateToolStatus(toolEl, isComplete) {
1699
- const statusEl = toolEl.querySelector('.tool-status');
1700
- statusEl.className = `tool-status ${isComplete ? 'success' : 'running'}`;
1701
- statusEl.textContent = isComplete ? 'Complete' : 'Running...';
1702
- }
1703
-
1704
- // ═══════════════════════════════════════════════════════════════
1705
- // Run Scenario
1706
- // ═══════════════════════════════════════════════════════════════
1707
- let isRunning = false;
1708
-
1709
- async function runScenario(scenarioId) {
1710
- if (isRunning) return;
1711
- isRunning = true;
1712
- currentScenario = scenarioId;
1713
-
1714
- // Update button states
1715
- const replayBtn = document.getElementById('replayBtn');
1716
- if (replayBtn) replayBtn.disabled = true;
1717
-
1718
- document.querySelectorAll('.scenario-btn').forEach(btn => {
1719
- const isActive = btn.dataset.scenario === scenarioId;
1720
- btn.classList.toggle('active', isActive);
1721
- btn.classList.toggle('playing', isActive);
1722
- btn.setAttribute('aria-selected', isActive ? 'true' : 'false');
1723
- });
1724
-
1725
- const container = document.getElementById('chatContainer');
1726
-
1727
- // Show scenario caption
1728
- const captions = {
1729
- search: "🔍 Searching Messages",
1730
- thread: "📜 Reading Thread",
1731
- list: "💬 Listing DMs",
1732
- send: "✉️ Sending Message",
1733
- multi: "🔗 Multi-Tool Workflow"
1734
- };
1735
- const caption = document.getElementById('scenarioCaption');
1736
- caption.textContent = captions[scenarioId] || scenarioId;
1737
- caption.classList.add('visible');
1738
- setTimeout(() => caption.classList.remove('visible'), 2000);
1739
-
1740
- // Smooth transition: fade out, clear, fade in
1741
- container.classList.add('fading');
1742
- await sleep(300);
1743
- container.innerHTML = '';
1744
- container.classList.remove('fading');
1745
-
1746
- const scenario = scenarios[scenarioId];
1747
- const now = new Date();
1748
- const timeStr = now.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
1749
-
1750
- // 1. User message appears
1751
- const userMsg = createMessage('user', 'You', timeStr);
1752
- container.appendChild(userMsg);
1753
- userMsg.querySelector('.message-content').textContent = scenario.userMessage;
1754
- container.scrollTop = container.scrollHeight;
1755
-
1756
- await sleep(600);
1757
-
1758
- // 2. Claude starts typing
1759
- const typing = createTypingIndicator();
1760
- container.appendChild(typing);
1761
- container.scrollTop = container.scrollHeight;
1762
-
1763
- await sleep(1200);
1764
-
1765
- // 3. Claude's intro
1766
- typing.remove();
1767
- const claudeMsg = createMessage('claude', 'Claude', timeStr);
1768
- container.appendChild(claudeMsg);
1769
- const contentEl = claudeMsg.querySelector('.message-content');
1770
- await typeText(contentEl, scenario.claudeIntro);
1771
- container.scrollTop = container.scrollHeight;
1772
-
1773
- await sleep(700);
1774
-
1775
- // 4. Tool call(s) - handle single or multi-tool scenarios
1776
- if (scenario.isMultiTool && scenario.toolCalls) {
1777
- // Multi-tool scenario
1778
- for (let i = 0; i < scenario.toolCalls.length; i++) {
1779
- const tool = scenario.toolCalls[i];
1780
- const toolCall = createToolCall(tool, true);
1781
- contentEl.appendChild(toolCall);
1782
- container.scrollTop = container.scrollHeight;
1783
-
1784
- await sleep(400);
1785
- toolCall.classList.add('expanded');
1786
- container.scrollTop = container.scrollHeight;
1787
-
1788
- await sleep(1200);
1789
- updateToolStatus(toolCall, true);
1790
- container.scrollTop = container.scrollHeight;
1791
-
1792
- if (i < scenario.toolCalls.length - 1) {
1793
- await sleep(600); // Pause between tools
1794
- }
1795
- }
1796
- } else {
1797
- // Single tool scenario
1798
- const toolCall = createToolCall(scenario.toolCall, true);
1799
- contentEl.appendChild(toolCall);
1800
- container.scrollTop = container.scrollHeight;
1801
-
1802
- await sleep(400);
1803
- toolCall.classList.add('expanded');
1804
- container.scrollTop = container.scrollHeight;
1805
-
1806
- await sleep(1500);
1807
- updateToolStatus(toolCall, true);
1808
- container.scrollTop = container.scrollHeight;
1809
- }
1810
-
1811
- await sleep(500);
1812
-
1813
- // 5. Claude's summary
1814
- const summaryP = document.createElement('div');
1815
- summaryP.style.marginTop = '16px';
1816
- contentEl.appendChild(summaryP);
1817
- await typeText(summaryP, scenario.claudeSummary);
1818
- container.scrollTop = container.scrollHeight;
1819
-
1820
- // Cleanup
1821
- document.querySelectorAll('.scenario-btn').forEach(btn => {
1822
- btn.classList.remove('playing');
1823
- });
1824
- if (replayBtn) replayBtn.disabled = false;
1825
- isRunning = false;
1826
- }
1827
-
1828
- // ═══════════════════════════════════════════════════════════════
1829
- // Auto-Play All Scenarios
1830
- // ═══════════════════════════════════════════════════════════════
1831
- let isAutoPlaying = false;
1832
-
1833
- async function autoPlayAll() {
1834
- if (isAutoPlaying || isRunning) return;
1835
- isAutoPlaying = true;
1836
-
1837
- const autoPlayBtn = document.getElementById('autoPlayBtn');
1838
- if (autoPlayBtn) {
1839
- autoPlayBtn.innerHTML = '<span>⏹</span><span>Stop</span>';
1840
- autoPlayBtn.onclick = stopAutoPlay;
1841
- }
1842
-
1843
- // Show title card for 3s before starting
1844
- const titleCard = document.getElementById('titleCard');
1845
- const chatContainer = document.getElementById('chatContainer');
1846
- chatContainer.style.display = 'none';
1847
- titleCard.classList.add('visible');
1848
- await sleep(3000);
1849
- titleCard.classList.remove('visible');
1850
- await sleep(500); // Fade transition
1851
- chatContainer.style.display = '';
1852
-
1853
- const scenarioOrder = ['search', 'thread', 'list', 'send', 'multi'];
1854
-
1855
- for (let i = 0; i < scenarioOrder.length; i++) {
1856
- if (!isAutoPlaying) break;
1857
- updateProgress(i + 1, scenarioOrder.length);
1858
- await runScenario(scenarioOrder[i]);
1859
- if (!isAutoPlaying) break;
1860
- await sleep(2000); // Pause between scenarios
1861
- }
1862
- updateProgress(0, 0); // Clear progress
1863
-
1864
- // Show closing card for 4s after completion (only if not stopped)
1865
- if (isAutoPlaying) {
1866
- const closingCard = document.getElementById('closingCard');
1867
- chatContainer.style.display = 'none';
1868
- await sleep(500);
1869
- closingCard.classList.add('visible');
1870
- await sleep(4000);
1871
- closingCard.classList.remove('visible');
1872
- await sleep(500);
1873
- chatContainer.style.display = '';
1874
- }
1875
-
1876
- stopAutoPlay();
1877
- }
1878
-
1879
- function stopAutoPlay() {
1880
- isAutoPlaying = false;
1881
- const autoPlayBtn = document.getElementById('autoPlayBtn');
1882
- if (autoPlayBtn) {
1883
- autoPlayBtn.innerHTML = '<span>▶</span><span>Auto-Play All</span>';
1884
- autoPlayBtn.onclick = autoPlayAll;
1885
- }
1886
- updateProgress(0, 0);
1887
- }
1888
-
1889
- function updateProgress(current, total) {
1890
- const indicator = document.getElementById('progressIndicator');
1891
- if (!indicator) return;
1892
-
1893
- if (current === 0 || total === 0) {
1894
- indicator.style.display = 'none';
1895
- return;
1896
- }
1897
-
1898
- indicator.style.display = 'flex';
1899
- indicator.querySelector('.progress-text').textContent = `${current}/${total}`;
1900
- indicator.querySelector('.progress-fill').style.width = `${(current / total) * 100}%`;
1901
- }
1902
-
1903
- async function copyShareLink() {
1904
- const btn = document.querySelector('.share-btn');
1905
- const url = window.location.href.split('?')[0]; // Remove any query params
1906
-
1907
- try {
1908
- await navigator.clipboard.writeText(url);
1909
- btn.classList.add('copied');
1910
- btn.querySelector('.share-text').textContent = 'Copied!';
1911
- btn.querySelector('.share-icon').textContent = '✓';
1912
-
1913
- setTimeout(() => {
1914
- btn.classList.remove('copied');
1915
- btn.querySelector('.share-text').textContent = 'Share';
1916
- btn.querySelector('.share-icon').textContent = '🔗';
1917
- }, 2000);
1918
- } catch (err) {
1919
- // Fallback for older browsers
1920
- const textarea = document.createElement('textarea');
1921
- textarea.value = url;
1922
- document.body.appendChild(textarea);
1923
- textarea.select();
1924
- document.execCommand('copy');
1925
- document.body.removeChild(textarea);
1926
- }
1927
- }
1928
-
1929
- // ═══════════════════════════════════════════════════════════════
1930
- // Keyboard Shortcuts
1931
- // ═══════════════════════════════════════════════════════════════
1932
- function toggleFullscreen() {
1933
- document.body.classList.toggle('fullscreen-mode');
1934
- }
1935
-
1936
- document.addEventListener('keydown', (e) => {
1937
- // Fullscreen can toggle anytime
1938
- if (e.key === 'f' || e.key === 'F') {
1939
- toggleFullscreen();
1940
- return;
1941
- }
1942
-
1943
- // Escape exits fullscreen first, then stops auto-play
1944
- if (e.key === 'Escape') {
1945
- if (document.body.classList.contains('fullscreen-mode')) {
1946
- toggleFullscreen();
1947
- } else {
1948
- stopAutoPlay();
1949
- }
1950
- return;
1951
- }
1952
-
1953
- if (isRunning) return;
1954
-
1955
- switch(e.key) {
1956
- case '1': runScenario('search'); break;
1957
- case '2': runScenario('thread'); break;
1958
- case '3': runScenario('list'); break;
1959
- case '4': runScenario('send'); break;
1960
- case '5': runScenario('multi'); break;
1961
- case 'r': case 'R': replayScenario(); break;
1962
- case 'a': case 'A': autoPlayAll(); break;
1963
- }
1964
- });
1965
-
1966
- // ═══════════════════════════════════════════════════════════════
1967
- // Initialize
1968
- // ═══════════════════════════════════════════════════════════════
1969
- document.addEventListener('DOMContentLoaded', () => {
1970
- runScenario('search');
1971
- });
1972
- </script>
1973
- </body>
1974
- </html>