Spaces:
Paused
Paused
Capture screen like similar to Claude (#1604)
Browse files* feat: capture screen
* feat: make it work with latest tool UI
* fix: add screenshot icon
* Update src/lib/components/icons/iconScreenshot.svelte
Co-authored-by: Victor Muštar <victor.mustar@gmail.com>
* fix: capitalization
* fix: stop tracks in finally block
* fix: revert chatwindow changes that shouldnt be there
* fix: tooltip classes
---------
Co-authored-by: Nathan Sarrazin <sarrazin.nathan@gmail.com>
Co-authored-by: Victor Muštar <victor.mustar@gmail.com>
src/lib/components/chat/ChatInput.svelte
CHANGED
|
@@ -21,6 +21,8 @@
|
|
| 21 |
import { goto } from "$app/navigation";
|
| 22 |
import { base } from "$app/paths";
|
| 23 |
import IconAdd from "~icons/carbon/add";
|
|
|
|
|
|
|
| 24 |
|
| 25 |
export let files: File[] = [];
|
| 26 |
export let mimeTypes: string[] = [];
|
|
@@ -250,6 +252,31 @@
|
|
| 250 |
</label>
|
| 251 |
</HoverTooltip>
|
| 252 |
</form>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
{/if}
|
| 254 |
{#if modelHasTools}
|
| 255 |
{#each extraTools as tool}
|
|
|
|
| 21 |
import { goto } from "$app/navigation";
|
| 22 |
import { base } from "$app/paths";
|
| 23 |
import IconAdd from "~icons/carbon/add";
|
| 24 |
+
import { captureScreen } from "$lib/utils/screenshot";
|
| 25 |
+
import IconScreenshot from "../icons/IconScreenshot.svelte";
|
| 26 |
|
| 27 |
export let files: File[] = [];
|
| 28 |
export let mimeTypes: string[] = [];
|
|
|
|
| 252 |
</label>
|
| 253 |
</HoverTooltip>
|
| 254 |
</form>
|
| 255 |
+
{#if mimeTypes.includes("image/*")}
|
| 256 |
+
<HoverTooltip
|
| 257 |
+
label="Capture screenshot"
|
| 258 |
+
position="top"
|
| 259 |
+
TooltipClassNames="text-xs !text-left !w-auto whitespace-nowrap !py-1 !mb-0 max-sm:hidden"
|
| 260 |
+
>
|
| 261 |
+
<button
|
| 262 |
+
class="base-tool"
|
| 263 |
+
on:click|preventDefault={async () => {
|
| 264 |
+
const screenshot = await captureScreen();
|
| 265 |
+
|
| 266 |
+
// Convert base64 to blob
|
| 267 |
+
const base64Response = await fetch(screenshot);
|
| 268 |
+
const blob = await base64Response.blob();
|
| 269 |
+
|
| 270 |
+
// Create a File object from the blob
|
| 271 |
+
const file = new File([blob], "screenshot.png", { type: "image/png" });
|
| 272 |
+
|
| 273 |
+
files = [...files, file];
|
| 274 |
+
}}
|
| 275 |
+
>
|
| 276 |
+
<IconScreenshot classNames="text-xl" />
|
| 277 |
+
</button>
|
| 278 |
+
</HoverTooltip>
|
| 279 |
+
{/if}
|
| 280 |
{/if}
|
| 281 |
{#if modelHasTools}
|
| 282 |
{#each extraTools as tool}
|
src/lib/components/icons/IconScreenshot.svelte
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
export let classNames = "";
|
| 3 |
+
</script>
|
| 4 |
+
|
| 5 |
+
<svg
|
| 6 |
+
class={classNames}
|
| 7 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 8 |
+
aria-hidden="true"
|
| 9 |
+
focusable="false"
|
| 10 |
+
role="img"
|
| 11 |
+
width="1em"
|
| 12 |
+
height="1em"
|
| 13 |
+
fill="currentColor"
|
| 14 |
+
viewBox="0 0 32 32"
|
| 15 |
+
><path
|
| 16 |
+
fill-rule="evenodd"
|
| 17 |
+
clip-rule="evenodd"
|
| 18 |
+
d="M5.98 6.01h20.04v16.1H5.98V6.02Zm-2.1 0c0-1.16.94-2.1 2.1-2.1h20.04c1.16 0 2.1.94 2.1 2.1v16.1a2.1 2.1 0 0 1-2.1 2.11H5.98a2.1 2.1 0 0 1-2.1-2.1V6.02Zm5.7 1.65a2.1 2.1 0 0 0-2.1 2.1v2.61a1.05 1.05 0 0 0 2.1 0v-2.6h2.96a1.05 1.05 0 1 0 0-2.11H9.58ZM24.41 18.4a2.1 2.1 0 0 1-2.1 2.1h-2.95a1.05 1.05 0 1 1 0-2.1h2.95v-2.61a1.05 1.05 0 0 1 2.1 0v2.6ZM10.1 25.9a1.05 1.05 0 1 0 0 2.1H22.3a1.05 1.05 0 1 0 0-2.1H10.1Z"
|
| 19 |
+
/>
|
| 20 |
+
</svg>
|
src/lib/utils/screenshot.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export async function captureScreen(): Promise<string> {
|
| 2 |
+
let stream: MediaStream | undefined;
|
| 3 |
+
try {
|
| 4 |
+
// This will show the native browser dialog for screen capture
|
| 5 |
+
stream = await navigator.mediaDevices.getDisplayMedia({
|
| 6 |
+
video: true,
|
| 7 |
+
audio: false,
|
| 8 |
+
});
|
| 9 |
+
|
| 10 |
+
// Create a canvas element to capture the screenshot
|
| 11 |
+
const canvas = document.createElement("canvas");
|
| 12 |
+
const video = document.createElement("video");
|
| 13 |
+
|
| 14 |
+
// Wait for the video to load metadata
|
| 15 |
+
await new Promise((resolve) => {
|
| 16 |
+
video.onloadedmetadata = () => {
|
| 17 |
+
canvas.width = video.videoWidth;
|
| 18 |
+
canvas.height = video.videoHeight;
|
| 19 |
+
video.play();
|
| 20 |
+
resolve(null);
|
| 21 |
+
};
|
| 22 |
+
if (stream) {
|
| 23 |
+
video.srcObject = stream;
|
| 24 |
+
} else {
|
| 25 |
+
throw Error("No stream available");
|
| 26 |
+
}
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
// Draw the video frame to canvas
|
| 30 |
+
const context = canvas.getContext("2d");
|
| 31 |
+
context?.drawImage(video, 0, 0, canvas.width, canvas.height);
|
| 32 |
+
// Convert to base64
|
| 33 |
+
return canvas.toDataURL("image/png");
|
| 34 |
+
} catch (error) {
|
| 35 |
+
console.error("Error capturing screenshot:", error);
|
| 36 |
+
throw error;
|
| 37 |
+
} finally {
|
| 38 |
+
// Stop all tracks
|
| 39 |
+
if (stream) {
|
| 40 |
+
stream.getTracks().forEach((track) => track.stop());
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
}
|