sparkecode-devtools 0.1.41 → 0.1.42
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.
- package/README.md +24 -3
- package/next.d.ts +8 -1
- package/next.js +10 -4
- package/package.json +1 -1
- package/sparkecode-select.js +404 -63
package/README.md
CHANGED
|
@@ -109,16 +109,37 @@ You can also send copy events to any endpoint:
|
|
|
109
109
|
|
|
110
110
|
## Controls
|
|
111
111
|
|
|
112
|
-
- Click the floating button to toggle selection
|
|
112
|
+
- Click the floating button to toggle selection mode
|
|
113
|
+
- Click the panel icon (right side of button) to open the SparkeCode web UI
|
|
113
114
|
- Drag the button to reposition
|
|
114
|
-
- Press `Cmd/Ctrl+Shift+S` to toggle
|
|
115
|
+
- Press `Cmd/Ctrl+Shift+S` to toggle selection mode
|
|
115
116
|
- Press `Esc` to exit selection mode
|
|
116
117
|
|
|
118
|
+
## Floating Web UI Panel
|
|
119
|
+
|
|
120
|
+
Click the panel icon on the button to open a draggable, collapsible panel showing the full SparkeCode web interface. This lets you see the agent's responses and chat history while working on your app.
|
|
121
|
+
|
|
122
|
+
- Drag the header to move the panel
|
|
123
|
+
- Click the minus button to collapse
|
|
124
|
+
- Click the X to close
|
|
125
|
+
|
|
126
|
+
Configure the web UI URL:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# In your .env.local
|
|
130
|
+
NEXT_PUBLIC_SPARKECODER_WEB_UI_URL=http://localhost:6969
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Or via prop:
|
|
134
|
+
```tsx
|
|
135
|
+
<SparkeCodeSelect webUiUrl="http://localhost:6969" />
|
|
136
|
+
```
|
|
137
|
+
|
|
117
138
|
## Auto-send to Agent
|
|
118
139
|
|
|
119
140
|
When connected to a running Sparkecoder instance, clicking a component will:
|
|
120
141
|
|
|
121
|
-
1. Show a prompt input
|
|
142
|
+
1. Show a prompt input
|
|
122
143
|
2. Let you type what you want to do with the component (e.g., "make this button red")
|
|
123
144
|
3. Press Enter to send the request + component context directly to the agent
|
|
124
145
|
4. The agent will start working immediately
|
package/next.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface SparkeCodeSelectConfig {
|
|
|
10
10
|
enabled?: boolean;
|
|
11
11
|
apiBase?: string | null;
|
|
12
12
|
sessionId?: string | null;
|
|
13
|
+
webUiUrl?: string | null;
|
|
13
14
|
};
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -20,10 +21,16 @@ export interface SparkeCodeSelectProps {
|
|
|
20
21
|
sparkecoderEnabled?: boolean;
|
|
21
22
|
strategy?: 'afterInteractive' | 'lazyOnload' | 'beforeInteractive' | 'worker';
|
|
22
23
|
/**
|
|
23
|
-
* Override the Sparkecoder server URL.
|
|
24
|
+
* Override the Sparkecoder server URL (API).
|
|
24
25
|
* Defaults to NEXT_PUBLIC_SPARKECODER_DEV_TOOLS_SERVER_URL or SPARKECODER_DEV_TOOLS_SERVER_URL env variable.
|
|
25
26
|
*/
|
|
26
27
|
serverUrl?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Override the Sparkecoder Web UI URL.
|
|
30
|
+
* Defaults to NEXT_PUBLIC_SPARKECODER_WEB_UI_URL or SPARKECODER_WEB_UI_URL env variable.
|
|
31
|
+
* Falls back to http://localhost:6969 if not set.
|
|
32
|
+
*/
|
|
33
|
+
webUiUrl?: string;
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
export function SparkeCodeSelect(props: SparkeCodeSelectProps): JSX.Element | null;
|
package/next.js
CHANGED
|
@@ -5,12 +5,16 @@ import { useEffect, useMemo } from 'react';
|
|
|
5
5
|
|
|
6
6
|
const DEFAULT_SCRIPT_URL = 'https://unpkg.com/sparkecode-devtools/sparkecode-select.js';
|
|
7
7
|
|
|
8
|
-
// Check for env
|
|
9
|
-
//
|
|
8
|
+
// Check for env variables to override defaults
|
|
9
|
+
// These need to be prefixed with NEXT_PUBLIC_ to be available in the browser
|
|
10
10
|
const ENV_SERVER_URL = typeof process !== 'undefined'
|
|
11
11
|
? (process.env.NEXT_PUBLIC_SPARKECODER_DEV_TOOLS_SERVER_URL || process.env.SPARKECODER_DEV_TOOLS_SERVER_URL)
|
|
12
12
|
: undefined;
|
|
13
13
|
|
|
14
|
+
const ENV_WEB_UI_URL = typeof process !== 'undefined'
|
|
15
|
+
? (process.env.NEXT_PUBLIC_SPARKECODER_WEB_UI_URL || process.env.SPARKECODER_WEB_UI_URL)
|
|
16
|
+
: undefined;
|
|
17
|
+
|
|
14
18
|
export function SparkeCodeSelect({
|
|
15
19
|
enabled = process.env.NODE_ENV !== 'production',
|
|
16
20
|
scriptUrl = DEFAULT_SCRIPT_URL,
|
|
@@ -18,16 +22,18 @@ export function SparkeCodeSelect({
|
|
|
18
22
|
sparkecoderEnabled = true,
|
|
19
23
|
strategy = 'afterInteractive',
|
|
20
24
|
serverUrl = ENV_SERVER_URL,
|
|
25
|
+
webUiUrl = ENV_WEB_UI_URL,
|
|
21
26
|
}) {
|
|
22
27
|
const mergedConfig = useMemo(() => ({
|
|
23
28
|
...(config || {}),
|
|
24
29
|
sparkecoder: {
|
|
25
30
|
enabled: sparkecoderEnabled,
|
|
26
|
-
// Use
|
|
31
|
+
// Use props which default to env variables if set
|
|
27
32
|
...(serverUrl ? { apiBase: serverUrl } : {}),
|
|
33
|
+
...(webUiUrl ? { webUiUrl } : {}),
|
|
28
34
|
...(config && config.sparkecoder ? config.sparkecoder : {}),
|
|
29
35
|
},
|
|
30
|
-
}), [config, sparkecoderEnabled, serverUrl]);
|
|
36
|
+
}), [config, sparkecoderEnabled, serverUrl, webUiUrl]);
|
|
31
37
|
|
|
32
38
|
useEffect(() => {
|
|
33
39
|
if (!enabled) return;
|
package/package.json
CHANGED
package/sparkecode-select.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
enabled: false,
|
|
26
26
|
apiBase: null,
|
|
27
27
|
sessionId: null,
|
|
28
|
+
webUiUrl: null, // Web UI URL (default: http://localhost:6969)
|
|
28
29
|
},
|
|
29
30
|
};
|
|
30
31
|
|
|
@@ -338,7 +339,7 @@
|
|
|
338
339
|
|
|
339
340
|
async function runAgentWithPrompt(promptText) {
|
|
340
341
|
const connection = await ensureSparkecoderConnection();
|
|
341
|
-
if (!connection) return false;
|
|
342
|
+
if (!connection) return { success: false };
|
|
342
343
|
|
|
343
344
|
try {
|
|
344
345
|
// Post to agents run endpoint to actually run the agent
|
|
@@ -350,10 +351,13 @@
|
|
|
350
351
|
body: JSON.stringify({ prompt: promptText }),
|
|
351
352
|
}
|
|
352
353
|
);
|
|
353
|
-
|
|
354
|
+
if (res.ok) {
|
|
355
|
+
return { success: true, sessionId: connection.sessionId };
|
|
356
|
+
}
|
|
357
|
+
return { success: false };
|
|
354
358
|
} catch (e) {
|
|
355
359
|
console.warn('[SparkeCode] Failed to run agent:', e.message);
|
|
356
|
-
return false;
|
|
360
|
+
return { success: false };
|
|
357
361
|
}
|
|
358
362
|
}
|
|
359
363
|
|
|
@@ -461,87 +465,139 @@
|
|
|
461
465
|
function createFloatingButton() {
|
|
462
466
|
floatingButton = document.createElement('div');
|
|
463
467
|
floatingButton.id = 'sparkecode-button';
|
|
464
|
-
// Clean pointer icon + text
|
|
465
|
-
floatingButton.innerHTML = `
|
|
466
|
-
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" style="flex-shrink: 0;">
|
|
467
|
-
<path d="M3 1.5L3 12.5L6.5 9L10 14L12 13L8.5 8L13 7.5L3 1.5Z"/>
|
|
468
|
-
</svg>
|
|
469
|
-
<span style="font-weight: 500; font-size: 12px;">SparkeCode Select</span>
|
|
470
|
-
`;
|
|
471
468
|
floatingButton.style.cssText = `
|
|
472
469
|
position: fixed;
|
|
473
470
|
top: ${buttonPos.y}px;
|
|
474
471
|
left: ${buttonPos.x}px;
|
|
475
472
|
height: 32px;
|
|
476
|
-
padding: 0 10px;
|
|
477
|
-
gap: 6px;
|
|
478
473
|
background: ${config.bgColor};
|
|
479
474
|
border: 1px solid ${config.borderColor};
|
|
480
475
|
border-radius: 6px;
|
|
481
476
|
display: flex;
|
|
482
477
|
align-items: center;
|
|
483
|
-
justify-content: center;
|
|
484
|
-
cursor: grab;
|
|
485
478
|
z-index: 999998;
|
|
486
479
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
487
480
|
transition: all 0.15s ease;
|
|
488
|
-
color: ${config.primaryColor};
|
|
489
481
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
490
482
|
user-select: none;
|
|
483
|
+
overflow: hidden;
|
|
491
484
|
`;
|
|
492
|
-
|
|
485
|
+
|
|
486
|
+
// Main button area (select toggle)
|
|
487
|
+
const mainBtn = document.createElement('div');
|
|
488
|
+
mainBtn.id = 'sparkecode-button-main';
|
|
489
|
+
mainBtn.innerHTML = `
|
|
490
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" style="flex-shrink: 0;">
|
|
491
|
+
<path d="M3 1.5L3 12.5L6.5 9L10 14L12 13L8.5 8L13 7.5L3 1.5Z"/>
|
|
492
|
+
</svg>
|
|
493
|
+
<span style="font-weight: 500; font-size: 12px;">SparkeCode Select</span>
|
|
494
|
+
`;
|
|
495
|
+
mainBtn.style.cssText = `
|
|
496
|
+
display: flex;
|
|
497
|
+
align-items: center;
|
|
498
|
+
gap: 6px;
|
|
499
|
+
padding: 0 10px;
|
|
500
|
+
height: 100%;
|
|
501
|
+
cursor: grab;
|
|
502
|
+
color: ${config.primaryColor};
|
|
503
|
+
`;
|
|
504
|
+
mainBtn.title = 'Click to toggle select mode • Drag to move';
|
|
505
|
+
|
|
506
|
+
// Expand button (show web UI)
|
|
507
|
+
const expandBtn = document.createElement('div');
|
|
508
|
+
expandBtn.id = 'sparkecode-button-expand';
|
|
509
|
+
expandBtn.innerHTML = `
|
|
510
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
511
|
+
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
|
512
|
+
<path d="M9 3v18"/>
|
|
513
|
+
</svg>
|
|
514
|
+
`;
|
|
515
|
+
expandBtn.style.cssText = `
|
|
516
|
+
display: flex;
|
|
517
|
+
align-items: center;
|
|
518
|
+
justify-content: center;
|
|
519
|
+
width: 32px;
|
|
520
|
+
height: 100%;
|
|
521
|
+
cursor: pointer;
|
|
522
|
+
color: ${config.mutedColor};
|
|
523
|
+
border-left: 1px solid ${config.borderColor};
|
|
524
|
+
transition: background 0.15s, color 0.15s;
|
|
525
|
+
`;
|
|
526
|
+
expandBtn.title = 'Open SparkeCode panel';
|
|
527
|
+
expandBtn.addEventListener('mouseenter', () => {
|
|
528
|
+
expandBtn.style.background = 'rgba(255,255,255,0.05)';
|
|
529
|
+
expandBtn.style.color = config.primaryColor;
|
|
530
|
+
});
|
|
531
|
+
expandBtn.addEventListener('mouseleave', () => {
|
|
532
|
+
expandBtn.style.background = 'transparent';
|
|
533
|
+
expandBtn.style.color = config.mutedColor;
|
|
534
|
+
});
|
|
535
|
+
expandBtn.addEventListener('click', (e) => {
|
|
536
|
+
e.stopPropagation();
|
|
537
|
+
if (isWebFrameVisible()) {
|
|
538
|
+
hideWebFrame();
|
|
539
|
+
} else {
|
|
540
|
+
showWebFrame();
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
floatingButton.appendChild(mainBtn);
|
|
545
|
+
floatingButton.appendChild(expandBtn);
|
|
493
546
|
|
|
494
|
-
// Drag handling
|
|
495
|
-
|
|
496
|
-
|
|
547
|
+
// Drag handling - only on main button area
|
|
548
|
+
mainBtn.addEventListener('mousedown', startDrag);
|
|
549
|
+
mainBtn.addEventListener('touchstart', startDrag, { passive: false });
|
|
497
550
|
|
|
498
551
|
document.body.appendChild(floatingButton);
|
|
499
552
|
}
|
|
500
553
|
|
|
501
554
|
function startDrag(e) {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
555
|
+
// Only handle drag on main button, not expand
|
|
556
|
+
if (e.target.closest('#sparkecode-button-expand')) return;
|
|
557
|
+
|
|
558
|
+
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
|
559
|
+
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
|
560
|
+
|
|
561
|
+
dragOffset.x = clientX - buttonPos.x;
|
|
562
|
+
dragOffset.y = clientY - buttonPos.y;
|
|
563
|
+
isDragging = false;
|
|
564
|
+
|
|
565
|
+
const startX = clientX;
|
|
566
|
+
const startY = clientY;
|
|
567
|
+
const mainBtn = document.getElementById('sparkecode-button-main');
|
|
568
|
+
|
|
569
|
+
const onMove = (e) => {
|
|
570
|
+
const moveX = e.touches ? e.touches[0].clientX : e.clientX;
|
|
571
|
+
const moveY = e.touches ? e.touches[0].clientY : e.clientY;
|
|
505
572
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
573
|
+
// Only start dragging if moved more than 5px
|
|
574
|
+
if (!isDragging && (Math.abs(moveX - startX) > 5 || Math.abs(moveY - startY) > 5)) {
|
|
575
|
+
isDragging = true;
|
|
576
|
+
if (mainBtn) mainBtn.style.cursor = 'grabbing';
|
|
577
|
+
}
|
|
509
578
|
|
|
510
|
-
|
|
511
|
-
|
|
579
|
+
if (isDragging) {
|
|
580
|
+
e.preventDefault();
|
|
581
|
+
buttonPos.x = Math.max(0, Math.min(window.innerWidth - 200, moveX - dragOffset.x));
|
|
582
|
+
buttonPos.y = Math.max(0, Math.min(window.innerHeight - 50, moveY - dragOffset.y));
|
|
583
|
+
floatingButton.style.left = buttonPos.x + 'px';
|
|
584
|
+
floatingButton.style.top = buttonPos.y + 'px';
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const onUp = () => {
|
|
589
|
+
document.removeEventListener('mousemove', onMove);
|
|
590
|
+
document.removeEventListener('mouseup', onUp);
|
|
591
|
+
document.removeEventListener('touchmove', onMove);
|
|
592
|
+
document.removeEventListener('touchend', onUp);
|
|
512
593
|
|
|
513
|
-
|
|
514
|
-
const moveX = e.touches ? e.touches[0].clientX : e.clientX;
|
|
515
|
-
const moveY = e.touches ? e.touches[0].clientY : e.clientY;
|
|
516
|
-
|
|
517
|
-
// Only start dragging if moved more than 5px
|
|
518
|
-
if (!isDragging && (Math.abs(moveX - startX) > 5 || Math.abs(moveY - startY) > 5)) {
|
|
519
|
-
isDragging = true;
|
|
520
|
-
floatingButton.style.cursor = 'grabbing';
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
if (isDragging) {
|
|
524
|
-
e.preventDefault();
|
|
525
|
-
buttonPos.x = Math.max(0, Math.min(window.innerWidth - 200, moveX - dragOffset.x));
|
|
526
|
-
buttonPos.y = Math.max(0, Math.min(window.innerHeight - 50, moveY - dragOffset.y));
|
|
527
|
-
floatingButton.style.left = buttonPos.x + 'px';
|
|
528
|
-
floatingButton.style.top = buttonPos.y + 'px';
|
|
529
|
-
}
|
|
530
|
-
};
|
|
594
|
+
if (mainBtn) mainBtn.style.cursor = 'grab';
|
|
531
595
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
floatingButton.style.cursor = 'grab';
|
|
539
|
-
|
|
540
|
-
// If not dragging, it was a click
|
|
541
|
-
if (!isDragging) {
|
|
542
|
-
toggleActive();
|
|
543
|
-
}
|
|
544
|
-
isDragging = false;
|
|
596
|
+
// If not dragging, it was a click - toggle select mode
|
|
597
|
+
if (!isDragging) {
|
|
598
|
+
toggleActive();
|
|
599
|
+
}
|
|
600
|
+
isDragging = false;
|
|
545
601
|
};
|
|
546
602
|
|
|
547
603
|
document.addEventListener('mousemove', onMove);
|
|
@@ -877,10 +933,15 @@
|
|
|
877
933
|
hidePromptBar();
|
|
878
934
|
showToast('Sending...');
|
|
879
935
|
|
|
880
|
-
const
|
|
936
|
+
const result = await runAgentWithPrompt(fullPrompt);
|
|
881
937
|
|
|
882
|
-
if (success) {
|
|
938
|
+
if (result.success) {
|
|
883
939
|
showToast('Sent ✓');
|
|
940
|
+
// Auto-show the web frame to see the agent working
|
|
941
|
+
if (result.sessionId) {
|
|
942
|
+
sparkecoderState.sessionId = result.sessionId;
|
|
943
|
+
showWebFrame();
|
|
944
|
+
}
|
|
884
945
|
} else {
|
|
885
946
|
await copyToClipboard(fullPrompt);
|
|
886
947
|
showToast('Copied (no connection)');
|
|
@@ -890,10 +951,284 @@
|
|
|
890
951
|
component: pendingComponentInfo?.name,
|
|
891
952
|
fileName: pendingComponentInfo?.fileName,
|
|
892
953
|
hasUserMessage: !!userMessage,
|
|
893
|
-
success,
|
|
954
|
+
success: result.success,
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// ============================================
|
|
959
|
+
// Floating Web UI Frame
|
|
960
|
+
// ============================================
|
|
961
|
+
|
|
962
|
+
const DEFAULT_WEB_UI_URL = 'http://localhost:6969';
|
|
963
|
+
let webFrame = null;
|
|
964
|
+
let webFrameCollapsed = false;
|
|
965
|
+
let webFramePos = { x: window.innerWidth - 420, y: 60 };
|
|
966
|
+
let webFrameSize = { width: 400, height: 600 };
|
|
967
|
+
let isWebFrameDragging = false;
|
|
968
|
+
let webFrameDragOffset = { x: 0, y: 0 };
|
|
969
|
+
|
|
970
|
+
function getWebUiUrl() {
|
|
971
|
+
return config.sparkecoder?.webUiUrl || DEFAULT_WEB_UI_URL;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function createWebFrame() {
|
|
975
|
+
if (webFrame) return;
|
|
976
|
+
|
|
977
|
+
webFrame = document.createElement('div');
|
|
978
|
+
webFrame.id = 'sparkecode-webframe';
|
|
979
|
+
webFrame.style.cssText = `
|
|
980
|
+
position: fixed;
|
|
981
|
+
top: ${webFramePos.y}px;
|
|
982
|
+
right: 16px;
|
|
983
|
+
width: ${webFrameSize.width}px;
|
|
984
|
+
height: ${webFrameSize.height}px;
|
|
985
|
+
background: ${config.bgColor};
|
|
986
|
+
border: 1px solid ${config.borderColor};
|
|
987
|
+
border-radius: 12px;
|
|
988
|
+
z-index: 999997;
|
|
989
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
|
990
|
+
display: flex;
|
|
991
|
+
flex-direction: column;
|
|
992
|
+
overflow: hidden;
|
|
993
|
+
opacity: 0;
|
|
994
|
+
transform: scale(0.95) translateY(10px);
|
|
995
|
+
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
996
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
997
|
+
`;
|
|
998
|
+
|
|
999
|
+
// Header
|
|
1000
|
+
const header = document.createElement('div');
|
|
1001
|
+
header.id = 'sparkecode-webframe-header';
|
|
1002
|
+
header.style.cssText = `
|
|
1003
|
+
display: flex;
|
|
1004
|
+
align-items: center;
|
|
1005
|
+
justify-content: space-between;
|
|
1006
|
+
padding: 10px 12px;
|
|
1007
|
+
background: rgba(255,255,255,0.03);
|
|
1008
|
+
border-bottom: 1px solid ${config.borderColor};
|
|
1009
|
+
cursor: grab;
|
|
1010
|
+
user-select: none;
|
|
1011
|
+
`;
|
|
1012
|
+
|
|
1013
|
+
const title = document.createElement('div');
|
|
1014
|
+
title.style.cssText = `
|
|
1015
|
+
font-size: 12px;
|
|
1016
|
+
font-weight: 500;
|
|
1017
|
+
color: ${config.mutedColor};
|
|
1018
|
+
display: flex;
|
|
1019
|
+
align-items: center;
|
|
1020
|
+
gap: 6px;
|
|
1021
|
+
`;
|
|
1022
|
+
title.innerHTML = `
|
|
1023
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="${config.primaryColor}">
|
|
1024
|
+
<path d="M3 1.5L3 12.5L6.5 9L10 14L12 13L8.5 8L13 7.5L3 1.5Z"/>
|
|
1025
|
+
</svg>
|
|
1026
|
+
<span>SparkeCode</span>
|
|
1027
|
+
`;
|
|
1028
|
+
|
|
1029
|
+
const buttons = document.createElement('div');
|
|
1030
|
+
buttons.style.cssText = `display: flex; gap: 6px;`;
|
|
1031
|
+
|
|
1032
|
+
// Collapse button
|
|
1033
|
+
const collapseBtn = document.createElement('button');
|
|
1034
|
+
collapseBtn.id = 'sparkecode-webframe-collapse';
|
|
1035
|
+
collapseBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14"/></svg>`;
|
|
1036
|
+
collapseBtn.style.cssText = `
|
|
1037
|
+
width: 24px;
|
|
1038
|
+
height: 24px;
|
|
1039
|
+
border: none;
|
|
1040
|
+
background: transparent;
|
|
1041
|
+
color: ${config.mutedColor};
|
|
1042
|
+
cursor: pointer;
|
|
1043
|
+
border-radius: 4px;
|
|
1044
|
+
display: flex;
|
|
1045
|
+
align-items: center;
|
|
1046
|
+
justify-content: center;
|
|
1047
|
+
transition: background 0.15s;
|
|
1048
|
+
`;
|
|
1049
|
+
collapseBtn.addEventListener('mouseenter', () => collapseBtn.style.background = 'rgba(255,255,255,0.1)');
|
|
1050
|
+
collapseBtn.addEventListener('mouseleave', () => collapseBtn.style.background = 'transparent');
|
|
1051
|
+
collapseBtn.addEventListener('click', toggleWebFrameCollapse);
|
|
1052
|
+
|
|
1053
|
+
// Close button
|
|
1054
|
+
const closeBtn = document.createElement('button');
|
|
1055
|
+
closeBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>`;
|
|
1056
|
+
closeBtn.style.cssText = `
|
|
1057
|
+
width: 24px;
|
|
1058
|
+
height: 24px;
|
|
1059
|
+
border: none;
|
|
1060
|
+
background: transparent;
|
|
1061
|
+
color: ${config.mutedColor};
|
|
1062
|
+
cursor: pointer;
|
|
1063
|
+
border-radius: 4px;
|
|
1064
|
+
display: flex;
|
|
1065
|
+
align-items: center;
|
|
1066
|
+
justify-content: center;
|
|
1067
|
+
transition: background 0.15s;
|
|
1068
|
+
`;
|
|
1069
|
+
closeBtn.addEventListener('mouseenter', () => closeBtn.style.background = 'rgba(255,255,255,0.1)');
|
|
1070
|
+
closeBtn.addEventListener('mouseleave', () => closeBtn.style.background = 'transparent');
|
|
1071
|
+
closeBtn.addEventListener('click', hideWebFrame);
|
|
1072
|
+
|
|
1073
|
+
buttons.appendChild(collapseBtn);
|
|
1074
|
+
buttons.appendChild(closeBtn);
|
|
1075
|
+
header.appendChild(title);
|
|
1076
|
+
header.appendChild(buttons);
|
|
1077
|
+
|
|
1078
|
+
// Drag handling for header
|
|
1079
|
+
header.addEventListener('mousedown', startWebFrameDrag);
|
|
1080
|
+
|
|
1081
|
+
// Iframe container
|
|
1082
|
+
const iframeContainer = document.createElement('div');
|
|
1083
|
+
iframeContainer.id = 'sparkecode-webframe-content';
|
|
1084
|
+
iframeContainer.style.cssText = `
|
|
1085
|
+
flex: 1;
|
|
1086
|
+
overflow: hidden;
|
|
1087
|
+
background: #0A0A0A;
|
|
1088
|
+
`;
|
|
1089
|
+
|
|
1090
|
+
// Loading state
|
|
1091
|
+
const loading = document.createElement('div');
|
|
1092
|
+
loading.id = 'sparkecode-webframe-loading';
|
|
1093
|
+
loading.style.cssText = `
|
|
1094
|
+
position: absolute;
|
|
1095
|
+
inset: 0;
|
|
1096
|
+
display: flex;
|
|
1097
|
+
align-items: center;
|
|
1098
|
+
justify-content: center;
|
|
1099
|
+
color: ${config.mutedColor};
|
|
1100
|
+
font-size: 13px;
|
|
1101
|
+
`;
|
|
1102
|
+
loading.textContent = 'Loading...';
|
|
1103
|
+
iframeContainer.appendChild(loading);
|
|
1104
|
+
|
|
1105
|
+
// Iframe
|
|
1106
|
+
const iframe = document.createElement('iframe');
|
|
1107
|
+
iframe.id = 'sparkecode-webframe-iframe';
|
|
1108
|
+
iframe.style.cssText = `
|
|
1109
|
+
width: 100%;
|
|
1110
|
+
height: 100%;
|
|
1111
|
+
border: none;
|
|
1112
|
+
opacity: 0;
|
|
1113
|
+
transition: opacity 0.2s;
|
|
1114
|
+
`;
|
|
1115
|
+
iframe.addEventListener('load', () => {
|
|
1116
|
+
loading.style.display = 'none';
|
|
1117
|
+
iframe.style.opacity = '1';
|
|
1118
|
+
});
|
|
1119
|
+
iframeContainer.appendChild(iframe);
|
|
1120
|
+
|
|
1121
|
+
webFrame.appendChild(header);
|
|
1122
|
+
webFrame.appendChild(iframeContainer);
|
|
1123
|
+
document.body.appendChild(webFrame);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function startWebFrameDrag(e) {
|
|
1127
|
+
if (e.target.closest('button')) return;
|
|
1128
|
+
|
|
1129
|
+
isWebFrameDragging = true;
|
|
1130
|
+
const rect = webFrame.getBoundingClientRect();
|
|
1131
|
+
webFrameDragOffset.x = e.clientX - rect.left;
|
|
1132
|
+
webFrameDragOffset.y = e.clientY - rect.top;
|
|
1133
|
+
|
|
1134
|
+
const header = document.getElementById('sparkecode-webframe-header');
|
|
1135
|
+
if (header) header.style.cursor = 'grabbing';
|
|
1136
|
+
|
|
1137
|
+
const onMove = (e) => {
|
|
1138
|
+
if (!isWebFrameDragging) return;
|
|
1139
|
+
const x = Math.max(0, Math.min(window.innerWidth - webFrameSize.width, e.clientX - webFrameDragOffset.x));
|
|
1140
|
+
const y = Math.max(0, Math.min(window.innerHeight - 100, e.clientY - webFrameDragOffset.y));
|
|
1141
|
+
webFrame.style.left = x + 'px';
|
|
1142
|
+
webFrame.style.right = 'auto';
|
|
1143
|
+
webFrame.style.top = y + 'px';
|
|
1144
|
+
webFramePos = { x, y };
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
const onUp = () => {
|
|
1148
|
+
isWebFrameDragging = false;
|
|
1149
|
+
const header = document.getElementById('sparkecode-webframe-header');
|
|
1150
|
+
if (header) header.style.cursor = 'grab';
|
|
1151
|
+
document.removeEventListener('mousemove', onMove);
|
|
1152
|
+
document.removeEventListener('mouseup', onUp);
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
document.addEventListener('mousemove', onMove);
|
|
1156
|
+
document.addEventListener('mouseup', onUp);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function showWebFrame() {
|
|
1160
|
+
createWebFrame();
|
|
1161
|
+
|
|
1162
|
+
const iframe = document.getElementById('sparkecode-webframe-iframe');
|
|
1163
|
+
const loading = document.getElementById('sparkecode-webframe-loading');
|
|
1164
|
+
|
|
1165
|
+
// Build embed URL with session ID
|
|
1166
|
+
let url = getWebUiUrl();
|
|
1167
|
+
if (sparkecoderState.sessionId) {
|
|
1168
|
+
url += `/embed/${sparkecoderState.sessionId}`;
|
|
1169
|
+
} else {
|
|
1170
|
+
url += '/embed';
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
if (iframe && iframe.src !== url) {
|
|
1174
|
+
if (loading) loading.style.display = 'flex';
|
|
1175
|
+
iframe.style.opacity = '0';
|
|
1176
|
+
iframe.src = url;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
webFrame.style.display = 'flex';
|
|
1180
|
+
webFrameCollapsed = false;
|
|
1181
|
+
updateWebFrameSize();
|
|
1182
|
+
|
|
1183
|
+
requestAnimationFrame(() => {
|
|
1184
|
+
webFrame.style.opacity = '1';
|
|
1185
|
+
webFrame.style.transform = 'scale(1) translateY(0)';
|
|
894
1186
|
});
|
|
895
1187
|
}
|
|
896
1188
|
|
|
1189
|
+
function hideWebFrame() {
|
|
1190
|
+
if (!webFrame) return;
|
|
1191
|
+
|
|
1192
|
+
webFrame.style.opacity = '0';
|
|
1193
|
+
webFrame.style.transform = 'scale(0.95) translateY(10px)';
|
|
1194
|
+
|
|
1195
|
+
setTimeout(() => {
|
|
1196
|
+
if (webFrame) webFrame.style.display = 'none';
|
|
1197
|
+
// Clear iframe to stop any running content
|
|
1198
|
+
const iframe = document.getElementById('sparkecode-webframe-iframe');
|
|
1199
|
+
if (iframe) iframe.src = 'about:blank';
|
|
1200
|
+
}, 200);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function toggleWebFrameCollapse() {
|
|
1204
|
+
webFrameCollapsed = !webFrameCollapsed;
|
|
1205
|
+
updateWebFrameSize();
|
|
1206
|
+
|
|
1207
|
+
const collapseBtn = document.getElementById('sparkecode-webframe-collapse');
|
|
1208
|
+
if (collapseBtn) {
|
|
1209
|
+
collapseBtn.innerHTML = webFrameCollapsed
|
|
1210
|
+
? `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>`
|
|
1211
|
+
: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14"/></svg>`;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function updateWebFrameSize() {
|
|
1216
|
+
if (!webFrame) return;
|
|
1217
|
+
|
|
1218
|
+
const content = document.getElementById('sparkecode-webframe-content');
|
|
1219
|
+
if (webFrameCollapsed) {
|
|
1220
|
+
webFrame.style.height = '44px';
|
|
1221
|
+
if (content) content.style.display = 'none';
|
|
1222
|
+
} else {
|
|
1223
|
+
webFrame.style.height = webFrameSize.height + 'px';
|
|
1224
|
+
if (content) content.style.display = 'block';
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function isWebFrameVisible() {
|
|
1229
|
+
return webFrame && webFrame.style.display !== 'none' && webFrame.style.opacity !== '0';
|
|
1230
|
+
}
|
|
1231
|
+
|
|
897
1232
|
// ============================================
|
|
898
1233
|
// Event Handlers
|
|
899
1234
|
// ============================================
|
|
@@ -959,12 +1294,18 @@
|
|
|
959
1294
|
page: getPagePath(),
|
|
960
1295
|
});
|
|
961
1296
|
|
|
962
|
-
// If Sparkecoder is enabled and connected,
|
|
1297
|
+
// If Sparkecoder is enabled and connected, open embed with context
|
|
963
1298
|
if (config.sparkecoder?.enabled) {
|
|
964
1299
|
const connection = await ensureSparkecoderConnection();
|
|
965
1300
|
if (connection) {
|
|
966
1301
|
deactivate();
|
|
967
|
-
|
|
1302
|
+
|
|
1303
|
+
// Send component context to pending-input so it appears in the embed's input
|
|
1304
|
+
await sendToSparkecoder(content);
|
|
1305
|
+
showToast(`Selected <${info?.name || 'Component'}>`);
|
|
1306
|
+
|
|
1307
|
+
// Open the embed panel
|
|
1308
|
+
showWebFrame();
|
|
968
1309
|
return;
|
|
969
1310
|
}
|
|
970
1311
|
}
|