mirror of
https://github.com/open-webui/desktop.git
synced 2026-04-28 09:59:28 +00:00
refac
This commit is contained in:
parent
d1b6d15330
commit
86cf4834cd
5 changed files with 747 additions and 477 deletions
|
|
@ -59,9 +59,7 @@
|
|||
</button>
|
||||
</div>
|
||||
<div class="flex-1 flex items-center justify-center">
|
||||
{#if activeConnectionName}
|
||||
<span class="text-[11px] opacity-80">{activeConnectionName}</span>
|
||||
{/if}
|
||||
<span class="text-[11px] opacity-80">{activeConnectionName || 'Open WebUI'}</span>
|
||||
</div>
|
||||
{#if isLocalConnection}
|
||||
<div class="pr-3 shrink-0 flex items-center">
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import { fly, fade } from 'svelte/transition'
|
||||
import { connections, config, appInfo, serverInfo, appState } from '../../stores'
|
||||
import LocalInstall from '../LocalInstall.svelte'
|
||||
import { tooltip } from '../../actions/tooltip'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { connections, config, serverInfo, appState } from '../../stores'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
import '@xterm/xterm/css/xterm.css'
|
||||
|
||||
import Sidebar from './Connections/Sidebar.svelte'
|
||||
import Content from './Connections/Content.svelte'
|
||||
|
||||
interface Props {
|
||||
onOpenSettings: () => void
|
||||
sidebarOpen: boolean
|
||||
|
|
@ -27,7 +28,7 @@
|
|||
let url = $state('')
|
||||
let connecting = $state(false)
|
||||
let error = $state('')
|
||||
let view = $state('welcome') // welcome | add | install | logs
|
||||
let view = $state('welcome') // welcome | add | install | logs | connected | open-terminal-logs
|
||||
let autoInstall = $state(false)
|
||||
let installPhase = $state('idle') // idle | working | error
|
||||
let installError = $state('')
|
||||
|
|
@ -38,6 +39,7 @@
|
|||
let connectedUrl = $state('')
|
||||
let activeConnectionId = $state('')
|
||||
let openConnections: Map<string, string> = $state(new Map())
|
||||
let localInstalled = $state(false)
|
||||
|
||||
// Terminal state (server logs)
|
||||
let terminalEl: HTMLDivElement | undefined = $state()
|
||||
|
|
@ -56,6 +58,12 @@
|
|||
|
||||
const isInitializing = $derived($appState === 'initializing')
|
||||
const hasLocal = $derived(($connections ?? []).some((c) => c.type === 'local'))
|
||||
const localConn = $derived(($connections ?? []).find((c) => c.type === 'local'))
|
||||
const remoteConnections = $derived(($connections ?? []).filter((c) => c.type !== 'local'))
|
||||
|
||||
// Open Terminal state
|
||||
let openTerminalStatus = $state<string | null>(null)
|
||||
let openTerminalInfo = $state<{ url?: string; apiKey?: string } | null>(null)
|
||||
|
||||
const startInstall = async () => {
|
||||
installPhase = 'working'
|
||||
|
|
@ -81,8 +89,8 @@
|
|||
connections.set(await window.electronAPI.getConnections())
|
||||
config.set(await window.electronAPI.getConfig())
|
||||
|
||||
// Wait for server to actually be reachable before connecting
|
||||
installStatus = 'Waiting for server to be ready…'
|
||||
// Wait for server to actually be reachable before showing connected view
|
||||
installStatus = 'Launching Open WebUI…'
|
||||
const maxWait = 120000
|
||||
const pollInterval = 2000
|
||||
const startTime = Date.now()
|
||||
|
|
@ -92,9 +100,11 @@
|
|||
await new Promise((r) => setTimeout(r, pollInterval))
|
||||
}
|
||||
|
||||
installPhase = 'idle'
|
||||
// Now connect — the server is ready
|
||||
installStatus = ''
|
||||
await window.electronAPI.connectTo('local')
|
||||
installPhase = 'idle'
|
||||
localInstalled = true
|
||||
await connect('local')
|
||||
} catch (e: any) {
|
||||
installPhase = 'error'
|
||||
installError = e?.message || 'Something went wrong'
|
||||
|
|
@ -104,10 +114,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Open Terminal state
|
||||
let openTerminalStatus = $state<string | null>(null) // null | starting | started | stopped | failed
|
||||
let openTerminalInfo = $state<{ url?: string; apiKey?: string } | null>(null)
|
||||
|
||||
const addConnection = async () => {
|
||||
if (!url.trim()) return
|
||||
let u = url.trim()
|
||||
|
|
@ -239,17 +245,14 @@
|
|||
})
|
||||
resizeObserver.observe(terminalEl)
|
||||
|
||||
// Keyboard input → PTY
|
||||
term.onData((data: string) => {
|
||||
window.electronAPI.writePty(data)
|
||||
})
|
||||
|
||||
// Connect to PTY — output comes via callback
|
||||
window.electronAPI.connectPty((data: string) => {
|
||||
term?.write(data)
|
||||
})
|
||||
|
||||
// Send initial resize
|
||||
if (term) {
|
||||
window.electronAPI.resizePty(term.cols, term.rows)
|
||||
}
|
||||
|
|
@ -331,11 +334,11 @@
|
|||
// Listen for connection:open from main process (auto-connect on launch)
|
||||
onMount(() => {
|
||||
window.electronAPI.onData((data: any) => {
|
||||
if (data.type === 'connection:open' && data.url) {
|
||||
const connId = data.connectionId ?? ''
|
||||
openConnections.set(connId, data.url)
|
||||
if (data.type === 'connection:open' && data.data?.url) {
|
||||
const connId = data.data.connectionId ?? ''
|
||||
openConnections.set(connId, data.data.url)
|
||||
openConnections = new Map(openConnections)
|
||||
connectedUrl = data.url
|
||||
connectedUrl = data.data.url
|
||||
activeConnectionId = connId
|
||||
view = 'connected'
|
||||
}
|
||||
|
|
@ -358,6 +361,11 @@
|
|||
openTerminalInfo = info
|
||||
}
|
||||
})
|
||||
|
||||
// Check if Open WebUI package is installed
|
||||
window.electronAPI.getPackageVersion('open-webui').then((v: string | null) => {
|
||||
localInstalled = v !== null
|
||||
})
|
||||
})
|
||||
|
||||
const toggleOpenTerminal = async () => {
|
||||
|
|
@ -378,465 +386,66 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toggleOtLogs = () => {
|
||||
view = view === 'open-terminal-logs' ? (activeConnectionId ? 'connected' : 'welcome') : 'open-terminal-logs'
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="h-full w-full flex bg-[#0a0a0a] text-[#fafafa]" in:fade={{ duration: 200 }}>
|
||||
<!-- Sidebar -->
|
||||
{#if sidebarOpen}
|
||||
<div
|
||||
class="w-[200px] shrink-0 flex flex-col bg-[#0a0a0a] relative"
|
||||
in:fly={{ x: -200, duration: 200 }}
|
||||
>
|
||||
<!-- Connections header -->
|
||||
<div class="flex items-center justify-between px-4 pt-2 pb-1.5">
|
||||
<span class="text-[10px] tracking-wider uppercase opacity-60">Connections</span>
|
||||
<button
|
||||
class="opacity-25 hover:opacity-60 transition bg-transparent border-none text-[#fafafa] leading-none"
|
||||
onclick={() => {
|
||||
disconnect()
|
||||
view = 'add'
|
||||
}}
|
||||
title="Add connection"
|
||||
>
|
||||
<svg
|
||||
class="w-[14px] h-[14px]"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Connection list -->
|
||||
<div class="flex-1 min-h-0 overflow-y-auto px-2">
|
||||
{#each $connections as conn (conn.id)}
|
||||
{@const isLocalDisabled = conn.type === 'local' && !serverReachable}
|
||||
<div
|
||||
class="w-full px-2 py-[6px] rounded-xl group flex items-center gap-2 transition-colors {isLocalDisabled ? 'opacity-40 cursor-default' : 'cursor-pointer'} {activeConnectionId ===
|
||||
conn.id
|
||||
? 'bg-white/[0.08]'
|
||||
: isLocalDisabled ? '' : 'hover:bg-white/[0.05]'}"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onclick={() => !isLocalDisabled && connect(conn.id)}
|
||||
onkeydown={(e) => e.key === 'Enter' && !isLocalDisabled && connect(conn.id)}
|
||||
>
|
||||
<!-- Status indicator for local connections -->
|
||||
{#if conn.type === 'local'}
|
||||
{#if serverStatus === 'starting'}
|
||||
<div class="w-[14px] h-[14px] shrink-0 flex items-center justify-center">
|
||||
<div
|
||||
class="w-2.5 h-2.5 rounded-full border-2 border-amber-400/60 border-t-transparent animate-spin"
|
||||
></div>
|
||||
</div>
|
||||
{:else if serverReachable}
|
||||
<div class="w-[14px] h-[14px] shrink-0 flex items-center justify-center">
|
||||
<div
|
||||
class="w-2 h-2 rounded-full bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.5)]"
|
||||
></div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-[14px] h-[14px] shrink-0 flex items-center justify-center">
|
||||
<div class="w-2 h-2 rounded-full bg-white/15"></div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<svg
|
||||
class="w-[14px] h-[14px] shrink-0 opacity-30"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5a17.92 17.92 0 01-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
<span
|
||||
class="text-[12px] {activeConnectionId === conn.id
|
||||
? 'opacity-90'
|
||||
: 'opacity-100'} transition-opacity truncate"
|
||||
>{conn.name}</span
|
||||
>
|
||||
|
||||
<button
|
||||
class="ml-auto opacity-0 group-hover:opacity-30 hover:!opacity-70 transition bg-transparent border-none text-[#fafafa] shrink-0"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation()
|
||||
window.electronAPI?.openInBrowser?.(conn.url)
|
||||
}}
|
||||
title="Open in browser"
|
||||
>
|
||||
<svg
|
||||
class="w-[12px] h-[12px]"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Open Terminal toggle -->
|
||||
<div class="px-2 pb-1">
|
||||
<div class="border-t border-white/[0.06] pt-2 pb-1">
|
||||
<span class="text-[10px] tracking-wider uppercase opacity-25 px-2">Services</span>
|
||||
</div>
|
||||
<button
|
||||
class="w-full flex items-center gap-2 px-2 py-[6px] rounded-xl text-[12px] transition bg-transparent border-none text-[#fafafa] text-left group {openTerminalStatus === 'started' ? 'opacity-70 hover:opacity-90' : 'opacity-40 hover:opacity-70'} {openTerminalStatus === 'starting' || openTerminalStatus === 'stopping' ? 'pointer-events-none' : ''}"
|
||||
onclick={() => {
|
||||
if (openTerminalStatus === 'started') {
|
||||
view = view === 'open-terminal-logs' ? (activeConnectionId ? 'connected' : 'welcome') : 'open-terminal-logs'
|
||||
} else {
|
||||
toggleOpenTerminal()
|
||||
}
|
||||
}}
|
||||
use:tooltip={openTerminalStatus === 'started'
|
||||
? (view === 'open-terminal-logs' ? 'Hide logs' : `Running · Click to view logs`)
|
||||
: openTerminalStatus === 'starting'
|
||||
? 'Starting…'
|
||||
: openTerminalStatus === 'failed'
|
||||
? 'Click to retry'
|
||||
: 'Start terminal server'}
|
||||
>
|
||||
<!-- Status indicator -->
|
||||
<div class="w-[14px] h-[14px] shrink-0 flex items-center justify-center">
|
||||
{#if openTerminalStatus === 'starting' || openTerminalStatus === 'stopping'}
|
||||
<div
|
||||
class="w-2.5 h-2.5 rounded-full border-2 border-amber-400/60 border-t-transparent animate-spin"
|
||||
></div>
|
||||
{:else if openTerminalStatus === 'started'}
|
||||
<div
|
||||
class="w-2 h-2 rounded-full bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.5)]"
|
||||
></div>
|
||||
{:else if openTerminalStatus === 'failed'}
|
||||
<div class="w-2 h-2 rounded-full bg-red-400/70"></div>
|
||||
{:else}
|
||||
<svg
|
||||
class="w-[14px] h-[14px] opacity-50"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="truncate">Open Terminal</span>
|
||||
<!-- Stop button (when running) -->
|
||||
{#if openTerminalStatus === 'started'}
|
||||
<button
|
||||
class="ml-auto opacity-0 group-hover:opacity-40 hover:!opacity-80 transition bg-transparent border-none text-[#fafafa] p-0 leading-none"
|
||||
onclick={(e) => { e.stopPropagation(); toggleOpenTerminal() }}
|
||||
use:tooltip={'Stop Open Terminal'}
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.636 5.636a9 9 0 1012.728 0M12 3v9" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Settings popover -->
|
||||
{#if settingsOpen}
|
||||
<div class="fixed inset-0 z-40" onclick={() => (settingsOpen = false)}></div>
|
||||
|
||||
<div
|
||||
class="absolute bottom-12 left-2 right-2 z-50 bg-[#1a1a1a]/90 backdrop-blur-xl border border-white/[0.08] rounded-2xl shadow-2xl py-0.5 overflow-hidden"
|
||||
in:fly={{ y: 8, duration: 150 }}
|
||||
out:fade={{ duration: 100 }}
|
||||
>
|
||||
<div class="px-3.5 py-2.5 border-b border-white/[0.06]">
|
||||
<div class="text-[11px] opacity-40">Open WebUI Desktop</div>
|
||||
<div class="text-[10px] opacity-20 mt-0.5">{$appInfo?.version ?? ''}</div>
|
||||
</div>
|
||||
|
||||
<div class="py-1 px-1.5">
|
||||
<button
|
||||
class="w-full flex items-center gap-2.5 px-3 py-1.5 text-left text-[12px] opacity-50 hover:opacity-90 hover:bg-white/[0.06] transition bg-transparent border-none text-[#fafafa] rounded-xl"
|
||||
onclick={() => {
|
||||
settingsOpen = false
|
||||
onOpenSettings()
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
class="w-[14px] h-[14px] shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 010 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 010-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
Settings
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="w-full flex items-center gap-2.5 px-3 py-1.5 text-left text-[12px] opacity-50 hover:opacity-90 hover:bg-white/[0.06] transition bg-transparent border-none text-[#fafafa] rounded-xl"
|
||||
onclick={openGithub}
|
||||
>
|
||||
<svg
|
||||
class="w-[14px] h-[14px] shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
|
||||
/>
|
||||
</svg>
|
||||
GitHub
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Settings button (bottom) -->
|
||||
<div class="px-2 pb-3">
|
||||
<button
|
||||
class="w-full flex items-center gap-2 px-2 py-[6px] rounded-xl text-[12px] opacity-40 hover:opacity-70 hover:bg-white/[0.05] transition bg-transparent border-none text-[#fafafa] text-left"
|
||||
onclick={() => (settingsOpen = !settingsOpen)}
|
||||
>
|
||||
<svg
|
||||
class="w-[14px] h-[14px] shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 010 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 010-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Sidebar
|
||||
{activeConnectionId}
|
||||
{localConn}
|
||||
{localInstalled}
|
||||
{remoteConnections}
|
||||
{serverStatus}
|
||||
{serverReachable}
|
||||
{openTerminalStatus}
|
||||
bind:settingsOpen
|
||||
{view}
|
||||
onConnect={connect}
|
||||
onDisconnect={disconnect}
|
||||
onAddView={() => { disconnect(); view = 'add' }}
|
||||
{onOpenSettings}
|
||||
onToggleOpenTerminal={toggleOpenTerminal}
|
||||
onToggleOtLogs={toggleOtLogs}
|
||||
{openGithub}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Main content -->
|
||||
<div
|
||||
class="flex-1 flex flex-col min-w-0 overflow-clip bg-[#111] border-t {sidebarOpen
|
||||
? 'border-l border-white/[0.08] rounded-tl-xl'
|
||||
: 'border-white/[0.10]'}"
|
||||
>
|
||||
<!-- Webviews — all open connections stay alive, only active one visible -->
|
||||
{#each [...openConnections] as [connId, connUrl] (connId)}
|
||||
<webview
|
||||
src={connUrl}
|
||||
class="flex-1 min-h-0 border-none"
|
||||
style="display: {view === 'connected' && activeConnectionId === connId ? 'flex' : 'none'}"
|
||||
allowpopups
|
||||
partition="persist:connection-{connId}"
|
||||
></webview>
|
||||
{/each}
|
||||
|
||||
{#if view === 'logs'}
|
||||
<!-- Terminal / Logs -->
|
||||
<div
|
||||
class="flex-1 min-h-0 overflow-hidden px-3 py-2 bg-[#0a0a0a]"
|
||||
bind:this={terminalEl}
|
||||
></div>
|
||||
{:else if view === 'open-terminal-logs'}
|
||||
<!-- Open Terminal Logs -->
|
||||
<div class="flex-1 min-h-0 flex flex-col bg-[#0a0a0a]">
|
||||
<div class="flex items-center justify-between px-3 py-1.5 border-b border-white/[0.06]">
|
||||
<span class="text-[11px] opacity-40">Open Terminal Logs</span>
|
||||
<button
|
||||
class="text-[11px] opacity-30 hover:opacity-60 transition bg-transparent border-none text-[#fafafa]"
|
||||
onclick={() => { view = activeConnectionId ? 'connected' : 'welcome' }}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="flex-1 min-h-0 overflow-hidden px-3 py-2"
|
||||
bind:this={otTerminalEl}
|
||||
></div>
|
||||
</div>
|
||||
{:else if view !== 'connected'}
|
||||
{#if isInitializing}
|
||||
<div class="px-5 py-1.5 text-[11px] opacity-25">
|
||||
Setting up…{$serverInfo?.status ? ` ${$serverInfo.status}` : ''}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex-1 flex items-center justify-center px-6 relative overflow-hidden">
|
||||
{#if view === 'welcome'}
|
||||
{#if ($connections ?? []).length > 0}
|
||||
<div class="text-center max-w-[320px]" in:fade={{ duration: 200 }}>
|
||||
<div class="text-lg opacity-80 mb-1.5">Open WebUI</div>
|
||||
<div class="text-[12px] opacity-30 mb-6">
|
||||
Select a connection to get started
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Video background -->
|
||||
<video
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
class="absolute inset-0 w-full h-full object-cover opacity-20 pointer-events-none"
|
||||
>
|
||||
<source src="https://community.s3.openwebui.com/landing.mp4" type="video/mp4" />
|
||||
</video>
|
||||
|
||||
<!-- Gradient overlay -->
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-[#111] via-[#111]/60 to-transparent pointer-events-none"></div>
|
||||
|
||||
<!-- Content positioned bottom-left -->
|
||||
<div class="absolute bottom-0 left-0 right-0 p-10" in:fade={{ duration: 300 }}>
|
||||
<div class="max-w-sm">
|
||||
<div class="text-3xl font-medium mb-3 tracking-tight">Open WebUI</div>
|
||||
<div class="text-base opacity-50 mb-8 leading-relaxed">
|
||||
Connect to an Open WebUI server, or set one up on this machine.
|
||||
</div>
|
||||
|
||||
{#if !hasLocal}
|
||||
<button
|
||||
class="inline-flex items-center gap-2 bg-white px-6 py-2 rounded-xl text-black text-[13px] transition hover:bg-gray-100 border-none disabled:opacity-50"
|
||||
onclick={startInstall}
|
||||
disabled={installPhase === 'working'}
|
||||
>
|
||||
{#if installPhase === 'working'}
|
||||
<div class="w-3.5 h-3.5 rounded-full border-2 border-black/30 border-t-black animate-spin"></div>
|
||||
Installing…
|
||||
{:else if installPhase === 'error'}
|
||||
Retry
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M20.015 4.356v4.992m0 0h-4.992m4.993 0l-3.181-3.183a8.25 8.25 0 00-13.803 3.7" />
|
||||
</svg>
|
||||
{:else}
|
||||
Get Started
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if installPhase === 'working' && installStatus}
|
||||
<div class="mt-3 text-[12px] opacity-40 font-mono" in:fade={{ duration: 200 }}>
|
||||
{installStatus}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="mt-6">
|
||||
<button
|
||||
class="text-sm opacity-40 hover:opacity-70 transition bg-transparent border-none text-[#fafafa]"
|
||||
onclick={() => (view = 'add')}
|
||||
>
|
||||
Connect to existing server →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Error toast -->
|
||||
{#if toastVisible && installError}
|
||||
<div
|
||||
class="absolute top-4 left-1/2 -translate-x-1/2 z-50 bg-red-500/90 backdrop-blur-sm text-white text-[12px] px-4 py-2 rounded-xl shadow-lg"
|
||||
in:fly={{ y: -10, duration: 200 }}
|
||||
out:fade={{ duration: 150 }}
|
||||
>
|
||||
{installError}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if view === 'add'}
|
||||
<div class="w-full max-w-[260px]" in:fade={{ duration: 150 }}>
|
||||
<div class="text-base opacity-70 mb-4">New Connection</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={url}
|
||||
placeholder="e.g. https://your-server.com"
|
||||
class="w-full px-4 py-2.5 rounded-xl bg-white/[0.06] text-[13px] text-[#fafafa] placeholder:opacity-20 outline-none focus:bg-white/[0.1] transition no-drag border-none"
|
||||
onkeydown={(e) => e.key === 'Enter' && addConnection()}
|
||||
/>
|
||||
|
||||
{#if error}
|
||||
<p class="text-[11px] opacity-60">{error}</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center gap-3 mt-1">
|
||||
<button
|
||||
class="inline-flex items-center gap-2 bg-white px-5 py-2 rounded-xl text-black text-[13px] transition hover:bg-gray-100 disabled:opacity-30 border-none"
|
||||
onclick={addConnection}
|
||||
disabled={connecting || !url.trim()}
|
||||
>
|
||||
{connecting ? 'Connecting…' : 'Connect'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="text-[12px] opacity-30 hover:opacity-60 transition bg-transparent border-none text-[#fafafa]"
|
||||
onclick={() => {
|
||||
view = 'welcome'
|
||||
error = ''
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if view === 'install'}
|
||||
<div class="w-full max-w-[260px]">
|
||||
<LocalInstall
|
||||
autoStart={autoInstall}
|
||||
onBack={() => { autoInstall = false; view = 'welcome' }}
|
||||
onComplete={async () => {
|
||||
connections.set(await window.electronAPI.getConnections())
|
||||
config.set(await window.electronAPI.getConfig())
|
||||
view = 'welcome'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<Content
|
||||
{sidebarOpen}
|
||||
bind:view
|
||||
{activeConnectionId}
|
||||
{openConnections}
|
||||
{localConn}
|
||||
{localInstalled}
|
||||
{remoteConnections}
|
||||
bind:installPhase
|
||||
bind:installError
|
||||
bind:installStatus
|
||||
bind:toastVisible
|
||||
bind:url
|
||||
bind:connecting
|
||||
bind:error
|
||||
bind:autoInstall
|
||||
bind:terminalEl
|
||||
bind:otTerminalEl
|
||||
onStartInstall={startInstall}
|
||||
onAddConnection={addConnection}
|
||||
onSetView={(v) => { view = v }}
|
||||
onCopyLogs={(type) => {
|
||||
const t = type === 'server' ? term : otTerm
|
||||
if (!t) return null
|
||||
const buf = t.buffer.active
|
||||
const lines: string[] = []
|
||||
for (let i = 0; i < buf.length; i++) {
|
||||
lines.push(buf.getLine(i)?.translateToString(true) ?? '')
|
||||
}
|
||||
return lines.join('\n').trimEnd()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
284
src/renderer/src/lib/components/Main/Connections/Content.svelte
Normal file
284
src/renderer/src/lib/components/Main/Connections/Content.svelte
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
<script lang="ts">
|
||||
import { fade, fly } from 'svelte/transition'
|
||||
import { connections, config, serverInfo, appState } from '../../../stores'
|
||||
import LocalInstall from '../../LocalInstall.svelte'
|
||||
|
||||
interface Props {
|
||||
sidebarOpen: boolean
|
||||
view: string
|
||||
activeConnectionId: string
|
||||
openConnections: Map<string, string>
|
||||
localConn: any
|
||||
localInstalled: boolean
|
||||
remoteConnections: any[]
|
||||
installPhase: string
|
||||
installError: string
|
||||
installStatus: string
|
||||
toastVisible: boolean
|
||||
url: string
|
||||
connecting: boolean
|
||||
error: string
|
||||
autoInstall: boolean
|
||||
terminalEl?: HTMLDivElement
|
||||
otTerminalEl?: HTMLDivElement
|
||||
onStartInstall: () => void
|
||||
onAddConnection: () => void
|
||||
onSetView: (v: string) => void
|
||||
onCopyLogs: (type: 'server' | 'open-terminal') => string | null
|
||||
}
|
||||
|
||||
let {
|
||||
sidebarOpen,
|
||||
view,
|
||||
activeConnectionId,
|
||||
openConnections,
|
||||
localConn,
|
||||
localInstalled,
|
||||
remoteConnections,
|
||||
installPhase = $bindable('idle'),
|
||||
installError = $bindable(''),
|
||||
installStatus = $bindable(''),
|
||||
toastVisible = $bindable(false),
|
||||
url = $bindable(''),
|
||||
connecting = $bindable(false),
|
||||
error = $bindable(''),
|
||||
autoInstall = $bindable(false),
|
||||
terminalEl = $bindable(),
|
||||
otTerminalEl = $bindable(),
|
||||
onStartInstall,
|
||||
onAddConnection,
|
||||
onSetView,
|
||||
onCopyLogs
|
||||
}: Props = $props()
|
||||
|
||||
const isInitializing = $derived($appState === 'initializing')
|
||||
let copied = $state(false)
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex-1 flex flex-col min-w-0 overflow-clip bg-[#111] border-t {sidebarOpen
|
||||
? 'border-l border-white/[0.08] rounded-tl-xl'
|
||||
: 'border-white/[0.10]'}"
|
||||
>
|
||||
<!-- Webviews — all open connections stay alive, only active one visible -->
|
||||
{#each [...openConnections] as [connId, connUrl] (connId)}
|
||||
<webview
|
||||
src={connUrl}
|
||||
class="flex-1 min-h-0 border-none"
|
||||
style="display: {view === 'connected' && activeConnectionId === connId ? 'flex' : 'none'}"
|
||||
allowpopups
|
||||
partition="persist:connection-{connId}"
|
||||
></webview>
|
||||
{/each}
|
||||
|
||||
{#if view === 'logs'}
|
||||
<!-- Terminal / Logs -->
|
||||
<div class="flex-1 min-h-0 overflow-hidden bg-[#0a0a0a] relative">
|
||||
<div
|
||||
class="absolute inset-0 px-3 py-2"
|
||||
bind:this={terminalEl}
|
||||
></div>
|
||||
<button
|
||||
class="absolute top-2 right-2 z-10 p-1.5 rounded-lg bg-white/[0.06] opacity-30 hover:opacity-90 hover:bg-white/[0.12] transition border-none text-[#fafafa] cursor-pointer"
|
||||
onclick={() => {
|
||||
const text = onCopyLogs('server')
|
||||
if (text) {
|
||||
navigator.clipboard.writeText(text)
|
||||
copied = true
|
||||
setTimeout(() => { copied = false }, 1500)
|
||||
}
|
||||
}}
|
||||
title="Copy logs"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
{#if copied}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
{:else}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9.75a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184" />
|
||||
{/if}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{:else if view === 'open-terminal-logs'}
|
||||
<!-- Open Terminal Logs -->
|
||||
<div class="flex-1 min-h-0 flex flex-col bg-[#0a0a0a]">
|
||||
<div class="flex items-center justify-between px-3 py-1.5 border-b border-white/[0.06]">
|
||||
<span class="text-[11px] opacity-40">Open Terminal Logs</span>
|
||||
<button
|
||||
class="text-[11px] opacity-30 hover:opacity-60 transition bg-transparent border-none text-[#fafafa]"
|
||||
onclick={() => { onSetView(activeConnectionId ? 'connected' : 'welcome') }}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 min-h-0 overflow-hidden relative">
|
||||
<div
|
||||
class="absolute inset-0 px-3 py-2"
|
||||
bind:this={otTerminalEl}
|
||||
></div>
|
||||
<button
|
||||
class="absolute top-2 right-2 z-10 p-1.5 rounded-lg bg-white/[0.06] opacity-30 hover:opacity-90 hover:bg-white/[0.12] transition border-none text-[#fafafa] cursor-pointer"
|
||||
onclick={() => {
|
||||
const text = onCopyLogs('open-terminal')
|
||||
if (text) {
|
||||
navigator.clipboard.writeText(text)
|
||||
copied = true
|
||||
setTimeout(() => { copied = false }, 1500)
|
||||
}
|
||||
}}
|
||||
title="Copy logs"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
{#if copied}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
{:else}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9.75a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184" />
|
||||
{/if}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else if view !== 'connected'}
|
||||
{#if isInitializing}
|
||||
<div class="px-5 py-1.5 text-[11px] opacity-25">
|
||||
Setting up…{$serverInfo?.status ? ` ${$serverInfo.status}` : ''}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex-1 flex items-center justify-center px-6 relative overflow-hidden">
|
||||
{#if view === 'welcome'}
|
||||
{#if remoteConnections.length > 0 || (localConn && localInstalled)}
|
||||
<div class="text-center max-w-[320px]" in:fade={{ duration: 200 }}>
|
||||
<div class="text-lg opacity-80 mb-1.5">Open WebUI</div>
|
||||
<div class="text-[12px] opacity-30 mb-6">
|
||||
Select a connection to get started
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Video background -->
|
||||
<video
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
class="absolute inset-0 w-full h-full object-cover opacity-20 pointer-events-none"
|
||||
>
|
||||
<source src="https://community.s3.openwebui.com/landing.mp4" type="video/mp4" />
|
||||
</video>
|
||||
|
||||
<!-- Gradient overlay -->
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-[#111] via-[#111]/60 to-transparent pointer-events-none"></div>
|
||||
|
||||
<!-- Content positioned bottom-left -->
|
||||
<div class="absolute bottom-0 left-0 right-0 p-10" in:fade={{ duration: 300 }}>
|
||||
<div class="max-w-sm">
|
||||
<div class="text-3xl font-medium mb-3 tracking-tight">Open WebUI</div>
|
||||
<div class="text-base opacity-50 mb-8 leading-relaxed">
|
||||
Connect to an Open WebUI server, or set one up on this machine.
|
||||
</div>
|
||||
|
||||
{#if !localInstalled}
|
||||
<button
|
||||
class="inline-flex items-center gap-2 bg-white px-6 py-2 rounded-xl text-black text-[13px] transition hover:bg-gray-100 border-none disabled:opacity-50"
|
||||
onclick={onStartInstall}
|
||||
disabled={installPhase === 'working'}
|
||||
>
|
||||
{#if installPhase === 'working'}
|
||||
<div class="w-3.5 h-3.5 rounded-full border-2 border-black/30 border-t-black animate-spin"></div>
|
||||
Installing…
|
||||
{:else if installPhase === 'error'}
|
||||
Retry
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M20.015 4.356v4.992m0 0h-4.992m4.993 0l-3.181-3.183a8.25 8.25 0 00-13.803 3.7" />
|
||||
</svg>
|
||||
{:else}
|
||||
Get Started
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if installPhase === 'working' && installStatus}
|
||||
<div class="mt-3 text-[12px] opacity-40 font-mono line-clamp-1" in:fade={{ duration: 200 }}>
|
||||
{installStatus}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="mt-6">
|
||||
<button
|
||||
class="text-sm opacity-40 hover:opacity-70 transition bg-transparent border-none text-[#fafafa]"
|
||||
onclick={() => onSetView('add')}
|
||||
>
|
||||
Connect to existing server →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Error toast -->
|
||||
{#if toastVisible && installError}
|
||||
<div
|
||||
class="absolute top-4 left-1/2 -translate-x-1/2 z-50 bg-red-500/90 backdrop-blur-sm text-white text-[12px] px-4 py-2 rounded-xl shadow-lg"
|
||||
in:fly={{ y: -10, duration: 200 }}
|
||||
out:fade={{ duration: 150 }}
|
||||
>
|
||||
{installError}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if view === 'add'}
|
||||
<div class="w-full max-w-[260px]" in:fade={{ duration: 150 }}>
|
||||
<div class="text-base opacity-70 mb-4">New Connection</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={url}
|
||||
placeholder="e.g. https://your-server.com"
|
||||
class="w-full px-4 py-2.5 rounded-xl bg-white/[0.06] text-[13px] text-[#fafafa] placeholder:opacity-20 outline-none focus:bg-white/[0.1] transition no-drag border-none"
|
||||
onkeydown={(e) => e.key === 'Enter' && onAddConnection()}
|
||||
/>
|
||||
|
||||
{#if error}
|
||||
<p class="text-[11px] opacity-60">{error}</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center gap-3 mt-1">
|
||||
<button
|
||||
class="inline-flex items-center gap-2 bg-white px-5 py-2 rounded-xl text-black text-[13px] transition hover:bg-gray-100 disabled:opacity-30 border-none"
|
||||
onclick={onAddConnection}
|
||||
disabled={connecting || !url.trim()}
|
||||
>
|
||||
{connecting ? 'Connecting…' : 'Connect'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="text-[12px] opacity-30 hover:opacity-60 transition bg-transparent border-none text-[#fafafa]"
|
||||
onclick={() => {
|
||||
onSetView('welcome')
|
||||
error = ''
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if view === 'install'}
|
||||
<div class="w-full max-w-[260px]">
|
||||
<LocalInstall
|
||||
autoStart={autoInstall}
|
||||
onBack={() => { autoInstall = false; onSetView('welcome') }}
|
||||
onComplete={async () => {
|
||||
connections.set(await window.electronAPI.getConnections())
|
||||
config.set(await window.electronAPI.getConfig())
|
||||
onSetView('welcome')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
356
src/renderer/src/lib/components/Main/Connections/Sidebar.svelte
Normal file
356
src/renderer/src/lib/components/Main/Connections/Sidebar.svelte
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
<script lang="ts">
|
||||
import { fly, fade } from 'svelte/transition'
|
||||
import { connections, config, appInfo, serverInfo } from '../../../stores'
|
||||
import { tooltip } from '../../../actions/tooltip'
|
||||
|
||||
interface Props {
|
||||
activeConnectionId: string
|
||||
localConn: any
|
||||
localInstalled: boolean
|
||||
remoteConnections: any[]
|
||||
serverStatus: string | undefined
|
||||
serverReachable: boolean | undefined
|
||||
openTerminalStatus: string | null
|
||||
settingsOpen: boolean
|
||||
view: string
|
||||
onConnect: (id: string) => void
|
||||
onDisconnect: () => void
|
||||
onAddView: () => void
|
||||
onOpenSettings: () => void
|
||||
onToggleOpenTerminal: () => void
|
||||
onToggleOtLogs: () => void
|
||||
openGithub: () => void
|
||||
}
|
||||
|
||||
let {
|
||||
activeConnectionId,
|
||||
localConn,
|
||||
localInstalled,
|
||||
remoteConnections,
|
||||
serverStatus,
|
||||
serverReachable,
|
||||
openTerminalStatus,
|
||||
settingsOpen = $bindable(false),
|
||||
view,
|
||||
onConnect,
|
||||
onDisconnect,
|
||||
onAddView,
|
||||
onOpenSettings,
|
||||
onToggleOpenTerminal,
|
||||
onToggleOtLogs,
|
||||
openGithub
|
||||
}: Props = $props()
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="w-[200px] shrink-0 flex flex-col bg-[#0a0a0a] relative"
|
||||
in:fly={{ x: -200, duration: 200 }}
|
||||
>
|
||||
<!-- Connections header -->
|
||||
<div class="flex items-center justify-between px-4 pt-2 pb-1.5">
|
||||
<span class="text-[10px] tracking-wider uppercase opacity-60">Connections</span>
|
||||
<button
|
||||
class="opacity-25 hover:opacity-60 transition bg-transparent border-none text-[#fafafa] leading-none"
|
||||
onclick={() => {
|
||||
onDisconnect()
|
||||
onAddView()
|
||||
}}
|
||||
title="Add connection"
|
||||
>
|
||||
<svg
|
||||
class="w-[14px] h-[14px]"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Connection list -->
|
||||
<div class="flex-1 min-h-0 overflow-y-auto px-2">
|
||||
<!-- Pinned: Open WebUI (local) -->
|
||||
{#if localConn && localInstalled}
|
||||
{@const isLocalDisabled = !serverReachable}
|
||||
<div
|
||||
class="w-full px-2 py-[6px] rounded-xl group flex items-center gap-2 transition-colors {isLocalDisabled ? 'opacity-40 cursor-default' : 'cursor-pointer'} {activeConnectionId ===
|
||||
localConn.id
|
||||
? 'bg-white/[0.08]'
|
||||
: isLocalDisabled ? '' : 'hover:bg-white/[0.05]'}"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onclick={() => !isLocalDisabled && onConnect(localConn.id)}
|
||||
onkeydown={(e) => e.key === 'Enter' && !isLocalDisabled && onConnect(localConn.id)}
|
||||
>
|
||||
{#if serverStatus === 'starting'}
|
||||
<div class="w-[14px] h-[14px] shrink-0 flex items-center justify-center">
|
||||
<div
|
||||
class="w-2.5 h-2.5 rounded-full border-2 border-amber-400/60 border-t-transparent animate-spin"
|
||||
></div>
|
||||
</div>
|
||||
{:else if serverReachable}
|
||||
<div class="w-[14px] h-[14px] shrink-0 flex items-center justify-center">
|
||||
<div
|
||||
class="w-2 h-2 rounded-full bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.5)]"
|
||||
></div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-[14px] h-[14px] shrink-0 flex items-center justify-center">
|
||||
<div class="w-2 h-2 rounded-full bg-white/15"></div>
|
||||
</div>
|
||||
{/if}
|
||||
<span
|
||||
class="text-[12px] {activeConnectionId === localConn.id
|
||||
? 'opacity-90'
|
||||
: 'opacity-100'} transition-opacity truncate"
|
||||
>Open WebUI</span
|
||||
>
|
||||
|
||||
<button
|
||||
class="ml-auto opacity-0 group-hover:opacity-30 hover:!opacity-70 transition bg-transparent border-none text-[#fafafa] shrink-0"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation()
|
||||
window.electronAPI?.openInBrowser?.(localConn.url)
|
||||
}}
|
||||
title="Open in browser"
|
||||
>
|
||||
<svg
|
||||
class="w-[12px] h-[12px]"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if localConn && localInstalled && remoteConnections.length > 0}
|
||||
<div class="my-1 mx-2 border-t border-white/[0.04]"></div>
|
||||
{/if}
|
||||
|
||||
{#each remoteConnections as conn (conn.id)}
|
||||
<div
|
||||
class="w-full px-2 py-[6px] rounded-xl group flex items-center gap-2 transition-colors cursor-pointer {activeConnectionId ===
|
||||
conn.id
|
||||
? 'bg-white/[0.08]'
|
||||
: 'hover:bg-white/[0.05]'}"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onclick={() => onConnect(conn.id)}
|
||||
onkeydown={(e) => e.key === 'Enter' && onConnect(conn.id)}
|
||||
>
|
||||
<svg
|
||||
class="w-[14px] h-[14px] shrink-0 opacity-30"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5a17.92 17.92 0 01-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="text-[12px] {activeConnectionId === conn.id
|
||||
? 'opacity-90'
|
||||
: 'opacity-100'} transition-opacity truncate"
|
||||
>{conn.name}</span
|
||||
>
|
||||
|
||||
<button
|
||||
class="ml-auto opacity-0 group-hover:opacity-30 hover:!opacity-70 transition bg-transparent border-none text-[#fafafa] shrink-0"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation()
|
||||
window.electronAPI?.openInBrowser?.(conn.url)
|
||||
}}
|
||||
title="Open in browser"
|
||||
>
|
||||
<svg
|
||||
class="w-[12px] h-[12px]"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Open Terminal toggle -->
|
||||
<div class="px-2 pb-1">
|
||||
<div class="border-t border-white/[0.06] pt-2 pb-1">
|
||||
<span class="text-[10px] tracking-wider uppercase opacity-25 px-2">Services</span>
|
||||
</div>
|
||||
<button
|
||||
class="w-full flex items-center gap-2 px-2 py-[6px] rounded-xl text-[12px] transition bg-transparent border-none text-[#fafafa] text-left group {openTerminalStatus === 'started' ? 'opacity-70 hover:opacity-90' : 'opacity-40 hover:opacity-70'} {openTerminalStatus === 'starting' || openTerminalStatus === 'stopping' ? 'pointer-events-none' : ''}"
|
||||
onclick={() => {
|
||||
if (openTerminalStatus === 'started') {
|
||||
onToggleOtLogs()
|
||||
} else {
|
||||
onToggleOpenTerminal()
|
||||
}
|
||||
}}
|
||||
use:tooltip={openTerminalStatus === 'started'
|
||||
? (view === 'open-terminal-logs' ? 'Hide logs' : `Running · Click to view logs`)
|
||||
: openTerminalStatus === 'starting'
|
||||
? 'Starting…'
|
||||
: openTerminalStatus === 'failed'
|
||||
? 'Click to retry'
|
||||
: 'Start terminal server'}
|
||||
>
|
||||
<!-- Status indicator -->
|
||||
<div class="w-[14px] h-[14px] shrink-0 flex items-center justify-center">
|
||||
{#if openTerminalStatus === 'starting' || openTerminalStatus === 'stopping'}
|
||||
<div
|
||||
class="w-2.5 h-2.5 rounded-full border-2 border-amber-400/60 border-t-transparent animate-spin"
|
||||
></div>
|
||||
{:else if openTerminalStatus === 'started'}
|
||||
<div
|
||||
class="w-2 h-2 rounded-full bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.5)]"
|
||||
></div>
|
||||
{:else if openTerminalStatus === 'failed'}
|
||||
<div class="w-2 h-2 rounded-full bg-red-400/70"></div>
|
||||
{:else}
|
||||
<svg
|
||||
class="w-[14px] h-[14px] opacity-50"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="truncate">Open Terminal</span>
|
||||
<!-- Stop button (when running) -->
|
||||
{#if openTerminalStatus === 'started'}
|
||||
<button
|
||||
class="ml-auto opacity-0 group-hover:opacity-40 hover:!opacity-80 transition bg-transparent border-none text-[#fafafa] p-0 leading-none"
|
||||
onclick={(e) => { e.stopPropagation(); onToggleOpenTerminal() }}
|
||||
use:tooltip={'Stop Open Terminal'}
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.636 5.636a9 9 0 1012.728 0M12 3v9" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Settings popover -->
|
||||
{#if settingsOpen}
|
||||
<div class="fixed inset-0 z-40" onclick={() => (settingsOpen = false)}></div>
|
||||
|
||||
<div
|
||||
class="absolute bottom-12 left-2 right-2 z-50 bg-[#1a1a1a]/90 backdrop-blur-xl border border-white/[0.08] rounded-2xl shadow-2xl py-0.5 overflow-hidden"
|
||||
in:fly={{ y: 8, duration: 150 }}
|
||||
out:fade={{ duration: 100 }}
|
||||
>
|
||||
<div class="px-3.5 py-2.5 border-b border-white/[0.06]">
|
||||
<div class="text-[11px] opacity-40">Open WebUI Desktop</div>
|
||||
<div class="text-[10px] opacity-20 mt-0.5">{$appInfo?.version ?? ''}</div>
|
||||
</div>
|
||||
|
||||
<div class="py-1 px-1.5">
|
||||
<button
|
||||
class="w-full flex items-center gap-2.5 px-3 py-1.5 text-left text-[12px] opacity-50 hover:opacity-90 hover:bg-white/[0.06] transition bg-transparent border-none text-[#fafafa] rounded-xl"
|
||||
onclick={() => {
|
||||
settingsOpen = false
|
||||
onOpenSettings()
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
class="w-[14px] h-[14px] shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 010 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 010-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
Settings
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="w-full flex items-center gap-2.5 px-3 py-1.5 text-left text-[12px] opacity-50 hover:opacity-90 hover:bg-white/[0.06] transition bg-transparent border-none text-[#fafafa] rounded-xl"
|
||||
onclick={openGithub}
|
||||
>
|
||||
<svg
|
||||
class="w-[14px] h-[14px] shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
|
||||
/>
|
||||
</svg>
|
||||
GitHub
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Settings button (bottom) -->
|
||||
<div class="px-2 pb-3">
|
||||
<button
|
||||
class="w-full flex items-center gap-2 px-2 py-[6px] rounded-xl text-[12px] opacity-40 hover:opacity-70 hover:bg-white/[0.05] transition bg-transparent border-none text-[#fafafa] text-left"
|
||||
onclick={() => (settingsOpen = !settingsOpen)}
|
||||
>
|
||||
<svg
|
||||
class="w-[14px] h-[14px] shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 010 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 010-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,6 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { appInfo } from '../../../stores'
|
||||
|
||||
let openWebuiVersion = $state<string | null>(null)
|
||||
let openTerminalVersion = $state<string | null>(null)
|
||||
|
||||
onMount(async () => {
|
||||
openWebuiVersion = await window.electronAPI.getPackageVersion('open-webui')
|
||||
openTerminalVersion = await window.electronAPI.getPackageVersion('open-terminal')
|
||||
})
|
||||
|
||||
const openGithub = () => {
|
||||
window.electronAPI?.openInBrowser?.('https://github.com/open-webui/desktop')
|
||||
}
|
||||
|
|
@ -8,10 +17,24 @@
|
|||
|
||||
<div class="flex flex-col divide-y divide-white/[0.04]">
|
||||
<div class="py-4 flex items-center justify-between">
|
||||
<div class="text-[13px] opacity-70">Version</div>
|
||||
<div class="text-[13px] opacity-70">Desktop Version</div>
|
||||
<div class="text-[12px] opacity-30">{$appInfo?.version ?? 'Unknown'}</div>
|
||||
</div>
|
||||
|
||||
{#if openWebuiVersion}
|
||||
<div class="py-4 flex items-center justify-between">
|
||||
<div class="text-[13px] opacity-70">Open WebUI Version</div>
|
||||
<div class="text-[12px] opacity-30">{openWebuiVersion}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if openTerminalVersion}
|
||||
<div class="py-4 flex items-center justify-between">
|
||||
<div class="text-[13px] opacity-70">Open Terminal Version</div>
|
||||
<div class="text-[12px] opacity-30">{openTerminalVersion}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="py-4 flex items-center justify-between">
|
||||
<div class="text-[13px] opacity-70">Platform</div>
|
||||
<div class="text-[12px] opacity-30">{$appInfo?.platform ?? 'Unknown'}</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue