ui-svelte: big convert to shadcn components
This commit is contained in:
@@ -60,7 +60,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<h1
|
<h1
|
||||||
contenteditable="true"
|
contenteditable="true"
|
||||||
class="truncate pb-0 text-base font-semibold outline-none rounded px-1 hover:bg-sidebar-accent group-data-[collapsible=icon]:hidden"
|
class="truncate pb-0 text-base font-semibold outline-none rounded-md px-1 hover:bg-sidebar-accent group-data-[collapsible=icon]:hidden"
|
||||||
onblur={handleBlur}
|
onblur={handleBlur}
|
||||||
onkeydown={handleKeyDown}
|
onkeydown={handleKeyDown}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ReqRespCapture } from "../lib/types";
|
import type { ReqRespCapture } from "../lib/types";
|
||||||
import { Button } from "$lib/components/ui/button/index.js";
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
capture: ReqRespCapture | null;
|
capture: ReqRespCapture | null;
|
||||||
@@ -10,22 +11,12 @@
|
|||||||
|
|
||||||
let { capture, open, onclose }: Props = $props();
|
let { capture, open, onclose }: Props = $props();
|
||||||
|
|
||||||
let dialogEl: HTMLDialogElement | undefined = $state();
|
|
||||||
|
|
||||||
type BodyTab = "raw" | "pretty" | "chat";
|
type BodyTab = "raw" | "pretty" | "chat";
|
||||||
let reqBodyTab: BodyTab = $state("pretty");
|
let reqBodyTab: BodyTab = $state("pretty");
|
||||||
let respBodyTab: BodyTab = $state("pretty");
|
let respBodyTab: BodyTab = $state("pretty");
|
||||||
let copiedReq = $state(false);
|
let copiedReq = $state(false);
|
||||||
let copiedResp = $state(false);
|
let copiedResp = $state(false);
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (open && dialogEl) {
|
|
||||||
dialogEl.showModal();
|
|
||||||
} else if (!open && dialogEl) {
|
|
||||||
dialogEl.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset tabs when capture changes
|
// Reset tabs when capture changes
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (capture) {
|
if (capture) {
|
||||||
@@ -40,10 +31,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleDialogClose() {
|
|
||||||
onclose();
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeBody(body: string | null | undefined): string {
|
function decodeBody(body: string | null | undefined): string {
|
||||||
if (!body) return "";
|
if (!body) return "";
|
||||||
try {
|
try {
|
||||||
@@ -191,26 +178,22 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<dialog
|
<Dialog.Root
|
||||||
bind:this={dialogEl}
|
{open}
|
||||||
onclose={handleDialogClose}
|
onOpenChange={(v) => {
|
||||||
class="bg-background text-foreground rounded-lg shadow-xl max-w-[80%] w-full max-h-[90vh] p-0 backdrop:bg-black/50 m-auto"
|
if (!v) onclose();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{#if capture}
|
<Dialog.Content class="max-h-[90vh] w-full max-w-[80%] gap-0 p-0">
|
||||||
<div class="flex flex-col max-h-[90vh]">
|
{#if capture}
|
||||||
<div
|
<Dialog.Header class="border-b border-border px-4 py-3">
|
||||||
class="flex justify-between items-center p-4 border-b border-border"
|
<Dialog.Title class="text-lg font-bold">
|
||||||
>
|
Capture #{capture.id + 1}{#if capture.req_path}
|
||||||
<h2 class="text-xl font-bold pb-0">Capture #{capture.id + 1}{#if capture.req_path} <span class="text-base font-mono font-normal text-muted-foreground">{capture.req_path}</span>{/if}</h2>
|
<span class="font-mono text-base font-normal text-muted-foreground">{capture.req_path}</span>{/if}
|
||||||
<button
|
</Dialog.Title>
|
||||||
onclick={() => dialogEl?.close()}
|
</Dialog.Header>
|
||||||
class="text-muted-foreground hover:text-foreground text-2xl leading-none"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="overflow-y-auto flex-1 p-4 space-y-4">
|
<div class="overflow-y-auto flex-1 space-y-4 p-4">
|
||||||
<!-- Request Headers -->
|
<!-- Request Headers -->
|
||||||
<details class="group" open>
|
<details class="group" open>
|
||||||
<summary
|
<summary
|
||||||
@@ -219,7 +202,7 @@
|
|||||||
Request Headers
|
Request Headers
|
||||||
</summary>
|
</summary>
|
||||||
<div
|
<div
|
||||||
class="mt-2 bg-background rounded border border-border overflow-auto max-h-48"
|
class="mt-2 bg-background rounded-md border border-border overflow-auto max-h-48"
|
||||||
>
|
>
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -272,14 +255,14 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mt-1 bg-background rounded border border-border overflow-auto max-h-96"
|
class="mt-1 bg-background rounded-md border border-border overflow-auto max-h-96"
|
||||||
>
|
>
|
||||||
<pre
|
<pre
|
||||||
class="p-3 text-sm font-mono whitespace-pre-wrap break-all">{displayedRequestBody}</pre>
|
class="p-3 text-sm font-mono whitespace-pre-wrap break-all">{displayedRequestBody}</pre>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class="mt-2 bg-background rounded border border-border overflow-auto max-h-96"
|
class="mt-2 bg-background rounded-md border border-border overflow-auto max-h-96"
|
||||||
>
|
>
|
||||||
<pre class="p-3 text-sm font-mono whitespace-pre-wrap break-all"
|
<pre class="p-3 text-sm font-mono whitespace-pre-wrap break-all"
|
||||||
>(empty)</pre
|
>(empty)</pre
|
||||||
@@ -296,7 +279,7 @@
|
|||||||
Response Headers
|
Response Headers
|
||||||
</summary>
|
</summary>
|
||||||
<div
|
<div
|
||||||
class="mt-2 bg-background rounded border border-border overflow-auto max-h-48"
|
class="mt-2 bg-background rounded-md border border-border overflow-auto max-h-48"
|
||||||
>
|
>
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -322,7 +305,7 @@
|
|||||||
</summary>
|
</summary>
|
||||||
{#if isResponseImage && capture.resp_body}
|
{#if isResponseImage && capture.resp_body}
|
||||||
<div
|
<div
|
||||||
class="mt-2 bg-background rounded border border-border overflow-auto max-h-96"
|
class="mt-2 bg-background rounded-md border border-border overflow-auto max-h-96"
|
||||||
>
|
>
|
||||||
<div class="p-3 flex justify-center">
|
<div class="p-3 flex justify-center">
|
||||||
<img
|
<img
|
||||||
@@ -369,7 +352,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mt-1 bg-background rounded border border-border overflow-auto max-h-96"
|
class="mt-1 bg-background rounded-md border border-border overflow-auto max-h-96"
|
||||||
>
|
>
|
||||||
{#if respBodyTab === "chat"}
|
{#if respBodyTab === "chat"}
|
||||||
<div class="p-3 text-sm space-y-3">
|
<div class="p-3 text-sm space-y-3">
|
||||||
@@ -408,7 +391,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else if responseBodyRaw}
|
{:else if responseBodyRaw}
|
||||||
<div
|
<div
|
||||||
class="mt-2 bg-background rounded border border-border overflow-auto max-h-96"
|
class="mt-2 bg-background rounded-md border border-border overflow-auto max-h-96"
|
||||||
>
|
>
|
||||||
<div class="p-3 text-sm text-muted-foreground italic">
|
<div class="p-3 text-sm text-muted-foreground italic">
|
||||||
(binary data - {responseContentType || "unknown content type"})
|
(binary data - {responseContentType || "unknown content type"})
|
||||||
@@ -416,7 +399,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class="mt-2 bg-background rounded border border-border overflow-auto max-h-96"
|
class="mt-2 bg-background rounded-md border border-border overflow-auto max-h-96"
|
||||||
>
|
>
|
||||||
<pre class="p-3 text-sm font-mono">(empty)</pre>
|
<pre class="p-3 text-sm font-mono">(empty)</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -424,26 +407,26 @@
|
|||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4 border-t border-border flex justify-end">
|
<Dialog.Footer class="border-t border-border px-4 py-3 sm:justify-end">
|
||||||
<Button variant="outline" onclick={() => dialogEl?.close()}>Close</Button>
|
<Button variant="outline" onclick={onclose}>Close</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col items-center justify-center p-12">
|
||||||
|
<p class="text-lg text-muted-foreground">Capture not found</p>
|
||||||
|
<p class="text-sm text-muted-foreground mt-1">The capture may have expired or was never recorded.</p>
|
||||||
|
<div class="mt-4">
|
||||||
|
<Button variant="outline" onclick={onclose}>Close</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{:else}
|
</Dialog.Content>
|
||||||
<div class="flex flex-col items-center justify-center p-12">
|
</Dialog.Root>
|
||||||
<p class="text-lg text-muted-foreground">Capture not found</p>
|
|
||||||
<p class="text-sm text-muted-foreground mt-1">The capture may have expired or was never recorded.</p>
|
|
||||||
<div class="mt-4">
|
|
||||||
<Button variant="outline" onclick={() => dialogEl?.close()}>Close</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</dialog>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.tab-btn {
|
.tab-btn {
|
||||||
padding: 2px 10px;
|
padding: 2px 10px;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
border-radius: 4px;
|
border-radius: 0;
|
||||||
color: var(--muted-foreground);
|
color: var(--muted-foreground);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
|
|||||||
@@ -85,7 +85,7 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card.Root class="bg-muted/30 h-full w-full gap-0 overflow-hidden rounded-none py-0">
|
<Card.Root class="bg-muted/30 h-full w-full gap-0 overflow-hidden py-0">
|
||||||
<Card.Header class="border-b px-4 py-2">
|
<Card.Header class="border-b px-4 py-2">
|
||||||
<Card.Title class="text-sm font-semibold">{title}</Card.Title>
|
<Card.Title class="text-sm font-semibold">{title}</Card.Title>
|
||||||
<Card.Action>
|
<Card.Action>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Snippet } from "svelte";
|
import type { Snippet } from "svelte";
|
||||||
|
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
metadata: Record<string, string> | undefined;
|
metadata: Record<string, string> | undefined;
|
||||||
@@ -9,77 +10,28 @@
|
|||||||
let { metadata, children }: Props = $props();
|
let { metadata, children }: Props = $props();
|
||||||
|
|
||||||
let entries = $derived(Object.entries(metadata || {}));
|
let entries = $derived(Object.entries(metadata || {}));
|
||||||
let triggerEl: HTMLElement | undefined = $state();
|
|
||||||
let tooltipEl: HTMLDivElement | undefined = $state();
|
|
||||||
let show = $state(false);
|
|
||||||
let tooltipStyle = $state("");
|
|
||||||
|
|
||||||
function positionTooltip() {
|
|
||||||
if (!triggerEl || !tooltipEl) return;
|
|
||||||
const triggerRect = triggerEl.getBoundingClientRect();
|
|
||||||
const tooltipRect = tooltipEl.getBoundingClientRect();
|
|
||||||
const margin = 8;
|
|
||||||
const viewportWidth = window.innerWidth;
|
|
||||||
const viewportHeight = window.innerHeight;
|
|
||||||
|
|
||||||
let left = triggerRect.left;
|
|
||||||
let top = triggerRect.bottom + margin;
|
|
||||||
|
|
||||||
// Keep tooltip within horizontal viewport bounds
|
|
||||||
if (left + tooltipRect.width > viewportWidth - margin) {
|
|
||||||
left = triggerRect.right - tooltipRect.width;
|
|
||||||
}
|
|
||||||
if (left < margin) {
|
|
||||||
left = margin;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flip above trigger if it would overflow the bottom
|
|
||||||
if (top + tooltipRect.height > viewportHeight - margin) {
|
|
||||||
top = triggerRect.top - tooltipRect.height - margin;
|
|
||||||
}
|
|
||||||
|
|
||||||
tooltipStyle = `left: ${left}px; top: ${top}px; max-width: calc(100vw - ${margin * 2}px);`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onEnter() {
|
|
||||||
show = true;
|
|
||||||
requestAnimationFrame(positionTooltip);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onLeave() {
|
|
||||||
show = false;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span
|
{#if entries.length > 0}
|
||||||
bind:this={triggerEl}
|
<Tooltip.Provider>
|
||||||
onmouseenter={onEnter}
|
<Tooltip.Root>
|
||||||
onmouseleave={onLeave}
|
<Tooltip.Trigger>
|
||||||
onfocus={onEnter}
|
{@render children()}
|
||||||
onblur={onLeave}
|
</Tooltip.Trigger>
|
||||||
class="inline-flex"
|
<Tooltip.Content class="min-w-[12rem] max-w-[24rem] normal-case">
|
||||||
role="button"
|
<table class="w-full text-left">
|
||||||
tabindex="0"
|
<tbody>
|
||||||
aria-label="Show metadata"
|
{#each entries as [key, value]}
|
||||||
>
|
<tr class="border-b border-white/10 last:border-0">
|
||||||
|
<td class="py-1 pr-3 font-medium whitespace-nowrap text-primary">{key}</td>
|
||||||
|
<td class="py-1 break-all">{value}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Root>
|
||||||
|
</Tooltip.Provider>
|
||||||
|
{:else}
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</span>
|
|
||||||
|
|
||||||
{#if show && entries.length > 0}
|
|
||||||
<div
|
|
||||||
bind:this={tooltipEl}
|
|
||||||
style={tooltipStyle}
|
|
||||||
class="fixed px-3 py-2 bg-gray-900 text-white text-sm rounded-md z-50 normal-case min-w-[12rem] max-w-[24rem] shadow-lg whitespace-normal"
|
|
||||||
>
|
|
||||||
<table class="w-full text-left">
|
|
||||||
<tbody>
|
|
||||||
{#each entries as [key, value]}
|
|
||||||
<tr class="border-b border-white/10 last:border-0">
|
|
||||||
<td class="py-1 pr-3 font-medium whitespace-nowrap text-primary">{key}</td>
|
|
||||||
<td class="py-1 break-all">{value}</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { Chart, registerables } from "chart.js";
|
import { Chart, registerables } from "chart.js";
|
||||||
import { isDarkMode } from "../stores/theme";
|
import { isDarkMode } from "../stores/theme";
|
||||||
|
import * as Card from "$lib/components/ui/card/index.js";
|
||||||
|
|
||||||
Chart.register(...registerables);
|
Chart.register(...registerables);
|
||||||
|
|
||||||
@@ -143,6 +144,8 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="bg-card text-card-foreground h-[300px] rounded-xl border p-4 shadow-sm">
|
<Card.Root class="h-[300px] py-0">
|
||||||
<canvas bind:this={canvas}></canvas>
|
<Card.Content class="h-full p-4">
|
||||||
</div>
|
<canvas bind:this={canvas}></canvas>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
|||||||
@@ -135,7 +135,7 @@
|
|||||||
<div
|
<div
|
||||||
role="separator"
|
role="separator"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="{handleClass} bg-primary hover:bg-success transition-colors rounded flex-shrink-0"
|
class="{handleClass} bg-primary hover:bg-success transition-colors rounded-md flex-shrink-0"
|
||||||
onmousedown={handleMouseDown}
|
onmousedown={handleMouseDown}
|
||||||
ontouchstart={handleTouchStart}
|
ontouchstart={handleTouchStart}
|
||||||
onkeydown={handleKeyDown}
|
onkeydown={handleKeyDown}
|
||||||
|
|||||||
@@ -157,7 +157,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- File upload / Result display area -->
|
<!-- File upload / Result display area -->
|
||||||
<div class="flex-1 overflow-auto mb-4 flex items-center justify-center bg-background border border-border rounded">
|
<div class="flex-1 overflow-auto mb-4 flex items-center justify-center bg-background border border-border rounded-md">
|
||||||
{#if isTranscribing}
|
{#if isTranscribing}
|
||||||
<div class="text-center text-muted-foreground">
|
<div class="text-center text-muted-foreground">
|
||||||
<div class="inline-block w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin mb-2"></div>
|
<div class="inline-block w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin mb-2"></div>
|
||||||
@@ -185,7 +185,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 overflow-auto p-3 rounded border border-border bg-background whitespace-pre-wrap">
|
<div class="flex-1 overflow-auto p-3 rounded-md border border-border bg-background whitespace-pre-wrap">
|
||||||
{transcriptionResult}
|
{transcriptionResult}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
import { Input } from "$lib/components/ui/input/index.js";
|
import { Input } from "$lib/components/ui/input/index.js";
|
||||||
import { Textarea } from "$lib/components/ui/textarea/index.js";
|
import { Textarea } from "$lib/components/ui/textarea/index.js";
|
||||||
import { Label } from "$lib/components/ui/label/index.js";
|
import { Label } from "$lib/components/ui/label/index.js";
|
||||||
|
import * as Select from "$lib/components/ui/select/index.js";
|
||||||
|
import { X } from "@lucide/svelte";
|
||||||
|
|
||||||
const selectedModelStore = persistentStore<string>("playground-selected-model", "");
|
const selectedModelStore = persistentStore<string>("playground-selected-model", "");
|
||||||
const systemPromptStore = persistentStore<string>("playground-system-prompt", "");
|
const systemPromptStore = persistentStore<string>("playground-system-prompt", "");
|
||||||
@@ -322,16 +324,18 @@
|
|||||||
<div class="bg-muted/40 mb-4 shrink-0 rounded-lg border p-4">
|
<div class="bg-muted/40 mb-4 shrink-0 rounded-lg border p-4">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<Label class="mb-1" for="endpoint">Endpoint</Label>
|
<Label class="mb-1" for="endpoint">Endpoint</Label>
|
||||||
<select
|
<Select.Root
|
||||||
id="endpoint"
|
type="single"
|
||||||
class="border-input bg-background focus-visible:border-ring focus-visible:ring-ring/50 dark:bg-input/30 w-full rounded-md border px-3 py-2 text-sm shadow-xs outline-none focus-visible:ring-[3px] disabled:opacity-50"
|
value={$endpointStore}
|
||||||
bind:value={$endpointStore}
|
onValueChange={(v) => v && endpointStore.set(v as Endpoint)}
|
||||||
disabled={isStreaming}
|
|
||||||
>
|
>
|
||||||
<option value="v1/chat/completions">/v1/chat/completions</option>
|
<Select.Trigger class="w-full">/{$endpointStore}</Select.Trigger>
|
||||||
<option value="v1/messages">/v1/messages</option>
|
<Select.Content>
|
||||||
<option value="v1/responses">/v1/responses</option>
|
<Select.Item value="v1/chat/completions">/v1/chat/completions</Select.Item>
|
||||||
</select>
|
<Select.Item value="v1/messages">/v1/messages</Select.Item>
|
||||||
|
<Select.Item value="v1/responses">/v1/responses</Select.Item>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<Label class="mb-1" for="system-prompt">System Prompt</Label>
|
<Label class="mb-1" for="system-prompt">System Prompt</Label>
|
||||||
@@ -415,15 +419,17 @@
|
|||||||
<img
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt="Attached image {idx + 1}"
|
alt="Attached image {idx + 1}"
|
||||||
class="h-20 w-20 rounded border object-cover"
|
class="h-20 w-20 rounded-md border object-cover"
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
class="bg-destructive text-destructive-foreground absolute -right-2 -top-2 flex h-6 w-6 items-center justify-center rounded-full opacity-0 transition-opacity group-hover:opacity-100"
|
variant="destructive"
|
||||||
|
size="icon-sm"
|
||||||
|
class="absolute -right-2 -top-2 h-6 w-6 rounded-full opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
onclick={() => removeImage(idx)}
|
onclick={() => removeImage(idx)}
|
||||||
title="Remove image"
|
title="Remove image"
|
||||||
>
|
>
|
||||||
×
|
<X class="size-3" />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -431,7 +437,7 @@
|
|||||||
|
|
||||||
<!-- Error message -->
|
<!-- Error message -->
|
||||||
{#if imageError}
|
{#if imageError}
|
||||||
<div class="bg-destructive/10 text-destructive mb-2 rounded p-2 text-sm">
|
<div class="bg-destructive/10 text-destructive mb-2 rounded-md p-2 text-sm">
|
||||||
{imageError}
|
{imageError}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -169,7 +169,7 @@
|
|||||||
>
|
>
|
||||||
{#if role === "assistant"}
|
{#if role === "assistant"}
|
||||||
{#if reasoning_content || isReasoning}
|
{#if reasoning_content || isReasoning}
|
||||||
<div class="mb-3 overflow-hidden rounded border">
|
<div class="mb-3 overflow-hidden rounded-md border">
|
||||||
<button
|
<button
|
||||||
class="bg-muted/50 hover:bg-muted flex w-full items-center gap-2 px-3 py-2 text-sm transition-colors"
|
class="bg-muted/50 hover:bg-muted flex w-full items-center gap-2 px-3 py-2 text-sm transition-colors"
|
||||||
onclick={() => showReasoning = !showReasoning}
|
onclick={() => showReasoning = !showReasoning}
|
||||||
@@ -203,12 +203,12 @@
|
|||||||
{#each imageUrls as imageUrl, idx (idx)}
|
{#each imageUrls as imageUrl, idx (idx)}
|
||||||
<button
|
<button
|
||||||
onclick={() => openModal(imageUrl)}
|
onclick={() => openModal(imageUrl)}
|
||||||
class="cursor-pointer rounded border transition-opacity hover:opacity-80"
|
class="cursor-pointer rounded-md border transition-opacity hover:opacity-80"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt="Image {idx + 1}"
|
alt="Image {idx + 1}"
|
||||||
class="max-h-64 rounded"
|
class="max-h-64 rounded-md"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -277,12 +277,12 @@
|
|||||||
{#each imageUrls as imageUrl, idx (idx)}
|
{#each imageUrls as imageUrl, idx (idx)}
|
||||||
<button
|
<button
|
||||||
onclick={() => openModal(imageUrl)}
|
onclick={() => openModal(imageUrl)}
|
||||||
class="cursor-pointer rounded border border-white/20 transition-opacity hover:opacity-80"
|
class="cursor-pointer rounded-md border border-white/20 transition-opacity hover:opacity-80"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt="Image {idx + 1}"
|
alt="Image {idx + 1}"
|
||||||
class="max-w-[200px] rounded"
|
class="max-w-[200px] rounded-md"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -322,7 +322,7 @@
|
|||||||
<img
|
<img
|
||||||
src={modalImageUrl}
|
src={modalImageUrl}
|
||||||
alt=""
|
alt=""
|
||||||
class="max-w-full max-h-full rounded pointer-events-none"
|
class="max-w-full max-h-full rounded-md pointer-events-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
import { persistentStore } from "../../stores/persistent";
|
import { persistentStore } from "../../stores/persistent";
|
||||||
import { streamChatCompletion } from "../../lib/chatApi";
|
import { streamChatCompletion } from "../../lib/chatApi";
|
||||||
import { Button } from "$lib/components/ui/button/index.js";
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import { Input } from "$lib/components/ui/input/index.js";
|
||||||
|
import { Textarea } from "$lib/components/ui/textarea/index.js";
|
||||||
|
import { X } from "@lucide/svelte";
|
||||||
|
|
||||||
type Status = "waiting" | "streaming" | "done" | "error";
|
type Status = "waiting" | "streaming" | "done" | "error";
|
||||||
type Phase = "waiting" | "loading" | "reasoning" | "content";
|
type Phase = "waiting" | "loading" | "reasoning" | "content";
|
||||||
@@ -389,22 +392,23 @@
|
|||||||
<div class="text-xs font-medium text-muted-foreground mb-1">
|
<div class="text-xs font-medium text-muted-foreground mb-1">
|
||||||
Models <span class="text-[10px] font-normal">— click to queue (add the same model more than once to test parallel requests)</span>
|
Models <span class="text-[10px] font-normal">— click to queue (add the same model more than once to test parallel requests)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 border border-border rounded overflow-y-auto min-h-0">
|
<div class="flex-1 border border-border rounded-md overflow-y-auto min-h-0">
|
||||||
{#if !hasModels}
|
{#if !hasModels}
|
||||||
<div class="p-3 text-sm text-muted-foreground text-center">No models configured.</div>
|
<div class="p-3 text-sm text-muted-foreground text-center">No models configured.</div>
|
||||||
{:else}
|
{:else}
|
||||||
<ul class="divide-y divide-gray-100 dark:divide-white/5">
|
<ul class="divide-y divide-gray-100 dark:divide-white/5">
|
||||||
{#each availableModels as m (m.id)}
|
{#each availableModels as m (m.id)}
|
||||||
<li>
|
<li>
|
||||||
<button
|
<Button
|
||||||
class="w-full text-left px-2 py-1.5 text-sm hover:bg-accent transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
variant="ghost"
|
||||||
|
class="w-full justify-start px-2 py-1.5 text-sm h-auto font-normal"
|
||||||
onclick={() => addModel(m.id)}
|
onclick={() => addModel(m.id)}
|
||||||
disabled={isRunning}
|
disabled={isRunning}
|
||||||
title="Add {m.id}"
|
title="Add {m.id}"
|
||||||
>
|
>
|
||||||
<span class="text-primary" aria-hidden="true">+</span>
|
<span class="text-primary" aria-hidden="true">+</span>
|
||||||
<span class="truncate flex-1">{m.id}</span>
|
<span class="truncate flex-1">{m.id}</span>
|
||||||
</button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -416,27 +420,29 @@
|
|||||||
<div class="flex flex-col gap-2 border-t border-border pt-3">
|
<div class="flex flex-col gap-2 border-t border-border pt-3">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<label for="concurrency-prompt" class="text-xs font-medium text-muted-foreground">Prompt</label>
|
<label for="concurrency-prompt" class="text-xs font-medium text-muted-foreground">Prompt</label>
|
||||||
<button
|
<Button
|
||||||
class="text-[10px] text-muted-foreground hover:text-foreground underline"
|
variant="link"
|
||||||
|
size="sm"
|
||||||
|
class="h-auto p-0 text-[10px]"
|
||||||
onclick={resetDefaults}
|
onclick={resetDefaults}
|
||||||
disabled={isRunning}
|
disabled={isRunning}
|
||||||
>
|
>
|
||||||
reset defaults
|
reset defaults
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<Textarea
|
||||||
id="concurrency-prompt"
|
id="concurrency-prompt"
|
||||||
class="w-full px-2 py-1.5 text-sm rounded border border-border bg-background focus:outline-none focus:ring-2 focus:ring-primary resize-none"
|
class="resize-none text-sm"
|
||||||
rows="3"
|
rows={3}
|
||||||
bind:value={$promptStore}
|
bind:value={$promptStore}
|
||||||
disabled={isRunning}
|
disabled={isRunning}
|
||||||
></textarea>
|
></Textarea>
|
||||||
<label for="concurrency-max-tokens" class="text-xs font-medium text-muted-foreground">max_tokens</label>
|
<label for="concurrency-max-tokens" class="text-xs font-medium text-muted-foreground">max_tokens</label>
|
||||||
<input
|
<Input
|
||||||
id="concurrency-max-tokens"
|
id="concurrency-max-tokens"
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
class="w-full px-2 py-1.5 text-sm rounded border border-border bg-background focus:outline-none focus:ring-2 focus:ring-primary"
|
class="h-8 text-sm"
|
||||||
bind:value={$maxTokensStore}
|
bind:value={$maxTokensStore}
|
||||||
disabled={isRunning}
|
disabled={isRunning}
|
||||||
/>
|
/>
|
||||||
@@ -463,9 +469,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Gantt-style timeline -->
|
<!-- Gantt-style timeline -->
|
||||||
<div class="mb-3 border border-border rounded">
|
<div class="mb-3 border border-border rounded-md">
|
||||||
<button
|
<button
|
||||||
class="w-full flex items-center gap-2 px-2 py-1.5 text-xs font-medium text-muted-foreground hover:bg-accent transition-colors {$timelineCollapsedStore ? 'rounded' : 'rounded-t border-b border-border'}"
|
class="w-full flex items-center gap-2 px-2 py-1.5 text-xs font-medium text-muted-foreground hover:bg-accent transition-colors {$timelineCollapsedStore ? 'rounded-md' : 'rounded-t border-b border-border'}"
|
||||||
onclick={() => timelineCollapsedStore.update((v) => !v)}
|
onclick={() => timelineCollapsedStore.update((v) => !v)}
|
||||||
aria-expanded={!$timelineCollapsedStore}
|
aria-expanded={!$timelineCollapsedStore}
|
||||||
>
|
>
|
||||||
@@ -574,7 +580,7 @@
|
|||||||
{@const run = runs[entry.id]}
|
{@const run = runs[entry.id]}
|
||||||
{@const status = run?.status ?? "waiting"}
|
{@const status = run?.status ?? "waiting"}
|
||||||
<div
|
<div
|
||||||
class="border rounded flex flex-col min-h-0 transition-colors {dragOverIndex === i && dragIndex !== i
|
class="border rounded-md flex flex-col min-h-0 transition-colors {dragOverIndex === i && dragIndex !== i
|
||||||
? 'border-primary ring-2 ring-primary/40'
|
? 'border-primary ring-2 ring-primary/40'
|
||||||
: 'border-border'} {dragIndex === i ? 'opacity-40' : ''}"
|
: 'border-border'} {dragIndex === i ? 'opacity-40' : ''}"
|
||||||
style="height: 280px;"
|
style="height: 280px;"
|
||||||
@@ -600,19 +606,21 @@
|
|||||||
{run ? formatElapsed(run.elapsedMs) : "—"}
|
{run ? formatElapsed(run.elapsedMs) : "—"}
|
||||||
</span>
|
</span>
|
||||||
<span class="status text-[10px] {statusBadgeClass(status)}">{status}</span>
|
<span class="status text-[10px] {statusBadgeClass(status)}">{status}</span>
|
||||||
<button
|
<Button
|
||||||
class="w-5 h-5 flex items-center justify-center text-muted-foreground hover:text-red-500 transition-colors rounded disabled:opacity-30 disabled:cursor-not-allowed"
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
class="h-5 w-5 text-muted-foreground hover:text-red-500"
|
||||||
onclick={() => removeEntry(entry.id)}
|
onclick={() => removeEntry(entry.id)}
|
||||||
disabled={isRunning}
|
disabled={isRunning}
|
||||||
aria-label="Remove"
|
aria-label="Remove"
|
||||||
tabindex="-1"
|
tabindex={-1}
|
||||||
>
|
>
|
||||||
×
|
<X class="size-3" />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-h-0 overflow-y-auto font-mono text-xs px-2 py-1.5">
|
<div class="flex-1 min-h-0 overflow-y-auto font-mono text-xs px-2 py-1.5">
|
||||||
{#if run?.loadingText}
|
{#if run?.loadingText}
|
||||||
<div class="bg-secondary/40 dark:bg-white/5 text-muted-foreground rounded px-2 py-1 mb-2 whitespace-pre-wrap">{run.loadingText.trim()}</div>
|
<div class="bg-secondary/40 dark:bg-white/5 text-muted-foreground rounded-md px-2 py-1 mb-2 whitespace-pre-wrap">{run.loadingText.trim()}</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if run?.reasoningContent}
|
{#if run?.reasoningContent}
|
||||||
<div class="text-purple-700 dark:text-purple-300 whitespace-pre-wrap">{run.reasoningContent}</div>
|
<div class="text-purple-700 dark:text-purple-300 whitespace-pre-wrap">{run.reasoningContent}</div>
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
import ExpandableTextarea from "./ExpandableTextarea.svelte";
|
import ExpandableTextarea from "./ExpandableTextarea.svelte";
|
||||||
import type { ImageApiMode, SdApiLora, SdApiLoraRef } from "../../lib/types";
|
import type { ImageApiMode, SdApiLora, SdApiLoraRef } from "../../lib/types";
|
||||||
import { Button } from "$lib/components/ui/button/index.js";
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import { Input } from "$lib/components/ui/input/index.js";
|
||||||
|
import { Textarea } from "$lib/components/ui/textarea/index.js";
|
||||||
|
import * as Select from "$lib/components/ui/select/index.js";
|
||||||
|
import { Download, X } from "@lucide/svelte";
|
||||||
|
|
||||||
const selectedModelStore = persistentStore<string>("playground-image-model", "");
|
const selectedModelStore = persistentStore<string>("playground-image-model", "");
|
||||||
const selectedSizeStore = persistentStore<string>("playground-image-size", "1024x1024");
|
const selectedSizeStore = persistentStore<string>("playground-image-size", "1024x1024");
|
||||||
@@ -62,18 +66,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addLora(event: Event) {
|
|
||||||
const select = event.target as HTMLSelectElement;
|
|
||||||
const path = select.value;
|
|
||||||
if (!path) return;
|
|
||||||
|
|
||||||
const lora = availableLoras.find((l) => l.path === path);
|
|
||||||
if (lora && !selectedLoras.some((l) => l.path === path)) {
|
|
||||||
selectedLoras = [...selectedLoras, { path: lora.path, multiplier: 1.0 }];
|
|
||||||
}
|
|
||||||
select.value = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeLora(path: string) {
|
function removeLora(path: string) {
|
||||||
selectedLoras = selectedLoras.filter((l) => l.path !== path);
|
selectedLoras = selectedLoras.filter((l) => l.path !== path);
|
||||||
}
|
}
|
||||||
@@ -196,35 +188,46 @@
|
|||||||
<div class="shrink-0 flex flex-wrap gap-2 mb-4">
|
<div class="shrink-0 flex flex-wrap gap-2 mb-4">
|
||||||
<ModelSelector bind:value={$selectedModelStore} placeholder="Select an image model..." disabled={isGenerating} capabilities={["image_generation", "image_to_image"]} matchAny={true} />
|
<ModelSelector bind:value={$selectedModelStore} placeholder="Select an image model..." disabled={isGenerating} capabilities={["image_generation", "image_to_image"]} matchAny={true} />
|
||||||
|
|
||||||
<select
|
<Select.Root
|
||||||
class="px-3 py-2 rounded border border-border bg-background focus:outline-none focus:ring-2 focus:ring-primary"
|
type="single"
|
||||||
bind:value={$apiModeStore}
|
value={$apiModeStore}
|
||||||
disabled={isGenerating}
|
onValueChange={(v) => v && apiModeStore.set(v as ImageApiMode)}
|
||||||
>
|
>
|
||||||
<option value="openai">OpenAI</option>
|
<Select.Trigger class="h-9 w-32">{$apiModeStore}</Select.Trigger>
|
||||||
<option value="sdapi">SDAPI</option>
|
<Select.Content>
|
||||||
</select>
|
<Select.Item value="openai">OpenAI</Select.Item>
|
||||||
|
<Select.Item value="sdapi">SDAPI</Select.Item>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
|
||||||
<select
|
<Select.Root
|
||||||
class="px-3 py-2 rounded border border-border bg-background focus:outline-none focus:ring-2 focus:ring-primary"
|
type="single"
|
||||||
bind:value={$selectedSizeStore}
|
value={$selectedSizeStore}
|
||||||
disabled={isGenerating}
|
onValueChange={(v) => v && selectedSizeStore.set(v)}
|
||||||
>
|
>
|
||||||
<optgroup label="Square">
|
<Select.Trigger class="h-9 w-40">{$selectedSizeStore}</Select.Trigger>
|
||||||
<option value="512x512">512x512</option>
|
<Select.Content>
|
||||||
<option value="1024x1024">1024x1024</option>
|
<Select.Group>
|
||||||
</optgroup>
|
<Select.Label>Square</Select.Label>
|
||||||
<optgroup label="Landscape">
|
<Select.Item value="512x512">512x512</Select.Item>
|
||||||
<option value="1024x768">1024x768 (4:3)</option>
|
<Select.Item value="1024x1024">1024x1024</Select.Item>
|
||||||
<option value="1280x720">1280x720 (16:9)</option>
|
</Select.Group>
|
||||||
<option value="1792x1024">1792x1024 (SDXL)</option>
|
<Select.Separator />
|
||||||
</optgroup>
|
<Select.Group>
|
||||||
<optgroup label="Portrait">
|
<Select.Label>Landscape</Select.Label>
|
||||||
<option value="768x1024">768x1024 (3:4)</option>
|
<Select.Item value="1024x768">1024x768 (4:3)</Select.Item>
|
||||||
<option value="720x1280">720x1280 (9:16)</option>
|
<Select.Item value="1280x720">1280x720 (16:9)</Select.Item>
|
||||||
<option value="1024x1792">1024x1792 (SDXL)</option>
|
<Select.Item value="1792x1024">1792x1024 (SDXL)</Select.Item>
|
||||||
</optgroup>
|
</Select.Group>
|
||||||
</select>
|
<Select.Separator />
|
||||||
|
<Select.Group>
|
||||||
|
<Select.Label>Portrait</Select.Label>
|
||||||
|
<Select.Item value="768x1024">768x1024 (3:4)</Select.Item>
|
||||||
|
<Select.Item value="720x1280">720x1280 (9:16)</Select.Item>
|
||||||
|
<Select.Item value="1024x1792">1024x1792 (SDXL)</Select.Item>
|
||||||
|
</Select.Group>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
|
||||||
{#if isSdapi}
|
{#if isSdapi}
|
||||||
<Button variant="outline" onclick={() => showSettings = !showSettings}>
|
<Button variant="outline" onclick={() => showSettings = !showSettings}>
|
||||||
@@ -235,13 +238,13 @@
|
|||||||
|
|
||||||
<!-- SDAPI Settings Panel -->
|
<!-- SDAPI Settings Panel -->
|
||||||
{#if isSdapi && showSettings}
|
{#if isSdapi && showSettings}
|
||||||
<div class="shrink-0 mb-4 p-4 rounded border border-border bg-background">
|
<div class="shrink-0 mb-4 p-4 rounded-md border border-border bg-background">
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-3">
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-3">
|
||||||
<label class="flex flex-col gap-1">
|
<label class="flex flex-col gap-1">
|
||||||
<span class="text-xs text-muted-foreground">Steps</span>
|
<span class="text-xs text-muted-foreground">Steps</span>
|
||||||
<input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
class="px-2 py-1 rounded border border-border bg-background focus:outline-none focus:ring-2 focus:ring-primary"
|
class="h-8"
|
||||||
bind:value={$sdStepsStore}
|
bind:value={$sdStepsStore}
|
||||||
min="1"
|
min="1"
|
||||||
max="150"
|
max="150"
|
||||||
@@ -249,9 +252,9 @@
|
|||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1">
|
<label class="flex flex-col gap-1">
|
||||||
<span class="text-xs text-muted-foreground">CFG Scale</span>
|
<span class="text-xs text-muted-foreground">CFG Scale</span>
|
||||||
<input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
class="px-2 py-1 rounded border border-border bg-background focus:outline-none focus:ring-2 focus:ring-primary"
|
class="h-8"
|
||||||
bind:value={$sdCfgScaleStore}
|
bind:value={$sdCfgScaleStore}
|
||||||
min="1"
|
min="1"
|
||||||
max="30"
|
max="30"
|
||||||
@@ -260,18 +263,18 @@
|
|||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1">
|
<label class="flex flex-col gap-1">
|
||||||
<span class="text-xs text-muted-foreground">Seed (-1 = random)</span>
|
<span class="text-xs text-muted-foreground">Seed (-1 = random)</span>
|
||||||
<input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
class="px-2 py-1 rounded border border-border bg-background focus:outline-none focus:ring-2 focus:ring-primary"
|
class="h-8"
|
||||||
bind:value={$sdSeedStore}
|
bind:value={$sdSeedStore}
|
||||||
min="-1"
|
min="-1"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1">
|
<label class="flex flex-col gap-1">
|
||||||
<span class="text-xs text-muted-foreground">Batch Size</span>
|
<span class="text-xs text-muted-foreground">Batch Size</span>
|
||||||
<input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
class="px-2 py-1 rounded border border-border bg-background focus:outline-none focus:ring-2 focus:ring-primary"
|
class="h-8"
|
||||||
bind:value={$sdBatchSizeStore}
|
bind:value={$sdBatchSizeStore}
|
||||||
min="1"
|
min="1"
|
||||||
max="8"
|
max="8"
|
||||||
@@ -279,49 +282,56 @@
|
|||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1">
|
<label class="flex flex-col gap-1">
|
||||||
<span class="text-xs text-muted-foreground">Sampler</span>
|
<span class="text-xs text-muted-foreground">Sampler</span>
|
||||||
<select
|
<Select.Root
|
||||||
class="px-2 py-1 rounded border border-border bg-background focus:outline-none focus:ring-2 focus:ring-primary"
|
type="single"
|
||||||
bind:value={$sdSamplerStore}
|
value={$sdSamplerStore}
|
||||||
|
onValueChange={(v) => sdSamplerStore.set(v ?? "")}
|
||||||
>
|
>
|
||||||
<option value="">Default</option>
|
<Select.Trigger class="h-8">{$sdSamplerStore || "Default"}</Select.Trigger>
|
||||||
<option value="euler_a">euler_a</option>
|
<Select.Content>
|
||||||
<option value="euler">euler</option>
|
<Select.Item value="">Default</Select.Item>
|
||||||
<option value="heun">heun</option>
|
<Select.Item value="euler_a">euler_a</Select.Item>
|
||||||
<option value="dpm2">dpm2</option>
|
<Select.Item value="euler">euler</Select.Item>
|
||||||
<option value="dpmpp2s_a">dpmpp2s_a</option>
|
<Select.Item value="heun">heun</Select.Item>
|
||||||
<option value="dpmpp2m">dpmpp2m</option>
|
<Select.Item value="dpm2">dpm2</Select.Item>
|
||||||
<option value="dpmpp2mv2">dpmpp2mv2</option>
|
<Select.Item value="dpmpp2s_a">dpmpp2s_a</Select.Item>
|
||||||
<option value="ipndm">ipndm</option>
|
<Select.Item value="dpmpp2m">dpmpp2m</Select.Item>
|
||||||
<option value="ipndm_v">ipndm_v</option>
|
<Select.Item value="dpmpp2mv2">dpmpp2mv2</Select.Item>
|
||||||
<option value="lcm">lcm</option>
|
<Select.Item value="ipndm">ipndm</Select.Item>
|
||||||
<option value="ddim_trailing">ddim_trailing</option>
|
<Select.Item value="ipndm_v">ipndm_v</Select.Item>
|
||||||
<option value="tcd">tcd</option>
|
<Select.Item value="lcm">lcm</Select.Item>
|
||||||
</select>
|
<Select.Item value="ddim_trailing">ddim_trailing</Select.Item>
|
||||||
|
<Select.Item value="tcd">tcd</Select.Item>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1">
|
<label class="flex flex-col gap-1">
|
||||||
<span class="text-xs text-muted-foreground">Scheduler</span>
|
<span class="text-xs text-muted-foreground">Scheduler</span>
|
||||||
<select
|
<Select.Root
|
||||||
class="px-2 py-1 rounded border border-border bg-background focus:outline-none focus:ring-2 focus:ring-primary"
|
type="single"
|
||||||
bind:value={$sdSchedulerStore}
|
value={$sdSchedulerStore}
|
||||||
|
onValueChange={(v) => sdSchedulerStore.set(v ?? "")}
|
||||||
>
|
>
|
||||||
<option value="">Auto for model</option>
|
<Select.Trigger class="h-8">{$sdSchedulerStore || "Auto for model"}</Select.Trigger>
|
||||||
<option value="discrete">discrete</option>
|
<Select.Content>
|
||||||
<option value="karras">karras</option>
|
<Select.Item value="">Auto for model</Select.Item>
|
||||||
<option value="exponential">exponential</option>
|
<Select.Item value="discrete">discrete</Select.Item>
|
||||||
<option value="ays">ays</option>
|
<Select.Item value="karras">karras</Select.Item>
|
||||||
<option value="gits">gits</option>
|
<Select.Item value="exponential">exponential</Select.Item>
|
||||||
</select>
|
<Select.Item value="ays">ays</Select.Item>
|
||||||
|
<Select.Item value="gits">gits</Select.Item>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="flex flex-col gap-1 mb-3">
|
<label class="flex flex-col gap-1 mb-3">
|
||||||
<span class="text-xs text-muted-foreground">Negative Prompt</span>
|
<span class="text-xs text-muted-foreground">Negative Prompt</span>
|
||||||
<textarea
|
<Textarea
|
||||||
class="px-2 py-1 rounded border border-border bg-background focus:outline-none focus:ring-2 focus:ring-primary resize-y text-sm"
|
|
||||||
bind:value={$sdNegativePromptStore}
|
bind:value={$sdNegativePromptStore}
|
||||||
rows="2"
|
rows={2}
|
||||||
placeholder="Elements to avoid..."
|
placeholder="Elements to avoid..."
|
||||||
></textarea>
|
></Textarea>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<!-- LoRA Selection -->
|
<!-- LoRA Selection -->
|
||||||
@@ -337,15 +347,25 @@
|
|||||||
{isLoadingLoras ? "Loading..." : lorasLoaded ? "Reload LoRAs" : "Load LoRAs"}
|
{isLoadingLoras ? "Loading..." : lorasLoaded ? "Reload LoRAs" : "Load LoRAs"}
|
||||||
</Button>
|
</Button>
|
||||||
{#if lorasLoaded && availableLoras.length > 0}
|
{#if lorasLoaded && availableLoras.length > 0}
|
||||||
<select
|
<Select.Root
|
||||||
class="flex-1 px-2 py-1.5 text-sm rounded border border-border bg-background focus:outline-none focus:ring-2 focus:ring-primary"
|
type="single"
|
||||||
onchange={addLora}
|
value=""
|
||||||
|
onValueChange={(v) => {
|
||||||
|
if (v) {
|
||||||
|
const lora = availableLoras.find((l) => l.path === v);
|
||||||
|
if (lora && !selectedLoras.some((s) => s.path === v)) {
|
||||||
|
selectedLoras = [...selectedLoras, { path: lora.path, multiplier: 1.0 }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<option value="">Add a LoRA...</option>
|
<Select.Trigger class="h-8 flex-1">Add a LoRA...</Select.Trigger>
|
||||||
{#each availableLoras.filter((l) => !selectedLoras.some((s) => s.path === l.path)) as lora}
|
<Select.Content>
|
||||||
<option value={lora.path}>{lora.name}</option>
|
{#each availableLoras.filter((l) => !selectedLoras.some((s) => s.path === l.path)) as lora (lora.path)}
|
||||||
{/each}
|
<Select.Item value={lora.path}>{lora.name}</Select.Item>
|
||||||
</select>
|
{/each}
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if loraError}
|
{#if loraError}
|
||||||
@@ -359,22 +379,24 @@
|
|||||||
{#each selectedLoras as lora}
|
{#each selectedLoras as lora}
|
||||||
<div class="flex items-center gap-2 text-sm">
|
<div class="flex items-center gap-2 text-sm">
|
||||||
<span class="flex-1 truncate">{getLoraName(lora.path)}</span>
|
<span class="flex-1 truncate">{getLoraName(lora.path)}</span>
|
||||||
<input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
class="w-20 px-1.5 py-1 text-xs rounded border border-border bg-background focus:outline-none focus:ring-1 focus:ring-primary"
|
class="h-7 w-20 text-xs"
|
||||||
value={lora.multiplier}
|
value={lora.multiplier}
|
||||||
oninput={(e) => updateLoraMultiplier(lora.path, parseFloat((e.target as HTMLInputElement).value) || 1)}
|
oninput={(e) => updateLoraMultiplier(lora.path, parseFloat((e.target as HTMLInputElement).value) || 1)}
|
||||||
min="0"
|
min="0"
|
||||||
max="2"
|
max="2"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
class="px-1.5 py-0.5 text-xs rounded border border-border hover:bg-red-500 hover:text-white hover:border-red-500 transition-colors"
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="h-7 px-1.5 text-xs hover:bg-destructive hover:text-destructive-foreground"
|
||||||
onclick={() => removeLora(lora.path)}
|
onclick={() => removeLora(lora.path)}
|
||||||
aria-label="Remove LoRA"
|
aria-label="Remove LoRA"
|
||||||
>
|
>
|
||||||
x
|
<X class="size-3" />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -390,7 +412,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Image display area -->
|
<!-- Image display area -->
|
||||||
<div class="flex-1 overflow-auto mb-4 flex items-center justify-center bg-background border border-border rounded">
|
<div class="flex-1 overflow-auto mb-4 flex items-center justify-center bg-background border border-border rounded-md">
|
||||||
{#if isGenerating}
|
{#if isGenerating}
|
||||||
<div class="text-center text-muted-foreground">
|
<div class="text-center text-muted-foreground">
|
||||||
<div class="inline-block w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin mb-2"></div>
|
<div class="inline-block w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin mb-2"></div>
|
||||||
@@ -402,7 +424,6 @@
|
|||||||
<p class="text-sm mt-1">{error}</p>
|
<p class="text-sm mt-1">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
{:else if generatedImages.length > 1}
|
{:else if generatedImages.length > 1}
|
||||||
<!-- Grid for multiple images (batch) -->
|
|
||||||
<div class="grid grid-cols-2 gap-2 p-2 w-full h-full overflow-auto">
|
<div class="grid grid-cols-2 gap-2 p-2 w-full h-full overflow-auto">
|
||||||
{#each generatedImages as img, i}
|
{#each generatedImages as img, i}
|
||||||
<div class="relative flex items-center justify-center">
|
<div class="relative flex items-center justify-center">
|
||||||
@@ -417,15 +438,15 @@
|
|||||||
class="max-w-full max-h-full object-contain hover:opacity-90 transition-opacity"
|
class="max-w-full max-h-full object-contain hover:opacity-90 transition-opacity"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<Button
|
||||||
class="absolute bottom-2 right-2 p-1.5 bg-black/60 hover:bg-black/80 text-white rounded-full transition-colors"
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
class="absolute bottom-2 right-2 h-8 w-8 bg-black/60 hover:bg-black/80 text-white"
|
||||||
onclick={(e) => { e.stopPropagation(); downloadImage(i); }}
|
onclick={(e) => { e.stopPropagation(); downloadImage(i); }}
|
||||||
aria-label="Download image"
|
aria-label="Download image"
|
||||||
>
|
>
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<Download class="size-4" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
</Button>
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -442,15 +463,15 @@
|
|||||||
class="max-w-full max-h-full object-contain hover:opacity-90 transition-opacity"
|
class="max-w-full max-h-full object-contain hover:opacity-90 transition-opacity"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<Button
|
||||||
class="absolute bottom-2 right-2 p-2 bg-black/60 hover:bg-black/80 text-white rounded-full transition-colors"
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
class="absolute bottom-2 right-2 bg-black/60 hover:bg-black/80 text-white"
|
||||||
onclick={(e) => { e.stopPropagation(); downloadImage(0); }}
|
onclick={(e) => { e.stopPropagation(); downloadImage(0); }}
|
||||||
aria-label="Download image"
|
aria-label="Download image"
|
||||||
>
|
>
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<Download class="size-5" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
</Button>
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="text-center text-muted-foreground">
|
<div class="text-center text-muted-foreground">
|
||||||
@@ -505,13 +526,15 @@
|
|||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<button
|
<Button
|
||||||
class="absolute top-4 right-4 text-white hover:text-gray-300 text-2xl w-10 h-10 flex items-center justify-center rounded-full hover:bg-white/10 transition-colors"
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
class="absolute top-4 right-4 bg-black/60 hover:bg-black/80 text-white"
|
||||||
onclick={() => closeFullscreen()}
|
onclick={() => closeFullscreen()}
|
||||||
aria-label="Close fullscreen"
|
aria-label="Close fullscreen"
|
||||||
>
|
>
|
||||||
×
|
<X class="size-6" />
|
||||||
</button>
|
</Button>
|
||||||
<img
|
<img
|
||||||
src={generatedImages[fullscreenIndex]}
|
src={generatedImages[fullscreenIndex]}
|
||||||
alt="AI generated content"
|
alt="AI generated content"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { models } from "../../stores/api";
|
import { models } from "../../stores/api";
|
||||||
import { groupModels } from "../../lib/modelUtils";
|
import { groupModels } from "../../lib/modelUtils";
|
||||||
|
import * as Select from "$lib/components/ui/select/index.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -18,42 +19,51 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if hasModels}
|
{#if hasModels}
|
||||||
<select
|
<Select.Root
|
||||||
class="border-input bg-background focus-visible:border-ring focus-visible:ring-ring/50 dark:bg-input/30 min-w-0 flex-1 basis-48 rounded-md border px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
|
type="single"
|
||||||
bind:value
|
{value}
|
||||||
|
onValueChange={(v) => v !== undefined && (value = v)}
|
||||||
{disabled}
|
{disabled}
|
||||||
>
|
>
|
||||||
<option value="">{placeholder}</option>
|
<Select.Trigger class="min-w-0 flex-1 basis-48">{value || placeholder}</Select.Trigger>
|
||||||
{#if hasMatching}
|
<Select.Content>
|
||||||
<optgroup label="Matching Capabilities">
|
<Select.Item value="">{placeholder}</Select.Item>
|
||||||
{#each grouped.localMatching as model (model.id)}
|
{#if hasMatching}
|
||||||
<option value={model.id}>{model.id}</option>
|
<Select.Group>
|
||||||
{#if model.aliases}
|
<Select.Label>Matching Capabilities</Select.Label>
|
||||||
{#each model.aliases as alias (alias)}
|
{#each grouped.localMatching as model (model.id)}
|
||||||
<option value={alias}> ↳ {alias}</option>
|
<Select.Item value={model.id}>{model.id}</Select.Item>
|
||||||
{/each}
|
{#if model.aliases}
|
||||||
{/if}
|
{#each model.aliases as alias (alias)}
|
||||||
{/each}
|
<Select.Item value={alias}>↳ {alias}</Select.Item>
|
||||||
</optgroup>
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
{#if grouped.local.length > 0}
|
{/each}
|
||||||
<optgroup label="Local">
|
</Select.Group>
|
||||||
{#each grouped.local as model (model.id)}
|
<Select.Separator />
|
||||||
<option value={model.id}>{model.id}</option>
|
{/if}
|
||||||
{#if model.aliases}
|
{#if grouped.local.length > 0}
|
||||||
{#each model.aliases as alias (alias)}
|
<Select.Group>
|
||||||
<option value={alias}> ↳ {alias}</option>
|
<Select.Label>Local</Select.Label>
|
||||||
{/each}
|
{#each grouped.local as model (model.id)}
|
||||||
{/if}
|
<Select.Item value={model.id}>{model.id}</Select.Item>
|
||||||
{/each}
|
{#if model.aliases}
|
||||||
</optgroup>
|
{#each model.aliases as alias (alias)}
|
||||||
{/if}
|
<Select.Item value={alias}>↳ {alias}</Select.Item>
|
||||||
{#each Object.entries(grouped.peersByProvider).sort(([a], [b]) => a.localeCompare(b)) as [peerId, peerModels] (peerId)}
|
{/each}
|
||||||
<optgroup label="Peer: {peerId}">
|
{/if}
|
||||||
{#each peerModels as model (model.id)}
|
{/each}
|
||||||
<option value={model.id}>{model.id}</option>
|
</Select.Group>
|
||||||
{/each}
|
<Select.Separator />
|
||||||
</optgroup>
|
{/if}
|
||||||
{/each}
|
{#each Object.entries(grouped.peersByProvider).sort(([a], [b]) => a.localeCompare(b)) as [peerId, peerModels] (peerId)}
|
||||||
</select>
|
<Select.Group>
|
||||||
|
<Select.Label>Peer: {peerId}</Select.Label>
|
||||||
|
{#each peerModels as model (model.id)}
|
||||||
|
<Select.Item value={model.id}>{model.id}</Select.Item>
|
||||||
|
{/each}
|
||||||
|
</Select.Group>
|
||||||
|
{/each}
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -335,9 +335,9 @@
|
|||||||
{#each displayRows as { row, i } (i)}
|
{#each displayRows as { row, i } (i)}
|
||||||
<tr class="border-b last:border-0">
|
<tr class="border-b last:border-0">
|
||||||
<td class="px-3 py-1.5">
|
<td class="px-3 py-1.5">
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
class="focus:ring-ring w-full rounded bg-transparent px-1 py-0.5 outline-none focus:ring-1"
|
class="border-0 focus-visible:ring-1 h-7 px-1 py-0.5 bg-transparent"
|
||||||
placeholder={i === rows.length - 1 ? "Add document..." : "Document text..."}
|
placeholder={i === rows.length - 1 ? "Add document..." : "Document text..."}
|
||||||
value={row.doc}
|
value={row.doc}
|
||||||
oninput={(e) => updateDoc(i, (e.target as HTMLInputElement).value)}
|
oninput={(e) => updateDoc(i, (e.target as HTMLInputElement).value)}
|
||||||
@@ -353,15 +353,17 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 py-1.5 text-center">
|
<td class="px-2 py-1.5 text-center">
|
||||||
<button
|
<Button
|
||||||
class="text-muted-foreground hover:text-destructive flex h-7 w-7 items-center justify-center rounded transition-colors disabled:cursor-not-allowed disabled:opacity-30"
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
class="h-7 w-7 text-muted-foreground hover:text-destructive"
|
||||||
onclick={() => deleteRow(i)}
|
onclick={() => deleteRow(i)}
|
||||||
disabled={rows.length <= 1}
|
disabled={rows.length <= 1}
|
||||||
tabindex="-1"
|
tabindex={-1}
|
||||||
aria-label="Remove row"
|
aria-label="Remove row"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import ModelSelector from "./ModelSelector.svelte";
|
import ModelSelector from "./ModelSelector.svelte";
|
||||||
import ExpandableTextarea from "./ExpandableTextarea.svelte";
|
import ExpandableTextarea from "./ExpandableTextarea.svelte";
|
||||||
import { Button } from "$lib/components/ui/button/index.js";
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import * as Select from "$lib/components/ui/select/index.js";
|
||||||
import { RefreshCw, Download } from "@lucide/svelte";
|
import { RefreshCw, Download } from "@lucide/svelte";
|
||||||
|
|
||||||
const selectedModelStore = persistentStore<string>("playground-speech-model", "");
|
const selectedModelStore = persistentStore<string>("playground-speech-model", "");
|
||||||
@@ -108,8 +109,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleVoiceChange(event: Event) {
|
function handleVoiceChange(value: string) {
|
||||||
const value = (event.target as HTMLSelectElement).value;
|
|
||||||
if (value === "(refresh)") {
|
if (value === "(refresh)") {
|
||||||
refreshVoices();
|
refreshVoices();
|
||||||
} else {
|
} else {
|
||||||
@@ -210,17 +210,19 @@
|
|||||||
<div class="shrink-0 flex gap-2 mb-4">
|
<div class="shrink-0 flex gap-2 mb-4">
|
||||||
<ModelSelector bind:value={$selectedModelStore} placeholder="Select a speech model..." disabled={isGenerating} capabilities={["audio_speech"]} />
|
<ModelSelector bind:value={$selectedModelStore} placeholder="Select a speech model..." disabled={isGenerating} capabilities={["audio_speech"]} />
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<select
|
<Select.Root
|
||||||
class="shrink-0 px-3 py-2 rounded border border-border bg-background focus:outline-none focus:ring-2 focus:ring-primary"
|
type="single"
|
||||||
value={$selectedVoiceStore}
|
value={$selectedVoiceStore}
|
||||||
onchange={handleVoiceChange}
|
onValueChange={(v) => v && handleVoiceChange(v)}
|
||||||
disabled={isGenerating || isLoadingVoices || !$selectedModelStore}
|
|
||||||
>
|
>
|
||||||
{#each availableVoices as voice (voice)}
|
<Select.Trigger class="h-9 w-40">{$selectedVoiceStore}</Select.Trigger>
|
||||||
<option value={voice}>{voice}</option>
|
<Select.Content>
|
||||||
{/each}
|
{#each availableVoices as voice (voice)}
|
||||||
<option value="(refresh)">(refresh)</option>
|
<Select.Item value={voice}>{voice}</Select.Item>
|
||||||
</select>
|
{/each}
|
||||||
|
<Select.Item value="(refresh)">(refresh)</Select.Item>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
{#if $selectedModelStore && !getVoicesCache()[$selectedModelStore]}
|
{#if $selectedModelStore && !getVoicesCache()[$selectedModelStore]}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -243,7 +245,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Audio display area -->
|
<!-- Audio display area -->
|
||||||
<div class="shrink-0 mb-4 bg-background border border-border rounded p-4 md:p-6">
|
<div class="shrink-0 mb-4 bg-background border border-border rounded-md p-4 md:p-6">
|
||||||
{#if isGenerating}
|
{#if isGenerating}
|
||||||
<div class="flex items-center justify-center text-muted-foreground py-8">
|
<div class="flex items-center justify-center text-muted-foreground py-8">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--radius: 0.625rem;
|
--radius: 0;
|
||||||
|
|
||||||
/* shadcn base palette (zinc) */
|
/* shadcn base palette (zinc) */
|
||||||
--background: oklch(1 0 0);
|
--background: oklch(1 0 0);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { type VariantProps, tv } from "tailwind-variants";
|
import { type VariantProps, tv } from "tailwind-variants";
|
||||||
|
|
||||||
export const badgeVariants = tv({
|
export const badgeVariants = tv({
|
||||||
base: "h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap transition-colors focus-visible:ring-[3px] [&>svg]:pointer-events-none",
|
base: "h-5 gap-1 rounded-none border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap transition-colors focus-visible:ring-[3px] [&>svg]:pointer-events-none",
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||||
|
|||||||
@@ -271,7 +271,7 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={isColumnVisible(key)}
|
checked={isColumnVisible(key)}
|
||||||
onchange={() => toggleColumn(key)}
|
onchange={() => toggleColumn(key)}
|
||||||
class="accent-primary rounded"
|
class="accent-primary rounded-none"
|
||||||
/>
|
/>
|
||||||
{col.label}
|
{col.label}
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -303,7 +303,7 @@
|
|||||||
<a href="/" class="text-primary hover:underline">Back to Playground</a>
|
<a href="/" class="text-primary hover:underline">Back to Playground</a>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
{:else}
|
{:else}
|
||||||
<Card.Root class="shrink-0 gap-0 overflow-hidden rounded-none py-0">
|
<Card.Root class="shrink-0 gap-0 overflow-hidden py-0">
|
||||||
<Card.Header class="shrink-0 gap-2 border-b px-4 py-3">
|
<Card.Header class="shrink-0 gap-2 border-b px-4 py-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class={`size-2.5 shrink-0 rounded-full ${statusDotColor(model)}`}></span>
|
<span class={`size-2.5 shrink-0 rounded-full ${statusDotColor(model)}`}></span>
|
||||||
@@ -352,9 +352,9 @@
|
|||||||
|
|
||||||
<Tabs.Root value="activity" class="min-h-0 flex-1">
|
<Tabs.Root value="activity" class="min-h-0 flex-1">
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
<Tabs.Trigger value="activity">Activity</Tabs.Trigger>
|
<Tabs.Trigger value="activity" class="data-active:bg-primary/15 data-active:text-primary border border-b-2 data-active:border-primary rounded-none shadow-none">Activity</Tabs.Trigger>
|
||||||
<Tabs.Trigger value="logs">Logs</Tabs.Trigger>
|
<Tabs.Trigger value="logs" class="data-active:bg-primary/15 data-active:text-primary border border-b-2 data-active:border-primary rounded-none shadow-none">Logs</Tabs.Trigger>
|
||||||
<Tabs.Trigger value="details">Details</Tabs.Trigger>
|
<Tabs.Trigger value="details" class="data-active:bg-primary/15 data-active:text-primary border border-b-2 data-active:border-primary rounded-none shadow-none">Details</Tabs.Trigger>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
|
|
||||||
<!-- Activity -->
|
<!-- Activity -->
|
||||||
@@ -427,7 +427,7 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={isColumnVisible(key)}
|
checked={isColumnVisible(key)}
|
||||||
onchange={() => toggleColumn(key)}
|
onchange={() => toggleColumn(key)}
|
||||||
class="accent-primary rounded"
|
class="accent-primary rounded-none"
|
||||||
/>
|
/>
|
||||||
{col.label}
|
{col.label}
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { persistentStore } from "../stores/persistent";
|
import { persistentStore } from "../stores/persistent";
|
||||||
import type { SysStat, GpuStat } from "../lib/types";
|
import type { SysStat, GpuStat } from "../lib/types";
|
||||||
import PerformanceChart from "../components/PerformanceChart.svelte";
|
import PerformanceChart from "../components/PerformanceChart.svelte";
|
||||||
|
import * as Card from "$lib/components/ui/card/index.js";
|
||||||
import { Button } from "$lib/components/ui/button/index.js";
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
import { RefreshCw } from "@lucide/svelte";
|
import { RefreshCw } from "@lucide/svelte";
|
||||||
|
|
||||||
@@ -395,7 +396,11 @@
|
|||||||
<section class="space-y-4">
|
<section class="space-y-4">
|
||||||
<h3 class="text-lg font-medium text-foreground">GPU</h3>
|
<h3 class="text-lg font-medium text-foreground">GPU</h3>
|
||||||
{#if !hasGpuData}
|
{#if !hasGpuData}
|
||||||
<p class="text-muted-foreground bg-card rounded-xl border p-4 shadow-sm">No GPU data available</p>
|
<Card.Root class="py-0">
|
||||||
|
<Card.Content class="p-4">
|
||||||
|
<p class="text-muted-foreground">No GPU data available</p>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
<PerformanceChart
|
<PerformanceChart
|
||||||
|
|||||||
Reference in New Issue
Block a user