Spaces:
Paused
Paused
Download src/lib/components/agent/AgentWorkspaceSetup.svelte from Mike0021/chat-ui-agent-demo: direct link, hf CLI and curl.
- Browser
- Download file 15.4 kB
-
https://huggingface.co/spaces/Mike0021/chat-ui-agent-demo/resolve/main/src/lib/components/agent/AgentWorkspaceSetup.svelte
- Command line
-
hf download hf://spaces/Mike0021/chat-ui-agent-demo/src/lib/components/agent/AgentWorkspaceSetup.svelte
-
curl -L -o AgentWorkspaceSetup.svelte https://huggingface.co/spaces/Mike0021/chat-ui-agent-demo/resolve/main/src/lib/components/agent/AgentWorkspaceSetup.svelte
15.4 kB
| <script lang="ts"> | |
| import { onDestroy, untrack } from "svelte"; | |
| import { base } from "$app/paths"; | |
| import type { Model } from "$lib/types/Model"; | |
| import type { WorkspacePolicy } from "$lib/types/Workspace"; | |
| import { | |
| isAgentCompatibleModel, | |
| type AgentRepositoryKind, | |
| type AgentRepositoryType, | |
| type AgentThinkingLevel, | |
| type AgentWorkspaceSetupValue, | |
| } from "./types"; | |
| interface Props { | |
| models: Model[]; | |
| initialValue: AgentWorkspaceSetupValue; | |
| onsave?: (value: AgentWorkspaceSetupValue) => void | Promise<void>; | |
| ondraftchange?: (value: AgentWorkspaceSetupValue) => void; | |
| oncancel?: () => void; | |
| } | |
| let { models, initialValue, onsave, ondraftchange, oncancel }: Props = $props(); | |
| const initial = untrack(() => initialValue); | |
| const initialModels = untrack(() => models.filter(isAgentCompatibleModel)); | |
| let repoKind = $state<AgentRepositoryKind>(initial.repo.kind); | |
| let hfRef = $state(initial.repo.kind === "hf" ? (initial.repo.ref ?? "") : ""); | |
| let gitRef = $state(initial.repo.kind === "git" ? (initial.repo.ref ?? "") : ""); | |
| let repoType = $state<AgentRepositoryType>(initial.repo.repoType ?? "space"); | |
| let branch = $state(initial.branch || "main"); | |
| let flavor = $state<"cpu-basic">("cpu-basic"); | |
| let model = $state( | |
| initialModels.some((entry) => entry.id === initial.model) | |
| ? initial.model | |
| : (initialModels[0]?.id ?? "") | |
| ); | |
| let thinkingLevel = $state<AgentThinkingLevel>(initial.thinkingLevel || "medium"); | |
| let policy = $state<WorkspacePolicy>(initial.policy); | |
| let validationError = $state(""); | |
| let saving = $state(false); | |
| let repositoryOptions = $state<Array<{ ref: string; private: boolean }>>([]); | |
| let repositoriesLoading = $state(false); | |
| let repositoriesLoadingFor = $state<AgentRepositoryType | undefined>(); | |
| let repositoriesLoadedFor = $state<AgentRepositoryType | undefined>(); | |
| let repositoriesError = $state(""); | |
| let repositoryLoadController: AbortController | undefined; | |
| let availableModels = $derived(models.filter(isAgentCompatibleModel)); | |
| const sourceOptions: Array<{ | |
| value: AgentRepositoryKind; | |
| title: string; | |
| description: string; | |
| }> = [ | |
| { | |
| value: "blank", | |
| title: "Blank workspace", | |
| description: "Start from your prompt with an empty working directory.", | |
| }, | |
| { | |
| value: "hf", | |
| title: "Hugging Face repo", | |
| description: "Work on one of your Spaces, models, or datasets.", | |
| }, | |
| { | |
| value: "git", | |
| title: "Git URL", | |
| description: "Clone a repository using a public HTTPS URL.", | |
| }, | |
| ]; | |
| const thinkingOptions: Array<{ value: AgentThinkingLevel; label: string }> = [ | |
| { value: "off", label: "Off" }, | |
| { value: "minimal", label: "Minimal" }, | |
| { value: "low", label: "Low" }, | |
| { value: "medium", label: "Medium" }, | |
| { value: "high", label: "High" }, | |
| { value: "xhigh", label: "Extra high" }, | |
| { value: "max", label: "Maximum" }, | |
| ]; | |
| const policyOptions: Array<{ | |
| value: WorkspacePolicy; | |
| label: string; | |
| description: string; | |
| }> = [ | |
| { | |
| value: "auto", | |
| label: "Auto", | |
| description: | |
| "Allow local edits and commands; ask before sensitive reads or external actions.", | |
| }, | |
| { | |
| value: "ask", | |
| label: "Ask more", | |
| description: "Ask before every file change or shell command.", | |
| }, | |
| { | |
| value: "read-only", | |
| label: "Read-only", | |
| description: "Block all file changes and shell commands; per-call overrides are disabled.", | |
| }, | |
| ]; | |
| function validate(): string | undefined { | |
| if (!model || !availableModels.some((entry) => entry.id === model)) { | |
| return "Choose an available Agent model."; | |
| } | |
| const normalizedBranch = branch.trim(); | |
| if (!normalizedBranch) return "Enter a branch name."; | |
| if ( | |
| normalizedBranch.startsWith("-") || | |
| normalizedBranch.startsWith("/") || | |
| normalizedBranch.endsWith("/") || | |
| normalizedBranch.endsWith(".") || | |
| normalizedBranch.endsWith(".lock") || | |
| normalizedBranch.includes("..") || | |
| normalizedBranch.includes("@{") || | |
| normalizedBranch.includes("//") || | |
| // Git rejects ASCII control characters in branch names. | |
| // eslint-disable-next-line no-control-regex | |
| /[~^:?*[\\\x00-\x20\x7f]/.test(normalizedBranch) | |
| ) { | |
| return "Enter a valid Git branch name."; | |
| } | |
| if ( | |
| repoKind === "hf" && | |
| !/^[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*$/.test(hfRef.trim()) | |
| ) { | |
| return "Enter a Hugging Face repository as namespace/name."; | |
| } | |
| if (repoKind === "git") { | |
| try { | |
| const url = new URL(gitRef.trim()); | |
| if ( | |
| url.protocol !== "https:" || | |
| !url.hostname || | |
| url.username || | |
| url.password || | |
| url.search || | |
| url.hash | |
| ) { | |
| throw new Error("invalid URL"); | |
| } | |
| } catch { | |
| return "Enter a public credential-free HTTPS Git URL."; | |
| } | |
| } | |
| return undefined; | |
| } | |
| function workspaceValue(normalize: boolean): AgentWorkspaceSetupValue { | |
| const refValue = repoKind === "hf" ? hfRef : repoKind === "git" ? gitRef : undefined; | |
| const ref = normalize ? refValue?.trim() : refValue; | |
| return { | |
| repo: { | |
| kind: repoKind, | |
| ...(ref ? { ref } : {}), | |
| ...(repoKind === "hf" ? { repoType } : {}), | |
| }, | |
| branch: normalize ? branch.trim() : branch, | |
| flavor, | |
| model, | |
| thinkingLevel, | |
| policy, | |
| }; | |
| } | |
| let draftEffectInitialized = false; | |
| $effect(() => { | |
| const value = workspaceValue(false); | |
| if (!draftEffectInitialized) { | |
| draftEffectInitialized = true; | |
| return; | |
| } | |
| ondraftchange?.(value); | |
| }); | |
| function cancel() { | |
| ondraftchange?.(workspaceValue(false)); | |
| oncancel?.(); | |
| } | |
| async function submit() { | |
| const error = validate(); | |
| if (error) { | |
| validationError = error; | |
| return; | |
| } | |
| validationError = ""; | |
| saving = true; | |
| try { | |
| await onsave?.(workspaceValue(true)); | |
| } catch (error) { | |
| validationError = | |
| error instanceof Error ? error.message : "Could not save the Agent workspace."; | |
| } finally { | |
| saving = false; | |
| } | |
| } | |
| async function loadRepositories(force = false) { | |
| const requestedType = repoType; | |
| if ( | |
| !force && | |
| (repositoriesLoadedFor === requestedType || | |
| (repositoriesLoading && repositoriesLoadingFor === requestedType)) | |
| ) { | |
| return; | |
| } | |
| repositoryLoadController?.abort(); | |
| const controller = new AbortController(); | |
| repositoryLoadController = controller; | |
| repositoriesLoading = true; | |
| repositoriesLoadingFor = requestedType; | |
| repositoriesError = ""; | |
| repositoryOptions = []; | |
| try { | |
| const response = await fetch( | |
| `${base}/api/agent/repositories?type=${encodeURIComponent(requestedType)}&limit=50`, | |
| { signal: controller.signal } | |
| ); | |
| if (!response.ok) { | |
| let message = "Could not load your Hugging Face repositories."; | |
| try { | |
| const failure = (await response.json()) as { message?: unknown }; | |
| if (typeof failure.message === "string" && failure.message.trim()) { | |
| message = failure.message; | |
| } | |
| } catch { | |
| // Keep the stable fallback for non-JSON gateway responses. | |
| } | |
| throw new Error(message); | |
| } | |
| const data = (await response.json()) as { | |
| repositories?: Array<{ ref?: unknown; private?: unknown }>; | |
| }; | |
| if (controller.signal.aborted || repoType !== requestedType) return; | |
| repositoryOptions = (data.repositories ?? []).flatMap((repository) => | |
| typeof repository.ref === "string" | |
| ? [{ ref: repository.ref, private: repository.private === true }] | |
| : [] | |
| ); | |
| repositoriesLoadedFor = requestedType; | |
| } catch (error) { | |
| if (controller.signal.aborted || repoType !== requestedType) return; | |
| repositoriesError = | |
| error instanceof Error ? error.message : "Could not load your Hugging Face repositories."; | |
| } finally { | |
| if (repositoryLoadController === controller) { | |
| repositoryLoadController = undefined; | |
| repositoriesLoading = false; | |
| repositoriesLoadingFor = undefined; | |
| } | |
| } | |
| } | |
| onDestroy(() => repositoryLoadController?.abort()); | |
| const fieldClass = | |
| "min-h-11 w-full rounded-xl border border-gray-200 bg-white px-3.5 py-2 text-sm text-gray-900 outline-hidden focus:border-blue-500 focus:ring-2 focus:ring-blue-500/25 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"; | |
| </script> | |
| <form | |
| class="flex w-full flex-col gap-5 p-5 sm:p-6" | |
| aria-labelledby="agent-workspace-setup-title" | |
| onsubmit={(event) => { | |
| event.preventDefault(); | |
| submit(); | |
| }} | |
| > | |
| <header class="pr-8"> | |
| <h2 | |
| id="agent-workspace-setup-title" | |
| class="text-xl font-semibold text-gray-900 dark:text-gray-100" | |
| > | |
| Set up an Agent workspace | |
| </h2> | |
| <p class="mt-1 text-sm leading-relaxed text-gray-500 dark:text-gray-400"> | |
| Choose where the agent will work. The sandbox starts only after you send your prompt. | |
| </p> | |
| </header> | |
| <fieldset class="flex flex-col gap-2"> | |
| <legend class="mb-1 text-sm font-medium text-gray-800 dark:text-gray-200" | |
| >Workspace source</legend | |
| > | |
| <div class="grid gap-2 sm:grid-cols-3"> | |
| {#each sourceOptions as option} | |
| <label | |
| class="min-h-24 cursor-pointer rounded-xl border p-3 outline-hidden transition-colors focus-within:border-blue-500 focus-within:ring-2 focus-within:ring-blue-500/25 {repoKind === | |
| option.value | |
| ? 'border-blue-500 bg-blue-50/70 dark:border-blue-500 dark:bg-blue-950/25' | |
| : 'border-gray-200 bg-white hover:border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:hover:border-gray-600'}" | |
| > | |
| <span class="flex items-start gap-2.5"> | |
| <input | |
| type="radio" | |
| name="agent-workspace-source" | |
| value={option.value} | |
| bind:group={repoKind} | |
| onchange={() => (validationError = "")} | |
| class="mt-0.5 size-4 shrink-0 accent-blue-600" | |
| /> | |
| <span> | |
| <span class="block text-sm font-semibold text-gray-900 dark:text-gray-100"> | |
| {option.title} | |
| </span> | |
| <span class="mt-1 block text-xs leading-relaxed text-gray-500 dark:text-gray-400"> | |
| {option.description} | |
| </span> | |
| </span> | |
| </span> | |
| </label> | |
| {/each} | |
| </div> | |
| </fieldset> | |
| {#if repoKind === "hf"} | |
| <div class="grid gap-3 sm:grid-cols-[9rem_1fr]"> | |
| <label class="flex flex-col gap-1.5"> | |
| <span class="text-sm font-medium text-gray-800 dark:text-gray-200">Repository type</span> | |
| <select | |
| bind:value={repoType} | |
| class={fieldClass} | |
| onchange={() => void loadRepositories(true)} | |
| > | |
| <option value="space">Space</option> | |
| <option value="model">Model</option> | |
| <option value="dataset">Dataset</option> | |
| </select> | |
| </label> | |
| <label class="flex min-w-0 flex-col gap-1.5"> | |
| <span class="text-sm font-medium text-gray-800 dark:text-gray-200" | |
| >Hugging Face repository</span | |
| > | |
| <input | |
| type="text" | |
| bind:value={hfRef} | |
| required | |
| list="agent-hf-repositories" | |
| autocomplete="off" | |
| placeholder="username/my-space" | |
| class={fieldClass} | |
| onfocus={() => void loadRepositories()} | |
| /> | |
| <datalist id="agent-hf-repositories"> | |
| {#each repositoryOptions as repository (repository.ref)} | |
| <option value={repository.ref}>{repository.private ? "Private" : "Public"}</option> | |
| {/each} | |
| </datalist> | |
| {#if repositoriesError} | |
| <span class="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs" role="status"> | |
| <span class="text-amber-700 dark:text-amber-300">{repositoriesError}</span> | |
| <button | |
| type="button" | |
| class="font-medium text-blue-600 hover:underline dark:text-blue-400" | |
| onclick={() => void loadRepositories(true)}>Retry</button | |
| > | |
| <span class="text-gray-500 dark:text-gray-400">or enter namespace/name manually</span> | |
| </span> | |
| {:else} | |
| <span class="text-xs text-gray-500 dark:text-gray-400"> | |
| {repositoriesLoading | |
| ? "Loading your repositories…" | |
| : repositoryOptions.length | |
| ? `${repositoryOptions.length} recent repositories available` | |
| : "Choose from your HF assets or enter namespace/name"} | |
| </span> | |
| {/if} | |
| </label> | |
| </div> | |
| {:else if repoKind === "git"} | |
| <label class="flex flex-col gap-1.5"> | |
| <span class="text-sm font-medium text-gray-800 dark:text-gray-200">HTTPS Git URL</span> | |
| <input | |
| type="url" | |
| bind:value={gitRef} | |
| required | |
| inputmode="url" | |
| autocomplete="url" | |
| placeholder="https://github.com/org/repository.git" | |
| class={fieldClass} | |
| /> | |
| </label> | |
| {/if} | |
| <div class="grid gap-3 sm:grid-cols-2"> | |
| <label class="flex flex-col gap-1.5"> | |
| <span class="text-sm font-medium text-gray-800 dark:text-gray-200">Working branch</span> | |
| <input type="text" bind:value={branch} required autocomplete="off" class={fieldClass} /> | |
| </label> | |
| <label class="flex flex-col gap-1.5"> | |
| <span class="text-sm font-medium text-gray-800 dark:text-gray-200">Hardware</span> | |
| <select bind:value={flavor} class={fieldClass}> | |
| <option value="cpu-basic">CPU Basic</option> | |
| </select> | |
| <span class="text-xs text-gray-500 dark:text-gray-400">Fast-starting CPU sandbox</span> | |
| </label> | |
| </div> | |
| <label class="flex flex-col gap-1.5"> | |
| <span class="text-sm font-medium text-gray-800 dark:text-gray-200">Approval policy</span> | |
| <select | |
| bind:value={policy} | |
| class={fieldClass} | |
| aria-label="Approval policy" | |
| aria-describedby="agent-policy-description" | |
| > | |
| {#each policyOptions as option (option.value)} | |
| <option value={option.value}>{option.label}</option> | |
| {/each} | |
| </select> | |
| <span id="agent-policy-description" class="text-xs text-gray-500 dark:text-gray-400"> | |
| {policyOptions.find((option) => option.value === policy)?.description} | |
| </span> | |
| </label> | |
| <div class="grid gap-3 sm:grid-cols-2"> | |
| <label class="flex min-w-0 flex-col gap-1.5"> | |
| <span class="text-sm font-medium text-gray-800 dark:text-gray-200">Agent model</span> | |
| <select bind:value={model} class={fieldClass} aria-label="Agent model"> | |
| {#each availableModels as option (option.id)} | |
| <option value={option.id}>{option.displayName || option.name || option.id}</option> | |
| {/each} | |
| {#if !availableModels.length} | |
| <option value="" disabled>No compatible Agent models</option> | |
| {/if} | |
| </select> | |
| </label> | |
| <label class="flex flex-col gap-1.5"> | |
| <span class="text-sm font-medium text-gray-800 dark:text-gray-200">Thinking level</span> | |
| <select bind:value={thinkingLevel} class={fieldClass} aria-label="Thinking level"> | |
| {#each thinkingOptions as option (option.value)} | |
| <option value={option.value}>{option.label}</option> | |
| {/each} | |
| </select> | |
| </label> | |
| </div> | |
| {#if validationError} | |
| <p | |
| role="alert" | |
| class="rounded-xl border border-red-300/70 bg-red-50 px-3.5 py-2.5 text-sm text-red-700 dark:border-red-500/40 dark:bg-red-500/10 dark:text-red-300" | |
| > | |
| {validationError} | |
| </p> | |
| {/if} | |
| <footer class="flex flex-col-reverse gap-2 pt-1 sm:flex-row sm:justify-end"> | |
| <button | |
| type="button" | |
| disabled={saving} | |
| class="min-h-11 rounded-xl border border-gray-300 bg-white px-4 text-sm font-medium text-gray-800 outline-hidden hover:bg-gray-50 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700 dark:focus-visible:ring-offset-gray-800" | |
| onclick={cancel} | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| type="submit" | |
| disabled={saving} | |
| class="min-h-11 rounded-xl bg-gray-900 px-4 text-sm font-semibold text-white outline-hidden hover:bg-black focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-white dark:focus-visible:ring-offset-gray-800" | |
| > | |
| {saving ? "Saving workspace…" : "Use this workspace"} | |
| </button> | |
| </footer> | |
| </form> | |