diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0b175bf --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,48 @@ +## Project Description: + +llama-swap is a light weight, transparent proxy server that provides automatic model swapping to llama.cpp's server. + +## Tech stack + +- golang +- typescript, vite and svelt5 for UI (located in ui/) + +## Workflow Tasks + +- when summarizing changes only include details that require further action +- just say "Done." when there is no further action +- use `gh` to create PRs and load issues +- keep PR descriptions short and focused on changes. + - never include a test plan + +## Testing + +- Follow test naming conventions like `TestProxyManager_`, `TestProcessGroup_`, etc. +- Use `go test -v -run ` to run any new tests you've written. +- Use `make test-dev` after running new tests for a quick over all test run. This runs `go test` and `staticcheck`. Fix any static checking errors. Use this only when changes are made to any code under the `proxy/` directory +- Use `make test-all` before completing work. This includes long running concurrency tests. + +### Commit message example format: + +``` +proxy: add new feature + +Add new feature that implements functionality X and Y. + +- key change 1 +- key change 2 +- key change 3 + +fixes #123 +``` + +## Code Reviews + +- use three levels High, Medium, Low severity +- label each discovered issue with a label like H1, M2, L3 respectively +- High severity are must fix issues (security, race conditions, critical bugs) +- Medium severity are recommended improvements (coding style, missing functionality, inconsistencies) +- Low severity are nice to have changes and nits +- Include a suggestion with each discovered item +- Limit your code review to three items with the highest priority first +- Double check your discovered items and recommended remediations diff --git a/CLAUDE.md b/CLAUDE.md index 7e82a0f..43c994c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,49 +1 @@ -## Project Description: - -llama-swap is a light weight, transparent proxy server that provides automatic model swapping to llama.cpp's server. - -## Tech stack - -- golang -- typescript, vite and react for UI (located in ui/) - -## Workflow Tasks - -- when summarizing changes only include details that require further action -- just say "Done." when there is no further action -- use `gh` to create PRs and load issues -- do include Co-Authored-By or created by when committing changes or creating PRs -- keep PR descriptions short and focused on changes. - - never include a test plan - -## Testing - -- Follow test naming conventions like `TestProxyManager_`, `TestProcessGroup_`, etc. -- Use `go test -v -run ` to run any new tests you've written. -- Use `make test-dev` after running new tests for a quick over all test run. This runs `go test` and `staticcheck`. Fix any static checking errors. Use this only when changes are made to any code under the `proxy/` directory -- Use `make test-all` before completing work. This includes long running concurrency tests. - -### Commit message example format: - -``` -proxy: add new feature - -Add new feature that implements functionality X and Y. - -- key change 1 -- key change 2 -- key change 3 - -fixes #123 -``` - -## Code Reviews - -- use three levels High, Medium, Low severity -- label each discovered issue with a label like H1, M2, L3 respectively -- High severity are must fix issues (security, race conditions, critical bugs) -- Medium severity are recommended improvements (coding style, missing functionality, inconsistencies) -- Low severity are nice to have changes and nits -- Include a suggestion with each discovered item -- Limit your code review to three items with the highest priority first -- Double check your discovered items and recommended remediations +@AGENTS.md diff --git a/ui-svelte/src/App.svelte b/ui-svelte/src/App.svelte index 41ec576..f3ca909 100644 --- a/ui-svelte/src/App.svelte +++ b/ui-svelte/src/App.svelte @@ -6,23 +6,28 @@ import Models from "./routes/Models.svelte"; import Activity from "./routes/Activity.svelte"; import Playground from "./routes/Playground.svelte"; + import PlaygroundStub from "./routes/PlaygroundStub.svelte"; import { enableAPIEvents } from "./stores/api"; import { initScreenWidth, isDarkMode, appTitle, connectionState } from "./stores/theme"; + import { currentRoute } from "./stores/route"; const routes = { - "/": Playground, + "/": PlaygroundStub, "/models": Models, "/logs": LogViewer, "/activity": Activity, - "*": Playground, + "*": PlaygroundStub, }; - // Sync theme to document attribute + function handleRouteLoaded(event: { detail: { route: string | RegExp } }) { + const route = event.detail.route; + currentRoute.set(typeof route === "string" ? route : "/"); + } + $effect(() => { document.documentElement.setAttribute("data-theme", $isDarkMode ? "dark" : "light"); }); - // Sync title to document $effect(() => { const icon = $connectionState === "connecting" ? "\u{1F7E1}" : $connectionState === "connected" ? "\u{1F7E2}" : "\u{1F534}"; document.title = `${icon} ${$appTitle}`; @@ -43,6 +48,11 @@
- +
+ +
+
+ +
diff --git a/ui-svelte/src/components/Header.svelte b/ui-svelte/src/components/Header.svelte index 310cb95..c3cf4a8 100644 --- a/ui-svelte/src/components/Header.svelte +++ b/ui-svelte/src/components/Header.svelte @@ -1,6 +1,8 @@
Playground @@ -56,7 +58,7 @@ href="/models" use:link class="text-gray-600 hover:text-black dark:text-gray-300 dark:hover:text-gray-100 p-1 whitespace-nowrap" - class:font-semibold={isActive("/models", $location)} + class:font-semibold={isActive("/models", $currentRoute)} > Models @@ -64,7 +66,7 @@ href="/activity" use:link class="text-gray-600 hover:text-black dark:text-gray-300 dark:hover:text-gray-100 p-1 whitespace-nowrap" - class:font-semibold={isActive("/activity", $location)} + class:font-semibold={isActive("/activity", $currentRoute)} > Activity @@ -72,7 +74,7 @@ href="/logs" use:link class="text-gray-600 hover:text-black dark:text-gray-300 dark:hover:text-gray-100 p-1 whitespace-nowrap" - class:font-semibold={isActive("/logs", $location)} + class:font-semibold={isActive("/logs", $currentRoute)} > Logs @@ -96,3 +98,23 @@
+ + diff --git a/ui-svelte/src/components/playground/AudioInterface.svelte b/ui-svelte/src/components/playground/AudioInterface.svelte index d22a598..a350f1a 100644 --- a/ui-svelte/src/components/playground/AudioInterface.svelte +++ b/ui-svelte/src/components/playground/AudioInterface.svelte @@ -2,6 +2,7 @@ import { models } from "../../stores/api"; import { persistentStore } from "../../stores/persistent"; import { transcribeAudio } from "../../lib/audioApi"; + import { playgroundStores } from "../../stores/playgroundActivity"; import ModelSelector from "./ModelSelector.svelte"; const selectedModelStore = persistentStore("playground-audio-model", ""); @@ -22,6 +23,10 @@ let canTranscribe = $derived(selectedFile !== null && $selectedModelStore !== "" && !isTranscribing); + $effect(() => { + playgroundStores.audioTranscribing.set(isTranscribing); + }); + function validateFile(file: File): { valid: boolean; error?: string } { const ext = '.' + file.name.split('.').pop()?.toLowerCase(); diff --git a/ui-svelte/src/components/playground/ChatInterface.svelte b/ui-svelte/src/components/playground/ChatInterface.svelte index 752d7e6..270e31a 100644 --- a/ui-svelte/src/components/playground/ChatInterface.svelte +++ b/ui-svelte/src/components/playground/ChatInterface.svelte @@ -2,6 +2,7 @@ import { models } from "../../stores/api"; import { persistentStore } from "../../stores/persistent"; import { streamChatCompletion } from "../../lib/chatApi"; + import { playgroundStores } from "../../stores/playgroundActivity"; import type { ChatMessage, ContentPart } from "../../lib/types"; import ChatMessageComponent from "./ChatMessage.svelte"; import ModelSelector from "./ModelSelector.svelte"; @@ -35,6 +36,10 @@ let hasModels = $derived($models.some((m) => !m.unlisted)); let userScrolledUp = $state(false); + $effect(() => { + playgroundStores.chatStreaming.set(isStreaming); + }); + function handleMessagesScroll() { if (!messagesContainer) return; const { scrollTop, scrollHeight, clientHeight } = messagesContainer; diff --git a/ui-svelte/src/components/playground/ImageInterface.svelte b/ui-svelte/src/components/playground/ImageInterface.svelte index 85e5d27..86e254f 100644 --- a/ui-svelte/src/components/playground/ImageInterface.svelte +++ b/ui-svelte/src/components/playground/ImageInterface.svelte @@ -2,6 +2,7 @@ import { models } from "../../stores/api"; import { persistentStore } from "../../stores/persistent"; import { generateImage } from "../../lib/imageApi"; + import { playgroundStores } from "../../stores/playgroundActivity"; import ModelSelector from "./ModelSelector.svelte"; import ExpandableTextarea from "./ExpandableTextarea.svelte"; @@ -17,6 +18,10 @@ let hasModels = $derived($models.some((m) => !m.unlisted)); + $effect(() => { + playgroundStores.imageGenerating.set(isGenerating); + }); + async function generate() { const trimmedPrompt = prompt.trim(); if (!trimmedPrompt || !$selectedModelStore || isGenerating) return; diff --git a/ui-svelte/src/components/playground/SpeechInterface.svelte b/ui-svelte/src/components/playground/SpeechInterface.svelte index e9ab20a..3922c59 100644 --- a/ui-svelte/src/components/playground/SpeechInterface.svelte +++ b/ui-svelte/src/components/playground/SpeechInterface.svelte @@ -2,6 +2,7 @@ import { models } from "../../stores/api"; import { persistentStore } from "../../stores/persistent"; import { generateSpeech } from "../../lib/speechApi"; + import { playgroundStores } from "../../stores/playgroundActivity"; import ModelSelector from "./ModelSelector.svelte"; import ExpandableTextarea from "./ExpandableTextarea.svelte"; @@ -20,11 +21,9 @@ let availableVoices = $state(["coral", "alloy", "echo", "fable", "onyx", "nova", "shimmer"]); let isLoadingVoices = $state(false); - // Default voices to fall back to if API call fails const defaultVoices = ["coral", "alloy", "echo", "fable", "onyx", "nova", "shimmer"]; const CACHE_KEY = "playground-speech-voices-cache"; - // Load voices cache from localStorage function getVoicesCache(): Record { if (typeof window === "undefined") return {}; try { @@ -35,7 +34,6 @@ } } - // Save voices cache to localStorage function saveVoicesCache(cache: Record) { if (typeof window === "undefined") return; try { @@ -47,9 +45,12 @@ let hasModels = $derived($models.some((m) => !m.unlisted)); - // Track if this is the initial page load to avoid fetching on refresh let isInitialLoad = $state(true); + $effect(() => { + playgroundStores.speechGenerating.set(isGenerating); + }); + // On page load, restore cached voices for the selected model if available $effect(() => { const model = $selectedModelStore; diff --git a/ui-svelte/src/routes/PlaygroundStub.svelte b/ui-svelte/src/routes/PlaygroundStub.svelte new file mode 100644 index 0000000..5f54547 --- /dev/null +++ b/ui-svelte/src/routes/PlaygroundStub.svelte @@ -0,0 +1 @@ + diff --git a/ui-svelte/src/stores/playgroundActivity.ts b/ui-svelte/src/stores/playgroundActivity.ts new file mode 100644 index 0000000..d7f6ed9 --- /dev/null +++ b/ui-svelte/src/stores/playgroundActivity.ts @@ -0,0 +1,18 @@ +import { writable, derived } from "svelte/store"; + +const chatStreaming = writable(false); +const imageGenerating = writable(false); +const speechGenerating = writable(false); +const audioTranscribing = writable(false); + +export const playgroundActivity = derived( + [chatStreaming, imageGenerating, speechGenerating, audioTranscribing], + ([$chat, $image, $speech, $audio]) => $chat || $image || $speech || $audio +); + +export const playgroundStores = { + chatStreaming, + imageGenerating, + speechGenerating, + audioTranscribing, +}; diff --git a/ui-svelte/src/stores/route.ts b/ui-svelte/src/stores/route.ts new file mode 100644 index 0000000..d6bb710 --- /dev/null +++ b/ui-svelte/src/stores/route.ts @@ -0,0 +1,3 @@ +import { writable } from "svelte/store"; + +export const currentRoute = writable("/");