Spaces:
Runtime error
Runtime error
Thomas G. Lopes
commited on
Commit
·
cf47645
1
Parent(s):
4766af6
add copy project option
Browse files- src/app.css +4 -0
- src/lib/components/inference-playground/project-select.svelte +30 -18
- src/lib/components/local-toasts.svelte +85 -0
- src/lib/components/share-modal.svelte +111 -0
- src/lib/utils/copy.ts +42 -0
- src/lib/utils/encode.ts +29 -0
- src/routes/+layout.svelte +3 -0
src/app.css
CHANGED
|
@@ -68,3 +68,7 @@ html {
|
|
| 68 |
body {
|
| 69 |
overflow: hidden;
|
| 70 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
body {
|
| 69 |
overflow: hidden;
|
| 70 |
}
|
| 71 |
+
|
| 72 |
+
body.dark {
|
| 73 |
+
color-scheme: dark;
|
| 74 |
+
}
|
src/lib/components/inference-playground/project-select.svelte
CHANGED
|
@@ -7,8 +7,10 @@
|
|
| 7 |
import IconEdit from "~icons/carbon/edit";
|
| 8 |
import IconSave from "~icons/carbon/save";
|
| 9 |
import IconDelete from "~icons/carbon/trash-can";
|
|
|
|
| 10 |
import { prompt } from "../prompts.svelte";
|
| 11 |
import Tooltip from "../tooltip.svelte";
|
|
|
|
| 12 |
|
| 13 |
interface Props {
|
| 14 |
class?: string;
|
|
@@ -49,29 +51,39 @@
|
|
| 49 |
</div>
|
| 50 |
</button>
|
| 51 |
|
| 52 |
-
|
| 53 |
<Tooltip>
|
| 54 |
{#snippet trigger(tooltip)}
|
| 55 |
-
<button class="btn size-[32px] p-0" {...tooltip.trigger} onclick={
|
| 56 |
-
<
|
| 57 |
</button>
|
| 58 |
{/snippet}
|
| 59 |
-
|
| 60 |
</Tooltip>
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
</div>
|
| 76 |
|
| 77 |
<div {...select.content} class="rounded-lg border bg-gray-100 dark:border-gray-700 dark:bg-gray-800">
|
|
|
|
| 7 |
import IconEdit from "~icons/carbon/edit";
|
| 8 |
import IconSave from "~icons/carbon/save";
|
| 9 |
import IconDelete from "~icons/carbon/trash-can";
|
| 10 |
+
import IconShare from "~icons/carbon/share";
|
| 11 |
import { prompt } from "../prompts.svelte";
|
| 12 |
import Tooltip from "../tooltip.svelte";
|
| 13 |
+
import { showShareModal } from "../share-modal.svelte";
|
| 14 |
|
| 15 |
interface Props {
|
| 16 |
class?: string;
|
|
|
|
| 51 |
</div>
|
| 52 |
</button>
|
| 53 |
|
| 54 |
+
<div class="flex items-center gap-2">
|
| 55 |
<Tooltip>
|
| 56 |
{#snippet trigger(tooltip)}
|
| 57 |
+
<button class="btn size-[32px] p-0" {...tooltip.trigger} onclick={() => showShareModal(session.project)}>
|
| 58 |
+
<IconShare />
|
| 59 |
</button>
|
| 60 |
{/snippet}
|
| 61 |
+
Share options
|
| 62 |
</Tooltip>
|
| 63 |
+
{#if isDefault}
|
| 64 |
+
<Tooltip>
|
| 65 |
+
{#snippet trigger(tooltip)}
|
| 66 |
+
<button class="btn size-[32px] p-0" {...tooltip.trigger} onclick={saveProject}>
|
| 67 |
+
<IconSave />
|
| 68 |
+
</button>
|
| 69 |
+
{/snippet}
|
| 70 |
+
Save to Project
|
| 71 |
+
</Tooltip>
|
| 72 |
+
{:else}
|
| 73 |
+
<Tooltip>
|
| 74 |
+
{#snippet trigger(tooltip)}
|
| 75 |
+
<button
|
| 76 |
+
class="btn size-[32px] p-0"
|
| 77 |
+
{...tooltip.trigger}
|
| 78 |
+
onclick={() => (session.$.activeProjectId = "default")}
|
| 79 |
+
>
|
| 80 |
+
<IconCross />
|
| 81 |
+
</button>
|
| 82 |
+
{/snippet}
|
| 83 |
+
Close project
|
| 84 |
+
</Tooltip>
|
| 85 |
+
{/if}
|
| 86 |
+
</div>
|
| 87 |
</div>
|
| 88 |
|
| 89 |
<div {...select.content} class="rounded-lg border bg-gray-100 dark:border-gray-700 dark:bg-gray-800">
|
src/lib/components/local-toasts.svelte
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import { onDestroy, type Snippet } from "svelte";
|
| 3 |
+
import { computePosition, autoUpdate } from "@floating-ui/dom";
|
| 4 |
+
import { fly } from "svelte/transition";
|
| 5 |
+
|
| 6 |
+
interface Props {
|
| 7 |
+
children: Snippet<[{ addToast: typeof addToast; trigger: typeof trigger }]>;
|
| 8 |
+
closeDelay?: number;
|
| 9 |
+
}
|
| 10 |
+
const { children, closeDelay = 2000 }: Props = $props();
|
| 11 |
+
|
| 12 |
+
const id = $props.id();
|
| 13 |
+
|
| 14 |
+
const trigger = {
|
| 15 |
+
id,
|
| 16 |
+
} as const;
|
| 17 |
+
|
| 18 |
+
type Toast = {
|
| 19 |
+
content: string;
|
| 20 |
+
id: string;
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
let toasts = $state<Toast[]>([]);
|
| 24 |
+
let timeouts: ReturnType<typeof window.setTimeout>[] = [];
|
| 25 |
+
|
| 26 |
+
function addToast(content: string) {
|
| 27 |
+
const id = crypto.randomUUID();
|
| 28 |
+
const timeout = setTimeout(() => {
|
| 29 |
+
toasts = toasts.filter(t => t.id !== id);
|
| 30 |
+
timeouts = timeouts.filter(t => t !== timeout);
|
| 31 |
+
}, closeDelay);
|
| 32 |
+
|
| 33 |
+
toasts.push({ content, id });
|
| 34 |
+
timeouts.push(timeout);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
onDestroy(() => {
|
| 38 |
+
timeouts.forEach(t => clearTimeout(t));
|
| 39 |
+
});
|
| 40 |
+
|
| 41 |
+
function float(node: HTMLElement) {
|
| 42 |
+
const triggerEl = document.getElementById(trigger.id);
|
| 43 |
+
if (!triggerEl) return;
|
| 44 |
+
|
| 45 |
+
const compute = () =>
|
| 46 |
+
computePosition(triggerEl, node, {
|
| 47 |
+
placement: "top",
|
| 48 |
+
strategy: "absolute",
|
| 49 |
+
}).then(({ x, y }) => {
|
| 50 |
+
Object.assign(node.style, {
|
| 51 |
+
left: `${x}px`,
|
| 52 |
+
top: `${y - 8}px`,
|
| 53 |
+
});
|
| 54 |
+
});
|
| 55 |
+
|
| 56 |
+
return {
|
| 57 |
+
destroy: autoUpdate(triggerEl, node, compute),
|
| 58 |
+
};
|
| 59 |
+
}
|
| 60 |
+
</script>
|
| 61 |
+
|
| 62 |
+
{@render children({ trigger, addToast })}
|
| 63 |
+
|
| 64 |
+
{#each toasts as toast (toast.id)}
|
| 65 |
+
<div
|
| 66 |
+
class="rounded-full border border-blue-400 bg-gradient-to-b from-blue-500 to-blue-600 px-2 py-1 text-xs"
|
| 67 |
+
in:fly={{ y: 10 }}
|
| 68 |
+
out:fly={{ y: -4 }}
|
| 69 |
+
use:float
|
| 70 |
+
>
|
| 71 |
+
{toast.content}
|
| 72 |
+
</div>
|
| 73 |
+
{/each}
|
| 74 |
+
|
| 75 |
+
<style>
|
| 76 |
+
div {
|
| 77 |
+
/* Float on top of the UI */
|
| 78 |
+
position: absolute;
|
| 79 |
+
|
| 80 |
+
/* Avoid layout interference */
|
| 81 |
+
width: max-content;
|
| 82 |
+
top: 0;
|
| 83 |
+
left: 0;
|
| 84 |
+
}
|
| 85 |
+
</style>
|
src/lib/components/share-modal.svelte
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts" module>
|
| 2 |
+
let project = $state<Project>();
|
| 3 |
+
|
| 4 |
+
export function showShareModal(p: Project) {
|
| 5 |
+
project = p;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
function close() {
|
| 9 |
+
project = undefined;
|
| 10 |
+
}
|
| 11 |
+
</script>
|
| 12 |
+
|
| 13 |
+
<script lang="ts">
|
| 14 |
+
import { clickOutside } from "$lib/actions/click-outside.js";
|
| 15 |
+
import { session } from "$lib/state/session.svelte";
|
| 16 |
+
import type { Project } from "$lib/types.js";
|
| 17 |
+
import { onMount } from "svelte";
|
| 18 |
+
import { fade, scale } from "svelte/transition";
|
| 19 |
+
import IconCross from "~icons/carbon/close";
|
| 20 |
+
import IconCopy from "~icons/carbon/copy";
|
| 21 |
+
import LocalToasts from "./local-toasts.svelte";
|
| 22 |
+
import { encodeObject } from "$lib/utils/encode.js";
|
| 23 |
+
import { copyToClipboard } from "$lib/utils/copy.js";
|
| 24 |
+
let dialog: HTMLDialogElement | undefined = $state();
|
| 25 |
+
|
| 26 |
+
onMount(() => {
|
| 27 |
+
// JUST FOR DEVELOPMENT
|
| 28 |
+
project = session.project;
|
| 29 |
+
});
|
| 30 |
+
|
| 31 |
+
const open = $derived(!!project);
|
| 32 |
+
const encoded = $derived(encodeObject(project));
|
| 33 |
+
|
| 34 |
+
$effect(() => {
|
| 35 |
+
if (open) {
|
| 36 |
+
dialog?.showModal();
|
| 37 |
+
} else {
|
| 38 |
+
setTimeout(() => dialog?.close(), 250);
|
| 39 |
+
}
|
| 40 |
+
});
|
| 41 |
+
</script>
|
| 42 |
+
|
| 43 |
+
<dialog class="backdrop:bg-transparent" bind:this={dialog} onclose={() => close()}>
|
| 44 |
+
{#if open}
|
| 45 |
+
<!-- Backdrop -->
|
| 46 |
+
<div
|
| 47 |
+
class="fixed inset-0 z-50 flex items-center justify-center overflow-hidden bg-black/50 backdrop-blur-sm"
|
| 48 |
+
transition:fade={{ duration: 150 }}
|
| 49 |
+
>
|
| 50 |
+
<!-- Content -->
|
| 51 |
+
<div
|
| 52 |
+
class="relative w-xl rounded-xl bg-white shadow-sm dark:bg-gray-900"
|
| 53 |
+
use:clickOutside={() => close()}
|
| 54 |
+
transition:scale={{ start: 0.975, duration: 250 }}
|
| 55 |
+
>
|
| 56 |
+
<div class="flex items-center justify-between rounded-t border-b p-4 md:px-5 md:py-4 dark:border-gray-800">
|
| 57 |
+
<h2 class="flex items-center gap-2.5 text-lg font-semibold text-gray-900 dark:text-white">Sharing</h2>
|
| 58 |
+
<button
|
| 59 |
+
type="button"
|
| 60 |
+
class="ms-auto inline-flex h-8 w-8 items-center justify-center rounded-lg bg-transparent text-sm text-gray-400 hover:bg-gray-200 hover:text-gray-900 dark:hover:bg-gray-600 dark:hover:text-white"
|
| 61 |
+
onclick={close}
|
| 62 |
+
>
|
| 63 |
+
<div class="text-xl">
|
| 64 |
+
<IconCross />
|
| 65 |
+
</div>
|
| 66 |
+
<span class="sr-only">Close modal</span>
|
| 67 |
+
</button>
|
| 68 |
+
</div>
|
| 69 |
+
<!-- Modal body -->
|
| 70 |
+
<div class="p-4 md:p-5">
|
| 71 |
+
<h3 class="text-lg font-semibold">Share your project</h3>
|
| 72 |
+
<p>Copy an unique string that shares your entire project until this point.</p>
|
| 73 |
+
<div class="mt-4 flex gap-2">
|
| 74 |
+
<input
|
| 75 |
+
class="grow cursor-not-allowed rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-400 dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
|
| 76 |
+
type="text"
|
| 77 |
+
value={encoded}
|
| 78 |
+
disabled
|
| 79 |
+
/>
|
| 80 |
+
<LocalToasts>
|
| 81 |
+
{#snippet children({ addToast, trigger })}
|
| 82 |
+
<button
|
| 83 |
+
{...trigger}
|
| 84 |
+
class="btn flex items-center gap-2"
|
| 85 |
+
onclick={() => {
|
| 86 |
+
copyToClipboard(encoded);
|
| 87 |
+
addToast("Copied to clipboard");
|
| 88 |
+
}}
|
| 89 |
+
>
|
| 90 |
+
<IconCopy />
|
| 91 |
+
Copy
|
| 92 |
+
</button>
|
| 93 |
+
{/snippet}
|
| 94 |
+
</LocalToasts>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
|
| 98 |
+
<!-- Modal footer -->
|
| 99 |
+
<!--
|
| 100 |
+
<div class="flex rounded-b border-t border-gray-200 p-4 md:p-5 dark:border-gray-800">
|
| 101 |
+
<button
|
| 102 |
+
type="submit"
|
| 103 |
+
class="ml-auto rounded-lg bg-black px-5 py-2.5 text-sm font-medium text-white hover:bg-gray-900 focus:ring-4 focus:ring-gray-300 focus:outline-hidden dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700 dark:focus:ring-gray-700"
|
| 104 |
+
>Submit</button
|
| 105 |
+
>
|
| 106 |
+
</div>
|
| 107 |
+
-->
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
{/if}
|
| 111 |
+
</dialog>
|
src/lib/utils/copy.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Copies a string to the clipboard, with a fallback for older browsers.
|
| 3 |
+
*
|
| 4 |
+
* @param text The string to copy to the clipboard.
|
| 5 |
+
* @returns A promise that resolves when the text has been successfully copied,
|
| 6 |
+
* or rejects if the copy operation fails.
|
| 7 |
+
*/
|
| 8 |
+
export async function copyToClipboard(text: string): Promise<void> {
|
| 9 |
+
if (navigator.clipboard) {
|
| 10 |
+
try {
|
| 11 |
+
await navigator.clipboard.writeText(text);
|
| 12 |
+
return; // Resolve immediately if successful
|
| 13 |
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
| 14 |
+
} catch (_) {
|
| 15 |
+
// Fallback to the older method
|
| 16 |
+
}
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
// Fallback for browsers that don't support the Clipboard API
|
| 20 |
+
try {
|
| 21 |
+
const textArea = document.createElement("textarea");
|
| 22 |
+
textArea.value = text;
|
| 23 |
+
|
| 24 |
+
// Avoid scrolling to bottom of page in MS Edge.
|
| 25 |
+
textArea.style.top = "0";
|
| 26 |
+
textArea.style.left = "0";
|
| 27 |
+
textArea.style.position = "fixed";
|
| 28 |
+
|
| 29 |
+
document.body.appendChild(textArea);
|
| 30 |
+
textArea.focus();
|
| 31 |
+
textArea.select();
|
| 32 |
+
|
| 33 |
+
const successful = document.execCommand("copy");
|
| 34 |
+
document.body.removeChild(textArea);
|
| 35 |
+
|
| 36 |
+
if (!successful) {
|
| 37 |
+
throw new Error("Failed to copy text using fallback method.");
|
| 38 |
+
}
|
| 39 |
+
} catch (err) {
|
| 40 |
+
return Promise.reject(err);
|
| 41 |
+
}
|
| 42 |
+
}
|
src/lib/utils/encode.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export function encodeObject(obj: unknown): string {
|
| 2 |
+
/**
|
| 3 |
+
* Encodes an object to a string using JSON serialization and Base64 encoding.
|
| 4 |
+
*
|
| 5 |
+
* Args:
|
| 6 |
+
* obj: The object to encode.
|
| 7 |
+
*
|
| 8 |
+
* Returns:
|
| 9 |
+
* A string representation of the object.
|
| 10 |
+
*/
|
| 11 |
+
const jsonString: string = JSON.stringify(obj);
|
| 12 |
+
const encodedString: string = btoa(unescape(encodeURIComponent(jsonString))); // btoa expects only ASCII chars
|
| 13 |
+
return encodedString;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export function decodeString(encodedString: string): unknown {
|
| 17 |
+
/**
|
| 18 |
+
* Decodes a string to an object using Base64 decoding and JSON deserialization.
|
| 19 |
+
*
|
| 20 |
+
* Args:
|
| 21 |
+
* encodedString: The string to decode.
|
| 22 |
+
*
|
| 23 |
+
* Returns:
|
| 24 |
+
* The decoded object.
|
| 25 |
+
*/
|
| 26 |
+
const jsonString: string = decodeURIComponent(escape(atob(encodedString)));
|
| 27 |
+
const obj: unknown = JSON.parse(jsonString);
|
| 28 |
+
return obj;
|
| 29 |
+
}
|
src/routes/+layout.svelte
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
import DebugMenu from "$lib/components/debug-menu.svelte";
|
| 3 |
import Prompts from "$lib/components/prompts.svelte";
|
| 4 |
import QuotaModal from "$lib/components/quota-modal.svelte";
|
|
|
|
| 5 |
import "../app.css";
|
| 6 |
|
| 7 |
interface Props {
|
|
@@ -12,6 +13,8 @@
|
|
| 12 |
</script>
|
| 13 |
|
| 14 |
{@render children?.()}
|
|
|
|
| 15 |
<DebugMenu />
|
| 16 |
<Prompts />
|
| 17 |
<QuotaModal />
|
|
|
|
|
|
| 2 |
import DebugMenu from "$lib/components/debug-menu.svelte";
|
| 3 |
import Prompts from "$lib/components/prompts.svelte";
|
| 4 |
import QuotaModal from "$lib/components/quota-modal.svelte";
|
| 5 |
+
import ShareModal from "$lib/components/share-modal.svelte";
|
| 6 |
import "../app.css";
|
| 7 |
|
| 8 |
interface Props {
|
|
|
|
| 13 |
</script>
|
| 14 |
|
| 15 |
{@render children?.()}
|
| 16 |
+
|
| 17 |
<DebugMenu />
|
| 18 |
<Prompts />
|
| 19 |
<QuotaModal />
|
| 20 |
+
<ShareModal />
|