@d34dman/flowdrop 0.0.29 → 0.0.31

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 (55) hide show
  1. package/dist/components/App.svelte +54 -6
  2. package/dist/components/NodeSidebar.svelte +17 -9
  3. package/dist/components/SchemaForm.svelte +14 -14
  4. package/dist/components/SchemaForm.svelte.d.ts +1 -1
  5. package/dist/components/WorkflowEditor.svelte +4 -0
  6. package/dist/components/form/FormFieldLight.svelte +66 -66
  7. package/dist/components/form/FormFieldLight.svelte.d.ts +1 -1
  8. package/dist/components/form/types.d.ts +1 -1
  9. package/dist/components/nodes/ToolNode.svelte +23 -8
  10. package/dist/components/playground/ChatPanel.svelte +523 -0
  11. package/dist/components/playground/ChatPanel.svelte.d.ts +20 -0
  12. package/dist/components/playground/ExecutionLogs.svelte +486 -0
  13. package/dist/components/playground/ExecutionLogs.svelte.d.ts +14 -0
  14. package/dist/components/playground/InputCollector.svelte +444 -0
  15. package/dist/components/playground/InputCollector.svelte.d.ts +16 -0
  16. package/dist/components/playground/MessageBubble.svelte +398 -0
  17. package/dist/components/playground/MessageBubble.svelte.d.ts +15 -0
  18. package/dist/components/playground/Playground.svelte +851 -0
  19. package/dist/components/playground/Playground.svelte.d.ts +25 -0
  20. package/dist/components/playground/SessionManager.svelte +537 -0
  21. package/dist/components/playground/SessionManager.svelte.d.ts +20 -0
  22. package/dist/config/endpoints.d.ts +16 -0
  23. package/dist/config/endpoints.js +9 -0
  24. package/dist/core/index.d.ts +25 -23
  25. package/dist/core/index.js +13 -12
  26. package/dist/display/index.d.ts +2 -2
  27. package/dist/display/index.js +2 -2
  28. package/dist/editor/index.d.ts +57 -49
  29. package/dist/editor/index.js +52 -42
  30. package/dist/form/code.d.ts +4 -4
  31. package/dist/form/code.js +11 -11
  32. package/dist/form/fieldRegistry.d.ts +2 -2
  33. package/dist/form/fieldRegistry.js +8 -10
  34. package/dist/form/full.d.ts +5 -5
  35. package/dist/form/full.js +7 -7
  36. package/dist/form/index.d.ts +16 -16
  37. package/dist/form/index.js +14 -14
  38. package/dist/form/markdown.d.ts +3 -3
  39. package/dist/form/markdown.js +6 -6
  40. package/dist/index.d.ts +6 -4
  41. package/dist/index.js +9 -4
  42. package/dist/playground/index.d.ts +92 -0
  43. package/dist/playground/index.js +114 -0
  44. package/dist/playground/mount.d.ts +183 -0
  45. package/dist/playground/mount.js +178 -0
  46. package/dist/services/playgroundService.d.ts +129 -0
  47. package/dist/services/playgroundService.js +317 -0
  48. package/dist/stores/playgroundStore.d.ts +199 -0
  49. package/dist/stores/playgroundStore.js +350 -0
  50. package/dist/styles/base.css +5 -0
  51. package/dist/types/playground.d.ts +230 -0
  52. package/dist/types/playground.js +28 -0
  53. package/dist/utils/colors.d.ts +42 -0
  54. package/dist/utils/colors.js +77 -0
  55. package/package.json +6 -1
@@ -0,0 +1,851 @@
1
+ <!--
2
+ Playground Component
3
+
4
+ Main component for the Playground feature.
5
+ Clean, conversational interface similar to Langflow.
6
+ Supports both embedded (panel) and standalone (page) modes.
7
+ Styled with BEM syntax.
8
+ -->
9
+
10
+ <script lang="ts">
11
+ import { onMount, onDestroy } from 'svelte';
12
+ import Icon from '@iconify/svelte';
13
+ import ChatPanel from './ChatPanel.svelte';
14
+ import type { Workflow } from '../../types/index.js';
15
+ import type { EndpointConfig } from '../../config/endpoints.js';
16
+ import type {
17
+ PlaygroundMode,
18
+ PlaygroundConfig,
19
+ PlaygroundMessagesApiResponse
20
+ } from '../../types/playground.js';
21
+ import { playgroundService } from '../../services/playgroundService.js';
22
+ import { setEndpointConfig } from '../../services/api.js';
23
+ import {
24
+ currentSession,
25
+ sessions,
26
+ messages,
27
+ isExecuting,
28
+ isLoading,
29
+ error,
30
+ playgroundActions,
31
+ inputFields
32
+ } from '../../stores/playgroundStore.js';
33
+ import { get } from 'svelte/store';
34
+
35
+ /**
36
+ * Component props
37
+ */
38
+ interface Props {
39
+ /** Target workflow ID */
40
+ workflowId: string;
41
+ /** Pre-loaded workflow (optional, will be fetched if not provided) */
42
+ workflow?: Workflow;
43
+ /** Display mode: embedded (panel) or standalone (page) */
44
+ mode?: PlaygroundMode;
45
+ /** Resume a specific session */
46
+ initialSessionId?: string;
47
+ /** API endpoint configuration */
48
+ endpointConfig?: EndpointConfig;
49
+ /** Playground configuration options */
50
+ config?: PlaygroundConfig;
51
+ /** Callback when playground is closed (for embedded mode) */
52
+ onClose?: () => void;
53
+ }
54
+
55
+ let {
56
+ workflowId,
57
+ workflow,
58
+ mode = 'standalone',
59
+ initialSessionId,
60
+ endpointConfig,
61
+ config = {},
62
+ onClose
63
+ }: Props = $props();
64
+
65
+ /** Current input values from InputCollector */
66
+ let inputValues = $state<Record<string, unknown>>({});
67
+
68
+ /** Track session being edited for rename */
69
+ let editingSessionId = $state<string | null>(null);
70
+
71
+ /** Track session pending delete */
72
+ let pendingDeleteId = $state<string | null>(null);
73
+
74
+ /**
75
+ * Initialize the playground
76
+ */
77
+ onMount(() => {
78
+ // Set endpoint config if provided
79
+ if (endpointConfig) {
80
+ setEndpointConfig(endpointConfig);
81
+ }
82
+
83
+ // Set workflow in store
84
+ if (workflow) {
85
+ playgroundActions.setWorkflow(workflow);
86
+ }
87
+
88
+ // Async initialization
89
+ const initializePlayground = async (): Promise<void> => {
90
+ try {
91
+ // Load sessions
92
+ await loadSessions();
93
+
94
+ // Resume initial session if provided
95
+ if (initialSessionId) {
96
+ await loadSession(initialSessionId);
97
+ }
98
+ } catch (err) {
99
+ console.error('[Playground] Initialization error:', err);
100
+ }
101
+ };
102
+
103
+ // Execute initialization
104
+ void initializePlayground();
105
+ });
106
+
107
+ /**
108
+ * Cleanup on destroy
109
+ */
110
+ onDestroy(() => {
111
+ playgroundService.stopPolling();
112
+ playgroundActions.reset();
113
+ });
114
+
115
+ /**
116
+ * Load sessions for the workflow
117
+ */
118
+ async function loadSessions(): Promise<void> {
119
+ playgroundActions.setLoading(true);
120
+ playgroundActions.setError(null);
121
+
122
+ try {
123
+ const sessionList = await playgroundService.listSessions(workflowId);
124
+ playgroundActions.setSessions(sessionList);
125
+ } catch (err) {
126
+ const errorMessage = err instanceof Error ? err.message : 'Failed to load sessions';
127
+ playgroundActions.setError(errorMessage);
128
+ console.error('Failed to load sessions:', err);
129
+ } finally {
130
+ playgroundActions.setLoading(false);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Load a specific session and its messages
136
+ */
137
+ async function loadSession(sessionId: string): Promise<void> {
138
+ playgroundActions.setLoading(true);
139
+ playgroundActions.setError(null);
140
+
141
+ try {
142
+ // Get session details
143
+ const session = await playgroundService.getSession(sessionId);
144
+ playgroundActions.setCurrentSession(session);
145
+
146
+ // Get messages
147
+ const response = await playgroundService.getMessages(sessionId);
148
+ playgroundActions.setMessages(response.data ?? []);
149
+
150
+ // Start polling if session is running
151
+ if (session.status === 'running') {
152
+ startPolling(sessionId);
153
+ }
154
+ } catch (err) {
155
+ const errorMessage = err instanceof Error ? err.message : 'Failed to load session';
156
+ playgroundActions.setError(errorMessage);
157
+ console.error('Failed to load session:', err);
158
+ } finally {
159
+ playgroundActions.setLoading(false);
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Create a new session
165
+ */
166
+ async function handleCreateSession(): Promise<void> {
167
+ playgroundActions.setLoading(true);
168
+ playgroundActions.setError(null);
169
+
170
+ try {
171
+ const sessionName = `Session ${get(sessions).length + 1}`;
172
+ const session = await playgroundService.createSession(workflowId, sessionName);
173
+ playgroundActions.addSession(session);
174
+ playgroundActions.setCurrentSession(session);
175
+ playgroundActions.clearMessages();
176
+ } catch (err) {
177
+ const errorMessage = err instanceof Error ? err.message : 'Failed to create session';
178
+ playgroundActions.setError(errorMessage);
179
+ console.error('Failed to create session:', err);
180
+ } finally {
181
+ playgroundActions.setLoading(false);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Select a session
187
+ */
188
+ async function handleSelectSession(sessionId: string): Promise<void> {
189
+ const currentSessionId = get(currentSession)?.id;
190
+ if (currentSessionId === sessionId) {
191
+ return;
192
+ }
193
+
194
+ // Stop polling for current session
195
+ playgroundService.stopPolling();
196
+
197
+ await loadSession(sessionId);
198
+ }
199
+
200
+ /**
201
+ * Delete a session
202
+ */
203
+ async function handleDeleteSession(sessionId: string): Promise<void> {
204
+ try {
205
+ await playgroundService.deleteSession(sessionId);
206
+ playgroundActions.removeSession(sessionId);
207
+
208
+ // If we deleted the current session, clear it
209
+ if (get(currentSession)?.id === sessionId) {
210
+ playgroundService.stopPolling();
211
+ }
212
+ pendingDeleteId = null;
213
+ } catch (err) {
214
+ const errorMessage = err instanceof Error ? err.message : 'Failed to delete session';
215
+ playgroundActions.setError(errorMessage);
216
+ console.error('Failed to delete session:', err);
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Handle delete click - show confirmation or execute
222
+ */
223
+ function handleDeleteClick(event: Event, sessionId: string): void {
224
+ event.stopPropagation();
225
+ if (pendingDeleteId === sessionId) {
226
+ // Confirm deletion
227
+ void handleDeleteSession(sessionId);
228
+ } else {
229
+ pendingDeleteId = sessionId;
230
+ // Auto-reset after 3 seconds
231
+ setTimeout(() => {
232
+ if (pendingDeleteId === sessionId) {
233
+ pendingDeleteId = null;
234
+ }
235
+ }, 3000);
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Close current session (go back to welcome)
241
+ */
242
+ function handleCloseSession(): void {
243
+ playgroundService.stopPolling();
244
+ playgroundActions.setCurrentSession(null);
245
+ playgroundActions.clearMessages();
246
+ }
247
+
248
+ /**
249
+ * Send a message
250
+ */
251
+ async function handleSendMessage(content: string): Promise<void> {
252
+ const session = get(currentSession);
253
+ if (!session) {
254
+ // Create a session first if none exists
255
+ await handleCreateSession();
256
+ const newSession = get(currentSession);
257
+ if (!newSession) {
258
+ return;
259
+ }
260
+ }
261
+
262
+ const sessionId = get(currentSession)?.id;
263
+ if (!sessionId) {
264
+ return;
265
+ }
266
+
267
+ playgroundActions.setExecuting(true);
268
+ playgroundActions.setError(null);
269
+
270
+ try {
271
+ // Prepare inputs from the input collector
272
+ const inputs: Record<string, unknown> = {};
273
+ const fields = get(inputFields);
274
+
275
+ fields.forEach((field) => {
276
+ const key = `${field.nodeId}:${field.fieldId}`;
277
+ if (inputValues[key] !== undefined) {
278
+ // Map to node ID and field ID for the backend
279
+ if (!inputs[field.nodeId]) {
280
+ inputs[field.nodeId] = {};
281
+ }
282
+ (inputs[field.nodeId] as Record<string, unknown>)[field.fieldId] = inputValues[key];
283
+ }
284
+ });
285
+
286
+ // Send message
287
+ const message = await playgroundService.sendMessage(sessionId, content, inputs);
288
+ playgroundActions.addMessage(message);
289
+
290
+ // Update session status
291
+ playgroundActions.updateSessionStatus('running');
292
+
293
+ // Start polling for responses
294
+ startPolling(sessionId);
295
+ } catch (err) {
296
+ const errorMessage = err instanceof Error ? err.message : 'Failed to send message';
297
+ playgroundActions.setError(errorMessage);
298
+ playgroundActions.setExecuting(false);
299
+ console.error('Failed to send message:', err);
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Stop execution
305
+ */
306
+ async function handleStopExecution(): Promise<void> {
307
+ const sessionId = get(currentSession)?.id;
308
+ if (!sessionId) {
309
+ return;
310
+ }
311
+
312
+ try {
313
+ await playgroundService.stopExecution(sessionId);
314
+ playgroundService.stopPolling();
315
+ playgroundActions.setExecuting(false);
316
+ playgroundActions.updateSessionStatus('idle');
317
+ } catch (err) {
318
+ const errorMessage = err instanceof Error ? err.message : 'Failed to stop execution';
319
+ playgroundActions.setError(errorMessage);
320
+ console.error('Failed to stop execution:', err);
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Start polling for messages
326
+ */
327
+ function startPolling(sessionId: string): void {
328
+ const pollingInterval = config.pollingInterval ?? 1500;
329
+
330
+ playgroundService.startPolling(
331
+ sessionId,
332
+ (response: PlaygroundMessagesApiResponse) => {
333
+ // Add new messages
334
+ if (response.data && response.data.length > 0) {
335
+ playgroundActions.addMessages(response.data);
336
+ }
337
+
338
+ // Update session status
339
+ if (response.sessionStatus) {
340
+ playgroundActions.updateSessionStatus(response.sessionStatus);
341
+
342
+ // Stop executing if completed or failed
343
+ if (response.sessionStatus === 'completed' || response.sessionStatus === 'failed') {
344
+ playgroundActions.setExecuting(false);
345
+ }
346
+ }
347
+ },
348
+ pollingInterval
349
+ );
350
+ }
351
+
352
+ /**
353
+ * Format date for display
354
+ */
355
+ function formatDate(dateString: string): string {
356
+ const date = new Date(dateString);
357
+ const now = new Date();
358
+ const diffMs = now.getTime() - date.getTime();
359
+ const diffMins = Math.floor(diffMs / 60000);
360
+ const diffHours = Math.floor(diffMs / 3600000);
361
+ const diffDays = Math.floor(diffMs / 86400000);
362
+
363
+ if (diffMins < 1) {
364
+ return 'Just now';
365
+ }
366
+ if (diffMins < 60) {
367
+ return `${diffMins}m ago`;
368
+ }
369
+ if (diffHours < 24) {
370
+ return `${diffHours}h ago`;
371
+ }
372
+ if (diffDays < 7) {
373
+ return `${diffDays}d ago`;
374
+ }
375
+ return date.toLocaleDateString('en-US', {
376
+ month: 'short',
377
+ day: 'numeric'
378
+ });
379
+ }
380
+ </script>
381
+
382
+ <div
383
+ class="playground"
384
+ class:playground--embedded={mode === 'embedded'}
385
+ class:playground--standalone={mode === 'standalone'}
386
+ >
387
+ <div class="playground__container">
388
+ <!-- Sidebar -->
389
+ <aside class="playground__sidebar">
390
+ <!-- Sidebar Header -->
391
+ <div class="playground__sidebar-header">
392
+ <div class="playground__sidebar-title">
393
+ <Icon icon="mdi:play-circle-outline" />
394
+ <span>Playground</span>
395
+ </div>
396
+ {#if mode === 'embedded' && onClose}
397
+ <button
398
+ type="button"
399
+ class="playground__sidebar-close"
400
+ onclick={onClose}
401
+ title="Close playground"
402
+ >
403
+ <Icon icon="mdi:dock-right" />
404
+ </button>
405
+ {/if}
406
+ </div>
407
+
408
+ <!-- Chat Section -->
409
+ <div class="playground__section">
410
+ <div class="playground__section-header">
411
+ <div class="playground__section-title">
412
+ <Icon icon="mdi:chat-outline" />
413
+ <span>Chat</span>
414
+ </div>
415
+ <button
416
+ type="button"
417
+ class="playground__add-btn"
418
+ onclick={handleCreateSession}
419
+ disabled={$isLoading}
420
+ title="New chat session"
421
+ >
422
+ <Icon icon="mdi:plus" />
423
+ </button>
424
+ </div>
425
+
426
+ <!-- Sessions List -->
427
+ <div class="playground__sessions">
428
+ {#if $sessions.length === 0 && !$isLoading}
429
+ <div class="playground__sessions-empty">
430
+ <span>No sessions yet</span>
431
+ </div>
432
+ {:else}
433
+ {#each $sessions as session (session.id)}
434
+ <div
435
+ class="playground__session"
436
+ class:playground__session--active={$currentSession?.id === session.id}
437
+ role="button"
438
+ tabindex="0"
439
+ onclick={() => handleSelectSession(session.id)}
440
+ onkeydown={(e) => e.key === 'Enter' && handleSelectSession(session.id)}
441
+ >
442
+ <span class="playground__session-name" title={session.name}>
443
+ {session.name}
444
+ </span>
445
+ <button
446
+ type="button"
447
+ class="playground__session-menu"
448
+ class:playground__session-menu--delete={pendingDeleteId === session.id}
449
+ onclick={(e) => handleDeleteClick(e, session.id)}
450
+ title={pendingDeleteId === session.id
451
+ ? 'Click to confirm delete'
452
+ : 'Delete session'}
453
+ >
454
+ {#if pendingDeleteId === session.id}
455
+ <Icon icon="mdi:check" />
456
+ {:else}
457
+ <Icon icon="mdi:dots-horizontal" />
458
+ {/if}
459
+ </button>
460
+ </div>
461
+ {/each}
462
+ {/if}
463
+ </div>
464
+ </div>
465
+ </aside>
466
+
467
+ <!-- Main Content -->
468
+ <main class="playground__main">
469
+ <!-- Session Header -->
470
+ {#if $currentSession}
471
+ <header class="playground__header">
472
+ <h2 class="playground__header-title">{$currentSession.name}</h2>
473
+ <button
474
+ type="button"
475
+ class="playground__header-close"
476
+ onclick={handleCloseSession}
477
+ title="Close session"
478
+ >
479
+ <Icon icon="mdi:close" />
480
+ </button>
481
+ </header>
482
+ {/if}
483
+
484
+ <!-- Error Banner -->
485
+ {#if $error}
486
+ <div class="playground__error">
487
+ <Icon icon="mdi:alert-circle" />
488
+ <span>{$error}</span>
489
+ <button
490
+ type="button"
491
+ class="playground__error-dismiss"
492
+ onclick={() => playgroundActions.setError(null)}
493
+ >
494
+ <Icon icon="mdi:close" />
495
+ </button>
496
+ </div>
497
+ {/if}
498
+
499
+ <!-- Chat Content -->
500
+ <div class="playground__content">
501
+ {#if $isLoading && !$currentSession}
502
+ <div class="playground__loading">
503
+ <Icon icon="mdi:loading" class="playground__loading-icon" />
504
+ <span>Loading...</span>
505
+ </div>
506
+ {:else}
507
+ <ChatPanel
508
+ showTimestamps={config.showTimestamps ?? true}
509
+ autoScroll={config.autoScroll ?? true}
510
+ showLogsInline={config.logDisplayMode === 'inline'}
511
+ onSendMessage={handleSendMessage}
512
+ onStopExecution={handleStopExecution}
513
+ />
514
+ {/if}
515
+ </div>
516
+ </main>
517
+ </div>
518
+ </div>
519
+
520
+ <style>
521
+ .playground {
522
+ display: flex;
523
+ flex-direction: column;
524
+ height: 100%;
525
+ background-color: #f8fafc;
526
+ font-family:
527
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
528
+ }
529
+
530
+ .playground--embedded {
531
+ border-left: 1px solid #e2e8f0;
532
+ box-shadow: -4px 0 20px rgba(0, 0, 0, 0.08);
533
+ }
534
+
535
+ .playground--standalone {
536
+ height: 100vh;
537
+ }
538
+
539
+ /* Container */
540
+ .playground__container {
541
+ display: flex;
542
+ flex: 1;
543
+ min-height: 0;
544
+ }
545
+
546
+ /* Sidebar */
547
+ .playground__sidebar {
548
+ width: 220px;
549
+ background-color: #fafbfc;
550
+ border-right: 1px solid #e5e7eb;
551
+ display: flex;
552
+ flex-direction: column;
553
+ }
554
+
555
+ .playground__sidebar-header {
556
+ display: flex;
557
+ align-items: center;
558
+ justify-content: space-between;
559
+ padding: 1rem;
560
+ border-bottom: 1px solid #e5e7eb;
561
+ }
562
+
563
+ .playground__sidebar-title {
564
+ display: flex;
565
+ align-items: center;
566
+ gap: 0.5rem;
567
+ font-size: 0.9375rem;
568
+ font-weight: 600;
569
+ color: #1f2937;
570
+ }
571
+
572
+ .playground__sidebar-close {
573
+ display: flex;
574
+ align-items: center;
575
+ justify-content: center;
576
+ width: 1.75rem;
577
+ height: 1.75rem;
578
+ border: none;
579
+ border-radius: 0.375rem;
580
+ background: transparent;
581
+ color: #6b7280;
582
+ cursor: pointer;
583
+ transition: all 0.15s ease;
584
+ }
585
+
586
+ .playground__sidebar-close:hover {
587
+ background-color: #f3f4f6;
588
+ color: #374151;
589
+ }
590
+
591
+ /* Section */
592
+ .playground__section {
593
+ flex: 1;
594
+ display: flex;
595
+ flex-direction: column;
596
+ min-height: 0;
597
+ }
598
+
599
+ .playground__section-header {
600
+ display: flex;
601
+ align-items: center;
602
+ justify-content: space-between;
603
+ padding: 0.75rem 1rem;
604
+ }
605
+
606
+ .playground__section-title {
607
+ display: flex;
608
+ align-items: center;
609
+ gap: 0.5rem;
610
+ font-size: 0.8125rem;
611
+ font-weight: 500;
612
+ color: #6b7280;
613
+ }
614
+
615
+ .playground__add-btn {
616
+ display: flex;
617
+ align-items: center;
618
+ justify-content: center;
619
+ width: 1.5rem;
620
+ height: 1.5rem;
621
+ border: none;
622
+ border-radius: 0.375rem;
623
+ background: transparent;
624
+ color: #6b7280;
625
+ cursor: pointer;
626
+ transition: all 0.15s ease;
627
+ }
628
+
629
+ .playground__add-btn:hover:not(:disabled) {
630
+ background-color: #e5e7eb;
631
+ color: #374151;
632
+ }
633
+
634
+ .playground__add-btn:disabled {
635
+ opacity: 0.5;
636
+ cursor: not-allowed;
637
+ }
638
+
639
+ /* Sessions */
640
+ .playground__sessions {
641
+ flex: 1;
642
+ overflow-y: auto;
643
+ padding: 0 0.5rem 1rem;
644
+ }
645
+
646
+ .playground__sessions-empty {
647
+ padding: 1rem;
648
+ text-align: center;
649
+ font-size: 0.8125rem;
650
+ color: #9ca3af;
651
+ }
652
+
653
+ .playground__session {
654
+ display: flex;
655
+ align-items: center;
656
+ justify-content: space-between;
657
+ padding: 0.625rem 0.75rem;
658
+ margin-bottom: 0.25rem;
659
+ border-radius: 0.5rem;
660
+ cursor: pointer;
661
+ transition: all 0.15s ease;
662
+ }
663
+
664
+ .playground__session:hover {
665
+ background-color: #f3f4f6;
666
+ }
667
+
668
+ .playground__session--active {
669
+ background-color: #e0e7ff;
670
+ }
671
+
672
+ .playground__session--active:hover {
673
+ background-color: #c7d2fe;
674
+ }
675
+
676
+ .playground__session-name {
677
+ flex: 1;
678
+ font-size: 0.875rem;
679
+ color: #374151;
680
+ white-space: nowrap;
681
+ overflow: hidden;
682
+ text-overflow: ellipsis;
683
+ }
684
+
685
+ .playground__session--active .playground__session-name {
686
+ color: #4338ca;
687
+ font-weight: 500;
688
+ }
689
+
690
+ .playground__session-menu {
691
+ display: flex;
692
+ align-items: center;
693
+ justify-content: center;
694
+ width: 1.5rem;
695
+ height: 1.5rem;
696
+ border: none;
697
+ border-radius: 0.25rem;
698
+ background: transparent;
699
+ color: #9ca3af;
700
+ cursor: pointer;
701
+ opacity: 0;
702
+ transition: all 0.15s ease;
703
+ }
704
+
705
+ .playground__session:hover .playground__session-menu {
706
+ opacity: 1;
707
+ }
708
+
709
+ .playground__session-menu:hover {
710
+ background-color: #fecaca;
711
+ color: #dc2626;
712
+ }
713
+
714
+ .playground__session-menu--delete {
715
+ opacity: 1;
716
+ background-color: #dcfce7;
717
+ color: #16a34a;
718
+ }
719
+
720
+ .playground__session-menu--delete:hover {
721
+ background-color: #bbf7d0;
722
+ color: #15803d;
723
+ }
724
+
725
+ /* Main Content */
726
+ .playground__main {
727
+ flex: 1;
728
+ display: flex;
729
+ flex-direction: column;
730
+ min-width: 0;
731
+ background-color: #ffffff;
732
+ }
733
+
734
+ /* Header */
735
+ .playground__header {
736
+ display: flex;
737
+ align-items: center;
738
+ justify-content: space-between;
739
+ padding: 0.875rem 1.25rem;
740
+ border-bottom: 1px solid #e5e7eb;
741
+ background-color: #fafbfc;
742
+ }
743
+
744
+ .playground__header-title {
745
+ font-size: 0.9375rem;
746
+ font-weight: 600;
747
+ color: #1f2937;
748
+ margin: 0;
749
+ }
750
+
751
+ .playground__header-close {
752
+ display: flex;
753
+ align-items: center;
754
+ justify-content: center;
755
+ width: 1.75rem;
756
+ height: 1.75rem;
757
+ border: none;
758
+ border-radius: 0.375rem;
759
+ background: transparent;
760
+ color: #6b7280;
761
+ cursor: pointer;
762
+ transition: all 0.15s ease;
763
+ }
764
+
765
+ .playground__header-close:hover {
766
+ background-color: #f3f4f6;
767
+ color: #374151;
768
+ }
769
+
770
+ /* Error */
771
+ .playground__error {
772
+ display: flex;
773
+ align-items: center;
774
+ gap: 0.5rem;
775
+ padding: 0.75rem 1rem;
776
+ background-color: #fef2f2;
777
+ border-bottom: 1px solid #fecaca;
778
+ color: #dc2626;
779
+ font-size: 0.875rem;
780
+ }
781
+
782
+ .playground__error-dismiss {
783
+ margin-left: auto;
784
+ display: flex;
785
+ align-items: center;
786
+ justify-content: center;
787
+ width: 1.5rem;
788
+ height: 1.5rem;
789
+ border: none;
790
+ border-radius: 0.25rem;
791
+ background: transparent;
792
+ color: #dc2626;
793
+ cursor: pointer;
794
+ transition: background-color 0.15s ease;
795
+ }
796
+
797
+ .playground__error-dismiss:hover {
798
+ background-color: #fee2e2;
799
+ }
800
+
801
+ /* Content */
802
+ .playground__content {
803
+ flex: 1;
804
+ min-height: 0;
805
+ display: flex;
806
+ flex-direction: column;
807
+ }
808
+
809
+ /* Loading */
810
+ .playground__loading {
811
+ display: flex;
812
+ flex-direction: column;
813
+ align-items: center;
814
+ justify-content: center;
815
+ flex: 1;
816
+ gap: 1rem;
817
+ color: #6b7280;
818
+ }
819
+
820
+ :global(.playground__loading-icon) {
821
+ font-size: 2rem;
822
+ animation: spin 1s linear infinite;
823
+ }
824
+
825
+ @keyframes spin {
826
+ from {
827
+ transform: rotate(0deg);
828
+ }
829
+ to {
830
+ transform: rotate(360deg);
831
+ }
832
+ }
833
+
834
+ /* Responsive */
835
+ @media (max-width: 768px) {
836
+ .playground__sidebar {
837
+ width: 180px;
838
+ }
839
+ }
840
+
841
+ @media (max-width: 640px) {
842
+ .playground__sidebar {
843
+ position: absolute;
844
+ left: 0;
845
+ top: 0;
846
+ bottom: 0;
847
+ z-index: 20;
848
+ box-shadow: 4px 0 20px rgba(0, 0, 0, 0.1);
849
+ }
850
+ }
851
+ </style>