ed-mcconnell commited on
Commit
7fdab0a
·
1 Parent(s): 8b7e18e

Second commit for screen cap feature

Browse files
app/components/workbench/Preview.tsx CHANGED
@@ -27,7 +27,7 @@ export const Preview = memo(() => {
27
  }
28
 
29
  const { baseUrl } = activePreview;
30
-
31
  setUrl(baseUrl);
32
  setIframeUrl(baseUrl);
33
  }, [activePreview, iframeUrl]);
@@ -80,19 +80,24 @@ export const Preview = memo(() => {
80
  )}
81
  <div className="bg-bolt-elements-background-depth-2 p-2 flex items-center gap-1.5">
82
  <IconButton icon="i-ph:arrow-clockwise" onClick={reloadPreview} />
83
- <IconButton icon="i-ph:selection" onClick={() => setIsSelectionMode(!isSelectionMode)} className={isSelectionMode ? 'bg-bolt-elements-background-depth-3' : ''} />
 
 
 
 
84
  <div
85
  className="flex items-center gap-1 flex-grow bg-bolt-elements-preview-addressBar-background border border-bolt-elements-borderColor text-bolt-elements-preview-addressBar-text rounded-full px-3 py-1 text-sm hover:bg-bolt-elements-preview-addressBar-backgroundHover hover:focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within:bg-bolt-elements-preview-addressBar-backgroundActive
86
  focus-within-border-bolt-elements-borderColorActive focus-within:text-bolt-elements-preview-addressBar-textActive"
87
  >
88
- <input title='URL'
 
89
  ref={inputRef}
90
  className="w-full bg-transparent outline-none"
91
  type="text"
92
  value={url}
93
  onChange={(event) => {
94
  setUrl(event.target.value);
95
- }}
96
  onKeyDown={(event) => {
97
  if (event.key === 'Enter' && validateUrl(url)) {
98
  setIframeUrl(url);
@@ -119,7 +124,11 @@ export const Preview = memo(() => {
119
  {activePreview ? (
120
  <>
121
  <iframe ref={iframeRef} title="preview" className="border-none w-full h-full bg-white" src={iframeUrl} />
122
- <ScreenshotSelector isSelectionMode={isSelectionMode} setIsSelectionMode={setIsSelectionMode} containerRef={iframeRef} />
 
 
 
 
123
  </>
124
  ) : (
125
  <div className="flex w-full h-full justify-center items-center bg-white">No preview available</div>
 
27
  }
28
 
29
  const { baseUrl } = activePreview;
30
+
31
  setUrl(baseUrl);
32
  setIframeUrl(baseUrl);
33
  }, [activePreview, iframeUrl]);
 
80
  )}
81
  <div className="bg-bolt-elements-background-depth-2 p-2 flex items-center gap-1.5">
82
  <IconButton icon="i-ph:arrow-clockwise" onClick={reloadPreview} />
83
+ <IconButton
84
+ icon="i-ph:selection"
85
+ onClick={() => setIsSelectionMode(!isSelectionMode)}
86
+ className={isSelectionMode ? 'bg-bolt-elements-background-depth-3' : ''}
87
+ />
88
  <div
89
  className="flex items-center gap-1 flex-grow bg-bolt-elements-preview-addressBar-background border border-bolt-elements-borderColor text-bolt-elements-preview-addressBar-text rounded-full px-3 py-1 text-sm hover:bg-bolt-elements-preview-addressBar-backgroundHover hover:focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within:bg-bolt-elements-preview-addressBar-backgroundActive
90
  focus-within-border-bolt-elements-borderColorActive focus-within:text-bolt-elements-preview-addressBar-textActive"
91
  >
92
+ <input
93
+ title="URL"
94
  ref={inputRef}
95
  className="w-full bg-transparent outline-none"
96
  type="text"
97
  value={url}
98
  onChange={(event) => {
99
  setUrl(event.target.value);
100
+ }}
101
  onKeyDown={(event) => {
102
  if (event.key === 'Enter' && validateUrl(url)) {
103
  setIframeUrl(url);
 
124
  {activePreview ? (
125
  <>
126
  <iframe ref={iframeRef} title="preview" className="border-none w-full h-full bg-white" src={iframeUrl} />
127
+ <ScreenshotSelector
128
+ isSelectionMode={isSelectionMode}
129
+ setIsSelectionMode={setIsSelectionMode}
130
+ containerRef={iframeRef}
131
+ />
132
  </>
133
  ) : (
134
  <div className="flex w-full h-full justify-center items-center bg-white">No preview available</div>
app/components/workbench/ScreenshotSelector.tsx CHANGED
@@ -7,197 +7,194 @@ interface ScreenshotSelectorProps {
7
  containerRef: React.RefObject<HTMLElement>;
8
  }
9
 
10
- export const ScreenshotSelector = memo(({ isSelectionMode, setIsSelectionMode, containerRef }: ScreenshotSelectorProps) => {
11
- const [isCapturing, setIsCapturing] = useState(false);
12
- const [selectionStart, setSelectionStart] = useState<{ x: number; y: number } | null>(null);
13
- const [selectionEnd, setSelectionEnd] = useState<{ x: number; y: number } | null>(null);
 
14
 
15
- const handleCopySelection = useCallback(async () => {
16
- if (!isSelectionMode || !selectionStart || !selectionEnd || !containerRef.current) return;
17
-
18
- setIsCapturing(true);
19
- try {
20
- const width = Math.abs(selectionEnd.x - selectionStart.x);
21
- const height = Math.abs(selectionEnd.y - selectionStart.y);
22
-
23
- // Create a video element to capture the screen
24
- const video = document.createElement('video');
25
- video.style.opacity = '0';
26
- document.body.appendChild(video);
27
 
 
28
  try {
29
- // Capture the entire screen
30
- const stream = await navigator.mediaDevices.getDisplayMedia({
31
- audio: false,
32
- video: {
33
- displaySurface: "window"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  }
35
- } as MediaStreamConstraints);
36
 
37
- // Set up video with the stream
38
- video.srcObject = stream;
39
- await video.play();
40
 
41
- // Wait for video to be ready
42
- await new Promise(resolve => setTimeout(resolve, 300));
43
 
44
- // Create temporary canvas for full screenshot
45
- const tempCanvas = document.createElement('canvas');
46
- tempCanvas.width = video.videoWidth;
47
- tempCanvas.height = video.videoHeight;
48
- const tempCtx = tempCanvas.getContext('2d');
49
 
50
- if (!tempCtx) {
51
- throw new Error('Failed to get temporary canvas context');
52
- }
 
 
53
 
54
- // Draw the full video frame
55
- tempCtx.drawImage(video, 0, 0);
56
-
57
- // Get the container's position in the page
58
- const containerRect = containerRef.current.getBoundingClientRect();
59
-
60
- // Calculate scale factor between video and screen
61
- const scaleX = video.videoWidth / window.innerWidth;
62
- const scaleY = video.videoHeight / window.innerHeight;
63
-
64
- // Calculate the scaled coordinates
65
- const scaledX = (containerRect.left + Math.min(selectionStart.x, selectionEnd.x)) * scaleX;
66
- const scaledY = (containerRect.top + Math.min(selectionStart.y, selectionEnd.y)) * scaleY;
67
- const scaledWidth = width * scaleX;
68
- const scaledHeight = height * scaleY;
69
-
70
- // Create final canvas for the cropped area
71
- const canvas = document.createElement('canvas');
72
- canvas.width = width;
73
- canvas.height = height;
74
- const ctx = canvas.getContext('2d');
75
-
76
- if (!ctx) {
77
- throw new Error('Failed to get canvas context');
78
- }
79
 
80
- // Draw the cropped area
81
- ctx.drawImage(
82
- tempCanvas,
83
- scaledX,
84
- scaledY,
85
- scaledWidth,
86
- scaledHeight,
87
- 0,
88
- 0,
89
- width,
90
- height
91
- );
92
-
93
- // Convert to blob
94
- const blob = await new Promise<Blob>((resolve, reject) => {
95
- canvas.toBlob((blob) => {
96
- if (blob) resolve(blob);
97
- else reject(new Error('Failed to create blob'));
98
- }, 'image/png');
99
- });
100
-
101
- // Create a FileReader to convert blob to base64
102
- const reader = new FileReader();
103
- reader.onload = (e) => {
104
- const base64Image = e.target?.result as string;
105
-
106
- // Find the textarea element
107
- const textarea = document.querySelector('textarea');
108
- if (textarea) {
109
- // Get the setters from the BaseChat component
110
- const setUploadedFiles = (window as any).__BOLT_SET_UPLOADED_FILES__;
111
- const setImageDataList = (window as any).__BOLT_SET_IMAGE_DATA_LIST__;
112
- const uploadedFiles = (window as any).__BOLT_UPLOADED_FILES__ || [];
113
- const imageDataList = (window as any).__BOLT_IMAGE_DATA_LIST__ || [];
114
-
115
- if (setUploadedFiles && setImageDataList) {
116
- // Update the files and image data
117
- const file = new File([blob], 'screenshot.png', { type: 'image/png' });
118
- setUploadedFiles([...uploadedFiles, file]);
119
- setImageDataList([...imageDataList, base64Image]);
120
- toast.success('Screenshot captured and added to chat');
121
- } else {
122
- toast.error('Could not add screenshot to chat');
123
- }
124
  }
125
- };
126
- reader.readAsDataURL(blob);
127
-
128
- // Stop all tracks
129
- stream.getTracks().forEach(track => track.stop());
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  } finally {
132
- // Clean up video element
133
- document.body.removeChild(video);
 
 
134
  }
135
- } catch (error) {
136
- console.error('Failed to capture screenshot:', error);
137
- toast.error('Failed to capture screenshot');
138
- } finally {
139
- setIsCapturing(false);
140
- setSelectionStart(null);
141
- setSelectionEnd(null);
142
- setIsSelectionMode(false);
143
- }
144
- }, [isSelectionMode, selectionStart, selectionEnd, containerRef, setIsSelectionMode]);
145
-
146
- const handleSelectionStart = useCallback((e: React.MouseEvent) => {
147
- e.preventDefault();
148
- e.stopPropagation();
149
- if (!isSelectionMode) return;
150
- const rect = e.currentTarget.getBoundingClientRect();
151
- const x = e.clientX - rect.left;
152
- const y = e.clientY - rect.top;
153
- setSelectionStart({ x, y });
154
- setSelectionEnd({ x, y });
155
- }, [isSelectionMode]);
156
-
157
- const handleSelectionMove = useCallback((e: React.MouseEvent) => {
158
- e.preventDefault();
159
- e.stopPropagation();
160
- if (!isSelectionMode || !selectionStart) return;
161
- const rect = e.currentTarget.getBoundingClientRect();
162
- const x = e.clientX - rect.left;
163
- const y = e.clientY - rect.top;
164
- setSelectionEnd({ x, y });
165
- }, [isSelectionMode, selectionStart]);
166
-
167
- if (!isSelectionMode) return null;
168
-
169
- return (
170
- <div
171
- className="absolute inset-0 cursor-crosshair"
172
- onMouseDown={handleSelectionStart}
173
- onMouseMove={handleSelectionMove}
174
- onMouseUp={handleCopySelection}
175
- onMouseLeave={() => {
176
- if (selectionStart) {
177
- setSelectionStart(null);
178
- }
179
- }}
180
- style={{
181
- backgroundColor: isCapturing ? 'transparent' : 'rgba(0, 0, 0, 0.1)',
182
- userSelect: 'none',
183
- WebkitUserSelect: 'none',
184
- pointerEvents: 'all',
185
- opacity: isCapturing ? 0 : 1,
186
- zIndex: 50,
187
- transition: 'opacity 0.1s ease-in-out',
188
- }}
189
- >
190
- {selectionStart && selectionEnd && !isCapturing && (
191
- <div
192
- className="absolute border-2 border-blue-500 bg-blue-200 bg-opacity-20"
193
- style={{
194
- left: Math.min(selectionStart.x, selectionEnd.x),
195
- top: Math.min(selectionStart.y, selectionEnd.y),
196
- width: Math.abs(selectionEnd.x - selectionStart.x),
197
- height: Math.abs(selectionEnd.y - selectionStart.y),
198
- }}
199
- />
200
- )}
201
- </div>
202
- );
203
- });
 
7
  containerRef: React.RefObject<HTMLElement>;
8
  }
9
 
10
+ export const ScreenshotSelector = memo(
11
+ ({ isSelectionMode, setIsSelectionMode, containerRef }: ScreenshotSelectorProps) => {
12
+ const [isCapturing, setIsCapturing] = useState(false);
13
+ const [selectionStart, setSelectionStart] = useState<{ x: number; y: number } | null>(null);
14
+ const [selectionEnd, setSelectionEnd] = useState<{ x: number; y: number } | null>(null);
15
 
16
+ const handleCopySelection = useCallback(async () => {
17
+ if (!isSelectionMode || !selectionStart || !selectionEnd || !containerRef.current) return;
 
 
 
 
 
 
 
 
 
 
18
 
19
+ setIsCapturing(true);
20
  try {
21
+ const width = Math.abs(selectionEnd.x - selectionStart.x);
22
+ const height = Math.abs(selectionEnd.y - selectionStart.y);
23
+
24
+ // Create a video element to capture the screen
25
+ const video = document.createElement('video');
26
+ video.style.opacity = '0';
27
+ document.body.appendChild(video);
28
+
29
+ try {
30
+ // Capture the entire screen
31
+ const stream = await navigator.mediaDevices.getDisplayMedia({
32
+ audio: false,
33
+ video: {
34
+ displaySurface: 'window',
35
+ },
36
+ } as MediaStreamConstraints);
37
+
38
+ // Set up video with the stream
39
+ video.srcObject = stream;
40
+ await video.play();
41
+
42
+ // Wait for video to be ready
43
+ await new Promise((resolve) => setTimeout(resolve, 300));
44
+
45
+ // Create temporary canvas for full screenshot
46
+ const tempCanvas = document.createElement('canvas');
47
+ tempCanvas.width = video.videoWidth;
48
+ tempCanvas.height = video.videoHeight;
49
+ const tempCtx = tempCanvas.getContext('2d');
50
+
51
+ if (!tempCtx) {
52
+ throw new Error('Failed to get temporary canvas context');
53
  }
 
54
 
55
+ // Draw the full video frame
56
+ tempCtx.drawImage(video, 0, 0);
 
57
 
58
+ // Get the container's position in the page
59
+ const containerRect = containerRef.current.getBoundingClientRect();
60
 
61
+ // Calculate scale factor between video and screen
62
+ const scaleX = video.videoWidth / window.innerWidth;
63
+ const scaleY = video.videoHeight / window.innerHeight;
 
 
64
 
65
+ // Calculate the scaled coordinates
66
+ const scaledX = (containerRect.left + Math.min(selectionStart.x, selectionEnd.x)) * scaleX;
67
+ const scaledY = (containerRect.top + Math.min(selectionStart.y, selectionEnd.y)) * scaleY;
68
+ const scaledWidth = width * scaleX;
69
+ const scaledHeight = height * scaleY;
70
 
71
+ // Create final canvas for the cropped area
72
+ const canvas = document.createElement('canvas');
73
+ canvas.width = width;
74
+ canvas.height = height;
75
+ const ctx = canvas.getContext('2d');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
+ if (!ctx) {
78
+ throw new Error('Failed to get canvas context');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  }
 
 
 
 
 
80
 
81
+ // Draw the cropped area
82
+ ctx.drawImage(tempCanvas, scaledX, scaledY, scaledWidth, scaledHeight, 0, 0, width, height);
83
+
84
+ // Convert to blob
85
+ const blob = await new Promise<Blob>((resolve, reject) => {
86
+ canvas.toBlob((blob) => {
87
+ if (blob) resolve(blob);
88
+ else reject(new Error('Failed to create blob'));
89
+ }, 'image/png');
90
+ });
91
+
92
+ // Create a FileReader to convert blob to base64
93
+ const reader = new FileReader();
94
+ reader.onload = (e) => {
95
+ const base64Image = e.target?.result as string;
96
+
97
+ // Find the textarea element
98
+ const textarea = document.querySelector('textarea');
99
+ if (textarea) {
100
+ // Get the setters from the BaseChat component
101
+ const setUploadedFiles = (window as any).__BOLT_SET_UPLOADED_FILES__;
102
+ const setImageDataList = (window as any).__BOLT_SET_IMAGE_DATA_LIST__;
103
+ const uploadedFiles = (window as any).__BOLT_UPLOADED_FILES__ || [];
104
+ const imageDataList = (window as any).__BOLT_IMAGE_DATA_LIST__ || [];
105
+
106
+ if (setUploadedFiles && setImageDataList) {
107
+ // Update the files and image data
108
+ const file = new File([blob], 'screenshot.png', { type: 'image/png' });
109
+ setUploadedFiles([...uploadedFiles, file]);
110
+ setImageDataList([...imageDataList, base64Image]);
111
+ toast.success('Screenshot captured and added to chat');
112
+ } else {
113
+ toast.error('Could not add screenshot to chat');
114
+ }
115
+ }
116
+ };
117
+ reader.readAsDataURL(blob);
118
+
119
+ // Stop all tracks
120
+ stream.getTracks().forEach((track) => track.stop());
121
+ } finally {
122
+ // Clean up video element
123
+ document.body.removeChild(video);
124
+ }
125
+ } catch (error) {
126
+ console.error('Failed to capture screenshot:', error);
127
+ toast.error('Failed to capture screenshot');
128
  } finally {
129
+ setIsCapturing(false);
130
+ setSelectionStart(null);
131
+ setSelectionEnd(null);
132
+ setIsSelectionMode(false);
133
  }
134
+ }, [isSelectionMode, selectionStart, selectionEnd, containerRef, setIsSelectionMode]);
135
+
136
+ const handleSelectionStart = useCallback(
137
+ (e: React.MouseEvent) => {
138
+ e.preventDefault();
139
+ e.stopPropagation();
140
+ if (!isSelectionMode) return;
141
+ const rect = e.currentTarget.getBoundingClientRect();
142
+ const x = e.clientX - rect.left;
143
+ const y = e.clientY - rect.top;
144
+ setSelectionStart({ x, y });
145
+ setSelectionEnd({ x, y });
146
+ },
147
+ [isSelectionMode],
148
+ );
149
+
150
+ const handleSelectionMove = useCallback(
151
+ (e: React.MouseEvent) => {
152
+ e.preventDefault();
153
+ e.stopPropagation();
154
+ if (!isSelectionMode || !selectionStart) return;
155
+ const rect = e.currentTarget.getBoundingClientRect();
156
+ const x = e.clientX - rect.left;
157
+ const y = e.clientY - rect.top;
158
+ setSelectionEnd({ x, y });
159
+ },
160
+ [isSelectionMode, selectionStart],
161
+ );
162
+
163
+ if (!isSelectionMode) return null;
164
+
165
+ return (
166
+ <div
167
+ className="absolute inset-0 cursor-crosshair"
168
+ onMouseDown={handleSelectionStart}
169
+ onMouseMove={handleSelectionMove}
170
+ onMouseUp={handleCopySelection}
171
+ onMouseLeave={() => {
172
+ if (selectionStart) {
173
+ setSelectionStart(null);
174
+ }
175
+ }}
176
+ style={{
177
+ backgroundColor: isCapturing ? 'transparent' : 'rgba(0, 0, 0, 0.1)',
178
+ userSelect: 'none',
179
+ WebkitUserSelect: 'none',
180
+ pointerEvents: 'all',
181
+ opacity: isCapturing ? 0 : 1,
182
+ zIndex: 50,
183
+ transition: 'opacity 0.1s ease-in-out',
184
+ }}
185
+ >
186
+ {selectionStart && selectionEnd && !isCapturing && (
187
+ <div
188
+ className="absolute border-2 border-blue-500 bg-blue-200 bg-opacity-20"
189
+ style={{
190
+ left: Math.min(selectionStart.x, selectionEnd.x),
191
+ top: Math.min(selectionStart.y, selectionEnd.y),
192
+ width: Math.abs(selectionEnd.x - selectionStart.x),
193
+ height: Math.abs(selectionEnd.y - selectionStart.y),
194
+ }}
195
+ />
196
+ )}
197
+ </div>
198
+ );
199
+ },
200
+ );