mirror of
https://github.com/doolijb/serene-pub.git
synced 2026-07-09 15:58:23 +00:00
WIP Ollama manager
This commit is contained in:
parent
1051f01808
commit
d6a0d1f5d7
27 changed files with 4493 additions and 147 deletions
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
|
@ -20,6 +20,8 @@
|
|||
"onconsider",
|
||||
"onfinalize",
|
||||
"pglite",
|
||||
"quantizations",
|
||||
"rfilename",
|
||||
"Roleplay",
|
||||
"skeletonlabs",
|
||||
"skio",
|
||||
|
|
|
|||
5
drizzle/0003_past_blue_marvel.sql
Normal file
5
drizzle/0003_past_blue_marvel.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
CREATE TABLE "system_settings" (
|
||||
"id" integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "system_settings_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
|
||||
"ollama_manager_enabled" boolean DEFAULT false NOT NULL,
|
||||
"ollama_base_url" text DEFAULT 'http://localhost:11434/' NOT NULL
|
||||
);
|
||||
2103
drizzle/meta/0003_snapshot.json
Normal file
2103
drizzle/meta/0003_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -22,6 +22,13 @@
|
|||
"when": 1751366812681,
|
||||
"tag": "0002_awesome_thena",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "7",
|
||||
"when": 1751942961856,
|
||||
"tag": "0003_past_blue_marvel",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
10
package.json
10
package.json
|
|
@ -40,11 +40,11 @@
|
|||
"@enviro/metadata": "^1.5.1",
|
||||
"@fontsource/fira-mono": "^5.0.0",
|
||||
"@neoconfetti/svelte": "^2.0.0",
|
||||
"@skeletonlabs/skeleton": "^3.1.3",
|
||||
"@skeletonlabs/skeleton-svelte": "^1.2.3",
|
||||
"@skeletonlabs/skeleton": "^3.1.4",
|
||||
"@skeletonlabs/skeleton-svelte": "^1.2.4",
|
||||
"@sveltejs/adapter-node": "^5.2.12",
|
||||
"@sveltejs/kit": "^2.16.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@sveltejs/kit": "^2.22.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.1.0",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"svelte": "^5.25.0",
|
||||
"svelte": "^5.35.4",
|
||||
"svelte-check": "^4.0.0",
|
||||
"sveltekit-sse": "^0.13.19",
|
||||
"tailwindcss": "^4.0.0",
|
||||
|
|
|
|||
161
src/app.d.ts
vendored
161
src/app.d.ts
vendored
|
|
@ -6,6 +6,7 @@ import type { Schema } from "inspector/promises"
|
|||
import type { P } from "ollama/dist/shared/ollama.d792a03f.mjs"
|
||||
import type { ChatCompletionMessageParam } from "openai/resources/chat/completions/completions"
|
||||
import { FileAcceptDetails } from "../node_modules/@zag-js/file-upload/dist/index.d"
|
||||
import type { ListResponse } from "ollama"
|
||||
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
|
|
@ -71,6 +72,13 @@ declare global {
|
|||
theme: string
|
||||
}
|
||||
|
||||
interface SystemSettingsCtx {
|
||||
settings: {
|
||||
ollamaManagerEnabled: boolean
|
||||
ollamaManagerBaseUrl: string
|
||||
}
|
||||
}
|
||||
|
||||
// Model select and insert
|
||||
type SelectUser = typeof schema.users.$inferSelect
|
||||
type InsertUser = typeof schema.users.$inferInsert
|
||||
|
|
@ -114,6 +122,20 @@ declare global {
|
|||
type InsertLorebookBinding = typeof schema.lorebookBindings.$inferInsert
|
||||
|
||||
namespace Sockets {
|
||||
namespace SystemSettings {
|
||||
interface Call {}
|
||||
interface Response {
|
||||
systemSettings: {
|
||||
ollamaManagerEnabled: boolean
|
||||
ollamaManagerBaseUrl: string
|
||||
}
|
||||
}
|
||||
}
|
||||
namespace Error {
|
||||
interface Response {
|
||||
error: string
|
||||
}
|
||||
}
|
||||
namespace SamplingConfig {
|
||||
interface Call {
|
||||
id: number
|
||||
|
|
@ -322,6 +344,131 @@ declare global {
|
|||
error: string | null
|
||||
}
|
||||
}
|
||||
// --- OLLAMA ---
|
||||
namespace OllamaSetBaseUrl {
|
||||
interface Call {
|
||||
baseUrl: string
|
||||
}
|
||||
interface Response {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
namespace OllamaModelsList {
|
||||
interface Call {}
|
||||
interface Response {
|
||||
models: any[]
|
||||
}
|
||||
}
|
||||
namespace OllamaDeleteModel {
|
||||
interface Call {
|
||||
modelName: string
|
||||
}
|
||||
interface Response {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
namespace OllamaConnectModel {
|
||||
interface Call {
|
||||
modelName: string
|
||||
}
|
||||
interface Response {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
namespace OllamaListRunningModels {
|
||||
interface Call {}
|
||||
interface Response {
|
||||
models: ListResponse["models"]
|
||||
}
|
||||
}
|
||||
namespace OllamaPullModel {
|
||||
interface Call {
|
||||
modelName: string
|
||||
}
|
||||
interface Response {
|
||||
success: boolean
|
||||
error?: string
|
||||
progress?: any
|
||||
}
|
||||
}
|
||||
namespace OllamaStopModel {
|
||||
interface Call {
|
||||
modelName: string
|
||||
}
|
||||
interface Response {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
namespace OllamaVersion {
|
||||
interface Call {}
|
||||
interface Response {
|
||||
version?: string
|
||||
}
|
||||
}
|
||||
namespace OllamaIsUpdateAvailable {
|
||||
interface Call {}
|
||||
interface Response {
|
||||
updateAvailable: boolean
|
||||
currentVersion?: string
|
||||
latestVersion?: string
|
||||
error?: string
|
||||
}
|
||||
}
|
||||
namespace OllamaSearchAvailableModels {
|
||||
interface Call {
|
||||
search: string
|
||||
source: string
|
||||
}
|
||||
interface Response {
|
||||
models: Array<{
|
||||
name: string
|
||||
description?: string
|
||||
size?: string
|
||||
tags?: string[]
|
||||
popular?: boolean
|
||||
url?: string
|
||||
downloads?: number
|
||||
updatedAtStr?: string
|
||||
createdAt?: Date
|
||||
likes?: number
|
||||
trendingScore?: number
|
||||
}>
|
||||
error?: string
|
||||
}
|
||||
}
|
||||
namespace OllamaHuggingFaceSiblingsList {
|
||||
interface Call {
|
||||
modelId: string
|
||||
}
|
||||
interface Response {
|
||||
baseUrl: string,
|
||||
siblings: Array<{
|
||||
id: string
|
||||
likes: number
|
||||
trendingScore: number
|
||||
private: boolean
|
||||
downloads: number
|
||||
tags: string[]
|
||||
library_name: string
|
||||
createdAt: string
|
||||
modelId: string
|
||||
rfilename?: string
|
||||
size?: number
|
||||
quantization?: string
|
||||
}>
|
||||
error?: string
|
||||
}
|
||||
}
|
||||
// --- APP SETTINGS ---
|
||||
namespace UpdateOllamaManagerEnabled {
|
||||
interface Call {
|
||||
enabled: boolean
|
||||
}
|
||||
interface Response {
|
||||
success: boolean
|
||||
enabled?: boolean
|
||||
}
|
||||
}
|
||||
// --- WEIGHTS ---
|
||||
namespace DeleteSamplingConfig {
|
||||
interface Call {
|
||||
|
|
@ -864,6 +1011,20 @@ declare global {
|
|||
}
|
||||
interface Response {}
|
||||
}
|
||||
// --- USER ---
|
||||
namespace User {
|
||||
interface Call {}
|
||||
interface Response {
|
||||
user:
|
||||
| (SelectUser & {
|
||||
activeConnection: SelectConnection | null
|
||||
activeSamplingConfig: SelectSamplingConfig | null
|
||||
activeContextConfig: SelectContextConfig | null
|
||||
activePromptConfig: SelectPromptConfig | null
|
||||
})
|
||||
| undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface CharaImportMetadata {
|
||||
|
|
|
|||
|
|
@ -57,11 +57,23 @@
|
|||
},
|
||||
digest: {}
|
||||
})
|
||||
// TODO use setTheme socket call
|
||||
let themeCtx: ThemeCtx = $state({
|
||||
mode: (localStorage.getItem("mode") as "light" | "dark") || "dark",
|
||||
theme: localStorage.getItem("theme") || Theme.HAMLINDIGO
|
||||
})
|
||||
let systemSettingsCtx: SystemSettingsCtx = $state({
|
||||
settings: {
|
||||
ollamaManagerEnabled: false,
|
||||
ollamaManagerBaseUrl: ""
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
console.log(
|
||||
"Layout systemSettingsCtx",
|
||||
$state.snapshot(systemSettingsCtx)
|
||||
)
|
||||
})
|
||||
|
||||
function openPanel({
|
||||
key,
|
||||
|
|
@ -168,24 +180,33 @@
|
|||
document.documentElement.setAttribute("data-theme", theme)
|
||||
})
|
||||
|
||||
setContext("panelsCtx", panelsCtx as PanelsCtx)
|
||||
setContext("userCtx", userCtx)
|
||||
setContext("themeCtx", themeCtx)
|
||||
|
||||
onMount(() => {
|
||||
socket.on("user", (message) => {
|
||||
setContext("panelsCtx", panelsCtx as PanelsCtx)
|
||||
setContext("userCtx", userCtx)
|
||||
setContext("themeCtx", themeCtx)
|
||||
setContext("systemSettingsCtx", systemSettingsCtx)
|
||||
|
||||
socket.on("user", (message: Sockets.User.Response) => {
|
||||
userCtx.user = message.user
|
||||
})
|
||||
socket.on(
|
||||
"systemSettings",
|
||||
(message: Sockets.SystemSettings.Response) => {
|
||||
systemSettingsCtx.settings = message.systemSettings
|
||||
}
|
||||
)
|
||||
|
||||
socket.on("error", (message: Sockets.Error.Response) => {
|
||||
toaster.error({ title: "Error", description: message.error })
|
||||
})
|
||||
|
||||
socket.emit("user", {})
|
||||
socket.emit("systemSettings", {})
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
socket.off("user")
|
||||
socket.off("systemSettings")
|
||||
socket.off("error")
|
||||
})
|
||||
</script>
|
||||
|
|
@ -195,7 +216,7 @@
|
|||
class="bg-surface-100-900 relative h-full max-h-[100dvh] w-full justify-between"
|
||||
>
|
||||
<div
|
||||
class="relative flex h-svh min-w-full max-w-full flex-1 flex-col lg:flex-row lg:gap-2 overflow-hidden"
|
||||
class="relative flex h-svh max-w-full min-w-full flex-1 flex-col overflow-hidden lg:flex-row lg:gap-2"
|
||||
>
|
||||
<!-- Left Sidebar -->
|
||||
<aside class="desktop-sidebar">
|
||||
|
|
@ -248,7 +269,7 @@
|
|||
{/if}
|
||||
</aside>
|
||||
<!-- Main Content -->
|
||||
<main class="flex flex-col h-full overflow-hidden">
|
||||
<main class="flex h-full flex-col overflow-hidden">
|
||||
<Header />
|
||||
<div class="flex-1 overflow-auto">
|
||||
{@render children?.()}
|
||||
|
|
@ -428,6 +449,6 @@
|
|||
/* w-[25%] max-w-[25%] */
|
||||
|
||||
.desktop-sidebar {
|
||||
@apply hidden min-h-full max-h-full basis-1/4 overflow-x-hidden lg:block py-1;
|
||||
@apply hidden max-h-full min-h-full basis-1/4 overflow-x-hidden py-1 lg:block;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,480 @@
|
|||
<script lang="ts">
|
||||
import * as Icons from "@lucide/svelte"
|
||||
import * as skio from "sveltekit-io"
|
||||
import { onMount, onDestroy } from "svelte"
|
||||
import { Modal } from "@skeletonlabs/skeleton-svelte"
|
||||
import { toaster } from "$lib/client/utils/toaster"
|
||||
import { OllamaModelSearchSource } from "$lib/shared/constants/OllamaModelSource"
|
||||
|
||||
interface OllamaModel {
|
||||
name: string
|
||||
size: number
|
||||
digest: string
|
||||
modified_at: string
|
||||
details?: {
|
||||
parameter_size: string
|
||||
quantization_level: string
|
||||
}
|
||||
}
|
||||
|
||||
const socket = skio.get()
|
||||
|
||||
// State
|
||||
let searchString = $state("")
|
||||
let downloadingModels = $state(new Set<string>())
|
||||
let installedModels: Sockets.OllamaModelsList.Response["models"] = $state(
|
||||
[]
|
||||
)
|
||||
let selectedSource = $state(OllamaModelSearchSource.HUGGING_FACE)
|
||||
let availableModels: Sockets.OllamaSearchAvailableModels.Response["models"] =
|
||||
$state([])
|
||||
let isSearching = $state(false)
|
||||
|
||||
// Hugging Face modal state
|
||||
let showHuggingFaceModal = $state(false)
|
||||
let selectedModelForDownload: string | null = $state(null)
|
||||
let huggingFaceSiblings: Sockets.OllamaHuggingFaceSiblingsList.Response["siblings"] = $state([])
|
||||
let isLoadingSiblings = $state(false)
|
||||
|
||||
// Check if model is already installed
|
||||
function isModelInstalled(modelName: string): boolean {
|
||||
return installedModels.some((model) => model.name.startsWith(modelName))
|
||||
}
|
||||
|
||||
// Download/Pull a model (mock)
|
||||
async function downloadModel(modelName: string) {
|
||||
try {
|
||||
downloadingModels.add(modelName)
|
||||
toaster.info({ title: `Downloading ${modelName}...` })
|
||||
|
||||
// Simulate download delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
|
||||
// Add to installed models
|
||||
const newModel: OllamaModel = {
|
||||
name: `${modelName}:latest`,
|
||||
size: Math.floor(Math.random() * 5000000000) + 1000000000, // Random size 1-5GB
|
||||
digest: `sha256:${Math.random().toString(36).substring(2, 15)}`,
|
||||
modified_at: new Date().toISOString(),
|
||||
details: {
|
||||
parameter_size: "7B",
|
||||
quantization_level: "Q4_0"
|
||||
}
|
||||
}
|
||||
|
||||
installedModels = [...installedModels, newModel]
|
||||
toaster.success({ title: `${modelName} downloaded successfully` })
|
||||
} catch (error) {
|
||||
console.error("Download failed:", error)
|
||||
toaster.error({ title: `Failed to download ${modelName}` })
|
||||
} finally {
|
||||
downloadingModels.delete(modelName)
|
||||
}
|
||||
}
|
||||
|
||||
// Search for available models
|
||||
function searchAvailableModels() {
|
||||
isSearching = true
|
||||
socket.emit("ollamaSearchAvailableModels", {
|
||||
search: searchString.trim(),
|
||||
source: selectedSource
|
||||
} as Sockets.OllamaSearchAvailableModels.Call)
|
||||
}
|
||||
|
||||
// Open Hugging Face download modal
|
||||
function openHuggingFaceModal(modelId: string) {
|
||||
selectedModelForDownload = modelId
|
||||
showHuggingFaceModal = true
|
||||
isLoadingSiblings = true
|
||||
huggingFaceSiblings = []
|
||||
|
||||
socket.emit("ollamaHuggingFaceSiblingsList", {
|
||||
modelId: modelId
|
||||
} as Sockets.OllamaHuggingFaceSiblingsList.Call)
|
||||
}
|
||||
|
||||
// Close Hugging Face modal
|
||||
function closeHuggingFaceModal() {
|
||||
showHuggingFaceModal = false
|
||||
selectedModelForDownload = null
|
||||
huggingFaceSiblings = []
|
||||
isLoadingSiblings = false
|
||||
}
|
||||
|
||||
// Helper function to extract numeric value from quantization for sorting
|
||||
function getQuantizationSortValue(quantization: string | undefined): number {
|
||||
if (!quantization) return 0
|
||||
|
||||
// Extract the main number (e.g., "Q4_K_M" -> 4, "Q8_0" -> 8)
|
||||
const match = quantization.match(/[Qq](\d+)/)
|
||||
return match ? parseInt(match[1]) : 0
|
||||
}
|
||||
|
||||
// Sort siblings by quantization level (highest to lowest)
|
||||
let sortedSiblings = $derived([...huggingFaceSiblings].sort((a, b) => {
|
||||
const aValue = getQuantizationSortValue(a.quantization)
|
||||
const bValue = getQuantizationSortValue(b.quantization)
|
||||
return bValue - aValue // Descending order
|
||||
}))
|
||||
|
||||
// Debounced search when query or source changes
|
||||
$effect(() => {
|
||||
const _search = searchString.trim()
|
||||
const _source = selectedSource
|
||||
const timeoutId = setTimeout(() => {
|
||||
searchAvailableModels()
|
||||
}, 500) // 500ms delay
|
||||
|
||||
return () => clearTimeout(timeoutId)
|
||||
})
|
||||
|
||||
async function refreshInstalled() {
|
||||
socket.emit("ollamaModelsList", {})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Socket event listeners
|
||||
socket.on(
|
||||
"ollamaModelsList",
|
||||
(message: Sockets.OllamaModelsList.Response) => {
|
||||
installedModels = message.models
|
||||
}
|
||||
)
|
||||
|
||||
socket.on(
|
||||
"ollamaSearchAvailableModels",
|
||||
(message: Sockets.OllamaSearchAvailableModels.Response) => {
|
||||
isSearching = false
|
||||
if (message.error) {
|
||||
toaster.error({ title: message.error })
|
||||
availableModels = []
|
||||
} else {
|
||||
availableModels = message.models || []
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
socket.on(
|
||||
"ollamaPullModel",
|
||||
(message: Sockets.OllamaPullModel.Response) => {
|
||||
// Handle model pull if needed
|
||||
if (message.success) {
|
||||
// Refresh installed models to update UI
|
||||
socket.emit("ollamaModelsList", {})
|
||||
toaster.success({ title: "Model downloaded successfully" })
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
socket.on(
|
||||
"ollamaHuggingFaceSiblingsList",
|
||||
(message: Sockets.OllamaHuggingFaceSiblingsList.Response) => {
|
||||
isLoadingSiblings = false
|
||||
if (message.error) {
|
||||
toaster.error({ title: message.error })
|
||||
huggingFaceSiblings = []
|
||||
} else {
|
||||
huggingFaceSiblings = message.siblings || []
|
||||
}
|
||||
console.log("Hugging Face siblings (sorted):", $state.snapshot(huggingFaceSiblings))
|
||||
}
|
||||
)
|
||||
|
||||
// Load initial installed models
|
||||
refreshInstalled()
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
socket.off("ollamaModelsList")
|
||||
socket.off("ollamaSearchAvailableModels")
|
||||
socket.off("ollamaPullModel")
|
||||
socket.off("ollamaHuggingFaceSiblingsList")
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- Search for available models -->
|
||||
<div class="px-4 py-2">
|
||||
<div class="flex gap-2">
|
||||
<div class="relative flex-1">
|
||||
<Icons.Search
|
||||
class="text-surface-500 absolute top-1/2 left-3 -translate-y-1/2 transform"
|
||||
size={16}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search available models..."
|
||||
class="input w-full pl-10"
|
||||
aria-label="Model search"
|
||||
bind:value={searchString}
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
id="source"
|
||||
name="source"
|
||||
aria-label="Model search source"
|
||||
class="select bg-background border-muted w-fit rounded border"
|
||||
bind:value={selectedSource}
|
||||
>
|
||||
{#each OllamaModelSearchSource.options as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 p-4">
|
||||
{#if isSearching}
|
||||
<div class="p-6 text-center">
|
||||
<Icons.Loader2 class="mx-auto mb-4 animate-spin" size={32} />
|
||||
<p class="text-sm opacity-75">Searching for models...</p>
|
||||
</div>
|
||||
{:else if availableModels.length === 0}
|
||||
<div class="p-6 text-center">
|
||||
<Icons.Search class="text-surface-500 mx-auto mb-4" size={48} />
|
||||
<h3 class="h4 mb-2">No models found</h3>
|
||||
<p class="mb-4 text-sm opacity-75">
|
||||
No available models match your search for "{searchString}".
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
{#each availableModels as model}
|
||||
<div class="card preset-tonal p-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<!-- Header with name and badges -->
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h4 class="text-lg font-semibold">
|
||||
{model.name}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{#if model.popular}
|
||||
<span
|
||||
class="badge bg-primary-500 rounded-full px-2 py-1 text-xs text-white"
|
||||
>
|
||||
<Icons.TrendingUp
|
||||
size={12}
|
||||
class="mr-1 inline"
|
||||
/>
|
||||
Popular
|
||||
</span>
|
||||
{/if}
|
||||
{#if model.trendingScore && model.trendingScore > 0.7}
|
||||
<span
|
||||
class="badge rounded-full bg-orange-500 px-2 py-1 text-xs text-white"
|
||||
>
|
||||
<Icons.Flame
|
||||
size={12}
|
||||
class="mr-1 inline"
|
||||
/>
|
||||
Trending
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="text-surface-500 mb-3 line-clamp-2 text-sm">
|
||||
{model.description || "No description available"}
|
||||
</p>
|
||||
|
||||
<!-- Tags -->
|
||||
{#if model.tags && model.tags.length > 0}
|
||||
<div class="mb-3 flex flex-wrap gap-1">
|
||||
{#each model.tags.slice(0, 4) as tag}
|
||||
<span
|
||||
class="badge bg-surface-200-800 text-surface-700-300 rounded px-2 py-1 text-xs"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
{/each}
|
||||
{#if model.tags.length > 4}
|
||||
<span class="text-surface-500 text-xs">
|
||||
+{model.tags.length - 4} more
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Metadata row -->
|
||||
<div
|
||||
class="text-surface-500 flex flex-wrap items-center gap-4 text-xs"
|
||||
>
|
||||
{#if model.size}
|
||||
<div class="flex items-center gap-1">
|
||||
<Icons.HardDrive size={12} />
|
||||
<span>{model.size}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if model.downloads}
|
||||
<div class="flex items-center gap-1">
|
||||
<Icons.Download size={12} />
|
||||
<span>
|
||||
{model.downloads.toLocaleString()} downloads
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if model.likes}
|
||||
<div class="flex items-center gap-1">
|
||||
<Icons.Heart size={12} />
|
||||
<span>
|
||||
{model.likes.toLocaleString()} likes
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if model.updatedAtStr}
|
||||
<div class="flex items-center gap-1">
|
||||
<Icons.Clock size={12} />
|
||||
<span>Updated {model.updatedAtStr}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex min-w-[100px] gap-2">
|
||||
<button
|
||||
class="btn btn-sm {isModelInstalled(model.name)
|
||||
? 'preset-filled-success-500'
|
||||
: 'preset-filled-primary-500'}"
|
||||
onclick={() => {
|
||||
if (selectedSource === OllamaModelSearchSource.HUGGING_FACE) {
|
||||
openHuggingFaceModal(model.name)
|
||||
} else {
|
||||
downloadModel(model.name)
|
||||
}
|
||||
}}
|
||||
disabled={downloadingModels.has(model.name) ||
|
||||
isModelInstalled(model.name)}
|
||||
>
|
||||
{#if downloadingModels.has(model.name)}
|
||||
<Icons.Loader2
|
||||
size={14}
|
||||
class="animate-spin"
|
||||
/>
|
||||
Installing...
|
||||
{:else if isModelInstalled(model.name)}
|
||||
<Icons.Check size={14} />
|
||||
Installed
|
||||
{:else}
|
||||
<Icons.Download size={14} />
|
||||
Install
|
||||
{/if}
|
||||
</button>
|
||||
{#if model.url}
|
||||
<a
|
||||
href={model.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-sm preset-filled-secondary-500 text-center"
|
||||
>
|
||||
<Icons.ExternalLink size={14} />
|
||||
View
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Hugging Face Download Modal -->
|
||||
<Modal
|
||||
open={showHuggingFaceModal}
|
||||
onOpenChange={(e) => (showHuggingFaceModal = e.open)}
|
||||
contentBase="card bg-surface-100-900 p-4 space-y-4 shadow-xl w-[50em] max-w-dvw-lg"
|
||||
backdropClasses="backdrop-blur-sm"
|
||||
>
|
||||
{#snippet content()}
|
||||
<header class="flex justify-between">
|
||||
<h2 class="h2">Select Quantization</h2>
|
||||
</header>
|
||||
<article class="space-y-4">
|
||||
<div>
|
||||
<p class="text-surface-500 text-sm mb-2">
|
||||
Choose a quantization level for <strong>{selectedModelForDownload}</strong>
|
||||
</p>
|
||||
<div class="bg-surface-200-800 p-3 rounded-lg">
|
||||
<p class="text-xs text-surface-600-400">
|
||||
<strong>About Quantizations:</strong> These are compressed variants of the model that reduce file size and memory usage while maintaining good performance. Higher numbers (Q8) preserve more quality but use more resources, while lower numbers (Q4) are more efficient but with slight quality trade-offs.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isLoadingSiblings}
|
||||
<div class="p-6 text-center">
|
||||
<Icons.Loader2 class="mx-auto mb-4 animate-spin" size={32} />
|
||||
<p class="text-sm opacity-75">Loading available quantizations...</p>
|
||||
</div>
|
||||
{:else if sortedSiblings.length === 0}
|
||||
<div class="p-6 text-center">
|
||||
<Icons.AlertCircle class="text-surface-500 mx-auto mb-4" size={48} />
|
||||
<p class="text-sm opacity-75">
|
||||
No GGUF quantizations are available for this model.
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-3 max-h-96 overflow-y-auto">
|
||||
{#each sortedSiblings as sibling, index}
|
||||
<div class="card bg-surface-200-800 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h5 class="font-semibold">
|
||||
{sibling.quantization || 'Unknown file'}
|
||||
</h5>
|
||||
|
||||
<!-- Add special labels -->
|
||||
{#if index === 0 && sortedSiblings.length > 1}
|
||||
<span class="badge bg-blue-500 text-white text-xs px-2 py-1 rounded">
|
||||
Larger
|
||||
</span>
|
||||
{:else if index === sortedSiblings.length - 1 && sortedSiblings.length > 1}
|
||||
<span class="badge bg-green-500 text-white text-xs px-2 py-1 rounded">
|
||||
Smaller
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if sibling.quantization === 'Q4_K_M'}
|
||||
<span class="badge bg-orange-500 text-white text-xs px-2 py-1 rounded">
|
||||
Recommended
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 text-xs text-surface-500">
|
||||
{#if sibling.size}
|
||||
<div class="flex items-center gap-1">
|
||||
<Icons.HardDrive size={12} />
|
||||
<span>{(sibling.size / (1024 * 1024 * 1024)).toFixed(2)} GB</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn btn-sm preset-filled-primary-500"
|
||||
onclick={() => {
|
||||
// TODO: Implement actual download logic
|
||||
toaster.info({ title: `Download will be implemented for ${sibling.rfilename}` })
|
||||
closeHuggingFaceModal()
|
||||
}}
|
||||
>
|
||||
<Icons.Download size={14} />
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</article>
|
||||
<footer class="flex justify-end gap-4">
|
||||
<button
|
||||
class="btn preset-tonal"
|
||||
onclick={closeHuggingFaceModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</footer>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
|
|
@ -0,0 +1,397 @@
|
|||
<script lang="ts">
|
||||
import * as Icons from "@lucide/svelte"
|
||||
import * as skio from "sveltekit-io"
|
||||
import { onMount, onDestroy, getContext } from "svelte"
|
||||
import { Modal } from "@skeletonlabs/skeleton-svelte"
|
||||
import { toaster } from "$lib/client/utils/toaster"
|
||||
import type { ListResponse, ModelDetails, ModelResponse } from "ollama"
|
||||
import { CONNECTION_TYPE } from "$lib/shared/constants/ConnectionTypes"
|
||||
|
||||
interface OllamaModel {
|
||||
name: string
|
||||
size: number
|
||||
digest: string
|
||||
modified_at: string
|
||||
details?: {
|
||||
parameter_size: string
|
||||
quantization_level: string
|
||||
}
|
||||
}
|
||||
|
||||
const socket = skio.get()
|
||||
|
||||
// State
|
||||
let installedModels: OllamaModel[] = $state([])
|
||||
let isConnected = $state(true)
|
||||
let isLoading = $state(false)
|
||||
let searchQuery = $state("")
|
||||
let runningModels: ListResponse["models"] = $state([])
|
||||
let userCtx: UserCtx = $state(getContext("userCtx"))
|
||||
let showDeleteModal = $state(false)
|
||||
let modelToDelete: OllamaModel | null = $state(null)
|
||||
|
||||
// Context
|
||||
let systemSettingsCtx: SystemSettingsCtx = $state(
|
||||
getContext("systemSettingsCtx")
|
||||
)
|
||||
|
||||
// Filtered models based on search
|
||||
let filteredModels = $derived(
|
||||
installedModels
|
||||
.filter((model) =>
|
||||
model.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
.sort((a, b) => {
|
||||
// Sort if the model is currently connected
|
||||
if (currentConnectionModelName === a.name) return -1
|
||||
if (currentConnectionModelName === b.name) return 1
|
||||
// Sort by name, then by size
|
||||
if (a.name < b.name) return -1
|
||||
if (a.name > b.name) return 1
|
||||
return a.size - b.size
|
||||
})
|
||||
)
|
||||
|
||||
let currentConnectionModelName: string | null = $derived.by(() => {
|
||||
if (userCtx?.user?.activeConnection?.type === CONNECTION_TYPE.OLLAMA) {
|
||||
const currentName = userCtx.user.activeConnection.model
|
||||
return currentName
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
console.log(
|
||||
"Current connection model name:",
|
||||
currentConnectionModelName
|
||||
)
|
||||
})
|
||||
|
||||
// Format file size
|
||||
function formatSize(bytes: number): string {
|
||||
const units = ["B", "KB", "MB", "GB", "TB"]
|
||||
let size = bytes
|
||||
let unitIndex = 0
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024
|
||||
unitIndex++
|
||||
}
|
||||
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
// Format date
|
||||
function formatDate(dateString: string): string {
|
||||
return new Date(dateString).toLocaleDateString()
|
||||
}
|
||||
|
||||
function isModelRunning(model: OllamaModel): boolean {
|
||||
const res = runningModels.some((runningModel) => {
|
||||
return runningModel.name === model.name
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
// Check Ollama connection and refresh models
|
||||
async function refreshModels() {
|
||||
isLoading = true
|
||||
socket.emit("ollamaModelsList", {})
|
||||
socket.emit("ollamaListRunningModels", {})
|
||||
socket.emit("connectionsList", {})
|
||||
}
|
||||
|
||||
// Delete a model
|
||||
async function deleteModel(model: OllamaModel) {
|
||||
if (!isConnected) {
|
||||
toaster.error({ title: "Not connected to Ollama" })
|
||||
return
|
||||
}
|
||||
|
||||
if (currentConnectionModelName === model.name) {
|
||||
toaster.error({
|
||||
title: "Cannot delete connected model",
|
||||
description:
|
||||
"Please choose a different connection before deleting it."
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
socket.emit("ollamaDeleteModel", { modelName: model.name })
|
||||
}
|
||||
|
||||
// Delete modal handlers
|
||||
function handleDeleteClick(model: OllamaModel) {
|
||||
modelToDelete = model
|
||||
showDeleteModal = true
|
||||
}
|
||||
|
||||
function handleDeleteModalConfirm() {
|
||||
if (modelToDelete) {
|
||||
deleteModel(modelToDelete)
|
||||
}
|
||||
showDeleteModal = false
|
||||
modelToDelete = null
|
||||
}
|
||||
|
||||
function handleDeleteModalCancel() {
|
||||
showDeleteModal = false
|
||||
modelToDelete = null
|
||||
}
|
||||
|
||||
function connectToModel(model: OllamaModel) {
|
||||
if (!isConnected) {
|
||||
toaster.error({ title: "Not connected to Ollama" })
|
||||
return
|
||||
}
|
||||
|
||||
if (currentConnectionModelName === model.name) {
|
||||
toaster.error({
|
||||
title: "Already connected to this model",
|
||||
description: "Please choose a different model to connect."
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
socket.emit("ollamaConnectModel", { modelName: model.name })
|
||||
}
|
||||
|
||||
// View model website
|
||||
function viewModelWebsite(model: OllamaModel) {
|
||||
const modelName = model.name.split(":")[0] // Remove version if present
|
||||
|
||||
// Determine if ollama.com or huggingface.co
|
||||
if (modelName.includes("hf.co")) {
|
||||
window.open("https://" + modelName, "_blank")
|
||||
} else {
|
||||
window.open(`https://ollama.com/library/${modelName}`, "_blank")
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Socket event listeners
|
||||
socket.on(
|
||||
"ollamaModelsList",
|
||||
(message: Sockets.OllamaModelsList.Response) => {
|
||||
installedModels = message.models
|
||||
isLoading = false
|
||||
console.log("ollamaModelsList", message.models)
|
||||
}
|
||||
)
|
||||
|
||||
socket.on(
|
||||
"ollamaDeleteModel",
|
||||
(message: Sockets.OllamaDeleteModel.Response) => {
|
||||
if (message.success) {
|
||||
refreshModels()
|
||||
toaster.success({ title: "Model deleted successfully" })
|
||||
} else {
|
||||
toaster.error({ title: "Failed to delete model" })
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
socket.on(
|
||||
"ollamaListRunningModels",
|
||||
(message: Sockets.OllamaListRunningModels.Response) => {
|
||||
runningModels = message.models
|
||||
}
|
||||
)
|
||||
|
||||
socket.on(
|
||||
"ollamaStopModel",
|
||||
(message: Sockets.OllamaStopModel.Response) => {
|
||||
if (message.success) {
|
||||
toaster.success({ title: "Model stopped successfully" })
|
||||
refreshModels()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
socket.on(
|
||||
"ollamaConnectModel",
|
||||
(message: Sockets.OllamaConnectModel.Response) => {
|
||||
if (message.success) {
|
||||
toaster.success({ title: "Model connected successfully" })
|
||||
refreshModels()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Initial load
|
||||
refreshModels()
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
socket.off("ollamaModelsList")
|
||||
socket.off("ollamaDeleteModel")
|
||||
socket.off("ollamaListRunningModels")
|
||||
socket.off("ollamaStopModel")
|
||||
socket.off("ollamaConnectModel")
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- Search for installed models -->
|
||||
<div class="px-4 py-2">
|
||||
<div class="flex gap-2">
|
||||
<div class="relative flex-1">
|
||||
<Icons.Search
|
||||
class="text-surface-500 absolute top-1/2 left-3 -translate-y-1/2 transform"
|
||||
size={16}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search installed models..."
|
||||
class="input w-full pl-10"
|
||||
bind:value={searchQuery}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="btn preset-filled-surface-500"
|
||||
onclick={refreshModels}
|
||||
title="Refresh models"
|
||||
>
|
||||
<Icons.RefreshCw size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !isConnected}
|
||||
<div class="p-6 text-center">
|
||||
<Icons.AlertCircle class="text-error-500 mx-auto mb-4" size={48} />
|
||||
<h3 class="h4 mb-2">Cannot connect to Ollama</h3>
|
||||
<p class="mb-4 text-sm opacity-75">
|
||||
Make sure Ollama is running and accessible at the configured URL.
|
||||
</p>
|
||||
<button class="btn preset-filled-primary-500" onclick={refreshModels}>
|
||||
<Icons.RefreshCw size={16} />
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
{:else if isLoading}
|
||||
<div class="p-6 text-center">
|
||||
<Icons.Loader2 class="mx-auto mb-4 animate-spin" size={32} />
|
||||
<p class="text-sm opacity-75">Loading installed models...</p>
|
||||
</div>
|
||||
{:else if filteredModels.length === 0}
|
||||
<div class="p-6 text-center">
|
||||
<Icons.Package class="text-surface-500 mx-auto mb-4" size={48} />
|
||||
<h3 class="h4 mb-2">No models installed</h3>
|
||||
<p class="mb-4 text-sm opacity-75">
|
||||
Install models from the Available tab to get started.
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-3 p-4">
|
||||
{#each filteredModels as model}
|
||||
{@const isRunning = isModelRunning(model)}
|
||||
{@const isConnected = currentConnectionModelName === model.name}
|
||||
<div class="card preset-tonal flex flex-col gap-2 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="font-semibold">
|
||||
{#if isConnected}
|
||||
<Icons.Check
|
||||
size={14}
|
||||
class="text-success-500 inline-block"
|
||||
/>
|
||||
{/if}
|
||||
{model.name}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="text-surface-600 space-y-1 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span>Size:</span>
|
||||
<span>{formatSize(model.size)}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>Modified:</span>
|
||||
<span>{formatDate(model.modified_at)}</span>
|
||||
</div>
|
||||
{#if model.details}
|
||||
<div class="flex justify-between">
|
||||
<span>Parameters:</span>
|
||||
<span>{model.details.parameter_size}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if isRunning}
|
||||
<div class="flex justify-between">
|
||||
<span>Status:</span>
|
||||
<span
|
||||
class="preset-filled-success-500 rounded-xl px-2 py-1"
|
||||
>
|
||||
Running
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex justify-between gap-2">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="btn btn-sm preset-filled-success-500"
|
||||
title="Connect to this model"
|
||||
aria-label="Connect to model"
|
||||
disabled={isConnected}
|
||||
onclick={() => connectToModel(model)}
|
||||
>
|
||||
{#if isConnected}
|
||||
<Icons.Check size={14} /> Connected
|
||||
{:else}
|
||||
<Icons.Cable size={14} /> Connect
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm preset-filled-surface-500"
|
||||
onclick={() => viewModelWebsite(model)}
|
||||
title="View model website"
|
||||
>
|
||||
<Icons.ExternalLink size={14} /> View
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-sm preset-filled-error-500"
|
||||
onclick={() => handleDeleteClick(model)}
|
||||
title="Delete model"
|
||||
>
|
||||
<Icons.Trash2 size={14} /> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Modal
|
||||
open={showDeleteModal}
|
||||
onOpenChange={(e) => (showDeleteModal = e.open)}
|
||||
contentBase="card bg-surface-100-900 p-4 space-y-4 shadow-xl max-w-dvw-sm"
|
||||
backdropClasses="backdrop-blur-sm"
|
||||
>
|
||||
{#snippet content()}
|
||||
<header class="flex justify-between">
|
||||
<h2 class="h2">Delete Model</h2>
|
||||
</header>
|
||||
<article>
|
||||
<p class="opacity-60">
|
||||
Are you sure you want to delete "{modelToDelete}" from Ollama?
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
<p class="opacity-60">
|
||||
Any associated connections to this model will be removed.
|
||||
</p>
|
||||
</article>
|
||||
<footer class="flex justify-end gap-4">
|
||||
<button
|
||||
class="btn preset-filled-surface-500"
|
||||
onclick={handleDeleteModalCancel}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="btn preset-filled-error-500"
|
||||
onclick={handleDeleteModalConfirm}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</footer>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
256
src/lib/client/components/ollamaManager/OllamaManager.svelte
Normal file
256
src/lib/client/components/ollamaManager/OllamaManager.svelte
Normal file
File diff suppressed because one or more lines are too long
270
src/lib/client/components/ollamaManager/OllamaSettingsTab.svelte
Normal file
270
src/lib/client/components/ollamaManager/OllamaSettingsTab.svelte
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
<script lang="ts">
|
||||
import * as Icons from "@lucide/svelte"
|
||||
import { toaster } from "$lib/client/utils/toaster"
|
||||
import * as skio from "sveltekit-io"
|
||||
import { onMount, onDestroy, getContext } from "svelte"
|
||||
|
||||
interface OllamaModel {
|
||||
name: string
|
||||
size: number
|
||||
digest: string
|
||||
modified_at: string
|
||||
details?: {
|
||||
parameter_size: string
|
||||
quantization_level: string
|
||||
}
|
||||
}
|
||||
|
||||
const socket = skio.get()
|
||||
|
||||
// State
|
||||
let currentVersion = $state("")
|
||||
let updateAvailable = $state(false)
|
||||
let latestVersion = $state("")
|
||||
let isConnected = $state(true)
|
||||
let isCheckingUpdates = $state(false)
|
||||
let isSavingBaseUrl = $state(false)
|
||||
let installedModels: OllamaModel[] = $state([])
|
||||
let showDeleteModal = $state(false)
|
||||
let modelToDelete: OllamaModel | null = $state(null)
|
||||
let baseUrlField = $state("")
|
||||
|
||||
// Context
|
||||
let systemSettingsCtx: SystemSettingsCtx = $state(
|
||||
getContext("systemSettingsCtx")
|
||||
)
|
||||
|
||||
$effect(() => {
|
||||
// Update baseUrl when system settings change
|
||||
baseUrlField = systemSettingsCtx.settings.ollamaManagerBaseUrl
|
||||
})
|
||||
|
||||
// Settings functions
|
||||
function checkOllamaVersion() {
|
||||
socket.emit("ollamaVersion", {})
|
||||
}
|
||||
|
||||
function checkForUpdates() {
|
||||
isCheckingUpdates = true
|
||||
socket.emit("ollamaIsUpdateAvailable", {})
|
||||
}
|
||||
|
||||
function saveBaseUrl() {
|
||||
if (!baseUrlField.trim()) {
|
||||
toaster.error({ title: "Base URL cannot be empty" })
|
||||
return
|
||||
}
|
||||
|
||||
isSavingBaseUrl = true
|
||||
socket.emit("ollamaSetBaseUrl", { baseUrl: baseUrlField.trim() })
|
||||
}
|
||||
|
||||
function handleSaveBaseUrl() {
|
||||
if (!baseUrlField.trim()) {
|
||||
toaster.error({ title: "Base URL cannot be empty" })
|
||||
return
|
||||
}
|
||||
saveBaseUrl()
|
||||
}
|
||||
|
||||
// Model management functions
|
||||
function handleDeleteModel(model: OllamaModel) {
|
||||
modelToDelete = model
|
||||
showDeleteModal = true
|
||||
}
|
||||
|
||||
function handleDeleteModalConfirm() {
|
||||
if (modelToDelete) {
|
||||
socket.emit("ollamaDeleteModel", { modelName: modelToDelete.name })
|
||||
}
|
||||
showDeleteModal = false
|
||||
modelToDelete = null
|
||||
}
|
||||
|
||||
function handleDeleteModalCancel() {
|
||||
showDeleteModal = false
|
||||
modelToDelete = null
|
||||
}
|
||||
|
||||
// Format file size
|
||||
function formatSize(bytes: number): string {
|
||||
const units = ["B", "KB", "MB", "GB", "TB"]
|
||||
let size = bytes
|
||||
let unitIndex = 0
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024
|
||||
unitIndex++
|
||||
}
|
||||
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
// Format date
|
||||
function formatDate(dateString: string): string {
|
||||
return new Date(dateString).toLocaleDateString()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Socket event listeners
|
||||
socket.on(
|
||||
"ollamaSetBaseUrl",
|
||||
(message: Sockets.OllamaSetBaseUrl.Response) => {
|
||||
isSavingBaseUrl = false
|
||||
if (message.success) {
|
||||
toaster.success({
|
||||
title: "Ollama URL updated successfully"
|
||||
})
|
||||
} else {
|
||||
toaster.error({ title: "Failed to update Ollama URL" })
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
socket.on(
|
||||
"ollamaVersion",
|
||||
(message: Sockets.OllamaVersion.Response) => {
|
||||
currentVersion = message.version || "Unknown"
|
||||
}
|
||||
)
|
||||
|
||||
socket.on(
|
||||
"ollamaIsUpdateAvailable",
|
||||
(message: Sockets.OllamaIsUpdateAvailable.Response) => {
|
||||
isCheckingUpdates = false
|
||||
updateAvailable = message.updateAvailable
|
||||
latestVersion = message.latestVersion || ""
|
||||
|
||||
if (message.error) {
|
||||
toaster.error({
|
||||
title: "Failed to check for updates",
|
||||
description: message.error
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Load version info when component mounts
|
||||
checkOllamaVersion()
|
||||
checkForUpdates()
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
socket.off("ollamaSetBaseUrl")
|
||||
socket.off("ollamaVersion")
|
||||
socket.off("ollamaIsUpdateAvailable")
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="space-y-6 p-4">
|
||||
<!-- Version Information -->
|
||||
<div class="card bg-surface-100-800 flex flex-col gap-4 p-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium" for="baseUrl">
|
||||
Ollama Base URL
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
id="baseUrl"
|
||||
name="baseUrl"
|
||||
type="url"
|
||||
class="input flex-1"
|
||||
placeholder="http://localhost:11434"
|
||||
bind:value={baseUrlField}
|
||||
/>
|
||||
<button
|
||||
class="btn preset-filled-primary-500"
|
||||
onclick={handleSaveBaseUrl}
|
||||
disabled={isSavingBaseUrl}
|
||||
>
|
||||
<Icons.Save size={14} />
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-surface-500 mt-1 text-xs">
|
||||
The URL where Ollama is running. Usually http://localhost:11434
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-surface-600">Current Version:</span>
|
||||
<span class="font-mono">{currentVersion || "Unknown"}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-surface-600">Latest Version:</span>
|
||||
<span class="text-warning-500 font-mono">
|
||||
{latestVersion}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{#if updateAvailable}
|
||||
<div
|
||||
class="bg-warning-100 dark:bg-warning-900 border-warning-300 dark:border-warning-700 rounded-lg border p-3"
|
||||
>
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<Icons.AlertTriangle
|
||||
size={16}
|
||||
class="text-warning-600"
|
||||
/>
|
||||
<span
|
||||
class="text-warning-800 dark:text-warning-200 font-medium"
|
||||
>
|
||||
Update Available
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
class="text-warning-700 dark:text-warning-300 mb-3 text-sm"
|
||||
>
|
||||
A new version of Ollama is available. Download it to get
|
||||
the latest features and bug fixes.
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/ollama/ollama/releases/latest"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-sm preset-filled-warning-500"
|
||||
>
|
||||
<Icons.Download size={14} />
|
||||
Download Update
|
||||
</a>
|
||||
</div>
|
||||
{:else if currentVersion}
|
||||
<div
|
||||
class="bg-success-100 dark:bg-success-900 border-success-300 dark:border-success-700 rounded-lg border p-3"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icons.Check size={16} class="text-success-600" />
|
||||
<span
|
||||
class="text-success-800 dark:text-success-200 font-medium"
|
||||
>
|
||||
You're up to date
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="btn btn-sm preset-filled-surface-500"
|
||||
onclick={checkOllamaVersion}
|
||||
>
|
||||
<Icons.RefreshCw size={14} />
|
||||
Check Version
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm preset-filled-surface-500"
|
||||
onclick={checkForUpdates}
|
||||
disabled={isCheckingUpdates}
|
||||
>
|
||||
{#if isCheckingUpdates}
|
||||
<Icons.Loader2 size={14} class="animate-spin" />
|
||||
Checking...
|
||||
{:else}
|
||||
<Icons.Search size={14} />
|
||||
Check for Updates
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -4,7 +4,6 @@
|
|||
import * as Icons from "@lucide/svelte"
|
||||
import { Modal } from "@skeletonlabs/skeleton-svelte"
|
||||
import OllamaForm from "$lib/client/connectionForms/OllamaForm.svelte"
|
||||
// import ChatGPTForm from "$lib/client/connectionForms/ChatGPTForm.svelte"
|
||||
import OpenAIForm from "$lib/client/connectionForms/OpenAIForm.svelte"
|
||||
import LmStudioForm from "$lib/client/connectionForms/LMStudioForm.svelte"
|
||||
import {
|
||||
|
|
@ -15,6 +14,7 @@
|
|||
import { toaster } from "$lib/client/utils/toaster"
|
||||
import { PromptFormats } from "$lib/shared/constants/PromptFormats"
|
||||
import { TokenCounterOptions } from "$lib/shared/constants/TokenCounters"
|
||||
import OllamaManager from "$lib/client/components/ollamaManager/OllamaManager.svelte"
|
||||
|
||||
interface Props {
|
||||
onclose?: () => Promise<boolean> | undefined
|
||||
|
|
@ -22,14 +22,19 @@
|
|||
|
||||
let { onclose = $bindable() }: Props = $props()
|
||||
let userCtx: UserCtx = getContext("userCtx")
|
||||
let systemSettingsCtx: SystemSettingsCtx = getContext("systemSettingsCtx")
|
||||
|
||||
const socket = skio.get()
|
||||
|
||||
const OAIChatPresets: {name:string, value: number, connectionDefaults: {
|
||||
baseUrl: string,
|
||||
promptFormat?: string,
|
||||
tokenCounter?: string,
|
||||
const OAIChatPresets: {
|
||||
name: string
|
||||
value: number
|
||||
connectionDefaults: {
|
||||
baseUrl: string
|
||||
promptFormat?: string
|
||||
tokenCounter?: string
|
||||
extraJson: {
|
||||
stream: boolean,
|
||||
stream: boolean
|
||||
prerenderPrompt: boolean
|
||||
apiKey: string
|
||||
}
|
||||
|
|
@ -59,7 +64,7 @@
|
|||
extraJson: {
|
||||
stream: true,
|
||||
prerenderPrompt: false,
|
||||
apiKey: "ollama",
|
||||
apiKey: "ollama"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -87,7 +92,7 @@
|
|||
extraJson: {
|
||||
stream: true,
|
||||
prerenderPrompt: false,
|
||||
apiKey: "",
|
||||
apiKey: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -101,7 +106,7 @@
|
|||
extraJson: {
|
||||
stream: true,
|
||||
prerenderPrompt: false,
|
||||
apiKey: "",
|
||||
apiKey: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -115,7 +120,7 @@
|
|||
extraJson: {
|
||||
stream: true,
|
||||
prerenderPrompt: false,
|
||||
apiKey: "",
|
||||
apiKey: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -129,7 +134,7 @@
|
|||
extraJson: {
|
||||
stream: true,
|
||||
prerenderPrompt: false,
|
||||
apiKey: "",
|
||||
apiKey: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -143,7 +148,7 @@
|
|||
extraJson: {
|
||||
stream: true,
|
||||
prerenderPrompt: false,
|
||||
apiKey: "",
|
||||
apiKey: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -157,7 +162,7 @@
|
|||
extraJson: {
|
||||
stream: true,
|
||||
prerenderPrompt: false,
|
||||
apiKey: "",
|
||||
apiKey: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -171,7 +176,7 @@
|
|||
extraJson: {
|
||||
stream: true,
|
||||
prerenderPrompt: false,
|
||||
apiKey: "",
|
||||
apiKey: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -185,7 +190,7 @@
|
|||
extraJson: {
|
||||
stream: true,
|
||||
prerenderPrompt: false,
|
||||
apiKey: "",
|
||||
apiKey: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -199,7 +204,7 @@
|
|||
extraJson: {
|
||||
stream: true,
|
||||
prerenderPrompt: false,
|
||||
apiKey: "",
|
||||
apiKey: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -213,7 +218,7 @@
|
|||
extraJson: {
|
||||
stream: true,
|
||||
prerenderPrompt: false,
|
||||
apiKey: "",
|
||||
apiKey: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -240,6 +245,7 @@
|
|||
let newConnectionType = $state(CONNECTION_TYPES[0].value)
|
||||
let newConnectionOAIChatPreset: number | undefined = $state()
|
||||
let showDeleteModal = $state(false)
|
||||
let showOllamaManager = $state(false)
|
||||
|
||||
function handleSelectChange(e: Event) {
|
||||
socket.emit("setUserActiveConnection", {
|
||||
|
|
@ -272,7 +278,7 @@
|
|||
...(newConnectionType === CONNECTION_TYPE.OPENAI_CHAT
|
||||
? OAIChatPresets.find(
|
||||
(p) => p.value === newConnectionOAIChatPreset
|
||||
)?.connectionDefaults
|
||||
)?.connectionDefaults
|
||||
: {})
|
||||
}
|
||||
socket.emit("createConnection", { connection: newConn })
|
||||
|
|
@ -318,6 +324,14 @@
|
|||
refreshModelsResult = null
|
||||
socket.emit("refreshModels", { baseUrl: connection?.baseUrl })
|
||||
}
|
||||
function onOllamaManagerClick() {
|
||||
if (systemSettingsCtx.settings.ollamaManagerEnabled) {
|
||||
showOllamaManager = true
|
||||
}
|
||||
}
|
||||
function onOllamaManagerClickClose() {
|
||||
showOllamaManager = false
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
socket.on(
|
||||
|
|
@ -344,20 +358,26 @@
|
|||
toaster.success({ title: "Connection Updated" })
|
||||
}
|
||||
)
|
||||
socket.on("deleteConnection", (msg: Sockets.DeleteConnection.Response) => {
|
||||
toaster.success({ title: "Connection Deleted" })
|
||||
connection = undefined
|
||||
originalConnection = undefined
|
||||
})
|
||||
socket.on("createConnection", (msg: Sockets.CreateConnection.Response) => {
|
||||
toaster.success({ title: "Connection Created" })
|
||||
})
|
||||
socket.on(
|
||||
"deleteConnection",
|
||||
(msg: Sockets.DeleteConnection.Response) => {
|
||||
toaster.success({ title: "Connection Deleted" })
|
||||
connection = undefined
|
||||
originalConnection = undefined
|
||||
}
|
||||
)
|
||||
socket.on(
|
||||
"createConnection",
|
||||
(msg: Sockets.CreateConnection.Response) => {
|
||||
toaster.success({ title: "Connection Created" })
|
||||
}
|
||||
)
|
||||
socket.emit("connectionsList", {})
|
||||
if (userCtx.user?.activeConnectionId) {
|
||||
socket.emit("connection", { id: userCtx.user.activeConnectionId })
|
||||
}
|
||||
onclose = handleOnClose
|
||||
// If ollama, fetch models
|
||||
|
||||
if (connection?.type === "ollama" && connection.baseUrl) {
|
||||
handleRefreshModels()
|
||||
}
|
||||
|
|
@ -375,109 +395,113 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<div class="text-foreground p-4">
|
||||
<div class="mt-2 mb-2 flex gap-2 sm:mt-0">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm preset-filled-primary-500"
|
||||
onclick={handleNew}
|
||||
>
|
||||
<Icons.Plus size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm preset-filled-secondary-500"
|
||||
onclick={handleReset}
|
||||
disabled={!unsavedChanges}
|
||||
>
|
||||
<Icons.RefreshCcw size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm preset-filled-error-500"
|
||||
onclick={handleDelete}
|
||||
disabled={!connection}
|
||||
>
|
||||
<Icons.X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mb-6 flex flex-col gap-2 sm:flex-row sm:items-center"
|
||||
class:hidden={!connectionsList.length}
|
||||
>
|
||||
<select
|
||||
class="select bg-background border-muted rounded border"
|
||||
onchange={handleSelectChange}
|
||||
bind:value={userCtx!.user!.activeConnectionId}
|
||||
disabled={unsavedChanges}
|
||||
>
|
||||
{#each connectionsList as c}
|
||||
<option value={c.id}>
|
||||
{c.name} ({CONNECTION_TYPE.options.find(
|
||||
(t) => t.value === c.type
|
||||
)!.label})
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{#if !!connection}
|
||||
{#key connection.id}
|
||||
<div class="my-4 flex">
|
||||
{#if showOllamaManager && systemSettingsCtx.settings.ollamaManagerEnabled}
|
||||
<OllamaManager onClose={() => (showOllamaManager = false)} close={onOllamaManagerClickClose} />
|
||||
{:else}
|
||||
<div class="text-foreground p-4">
|
||||
<div class="mt-2 mb-2 flex justify-between gap-2 sm:mt-0">
|
||||
<div class="gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm preset-filled-success-500 w-full"
|
||||
onclick={handleUpdate}
|
||||
disabled={!unsavedChanges}
|
||||
class="btn btn-sm preset-filled-primary-500"
|
||||
onclick={handleNew}
|
||||
title="Create New Connection"
|
||||
aria-label="Create New Connection"
|
||||
>
|
||||
<Icons.Save size={16} />
|
||||
Save
|
||||
<Icons.Plus size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm preset-filled-secondary-500"
|
||||
onclick={handleReset}
|
||||
disabled={!unsavedChanges}
|
||||
title="Reset Changes"
|
||||
aria-label="Reset Changes"
|
||||
>
|
||||
<Icons.RefreshCcw size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm preset-filled-error-500"
|
||||
onclick={handleDelete}
|
||||
disabled={!connection}
|
||||
>
|
||||
<Icons.X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="font-semibold" for="name">Name</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
bind:value={connection.name}
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
{#if connection.type === CONNECTION_TYPE.OLLAMA}
|
||||
<OllamaForm bind:connection />
|
||||
{:else if connection.type === CONNECTION_TYPE.OPENAI_CHAT}
|
||||
<OpenAIForm bind:connection />
|
||||
{:else if connection.type === CONNECTION_TYPE.LM_STUDIO}
|
||||
<LmStudioForm bind:connection />
|
||||
{:else if connection.type === CONNECTION_TYPE.LLAMACPP_COMPLETION}
|
||||
<LlamaCppForm bind:connection />
|
||||
{/if}
|
||||
<div class="mt-4 flex flex-col gap-2">
|
||||
{#if connection.type === "ollama"}
|
||||
{#if refreshModelsResult}
|
||||
<div class="mt-1 text-sm">
|
||||
{#if refreshModelsResult.models?.length}
|
||||
<div>
|
||||
Available Models: {refreshModelsResult.models.join(
|
||||
", "
|
||||
)}
|
||||
</div>
|
||||
{:else if refreshModelsResult.error}
|
||||
<span class="text-error">
|
||||
{refreshModelsResult.error}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
{#if systemSettingsCtx.settings.ollamaManagerEnabled}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm preset-filled-secondary-500"
|
||||
aria-label="Ollama Manager"
|
||||
onclick={onOllamaManagerClick}
|
||||
title="Ollama Manager"
|
||||
>
|
||||
Ollama
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
{#if !connectionsList.length}
|
||||
<div class="text-muted-foreground py-8 text-center">
|
||||
No connections found. Create a new connection to get started.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
class="mb-6 flex flex-col gap-2 sm:flex-row sm:items-center"
|
||||
class:hidden={!connectionsList.length}
|
||||
>
|
||||
<select
|
||||
class="select bg-background border-muted rounded border"
|
||||
onchange={handleSelectChange}
|
||||
bind:value={userCtx!.user!.activeConnectionId}
|
||||
disabled={unsavedChanges}
|
||||
>
|
||||
{#each connectionsList as c}
|
||||
<option value={c.id}>
|
||||
{c.name} ({CONNECTION_TYPE.options.find(
|
||||
(t) => t.value === c.type
|
||||
)!.label})
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{#if !!connection}
|
||||
{#key connection.id}
|
||||
<div class="my-4 flex">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm preset-filled-success-500 w-full"
|
||||
onclick={handleUpdate}
|
||||
disabled={!unsavedChanges}
|
||||
>
|
||||
<Icons.Save size={16} />
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="font-semibold" for="name">Name</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
bind:value={connection.name}
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
{#if connection.type === CONNECTION_TYPE.OLLAMA}
|
||||
<OllamaForm bind:connection />
|
||||
{:else if connection.type === CONNECTION_TYPE.OPENAI_CHAT}
|
||||
<OpenAIForm bind:connection />
|
||||
{:else if connection.type === CONNECTION_TYPE.LM_STUDIO}
|
||||
<LmStudioForm bind:connection />
|
||||
{:else if connection.type === CONNECTION_TYPE.LLAMACPP_COMPLETION}
|
||||
<LlamaCppForm bind:connection />
|
||||
{/if}
|
||||
{/key}
|
||||
{/if}
|
||||
{#if !connectionsList.length}
|
||||
<div class="text-muted-foreground py-8 text-center">
|
||||
No connections found. Create a new connection to get started.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Modal
|
||||
open={showConfirmModal}
|
||||
|
|
@ -571,7 +595,9 @@
|
|||
{@const connectionType = CONNECTION_TYPES.find(
|
||||
(t) => t.value === newConnectionType
|
||||
)}
|
||||
<div class="bg-surface-500/25 flex flex-col gap-2 rounded p-4 mt-4">
|
||||
<div
|
||||
class="bg-surface-500/25 mt-4 flex flex-col gap-2 rounded p-4"
|
||||
>
|
||||
<span class="preset-filled-primary-500 p-2">
|
||||
Difficulty: {connectionType?.difficulty}
|
||||
</span>
|
||||
|
|
@ -608,7 +634,8 @@
|
|||
</header>
|
||||
<article>
|
||||
<p class="opacity-60">
|
||||
Are you sure you want to delete this connection? This cannot be undone.
|
||||
Are you sure you want to delete this connection? This cannot be
|
||||
undone.
|
||||
</p>
|
||||
</article>
|
||||
<footer class="flex justify-end gap-4">
|
||||
|
|
|
|||
|
|
@ -5,15 +5,24 @@
|
|||
import { appVersion, appVersionDisplay } from "$lib/shared/constants/version"
|
||||
import * as Icons from "@lucide/svelte"
|
||||
import { page } from "$app/state"
|
||||
import * as skio from "sveltekit-io"
|
||||
|
||||
interface Props {
|
||||
onclose?: () => Promise<boolean> | undefined
|
||||
}
|
||||
let { onclose = $bindable() }: Props = $props()
|
||||
|
||||
const socket = skio.get()
|
||||
|
||||
let isDarkMode = $state(false)
|
||||
let selectedTheme: string = $state("")
|
||||
let themeCtx: ThemeCtx = $state(getContext("themeCtx"))
|
||||
let systemSettingsCtx: SystemSettingsCtx = $state(getContext("systemSettingsCtx"))
|
||||
|
||||
$effect(() => {
|
||||
const _s = {...$state.snapshot(systemSettingsCtx)}
|
||||
console.log("SettingsSidebar systemSettingsCtx", $state.snapshot(systemSettingsCtx))
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
isDarkMode = themeCtx.mode === "dark"
|
||||
|
|
@ -34,6 +43,13 @@
|
|||
// TODO use setTheme socket call
|
||||
}
|
||||
|
||||
async function onOllamaManagerEnabledClick(event: { checked: boolean }) {
|
||||
const res: Sockets.UpdateOllamaManagerEnabled.Call = {
|
||||
enabled: event.checked
|
||||
}
|
||||
socket.emit("updateOllamaManagerEnabled", res)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
onclose = async () => {
|
||||
return true
|
||||
|
|
@ -85,6 +101,14 @@
|
|||
></Switch>
|
||||
<label for="dark-mode" class="font-semibold">Dark Mode</label>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Switch
|
||||
name="dark-mode"
|
||||
checked={systemSettingsCtx.settings.ollamaManagerEnabled}
|
||||
onCheckedChange={onOllamaManagerEnabledClick}
|
||||
></Switch>
|
||||
<label for="dark-mode" class="font-semibold">Enable Ollama Manager</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="about-section mt-6 rounded-lg bg-surface-500/25 p-4 shadow-md flex flex-col gap-2 items-start">
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ export async function loadSocketsClient({domain}:{domain: string}) {
|
|||
|
||||
const res = await axios.get("/api/sockets-endpoint")
|
||||
const serverUrl = new URL(res.data.endpoint)
|
||||
console.log("Server URL:", serverUrl)
|
||||
const host = `${serverUrl.protocol}//${domain}:${serverUrl.port}`
|
||||
console.log("Connecting to socket server at:", host)
|
||||
|
||||
|
|
@ -18,8 +17,4 @@ export async function loadSocketsClient({domain}:{domain: string}) {
|
|||
if (typeof io.to !== "function") {
|
||||
io.to = () => ({ emit: () => {} })
|
||||
}
|
||||
|
||||
if (dev) {
|
||||
console.log("Client socket initialized:", host)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
LMStudioClient,
|
||||
type OngoingPrediction
|
||||
} from "@lmstudio/sdk"
|
||||
import { CONNECTION_TYPE } from "$lib/shared/constants/ConnectionTypes"
|
||||
|
||||
class LMStudioAdapter extends BaseConnectionAdapter {
|
||||
private _client?: LMStudioClient
|
||||
|
|
@ -331,6 +332,7 @@ class LMStudioAdapter extends BaseConnectionAdapter {
|
|||
}
|
||||
|
||||
const connectionDefaults = {
|
||||
type: CONNECTION_TYPE.LM_STUDIO,
|
||||
baseUrl: "ws://localhost:1234",
|
||||
promptFormat: PromptFormats.VICUNA,
|
||||
tokenCounter: TokenCounterOptions.ESTIMATE, // Use Gemma tokenizer for better accuracy with Gemma models
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
} from "./BaseConnectionAdapter"
|
||||
import axios from "axios"
|
||||
import { Readable } from "stream"
|
||||
import { CONNECTION_TYPE } from "$lib/shared/constants/ConnectionTypes"
|
||||
|
||||
// GET /health
|
||||
export type HealthResponse =
|
||||
|
|
@ -411,6 +412,7 @@ class LlamaCppAdapter extends BaseConnectionAdapter {
|
|||
}
|
||||
|
||||
const connectionDefaults = {
|
||||
type: CONNECTION_TYPE.LLAMACPP_COMPLETION,
|
||||
baseUrl: "http://localhost:8080/",
|
||||
promptFormat: PromptFormats.VICUNA,
|
||||
tokenCounter: TokenCounterOptions.ESTIMATE,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
type AdapterExports,
|
||||
type BaseChat
|
||||
} from "./BaseConnectionAdapter"
|
||||
import { CONNECTION_TYPE } from "$lib/shared/constants/ConnectionTypes"
|
||||
|
||||
class OllamaAdapter extends BaseConnectionAdapter {
|
||||
private _client?: Ollama
|
||||
|
|
@ -288,6 +289,7 @@ class OllamaAdapter extends BaseConnectionAdapter {
|
|||
}
|
||||
|
||||
const connectionDefaults = {
|
||||
type: CONNECTION_TYPE.OLLAMA,
|
||||
baseUrl: "http://localhost:11434/",
|
||||
promptFormat: PromptFormats.VICUNA,
|
||||
tokenCounter: TokenCounterOptions.ESTIMATE,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type {
|
|||
ChatCompletionCreateParamsBase,
|
||||
ChatCompletionMessageParam
|
||||
} from "../../../../node_modules/openai/src/resources/chat/completions/completions"
|
||||
import { CONNECTION_TYPE } from "$lib/shared/constants/ConnectionTypes"
|
||||
|
||||
export class OpenAIChatAdapter extends BaseConnectionAdapter {
|
||||
constructor({
|
||||
|
|
@ -184,6 +185,7 @@ export class OpenAIChatAdapter extends BaseConnectionAdapter {
|
|||
}
|
||||
|
||||
const connectionDefaults = {
|
||||
type: CONNECTION_TYPE.OPENAI_CHAT,
|
||||
baseUrl: "",
|
||||
promptFormat: PromptFormats.VICUNA,
|
||||
tokenCounter: TokenCounterOptions.ESTIMATE,
|
||||
|
|
|
|||
|
|
@ -300,6 +300,19 @@ Story history:
|
|||
console.error("Error syncing database defaults:", error)
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await db.query.systemSettings.findFirst({where: (s, {eq}) => eq(s.id, 1)})
|
||||
if (!res) {
|
||||
await db.insert(schema.systemSettings).values({
|
||||
id: 1,
|
||||
ollamaManagerEnabled: true,
|
||||
ollamaManagerBaseUrl: "http://localhost:11434/"
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error syncing system settings:", error)
|
||||
}
|
||||
|
||||
const tables = [
|
||||
"chat_messages",
|
||||
"chats",
|
||||
|
|
|
|||
|
|
@ -564,3 +564,12 @@ export const chatLorebooksRelations = relations(chatLorebooks, ({ one }) => ({
|
|||
references: [lorebooks.id]
|
||||
})
|
||||
}))
|
||||
|
||||
/**
|
||||
* Singleton table for system-wide settings
|
||||
*/
|
||||
export const systemSettings = pgTable("system_settings", {
|
||||
id: integer("id").primaryKey().generatedByDefaultAsIdentity(),
|
||||
ollamaManagerEnabled: boolean("ollama_manager_enabled").notNull().default(false),
|
||||
ollamaManagerBaseUrl: text("ollama_base_url").notNull().default("http://localhost:11434/"),
|
||||
})
|
||||
|
|
@ -1,8 +1,12 @@
|
|||
import { db } from "$lib/server/db"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { and, eq } from "drizzle-orm"
|
||||
import * as schema from "$lib/server/db/schema"
|
||||
import { user as loadUser, user } from "./users"
|
||||
import { getConnectionAdapter } from "../utils/getConnectionAdapter"
|
||||
import { Ollama } from "ollama"
|
||||
import ollamaAdapter from "$lib/server/connectionAdapters/OllamaAdapter"
|
||||
import { OllamaModelSearchSource } from "$lib/shared/constants/OllamaModelSource"
|
||||
|
||||
|
||||
// --- CONNECTIONS SOCKET HANDLERS ---
|
||||
|
||||
|
|
@ -230,3 +234,457 @@ export async function refreshModels(
|
|||
emitToUser("refreshModels", res)
|
||||
}
|
||||
}
|
||||
|
||||
// --- OLLAMA SPECIFIC FUNCTIONS ---
|
||||
|
||||
export async function ollamaSetBaseUrl(
|
||||
socket: any,
|
||||
message: Sockets.OllamaSetBaseUrl.Call,
|
||||
emitToUser: (event: string, data: any) => void
|
||||
) {
|
||||
try {
|
||||
// This would typically update the active Ollama connection's baseUrl
|
||||
// For now, we'll just validate the URL format
|
||||
const url = new URL(message.baseUrl)
|
||||
if (!["http:", "https:"].includes(url.protocol)) {
|
||||
emitToUser("error", {
|
||||
error: 'Invalid URL protocol'
|
||||
})
|
||||
throw new Error('Invalid URL protocol')
|
||||
}
|
||||
|
||||
await db.update(schema.systemSettings).set({
|
||||
ollamaManagerBaseUrl: message.baseUrl
|
||||
})
|
||||
|
||||
const res: Sockets.OllamaSetBaseUrl.Response = {
|
||||
success: true
|
||||
}
|
||||
emitToUser("ollamaSetBaseUrl", res)
|
||||
} catch (error: any) {
|
||||
console.error("Ollama set base URL error:", error)
|
||||
const res = {
|
||||
error: "Failed to set base URL"
|
||||
}
|
||||
emitToUser("error", res)
|
||||
}
|
||||
}
|
||||
|
||||
export async function ollamaModelsList(
|
||||
socket: any,
|
||||
message: Sockets.OllamaModelsList.Call,
|
||||
emitToUser: (event: string, data: any) => void
|
||||
) {
|
||||
try {
|
||||
const { ollamaManagerBaseUrl: baseUrl } = (await db.query.systemSettings.findFirst())!
|
||||
const ollama = new Ollama({
|
||||
host: baseUrl
|
||||
})
|
||||
|
||||
const result = await ollama.list()
|
||||
const res: Sockets.OllamaModelsList.Response = {
|
||||
models: result.models || []
|
||||
}
|
||||
emitToUser("ollamaModelsList", res)
|
||||
} catch (error: any) {
|
||||
console.error("Ollama models list error:", error)
|
||||
const res = {
|
||||
error: "Failed to list models",
|
||||
}
|
||||
emitToUser("error", res)
|
||||
}
|
||||
}
|
||||
|
||||
export async function ollamaDeleteModel(
|
||||
socket: any,
|
||||
message: Sockets.OllamaDeleteModel.Call,
|
||||
emitToUser: (event: string, data: any) => void
|
||||
) {
|
||||
try {
|
||||
const { ollamaManagerBaseUrl: baseUrl } = (await db.query.systemSettings.findFirst())!
|
||||
const ollama = new Ollama({
|
||||
host: baseUrl
|
||||
})
|
||||
|
||||
await ollama.delete({ model: message.modelName })
|
||||
const res: Sockets.OllamaDeleteModel.Response = {
|
||||
success: true
|
||||
}
|
||||
emitToUser("ollamaDeleteModel", res)
|
||||
|
||||
await db.delete(schema.connections).where(and(
|
||||
eq(schema.connections.type, "ollama"),
|
||||
eq(schema.connections.model, message.modelName)
|
||||
))
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("Ollama delete model error:", error)
|
||||
const res: Sockets.OllamaDeleteModel.Response = {
|
||||
success: false
|
||||
}
|
||||
emitToUser("error", {error: "Failed to delete model"})
|
||||
}
|
||||
}
|
||||
|
||||
export async function ollamaConnectModel(
|
||||
socket: any,
|
||||
message: Sockets.OllamaConnectModel.Call,
|
||||
emitToUser: (event: string, data: any) => void
|
||||
) {
|
||||
|
||||
const userId = 1
|
||||
|
||||
try {
|
||||
|
||||
let existingConnection = await db.query.connections.findFirst({
|
||||
where: (c, { eq }) => and(
|
||||
eq(c.type, "ollama"),
|
||||
eq(c.model, message.modelName)
|
||||
)
|
||||
})
|
||||
|
||||
if (!existingConnection) {
|
||||
// Parse and create a shorter name for the connection
|
||||
const connectionName: string = message.modelName.split("/").pop()! as string
|
||||
// Create a new connection if it doesn't exist
|
||||
const data = {
|
||||
...ollamaAdapter.connectionDefaults,
|
||||
name: connectionName,
|
||||
model: message.modelName,
|
||||
}
|
||||
console.log("Creating connection", data)
|
||||
const [newConnection] = await db.insert(schema.connections).values(data as InsertConnection).returning()
|
||||
existingConnection = newConnection
|
||||
}
|
||||
|
||||
await db.update(schema.users)
|
||||
.set({
|
||||
activeConnectionId: existingConnection.id
|
||||
}).where(eq(schema.users.id, userId))
|
||||
|
||||
await loadUser(socket, {}, emitToUser)
|
||||
await connectionsList(socket, {}, emitToUser)
|
||||
|
||||
const res: Sockets.OllamaConnectModel.Response = {
|
||||
success: true
|
||||
}
|
||||
emitToUser("ollamaConnectModel", res)
|
||||
} catch (error: any) {
|
||||
console.error("Ollama connect model error:", error)
|
||||
const res = {
|
||||
error: "Failed to connect to model"
|
||||
}
|
||||
emitToUser("error", res)
|
||||
}
|
||||
}
|
||||
|
||||
export async function ollamaListRunningModels(
|
||||
socket: any,
|
||||
message: Sockets.OllamaListRunningModels.Call,
|
||||
emitToUser: (event: string, data: any) => void
|
||||
) {
|
||||
try {
|
||||
const { ollamaManagerBaseUrl: baseUrl } = (await db.query.systemSettings.findFirst())!
|
||||
const ollama = new Ollama({
|
||||
host: baseUrl
|
||||
})
|
||||
|
||||
const result = await ollama.ps()
|
||||
const res: Sockets.OllamaListRunningModels.Response = {
|
||||
models: result.models || []
|
||||
}
|
||||
emitToUser("ollamaListRunningModels", res)
|
||||
} catch (error: any) {
|
||||
console.error("Ollama list running models error:", error)
|
||||
const res = {
|
||||
error: "Failed to list running models"
|
||||
}
|
||||
emitToUser("error", res)
|
||||
}
|
||||
}
|
||||
|
||||
export async function ollamaPullModel(
|
||||
socket: any,
|
||||
message: Sockets.OllamaPullModel.Call,
|
||||
emitToUser: (event: string, data: any) => void
|
||||
) {
|
||||
try {
|
||||
const { ollamaManagerBaseUrl: baseUrl } = (await db.query.systemSettings.findFirst())!
|
||||
const ollama = new Ollama({
|
||||
host: baseUrl
|
||||
})
|
||||
|
||||
// For streaming progress, we could implement progress callbacks
|
||||
const stream = await ollama.pull({
|
||||
model: message.modelName,
|
||||
stream: true
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
// Emit progress updates
|
||||
if (chunk.status) {
|
||||
emitToUser("ollamaPullProgress", {
|
||||
modelName: message.modelName,
|
||||
status: chunk.status,
|
||||
completed: chunk.completed,
|
||||
total: chunk.total
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const res: Sockets.OllamaPullModel.Response = {
|
||||
success: true
|
||||
}
|
||||
emitToUser("ollamaPullModel", res)
|
||||
} catch (error: any) {
|
||||
console.error("Ollama pull model error:", error)
|
||||
const res = {
|
||||
error: "Failed to download model"
|
||||
}
|
||||
emitToUser("error", res)
|
||||
}
|
||||
}
|
||||
|
||||
export async function ollamaVersion(
|
||||
socket: any,
|
||||
message: Sockets.OllamaVersion.Call,
|
||||
emitToUser: (event: string, data: any) => void
|
||||
) {
|
||||
try {
|
||||
|
||||
const { ollamaManagerBaseUrl: baseUrl } = (await db.query.systemSettings.findFirst())!
|
||||
// const ollama = new Ollama({
|
||||
// host: baseUrl
|
||||
// })
|
||||
const response = await fetch(`${baseUrl}/api/version`)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
const res: Sockets.OllamaVersion.Response = {
|
||||
version: result.version
|
||||
}
|
||||
emitToUser("ollamaVersion", res)
|
||||
} catch (error: any) {
|
||||
console.error("Ollama version error:", error)
|
||||
const res = {
|
||||
error: "Failed to connect to Ollama or get version"
|
||||
}
|
||||
emitToUser("error", res)
|
||||
}
|
||||
}
|
||||
|
||||
export async function ollamaIsUpdateAvailable(
|
||||
socket: any,
|
||||
message: Sockets.OllamaIsUpdateAvailable.Call,
|
||||
emitToUser: (event: string, data: any) => void
|
||||
) {
|
||||
try {
|
||||
// Get current version using direct HTTP request
|
||||
const { ollamaManagerBaseUrl: baseUrl } = (await db.query.systemSettings.findFirst())!
|
||||
const versionResponse = await fetch(`${baseUrl}/api/version`)
|
||||
|
||||
if (!versionResponse.ok) {
|
||||
throw new Error(`HTTP ${versionResponse.status}: ${versionResponse.statusText}`)
|
||||
}
|
||||
|
||||
const versionResult = await versionResponse.json()
|
||||
const currentVersion = versionResult.version
|
||||
|
||||
// Fetch the latest version from Ollama's GitHub releases API
|
||||
const githubResponse = await fetch('https://api.github.com/repos/ollama/ollama/releases/latest')
|
||||
|
||||
if (!githubResponse.ok) {
|
||||
throw new Error(`GitHub API error: ${githubResponse.status}`)
|
||||
}
|
||||
|
||||
const latestRelease = await githubResponse.json()
|
||||
const latestVersion = latestRelease.tag_name
|
||||
|
||||
// Compare versions (remove 'v' prefix if present)
|
||||
const currentVersionClean = currentVersion.replace(/^v/, '')
|
||||
const latestVersionClean = latestVersion.replace(/^v/, '')
|
||||
|
||||
// Simple version comparison (works for semantic versioning)
|
||||
const updateAvailable = compareVersions(latestVersionClean, currentVersionClean) > 0
|
||||
|
||||
const res: Sockets.OllamaIsUpdateAvailable.Response = {
|
||||
updateAvailable,
|
||||
currentVersion: currentVersion,
|
||||
latestVersion: latestVersion
|
||||
}
|
||||
emitToUser("ollamaIsUpdateAvailable", res)
|
||||
} catch (error: any) {
|
||||
console.error("Ollama update check error:", error)
|
||||
const res = {
|
||||
error: "Failed to check for updates"
|
||||
}
|
||||
emitToUser("error", res)
|
||||
}
|
||||
}
|
||||
|
||||
export async function ollamaSearchAvailableModels(
|
||||
socket: any,
|
||||
message: Sockets.OllamaSearchAvailableModels.Call,
|
||||
emitToUser: (event: string, data: any) => void
|
||||
) {
|
||||
try {
|
||||
const { search, source } = message
|
||||
let models: Array<{
|
||||
name: string
|
||||
description?: string
|
||||
size?: string
|
||||
tags?: string[]
|
||||
popular?: boolean
|
||||
url?: string
|
||||
downloads?: number
|
||||
updatedAtStr?: string
|
||||
createdAt?: Date
|
||||
likes?: number
|
||||
trendingScore?: number
|
||||
}> = []
|
||||
|
||||
if (source === OllamaModelSearchSource.OLLAMA_DB) {
|
||||
const response = await fetch(`https://ollamadb.dev/api/v1/models?limit=25&search=${encodeURIComponent(search)}`)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`OllamaDB API error: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// console.log("OllamaDB response:", data)
|
||||
|
||||
// Transform ollamadb.dev response to our format
|
||||
models = (data.models || []).map((model: any) => ({
|
||||
name: model.model_identifier || model.model_name,
|
||||
description: model.description,
|
||||
size: model.size,
|
||||
url: model.url,
|
||||
downloads: model.pulls,
|
||||
updatedAtStr: model.last_updated_str,
|
||||
}))
|
||||
} else if (source === OllamaModelSearchSource.HUGGING_FACE) {
|
||||
const response = await fetch(
|
||||
`https://huggingface.co/api/models?search=${encodeURIComponent(search)}&filter=gguf&limit=25&sort=trendingScore`
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Hugging Face API error: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// console.log("Hugging Face response:", data)
|
||||
|
||||
// Transform Hugging Face response to our format
|
||||
models = (data || []).map((model: any) => ({
|
||||
name: model.id || model.modelId,
|
||||
description: model.description || model.pipeline_tag,
|
||||
size: undefined, // Hugging Face doesn't provide size in search
|
||||
tags: model.tags || [],
|
||||
popular: model.likes > 100 || false,
|
||||
url: `https://hf.co/${model.id || model.modelId}`,
|
||||
createdAt: model.createdAt,
|
||||
downloads: model.downloads,
|
||||
likes: model.likes,
|
||||
trendingScore: model.trendingScore,
|
||||
}))
|
||||
}
|
||||
|
||||
const res: Sockets.OllamaSearchAvailableModels.Response = {
|
||||
models
|
||||
}
|
||||
emitToUser("ollamaSearchAvailableModels", res)
|
||||
} catch (error: any) {
|
||||
console.error("Ollama search available models error:", error)
|
||||
const res: Sockets.OllamaSearchAvailableModels.Response = {
|
||||
models: [],
|
||||
error: "Failed to search available models"
|
||||
}
|
||||
emitToUser("ollamaSearchAvailableModels", res)
|
||||
}
|
||||
}
|
||||
|
||||
export async function ollamaHuggingFaceSiblingsList(
|
||||
socket: any,
|
||||
message: Sockets.OllamaHuggingFaceSiblingsList.Call,
|
||||
emitToUser: (event: string, data: any) => void
|
||||
) {
|
||||
try {
|
||||
const { modelId } = message
|
||||
|
||||
// Fetch the model details from Hugging Face API
|
||||
const response = await fetch(`https://huggingface.co/api/models/${encodeURIComponent(modelId)}`)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Hugging Face API error: ${response.status}`)
|
||||
}
|
||||
|
||||
const modelData = await response.json()
|
||||
|
||||
console.log("Hugging Face model data:", modelData)
|
||||
|
||||
// Get siblings from the model data
|
||||
const siblings = modelData.siblings || []
|
||||
|
||||
// Transform siblings to our format and extract quantization info
|
||||
const transformedSiblings = siblings.map((sibling: any) => {
|
||||
// Extract quantization level from filename if it's a GGUF file
|
||||
let quantization: string | undefined
|
||||
if (sibling.rfilename && sibling.rfilename.includes('.gguf')) {
|
||||
// Common quantization patterns: Q4_0, Q4_K_M, Q5_K_S, Q8_0, etc.
|
||||
const quantMatch = sibling.rfilename.match(/[Qq](\d+)(?:_[KkMmSs])?(?:_[MmSs])?/i)
|
||||
if (quantMatch) {
|
||||
quantization = quantMatch[0].toUpperCase()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
rfilename: sibling.rfilename,
|
||||
size: sibling.size,
|
||||
quantization: quantization
|
||||
}
|
||||
})
|
||||
|
||||
// Filter to only include GGUF files
|
||||
const ggufSiblings = transformedSiblings.filter((sibling: any) =>
|
||||
sibling.rfilename && sibling.rfilename.toLowerCase().includes('.gguf') && !!sibling.quantization
|
||||
)
|
||||
|
||||
const res: Sockets.OllamaHuggingFaceSiblingsList.Response = {
|
||||
baseUrl: `https://hf.com/${modelData.id || modelData.modelId}`,
|
||||
siblings: ggufSiblings
|
||||
}
|
||||
emitToUser("ollamaHuggingFaceSiblingsList", res)
|
||||
} catch (error: any) {
|
||||
console.error("Ollama Hugging Face siblings list error:", error)
|
||||
const res: Sockets.OllamaHuggingFaceSiblingsList.Response = {
|
||||
siblings: [],
|
||||
error: "Failed to fetch model siblings"
|
||||
}
|
||||
emitToUser("ollamaHuggingFaceSiblingsList", res)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to compare semantic versions
|
||||
function compareVersions(version1: string, version2: string): number {
|
||||
const v1parts = version1.split('.').map(Number)
|
||||
const v2parts = version2.split('.').map(Number)
|
||||
|
||||
const maxLength = Math.max(v1parts.length, v2parts.length)
|
||||
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
const v1part = v1parts[i] || 0
|
||||
const v2part = v2parts[i] || 0
|
||||
|
||||
if (v1part > v2part) return 1
|
||||
if (v1part < v2part) return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,17 @@ import {
|
|||
setUserActiveConnection,
|
||||
testConnection,
|
||||
refreshModels,
|
||||
createConnection
|
||||
createConnection,
|
||||
ollamaSetBaseUrl,
|
||||
ollamaModelsList,
|
||||
ollamaDeleteModel,
|
||||
ollamaListRunningModels,
|
||||
ollamaPullModel,
|
||||
ollamaVersion,
|
||||
ollamaIsUpdateAvailable,
|
||||
ollamaConnectModel,
|
||||
ollamaSearchAvailableModels,
|
||||
ollamaHuggingFaceSiblingsList
|
||||
} from "./connections"
|
||||
import {
|
||||
sampling,
|
||||
|
|
@ -91,6 +101,7 @@ import {
|
|||
iterateNextHistoryEntry,
|
||||
lorebookImport
|
||||
} from "./lorebooks"
|
||||
import { updateOllamaManagerEnabled, systemSettings } from "./systemSettings"
|
||||
|
||||
const userId = 1 // Replace with actual user id
|
||||
|
||||
|
|
@ -126,6 +137,23 @@ export function connectSockets(io: {
|
|||
register(socket, setUserActiveConnection, emitToUser)
|
||||
register(socket, testConnection, emitToUser)
|
||||
register(socket, refreshModels, emitToUser)
|
||||
register(socket, ollamaConnectModel, emitToUser)
|
||||
register(socket, ollamaSearchAvailableModels, emitToUser)
|
||||
register(socket, ollamaHuggingFaceSiblingsList, emitToUser)
|
||||
|
||||
// Ollama
|
||||
register(socket, ollamaSetBaseUrl, emitToUser)
|
||||
register(socket, ollamaModelsList, emitToUser)
|
||||
register(socket, ollamaDeleteModel, emitToUser)
|
||||
register(socket, ollamaListRunningModels, emitToUser)
|
||||
register(socket, ollamaPullModel, emitToUser)
|
||||
register(socket, ollamaVersion, emitToUser)
|
||||
register(socket, ollamaIsUpdateAvailable, emitToUser)
|
||||
|
||||
// App Settings
|
||||
register(socket, systemSettings, emitToUser)
|
||||
register(socket, updateOllamaManagerEnabled, emitToUser)
|
||||
|
||||
// Characters
|
||||
register(socket, characterList, emitToUser)
|
||||
register(socket, character, emitToUser)
|
||||
|
|
@ -193,7 +221,7 @@ export function connectSockets(io: {
|
|||
register(socket, updateHistoryEntry, emitToUser)
|
||||
register(socket, deleteHistoryEntry, emitToUser)
|
||||
register(socket, iterateNextHistoryEntry, emitToUser)
|
||||
register(socket, lorebookImport, emitToUser)
|
||||
register(socket, lorebookImport, emitToUser)
|
||||
console.log(`Socket connected: ${socket.id} for user ${userId}`)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
59
src/lib/server/sockets/systemSettings.ts
Normal file
59
src/lib/server/sockets/systemSettings.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { db } from "$lib/server/db"
|
||||
import * as schema from "$lib/server/db/schema"
|
||||
import { eq } from "drizzle-orm"
|
||||
|
||||
export async function systemSettings(
|
||||
socket: any,
|
||||
message: Sockets.SystemSettings.Call,
|
||||
emitToUser: (event: string, data: any) => void
|
||||
) {
|
||||
try {
|
||||
const settings = await db.query.systemSettings.findFirst({
|
||||
where: eq(schema.systemSettings.id, 1),
|
||||
columns: {
|
||||
id: false, // We don't need the ID in the response
|
||||
}
|
||||
})
|
||||
|
||||
if (!settings) {
|
||||
throw new Error("System settings not found")
|
||||
}
|
||||
|
||||
const res: Sockets.SystemSettings.Response = {
|
||||
systemSettings: settings
|
||||
}
|
||||
|
||||
emitToUser("systemSettings", res)
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching system settings:", error)
|
||||
emitToUser("error", {
|
||||
error: "Failed to fetch system settings"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateOllamaManagerEnabled(
|
||||
socket: any,
|
||||
message: Sockets.UpdateOllamaManagerEnabled.Call,
|
||||
emitToUser: (event: string, data: any) => void
|
||||
) {
|
||||
try {
|
||||
await db.update(schema.systemSettings).set({
|
||||
ollamaManagerEnabled: message.enabled
|
||||
}).where(eq(schema.systemSettings.id, 1))
|
||||
|
||||
const res: Sockets.UpdateOllamaManagerEnabled.Response = {
|
||||
success: true,
|
||||
enabled: message.enabled
|
||||
}
|
||||
emitToUser("updateOllamaManagerEnabled", res)
|
||||
await systemSettings(socket, {}, emitToUser) // Refresh system settings after update
|
||||
} catch (error: any) {
|
||||
console.error("Update Ollama Manager enabled error:", error)
|
||||
const res = {
|
||||
error: "Failed to update Ollama Manager setting"
|
||||
}
|
||||
emitToUser("error", res)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import { db } from "$lib/server/db"
|
|||
import { eq } from "drizzle-orm"
|
||||
import * as schema from "$lib/server/db/schema"
|
||||
|
||||
export async function user(socket: any, message: {}, emitToUser: (event: string, data: any) => void) {
|
||||
export async function user(socket: any, message: Sockets.User.Call, emitToUser: (event: string, data: any) => void) {
|
||||
const userId = 1 // Replace with actual user id
|
||||
const user = await db.query.users.findFirst({
|
||||
where: (u, { eq }) => eq(u.id, userId),
|
||||
|
|
|
|||
9
src/lib/shared/constants/OllamaModelSource.ts
Normal file
9
src/lib/shared/constants/OllamaModelSource.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export class OllamaModelSearchSource {
|
||||
static OLLAMA_DB = "ollamadb"
|
||||
static HUGGING_FACE = "huggingface"
|
||||
|
||||
static options = [
|
||||
{ value: OllamaModelSearchSource.HUGGING_FACE, label: "Hugging Face" },
|
||||
{ value: OllamaModelSearchSource.OLLAMA_DB, label: "ollamadb.dev" },
|
||||
]
|
||||
}
|
||||
7
static/ollama-dark.svg
Normal file
7
static/ollama-dark.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.4 KiB |
7
static/ollama.svg
Normal file
7
static/ollama.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.4 KiB |
Loading…
Add table
Add a link
Reference in a new issue