Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Capture screen like similar to Claude #1604

Merged
merged 11 commits into from
Jan 8, 2025
23 changes: 23 additions & 0 deletions src/lib/components/chat/ChatInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import { goto } from "$app/navigation";
import { base } from "$app/paths";
import IconAdd from "~icons/carbon/add";
import CarbonCamera from "~icons/carbon/camera";
import { captureScreen } from "$lib/utils/screenshot";

export let files: File[] = [];
export let mimeTypes: string[] = [];
Expand Down Expand Up @@ -250,6 +252,27 @@
</label>
</HoverTooltip>
</form>
{#if mimeTypes.includes("image/*")}
<HoverTooltip label="Capture screenshot" position="top">
nsarrazin marked this conversation as resolved.
Show resolved Hide resolved
<button
class="base-tool"
on:click|preventDefault={async () => {
const screenshot = await captureScreen();

// Convert base64 to blob
const base64Response = await fetch(screenshot);
const blob = await base64Response.blob();

// Create a File object from the blob
const file = new File([blob], "screenshot.png", { type: "image/png" });

files = [...files, file];
}}
>
<CarbonCamera class="text-base" />
</button>
</HoverTooltip>
{/if}
{/if}
{#if modelHasTools}
{#each extraTools as tool}
Expand Down
122 changes: 62 additions & 60 deletions src/lib/components/chat/ChatWindow.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@
}
});

let chatContainer: HTMLElement;
let chatContainer: HTMLDivElement;

async function scrollToBottom() {
await tick();
Expand Down Expand Up @@ -399,7 +399,7 @@
}}
/>
{:else}
<div class="ml-auto gap-2">
<div class="ml-auto flex items-center gap-2">
{#if messages && lastMessage && lastMessage.interrupted && !isReadOnly}
<ContinueBtn
on:click={() => {
Expand All @@ -414,65 +414,67 @@
</div>
{/if}
</div>
<form
tabindex="-1"
aria-label={isFileUploadEnabled ? "file dropzone" : undefined}
on:submit|preventDefault={handleSubmit}
class="relative flex w-full max-w-4xl flex-1 items-center rounded-xl border bg-gray-100 focus-within:border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:focus-within:border-gray-500
<div class="flex w-full items-center gap-2">
<form
nsarrazin marked this conversation as resolved.
Show resolved Hide resolved
tabindex="-1"
aria-label={isFileUploadEnabled ? "file dropzone" : undefined}
on:submit|preventDefault={handleSubmit}
class="relative flex flex-1 items-center rounded-xl border bg-gray-100 focus-within:border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:focus-within:border-gray-500
{isReadOnly ? 'opacity-30' : ''}"
>
{#if onDrag && isFileUploadEnabled}
<FileDropzone bind:files bind:onDrag mimeTypes={activeMimeTypes} />
{:else}
<div
class="flex w-full flex-1 rounded-xl border-none bg-transparent"
class:paste-glow={pastedLongContent}
>
{#if lastIsError}
<ChatInput value="Sorry, something went wrong. Please try again." disabled={true} />
{:else}
<ChatInput
{assistant}
placeholder={isReadOnly ? "This conversation is read-only." : "Ask anything"}
{loading}
bind:value={message}
bind:files
mimeTypes={activeMimeTypes}
on:submit={handleSubmit}
on:beforeinput={(ev) => {
if ($page.data.loginRequired) {
ev.preventDefault();
loginModalOpen = true;
}
}}
on:paste={onPaste}
disabled={isReadOnly || lastIsError}
modelHasTools={currentModel.tools}
modelIsMultimodal={currentModel.multimodal}
/>
{/if}

{#if loading}
<button
disabled
class="btn absolute bottom-1 right-0.5 size-10 self-end rounded-lg bg-transparent text-gray-400"
>
<EosIconsLoading />
</button>
{:else}
<button
class="btn absolute bottom-1 right-0.5 size-10 self-end rounded-lg bg-transparent text-gray-400 enabled:hover:text-gray-700 disabled:opacity-60 enabled:dark:hover:text-gray-100 dark:disabled:opacity-40"
disabled={!message || isReadOnly}
type="submit"
aria-label="Send message"
name="submit"
>
<CarbonSendAltFilled />
</button>
{/if}
</div>
{/if}
</form>
>
{#if onDrag && isFileUploadEnabled}
<FileDropzone bind:files bind:onDrag mimeTypes={activeMimeTypes} />
{:else}
<div
class="flex w-full flex-1 rounded-xl border-none bg-transparent"
class:paste-glow={pastedLongContent}
>
{#if lastIsError}
<ChatInput value="Sorry, something went wrong. Please try again." disabled={true} />
{:else}
<ChatInput
{assistant}
placeholder={isReadOnly ? "This conversation is read-only." : "Ask anything"}
{loading}
bind:value={message}
bind:files
mimeTypes={activeMimeTypes}
on:submit={handleSubmit}
on:beforeinput={(ev) => {
if ($page.data.loginRequired) {
ev.preventDefault();
loginModalOpen = true;
}
}}
on:paste={onPaste}
disabled={isReadOnly || lastIsError}
modelHasTools={currentModel.tools}
modelIsMultimodal={currentModel.multimodal}
/>
{/if}

{#if loading}
<button
disabled
class="btn absolute bottom-1 right-0.5 size-10 self-end rounded-lg bg-transparent text-gray-400"
>
<EosIconsLoading />
</button>
{:else}
<button
class="btn absolute bottom-1 right-0.5 size-10 self-end rounded-lg bg-transparent text-gray-400 enabled:hover:text-gray-700 disabled:opacity-60 enabled:dark:hover:text-gray-100 dark:disabled:opacity-40"
disabled={!message || isReadOnly}
type="submit"
aria-label="Send message"
name="submit"
>
<CarbonSendAltFilled />
</button>
{/if}
</div>
{/if}
</form>
</div>
<div
class="mt-2 flex justify-between self-stretch px-1 text-xs text-gray-400/90 max-md:mb-2 max-sm:gap-2"
>
Expand Down
37 changes: 37 additions & 0 deletions src/lib/utils/screenshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export async function captureScreen(): Promise<string> {
try {
// This will show the native browser dialog for screen capture
const stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: false,
});

// Create a canvas element to capture the screenshot
const canvas = document.createElement("canvas");
const video = document.createElement("video");

// Wait for the video to load metadata
await new Promise((resolve) => {
video.onloadedmetadata = () => {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
video.play();
resolve(null);
};
video.srcObject = stream;
});

// Draw the video frame to canvas
const context = canvas.getContext("2d");
context?.drawImage(video, 0, 0, canvas.width, canvas.height);

// Stop all tracks
stream.getTracks().forEach((track) => track.stop());

// Convert to base64
return canvas.toDataURL("image/png");
} catch (error) {
console.error("Error capturing screenshot:", error);
throw error;
}
}
nsarrazin marked this conversation as resolved.
Show resolved Hide resolved
Loading