4384315b44
Trying out svelte for the UI. The port was done by Claude Code on the iOS app w/ Opus 4.5. --- * ui: add Svelte port of React UI Port the React-based UI to Svelte 5 with the following changes: - Create new ui-svelte directory with complete Svelte 5 implementation - Use Svelte stores instead of React contexts for state management - Implement custom ResizablePanels component to replace react-resizable-panels - Port all pages: LogViewer, Models, Activity - Port all components: Header, ConnectionStatus, LogPanel, ModelsPanel, etc. - Use svelte-spa-router for client-side routing - Same build output directory (proxy/ui_dist) and base path (/ui/) - Tailwind CSS 4 with same theme configuration https://claude.ai/code/session_01F3xXLYsd62gePVSFv7aboP * ui-svelte: simplify state management - Remove redundant state syncing pattern in LogPanel and ModelsPanel - Use store values directly with $ syntax instead of manual subscriptions - Consolidate duplicate title sync logic in App.svelte - Use existing syncTitleToDocument() from theme.ts https://claude.ai/code/session_01F3xXLYsd62gePVSFv7aboP * ui-svelte: use idiomatic Svelte 5 patterns - Use $effect for document side effects (theme, title) instead of store subscriptions - Use class: directive for active nav links in Header - Remove SSR guards (unnecessary for client-only SPA) - Remove leaked subscription in syncThemeToDocument - Simplify theme.ts by removing sync functions https://claude.ai/code/session_01F3xXLYsd62gePVSFv7aboP * ui-svelte: fix build warnings and improve accessibility Fix Svelte build warnings and add proper accessibility support to interactive components. - add aria-labels to buttons for screen readers - implement keyboard navigation for resizable separator - suppress intentional state initialization warnings - update Makefile to use ui-svelte build directory - add peer:true to package-lock.json dependencies * ui-svelte: reorganize navigation and add log view toggle Make Models the default landing page and add view mode toggle to the Logs page with persistent state. - set Models as default route at / - move Logs to /logs route - reorder navigation: Models, Activity, Logs - add view toggle with three modes: Panels, Proxy only, Upstream only - fix horizontal overflow with width constraints
133 lines
5.8 KiB
Svelte
133 lines
5.8 KiB
Svelte
<script lang="ts">
|
|
import { persistentStore } from "../stores/persistent";
|
|
|
|
interface Props {
|
|
id: string;
|
|
title: string;
|
|
logData: string;
|
|
}
|
|
|
|
let { id, title, logData }: Props = $props();
|
|
|
|
let filterRegex = $state("");
|
|
|
|
// Create persistent stores for this panel (id is intentionally captured at init time)
|
|
// svelte-ignore state_referenced_locally
|
|
const fontSizeStore = persistentStore<"xxs" | "xs" | "small" | "normal">(`logPanel-${id}-fontSize`, "normal");
|
|
// svelte-ignore state_referenced_locally
|
|
const wrapTextStore = persistentStore<boolean>(`logPanel-${id}-wrapText`, false);
|
|
// svelte-ignore state_referenced_locally
|
|
const showFilterStore = persistentStore<boolean>(`logPanel-${id}-showFilter`, false);
|
|
|
|
let textWrapClass = $derived($wrapTextStore ? "whitespace-pre-wrap" : "whitespace-pre");
|
|
|
|
function toggleFontSize(): void {
|
|
fontSizeStore.update((prev) => {
|
|
switch (prev) {
|
|
case "xxs": return "xs";
|
|
case "xs": return "small";
|
|
case "small": return "normal";
|
|
case "normal": return "xxs";
|
|
}
|
|
});
|
|
}
|
|
|
|
function toggleWrapText(): void {
|
|
wrapTextStore.update((prev) => !prev);
|
|
}
|
|
|
|
function toggleFilter(): void {
|
|
if ($showFilterStore) {
|
|
showFilterStore.set(false);
|
|
filterRegex = "";
|
|
} else {
|
|
showFilterStore.set(true);
|
|
}
|
|
}
|
|
|
|
let fontSizeClass = $derived.by(() => {
|
|
switch ($fontSizeStore) {
|
|
case "xxs": return "text-[0.5rem]";
|
|
case "xs": return "text-[0.75rem]";
|
|
case "small": return "text-[0.875rem]";
|
|
case "normal": return "text-base";
|
|
}
|
|
});
|
|
|
|
let filteredLogs = $derived.by(() => {
|
|
if (!filterRegex) return logData;
|
|
try {
|
|
const regex = new RegExp(filterRegex, "i");
|
|
return logData.split("\n").filter((line) => regex.test(line)).join("\n");
|
|
} catch {
|
|
return logData;
|
|
}
|
|
});
|
|
|
|
let preElement: HTMLPreElement;
|
|
|
|
// Auto scroll to bottom when logs change
|
|
$effect(() => {
|
|
if (preElement && filteredLogs) {
|
|
preElement.scrollTop = preElement.scrollHeight;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<div class="rounded-lg overflow-hidden flex flex-col bg-gray-950/5 dark:bg-white/10 h-full w-full p-1">
|
|
<div class="p-4">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="m-0 text-lg p-0">{title}</h3>
|
|
|
|
<div class="flex gap-2 items-center">
|
|
<button class="btn border-0" onclick={toggleFontSize} title="Change font size">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">
|
|
<path fill-rule="evenodd" d="M10.5 3.75a6 6 0 0 0-5.98 6.496A5.25 5.25 0 0 0 6.75 20.25H18a4.5 4.5 0 0 0 2.206-8.423 3.75 3.75 0 0 0-4.133-4.303A6.001 6.001 0 0 0 10.5 3.75Zm2.25 6a.75.75 0 0 0-1.5 0v4.94l-1.72-1.72a.75.75 0 0 0-1.06 1.06l3 3a.75.75 0 0 0 1.06 0l3-3a.75.75 0 1 0-1.06-1.06l-1.72 1.72V9.75Z" clip-rule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
<button class="btn border-0" onclick={toggleWrapText} title="Toggle text wrap">
|
|
{#if $wrapTextStore}
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">
|
|
<path fill-rule="evenodd" d="M3 6.75A.75.75 0 0 1 3.75 6h16.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 6.75ZM3 12a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 12Zm0 5.25a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75a.75.75 0 0 1-.75-.75Z" clip-rule="evenodd" />
|
|
</svg>
|
|
{:else}
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">
|
|
<path fill-rule="evenodd" d="M3 6.75A.75.75 0 0 1 3.75 6h16.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 6.75ZM3 12a.75.75 0 0 1 .75-.75h10.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 12Zm0 5.25a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75a.75.75 0 0 1-.75-.75Z" clip-rule="evenodd" />
|
|
</svg>
|
|
{/if}
|
|
</button>
|
|
<button class="btn border-0" onclick={toggleFilter} title="Toggle filter">
|
|
{#if $showFilterStore}
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">
|
|
<path fill-rule="evenodd" d="M10.5 3.75a6.75 6.75 0 1 0 0 13.5 6.75 6.75 0 0 0 0-13.5ZM2.25 10.5a8.25 8.25 0 1 1 14.59 5.28l4.69 4.69a.75.75 0 1 1-1.06 1.06l-4.69-4.69A8.25 8.25 0 0 1 2.25 10.5Z" clip-rule="evenodd" />
|
|
</svg>
|
|
{:else}
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
|
</svg>
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{#if $showFilterStore}
|
|
<div class="mt-2 flex gap-2 items-center w-full">
|
|
<input
|
|
type="text"
|
|
class="w-full text-sm border border-gray-950/10 dark:border-white/5 p-2 rounded outline-none"
|
|
placeholder="Filter logs (regex)..."
|
|
bind:value={filterRegex}
|
|
/>
|
|
<button class="pl-2" onclick={() => (filterRegex = "")} aria-label="Clear filter">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
|
|
<path fill-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm-1.72 6.97a.75.75 0 1 0-1.06 1.06L10.94 12l-1.72 1.72a.75.75 0 1 0 1.06 1.06L12 13.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L13.06 12l1.72-1.72a.75.75 0 1 0-1.06-1.06L12 10.94l-1.72-1.72Z" clip-rule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
<div class="rounded-lg bg-background font-mono text-sm flex-1 overflow-hidden">
|
|
<pre bind:this={preElement} class="{textWrapClass} {fontSizeClass} h-full overflow-auto p-4">{filteredLogs}</pre>
|
|
</div>
|
|
</div>
|