mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-02 06:39:02 -05:00
refac
This commit is contained in:
@@ -216,12 +216,14 @@ async def set_tool_servers_config(
|
||||
|
||||
|
||||
class TerminalServerConnection(BaseModel):
|
||||
id: str
|
||||
id: Optional[str] = ""
|
||||
url: str
|
||||
path: Optional[str] = "/openapi.json"
|
||||
auth_type: Optional[str] = "bearer"
|
||||
key: Optional[str] = ""
|
||||
name: Optional[str] = ""
|
||||
auth_type: Optional[str] = "bearer"
|
||||
config: Optional[dict] = None # holds access_grants, etc.
|
||||
enabled: Optional[bool] = True
|
||||
config: Optional[dict] = None
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ async def list_terminal_servers(request: Request, user=Depends(get_verified_user
|
||||
return [
|
||||
{"id": connection.get("id", ""), "url": connection.get("url", ""), "name": connection.get("name", "")}
|
||||
for connection in connections
|
||||
if has_connection_access(user, connection, user_group_ids)
|
||||
if connection.get("enabled", True) and has_connection_access(user, connection, user_group_ids)
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -210,7 +210,8 @@ def convert_output_to_messages(output: list, raw: bool = False) -> list[dict]:
|
||||
content = ""
|
||||
for part in output_parts:
|
||||
if part.get("type") == "input_text":
|
||||
content += part.get("text", "")
|
||||
text_val = part.get("text", "")
|
||||
content += str(text_val) if not isinstance(text_val, str) else text_val
|
||||
|
||||
messages.append(
|
||||
{
|
||||
|
||||
@@ -900,26 +900,27 @@ async def set_terminal_servers(request: Request):
|
||||
"""Load and cache OpenAPI specs from all TERMINAL_SERVER_CONNECTIONS."""
|
||||
connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
|
||||
|
||||
# Build server configs with info containing the connection ID
|
||||
# Build server configs compatible with get_tool_servers_data
|
||||
# Terminal connections store id/name at top level; translate to info dict
|
||||
server_configs = []
|
||||
for connection in connections:
|
||||
conn_id = connection.get("id", "")
|
||||
if not connection.get("url"):
|
||||
continue
|
||||
|
||||
auth_type = connection.get("auth_type", "bearer")
|
||||
token = None
|
||||
if auth_type == "bearer":
|
||||
token = connection.get("key", "")
|
||||
enabled = connection.get("enabled", True)
|
||||
|
||||
server_configs.append({
|
||||
"url": connection.get("url", ""),
|
||||
"key": token or "",
|
||||
"auth_type": auth_type,
|
||||
"path": "openapi.json",
|
||||
"key": connection.get("key", ""),
|
||||
"auth_type": connection.get("auth_type", "bearer"),
|
||||
"path": connection.get("path", "/openapi.json"),
|
||||
"spec_type": "url",
|
||||
"config": {"enable": True},
|
||||
"info": {"id": conn_id, "name": connection.get("name", "")},
|
||||
# get_tool_servers_data reads config.enable to filter active servers
|
||||
"config": {"enable": enabled},
|
||||
"info": {
|
||||
"id": connection.get("id", ""),
|
||||
"name": connection.get("name", ""),
|
||||
},
|
||||
})
|
||||
|
||||
request.app.state.TERMINAL_SERVERS = await get_tool_servers_data(server_configs)
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
let showConnectionModal = false;
|
||||
|
||||
// Terminal server admin connections
|
||||
let terminalConnections: { id: string; url: string; key: string; name: string }[] = [];
|
||||
let terminalConnections = [];
|
||||
let showAddTerminalModal = false;
|
||||
let editTerminalIdx: number | null = null;
|
||||
let showDeleteTerminalConfirm = false;
|
||||
@@ -72,20 +72,17 @@
|
||||
}
|
||||
};
|
||||
|
||||
const addTerminalConnection = (server: { url: string; key: string; name?: string }) => {
|
||||
const addTerminalConnection = (server) => {
|
||||
terminalConnections = [
|
||||
...terminalConnections,
|
||||
{ id: uuidv4(), url: server.url, key: server.key, name: server.name ?? '' }
|
||||
{ ...server, id: server.id ?? uuidv4() }
|
||||
];
|
||||
saveTerminalServers();
|
||||
};
|
||||
|
||||
const updateTerminalConnection = (
|
||||
idx: number,
|
||||
updated: { url: string; key: string; name?: string }
|
||||
) => {
|
||||
const updateTerminalConnection = (idx: number, updated) => {
|
||||
terminalConnections = terminalConnections.map((c, i) =>
|
||||
i === idx ? { ...c, url: updated.url, key: updated.key, name: updated.name ?? '' } : c
|
||||
i === idx ? { ...c, ...updated, id: updated.id ?? c.id } : c
|
||||
);
|
||||
saveTerminalServers();
|
||||
};
|
||||
@@ -191,15 +188,15 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if servers.length === 0}
|
||||
<div class="text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('No tool server connections configured.')}
|
||||
</div>
|
||||
{/if}
|
||||
{#if servers.length === 0}
|
||||
<div class="text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('No tool server connections configured.')}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="my-1.5">
|
||||
<div class="text-xs text-gray-500">
|
||||
{$i18n.t('Connect to your own OpenAPI compatible external tool servers.')}
|
||||
<div class="my-1.5">
|
||||
<div class="text-xs text-gray-500">
|
||||
{$i18n.t('Connect to your own OpenAPI compatible external tool servers.')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -235,7 +232,7 @@
|
||||
<div class="flex w-full gap-2 items-center">
|
||||
<Tooltip className="w-full relative" content={''} placement="top-start">
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 relative flex gap-1.5 items-center">
|
||||
<div class="flex-1 relative flex gap-1.5 items-center {connection?.enabled === false ? 'opacity-50' : ''}">
|
||||
<Tooltip content={$i18n.t('Terminal')}>
|
||||
<Cloud className="size-4" strokeWidth="1.5" />
|
||||
</Tooltip>
|
||||
@@ -260,6 +257,18 @@
|
||||
<Cog6 />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={(connection?.enabled !== false) ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
|
||||
<Switch
|
||||
state={connection?.enabled !== false}
|
||||
on:change={() => {
|
||||
terminalConnections = terminalConnections.map((c, i) =>
|
||||
i === idx ? { ...c, enabled: !(c?.enabled !== false) } : c
|
||||
);
|
||||
saveTerminalServers();
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -42,12 +42,12 @@
|
||||
functions,
|
||||
selectedFolder,
|
||||
pinnedChats,
|
||||
showEmbeds
|
||||
showEmbeds,
|
||||
selectedTerminalId
|
||||
} from '$lib/stores';
|
||||
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
|
||||
import {
|
||||
convertMessagesToHistory,
|
||||
copyToClipboard,
|
||||
@@ -140,13 +140,18 @@
|
||||
|
||||
let selectedToolIds = [];
|
||||
let selectedFilterIds = [];
|
||||
|
||||
let imageGenerationEnabled = false;
|
||||
let webSearchEnabled = false;
|
||||
let codeInterpreterEnabled = false;
|
||||
|
||||
// Auto-inject terminal servers into selected tool IDs so they act like toggled-on tools
|
||||
// Auto-inject direct terminal servers into selected tool IDs so they act like toggled-on tools
|
||||
// System terminals (with id field) are handled server-side via terminal_id, not as direct tool servers
|
||||
$: if ($terminalServers && $terminalServers.length > 0) {
|
||||
const terminalIds = $terminalServers.map((_, i) => `direct_server:terminal_${i}`);
|
||||
const directTerminalServers = $terminalServers.filter((t) => !t.id);
|
||||
const terminalIds = directTerminalServers.map(
|
||||
(_, i) => `direct_server:terminal_${$terminalServers.indexOf(directTerminalServers[i])}`
|
||||
);
|
||||
const missingIds = terminalIds.filter((id) => !selectedToolIds.includes(id));
|
||||
if (missingIds.length > 0) {
|
||||
selectedToolIds = [...selectedToolIds, ...missingIds];
|
||||
@@ -155,7 +160,10 @@
|
||||
|
||||
// Remove disabled terminal servers from selectedToolIds automatically
|
||||
$: if (selectedToolIds.length > 0) {
|
||||
const terminalIds = ($terminalServers ?? []).map((_, i) => `direct_server:terminal_${i}`);
|
||||
const directTerminalServers = ($terminalServers ?? []).filter((t) => !t.id);
|
||||
const terminalIds = directTerminalServers.map(
|
||||
(_, i) => `direct_server:terminal_${($terminalServers ?? []).indexOf(directTerminalServers[i])}`
|
||||
);
|
||||
const invalidTerminalIds = selectedToolIds.filter(
|
||||
(id) => id.startsWith('direct_server:terminal_') && !terminalIds.includes(id)
|
||||
);
|
||||
@@ -354,9 +362,12 @@
|
||||
selectedToolIds = selectedToolIds.filter((id) => !id.startsWith('direct_server:'));
|
||||
}
|
||||
|
||||
// Auto-inject terminal servers
|
||||
// Auto-inject direct terminal servers (system ones are handled via terminal_id)
|
||||
if ($terminalServers && $terminalServers.length > 0) {
|
||||
const terminalIds = $terminalServers.map((_, i) => `direct_server:terminal_${i}`);
|
||||
const directTerminalServers = $terminalServers.filter((t) => !t.id);
|
||||
const terminalIds = directTerminalServers.map(
|
||||
(_, i) => `direct_server:terminal_${$terminalServers.indexOf(directTerminalServers[i])}`
|
||||
);
|
||||
selectedToolIds = [...new Set([...selectedToolIds, ...terminalIds])];
|
||||
}
|
||||
|
||||
@@ -650,6 +661,14 @@
|
||||
|
||||
audioQueue.set(new AudioQueue(document.getElementById('audioElement')));
|
||||
|
||||
// Reset direct terminal enabled states — selectedTerminalId starts null on every page load
|
||||
if ($settings?.terminalServers?.some((s) => s.enabled)) {
|
||||
settings.set({
|
||||
...$settings,
|
||||
terminalServers: ($settings.terminalServers ?? []).map((s) => ({ ...s, enabled: false }))
|
||||
});
|
||||
}
|
||||
|
||||
pageSubscribe = page.subscribe(async (p) => {
|
||||
if (p.url.pathname === '/') {
|
||||
await tick();
|
||||
@@ -2130,9 +2149,8 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Determine the active terminal (first accessible backend terminal, or user-configured)
|
||||
const activeTerminal = $terminalServers?.[0] ?? null;
|
||||
const activeTerminalId = activeTerminal?.id ?? null;
|
||||
// Use the user-selected terminal from the dropdown
|
||||
const activeTerminalId = $selectedTerminalId ?? null;
|
||||
|
||||
const res = await generateOpenAIChatCompletion(
|
||||
localStorage.token,
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
showArtifacts,
|
||||
showEmbeds,
|
||||
settings,
|
||||
showFileNavPath
|
||||
showFileNavPath,
|
||||
selectedTerminalId,
|
||||
user
|
||||
} from '$lib/stores';
|
||||
|
||||
import { uploadFile } from '$lib/apis/files';
|
||||
@@ -59,10 +61,24 @@
|
||||
// Tab state for Controls+Files panel
|
||||
let activeTab: 'controls' | 'files' | 'overview' = savedTab;
|
||||
$: savedTab = activeTab;
|
||||
$: hasTerminal = !!($settings?.terminalServers ?? []).find((s) => s.enabled)?.url
|
||||
|| $terminalServers.length > 0;
|
||||
$: hasMessages = history?.messages && Object.keys(history.messages).length > 0;
|
||||
$: if (!hasMessages && activeTab === 'overview') activeTab = 'controls';
|
||||
|
||||
$: showControlsTab = $user?.role === 'admin' || ($user?.permissions?.chat?.controls ?? true);
|
||||
$: showFilesTab = !!$selectedTerminalId;
|
||||
$: showOverviewTab = hasMessages;
|
||||
|
||||
// Tab fallback: if active tab becomes hidden, switch to next available
|
||||
$: if (!showOverviewTab && activeTab === 'overview') activeTab = 'controls';
|
||||
$: if (!showFilesTab && activeTab === 'files') activeTab = 'controls';
|
||||
$: if (!showControlsTab && activeTab === 'controls') {
|
||||
if (showFilesTab) activeTab = 'files';
|
||||
else if (showOverviewTab) activeTab = 'overview';
|
||||
}
|
||||
|
||||
// Auto-close if there are no visible tabs
|
||||
$: if (!showControlsTab && !showFilesTab && !showOverviewTab) {
|
||||
showControls.set(false);
|
||||
}
|
||||
|
||||
// Auto-switch to Files tab when display_file is triggered
|
||||
$: if ($showFileNavPath) {
|
||||
@@ -70,6 +86,12 @@
|
||||
showControls.set(true);
|
||||
}
|
||||
|
||||
// Auto-open Files tab when a terminal is selected
|
||||
$: if ($selectedTerminalId) {
|
||||
activeTab = 'files';
|
||||
showControls.set(true);
|
||||
}
|
||||
|
||||
// Attach a terminal file to the chat input
|
||||
const handleTerminalAttach = async (blob: Blob, name: string, contentType: string) => {
|
||||
const tempItemId = uuidv4();
|
||||
@@ -182,7 +204,9 @@
|
||||
document.addEventListener('mousedown', onMouseDown);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
|
||||
setTimeout(() => { paneReady = true; }, 0);
|
||||
setTimeout(() => {
|
||||
paneReady = true;
|
||||
}, 0);
|
||||
|
||||
// If controls were persisted as open, set the pane to the saved size
|
||||
if ($showControls && pane) {
|
||||
@@ -247,15 +271,17 @@
|
||||
<!-- Tab bar -->
|
||||
<div class="flex items-center justify-between px-2 pt-2.5 pb-2 shrink-0">
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'controls'
|
||||
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
|
||||
on:click={() => (activeTab = 'controls')}
|
||||
>
|
||||
{$i18n.t('Controls')}
|
||||
</button>
|
||||
{#if hasTerminal}
|
||||
{#if showControlsTab}
|
||||
<button
|
||||
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'controls'
|
||||
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
|
||||
on:click={() => (activeTab = 'controls')}
|
||||
>
|
||||
{$i18n.t('Controls')}
|
||||
</button>
|
||||
{/if}
|
||||
{#if showFilesTab}
|
||||
<button
|
||||
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'files'
|
||||
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
|
||||
@@ -265,7 +291,7 @@
|
||||
{$i18n.t('Files')}
|
||||
</button>
|
||||
{/if}
|
||||
{#if hasMessages}
|
||||
{#if showOverviewTab}
|
||||
<button
|
||||
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'overview'
|
||||
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
|
||||
@@ -310,7 +336,7 @@
|
||||
}}
|
||||
onClose={() => showControls.set(false)}
|
||||
/>
|
||||
{:else if activeTab === 'files' && hasTerminal}
|
||||
{:else if activeTab === 'files' && $selectedTerminalId}
|
||||
<FileNav onAttach={handleTerminalAttach} />
|
||||
{:else}
|
||||
<Controls embed={true} {models} bind:chatFiles bind:params />
|
||||
@@ -383,15 +409,17 @@
|
||||
<!-- Tab bar -->
|
||||
<div class="flex items-center justify-between px-2 pt-2.5 pb-2 shrink-0">
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'controls'
|
||||
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
|
||||
on:click={() => (activeTab = 'controls')}
|
||||
>
|
||||
{$i18n.t('Controls')}
|
||||
</button>
|
||||
{#if hasTerminal}
|
||||
{#if showControlsTab}
|
||||
<button
|
||||
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'controls'
|
||||
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
|
||||
on:click={() => (activeTab = 'controls')}
|
||||
>
|
||||
{$i18n.t('Controls')}
|
||||
</button>
|
||||
{/if}
|
||||
{#if showFilesTab}
|
||||
<button
|
||||
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'files'
|
||||
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
|
||||
@@ -401,7 +429,7 @@
|
||||
{$i18n.t('Files')}
|
||||
</button>
|
||||
{/if}
|
||||
{#if hasMessages}
|
||||
{#if showOverviewTab}
|
||||
<button
|
||||
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'overview'
|
||||
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
|
||||
@@ -451,7 +479,7 @@
|
||||
}}
|
||||
onClose={() => showControls.set(false)}
|
||||
/>
|
||||
{:else if activeTab === 'files' && hasTerminal}
|
||||
{:else if activeTab === 'files' && $selectedTerminalId}
|
||||
<FileNav onAttach={handleTerminalAttach} />
|
||||
{:else}
|
||||
<Controls embed={true} {models} bind:chatFiles bind:params />
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { getContext, onMount, onDestroy, tick } from 'svelte';
|
||||
import { terminalServers, settings, showFileNavPath } from '$lib/stores';
|
||||
import { terminalServers, settings, showFileNavPath, selectedTerminalId } from '$lib/stores';
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
import {
|
||||
getCwd,
|
||||
@@ -59,12 +59,31 @@
|
||||
let showDeleteConfirm = false;
|
||||
let shiftKey = false;
|
||||
|
||||
$: firstTerminal = $terminalServers?.[0] ?? null;
|
||||
$: activeUserTerminal = ($settings?.terminalServers ?? []).find((s) => s.enabled);
|
||||
$: firstTerminal = $selectedTerminalId
|
||||
? ($terminalServers ?? []).find((t) => t.id === $selectedTerminalId) ?? null
|
||||
: ($terminalServers?.[0] ?? null);
|
||||
$: activeUserTerminal = ($settings?.terminalServers ?? []).find(
|
||||
(s) => s.url === $selectedTerminalId
|
||||
);
|
||||
// System terminals go through the open-webui proxy and need the session token.
|
||||
// Direct user terminals talk directly to the server and need the terminal's own key.
|
||||
$: isSystemTerminal = !!firstTerminal;
|
||||
$: terminalUrl = firstTerminal?.url ?? activeUserTerminal?.url ?? '';
|
||||
$: terminalKey = firstTerminal?.key ?? activeUserTerminal?.key ?? '';
|
||||
$: terminalKey = isSystemTerminal ? localStorage.token : (activeUserTerminal?.key ?? '');
|
||||
$: configured = !!terminalUrl;
|
||||
|
||||
// Reload file list when terminal is swapped
|
||||
let prevTerminalUrl = '';
|
||||
$: if (terminalUrl && terminalUrl !== prevTerminalUrl) {
|
||||
prevTerminalUrl = terminalUrl;
|
||||
(async () => {
|
||||
const cwd = await getCwd(terminalUrl, terminalKey);
|
||||
const dir = cwd ? (cwd.endsWith('/') ? cwd : cwd + '/') : '/';
|
||||
savedPath = dir;
|
||||
loadDir(dir);
|
||||
})();
|
||||
}
|
||||
|
||||
$: breadcrumbs = currentPath
|
||||
.split('/')
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -20,7 +20,10 @@
|
||||
|
||||
const MD_EXTS = new Set(['md', 'markdown', 'mdx']);
|
||||
$: isMarkdown = MD_EXTS.has(selectedFile?.split('.').pop()?.toLowerCase() ?? '');
|
||||
$: renderedHtml = isMarkdown && fileContent ? DOMPurify.sanitize(marked.parse(fileContent, { async: false }) as string) : '';
|
||||
$: renderedHtml =
|
||||
isMarkdown && fileContent
|
||||
? DOMPurify.sanitize(marked.parse(fileContent, { async: false }) as string)
|
||||
: '';
|
||||
|
||||
let showRaw = false;
|
||||
// Reset to preview mode when switching files
|
||||
@@ -55,7 +58,11 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex-1 {fileImageUrl !== null || filePdfData !== null ? 'overflow-hidden' : 'overflow-y-auto'} min-h-0 relative h-full">
|
||||
<div
|
||||
class="flex-1 {fileImageUrl !== null || filePdfData !== null
|
||||
? 'overflow-hidden'
|
||||
: 'overflow-y-auto'} min-h-0 relative h-full"
|
||||
>
|
||||
<!-- Floating action buttons -->
|
||||
<div class="absolute top-2 right-2 z-10 flex gap-1">
|
||||
{#if fileImageUrl !== null}
|
||||
@@ -78,14 +85,40 @@
|
||||
>
|
||||
{#if showRaw}
|
||||
<!-- Eye icon: switch to preview -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="size-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<!-- Code icon: switch to raw -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="size-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75 22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M17.25 6.75 22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -28,8 +28,11 @@
|
||||
showCallOverlay,
|
||||
tools,
|
||||
toolServers,
|
||||
terminalServers,
|
||||
user as _user,
|
||||
showControls,
|
||||
showSettings,
|
||||
selectedTerminalId,
|
||||
TTSWorker,
|
||||
temporaryChatEnabled
|
||||
} from '$lib/stores';
|
||||
@@ -81,8 +84,13 @@
|
||||
import Voice from '../icons/Voice.svelte';
|
||||
import Cloud from '../icons/Cloud.svelte';
|
||||
import IntegrationsMenu from './MessageInput/IntegrationsMenu.svelte';
|
||||
import TerminalMenu from './MessageInput/TerminalMenu.svelte';
|
||||
import Component from '../icons/Component.svelte';
|
||||
import PlusAlt from '../icons/PlusAlt.svelte';
|
||||
import Dropdown from '../common/Dropdown.svelte';
|
||||
|
||||
import { DropdownMenu } from 'bits-ui';
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
|
||||
import CommandSuggestionList from './MessageInput/CommandSuggestionList.svelte';
|
||||
import Knobs from '../icons/Knobs.svelte';
|
||||
@@ -124,6 +132,8 @@
|
||||
export let webSearchEnabled = false;
|
||||
export let codeInterpreterEnabled = false;
|
||||
|
||||
let showTerminalMenu = false;
|
||||
|
||||
export let messageQueue: { id: string; prompt: string; files: any[] }[] = [];
|
||||
export let onQueueSendNow: (id: string) => void = () => {};
|
||||
export let onQueueEdit: (id: string) => void = () => {};
|
||||
@@ -1019,7 +1029,6 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<ToolServersModal bind:show={showTools} {selectedToolIds} />
|
||||
|
||||
<InputVariablesModal
|
||||
@@ -1793,23 +1802,8 @@
|
||||
{/if}
|
||||
|
||||
{#if (!history?.currentId || history.messages[history.currentId]?.done == true) && ($_user?.role === 'admin' || ($_user?.permissions?.chat?.stt ?? true))}
|
||||
<!-- Active Terminal Indicator (Always On) -->
|
||||
{@const activeTerminal = ($settings?.terminalServers ?? []).find(
|
||||
(s) => s.enabled
|
||||
)}
|
||||
{#if activeTerminal}
|
||||
<div class="flex items-end mr-0.5">
|
||||
<div
|
||||
class="flex items-center gap-1.5 px-2.5 py-1 text-sm hover:bg-gray-50 hover:dark:bg-gray-850 transition-all rounded-lg cursor-pointer select-none max-w-[120px] sm:max-w-[200px] truncate"
|
||||
>
|
||||
<Cloud className="size-3 shrink-0" strokeWidth="2" />
|
||||
<span class="truncate"
|
||||
>{activeTerminal.name ||
|
||||
activeTerminal.url.replace(/^https?:\/\//, '')}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Terminal Server Selector -->
|
||||
<TerminalMenu bind:show={showTerminalMenu} />
|
||||
|
||||
<!-- {$i18n.t('Record voice')} -->
|
||||
<Tooltip content={$i18n.t('Dictate')}>
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { DropdownMenu } from 'bits-ui';
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
|
||||
import { settings, showSettings, terminalServers, selectedTerminalId, user } from '$lib/stores';
|
||||
import { getToolServersData } from '$lib/apis';
|
||||
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Cloud from '$lib/components/icons/Cloud.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let show = false;
|
||||
|
||||
$: systemTerminals = ($terminalServers ?? []).filter((t) => t.id);
|
||||
$: directTerminals = ($settings?.terminalServers ?? []).filter((s) => s.url);
|
||||
|
||||
const refreshTerminalServersStore = async (servers: typeof directTerminals) => {
|
||||
const activeTerminals = servers.filter((s) => s.enabled);
|
||||
if (activeTerminals.length > 0) {
|
||||
let data = await getToolServersData(
|
||||
activeTerminals.map((t) => ({
|
||||
url: t.url,
|
||||
auth_type: t.auth_type ?? 'bearer',
|
||||
key: t.key ?? '',
|
||||
path: t.path ?? '/openapi.json',
|
||||
config: { enable: true }
|
||||
}))
|
||||
);
|
||||
data = data.filter((d) => d && !d.error);
|
||||
terminalServers.set(data);
|
||||
} else {
|
||||
terminalServers.set([]);
|
||||
}
|
||||
};
|
||||
|
||||
const selectDirect = async (terminal: (typeof directTerminals)[0]) => {
|
||||
const newId = $selectedTerminalId === terminal.url ? null : terminal.url;
|
||||
selectedTerminalId.set(newId);
|
||||
|
||||
// Enable the selected direct terminal, disable all others
|
||||
const updatedServers = ($settings?.terminalServers ?? []).map((s) => ({
|
||||
...s,
|
||||
enabled: newId !== null && s.url === terminal.url
|
||||
}));
|
||||
|
||||
settings.set({
|
||||
...$settings,
|
||||
terminalServers: updatedServers
|
||||
});
|
||||
|
||||
show = false;
|
||||
|
||||
// Refresh the store so Chat.svelte can inject it as a tool
|
||||
await refreshTerminalServersStore(updatedServers);
|
||||
};
|
||||
|
||||
const selectSystem = async (terminal: (typeof systemTerminals)[0]) => {
|
||||
selectedTerminalId.set($selectedTerminalId === terminal.id ? null : terminal.id);
|
||||
|
||||
// Disable all direct terminals when switching to a system terminal
|
||||
if ($settings?.terminalServers?.some((s) => s.enabled)) {
|
||||
const updatedServers = ($settings.terminalServers ?? []).map((s) => ({ ...s, enabled: false }));
|
||||
settings.set({
|
||||
...$settings,
|
||||
terminalServers: updatedServers
|
||||
});
|
||||
await refreshTerminalServersStore(updatedServers);
|
||||
}
|
||||
|
||||
show = false;
|
||||
};
|
||||
|
||||
$: selectedSystemTerminal = systemTerminals.find((t) => t.id === $selectedTerminalId);
|
||||
$: selectedDirectTerminal = directTerminals.find((t) => t.url === $selectedTerminalId);
|
||||
|
||||
$: selectedLabel =
|
||||
selectedSystemTerminal?.name ??
|
||||
selectedDirectTerminal?.name ??
|
||||
selectedDirectTerminal?.url?.replace(/^https?:\/\//, '') ??
|
||||
null;
|
||||
</script>
|
||||
|
||||
<Dropdown bind:show>
|
||||
<Tooltip content={$i18n.t('Terminal')} placement="top">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1.5 px-2 py-1.5 text-sm transition rounded-lg cursor-pointer"
|
||||
>
|
||||
<Cloud className="size-3.5" strokeWidth="2" />
|
||||
{#if $selectedTerminalId && selectedLabel}
|
||||
<span class="truncate text-xs max-w-[100px] sm:max-w-[150px]">{selectedLabel}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<div slot="content">
|
||||
<DropdownMenu.Content
|
||||
class="w-full max-w-56 rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg max-h-72 overflow-y-auto overflow-x-hidden scrollbar-thin"
|
||||
sideOffset={4}
|
||||
side="bottom"
|
||||
align="end"
|
||||
transition={flyAndScale}
|
||||
>
|
||||
<!-- Direct terminals (gated by permission) -->
|
||||
{#if $user?.role === 'admin' || ($user?.permissions?.features?.direct_tool_servers ?? true)}
|
||||
<div class="flex items-center justify-between px-3 py-1">
|
||||
<span class="text-[10px] font-medium text-gray-400 dark:text-gray-500 uppercase tracking-wider">
|
||||
{$i18n.t('Direct')}
|
||||
</span>
|
||||
<Tooltip content={$i18n.t('Add Terminal')} placement="top">
|
||||
<button
|
||||
type="button"
|
||||
class="p-0.5 rounded-md text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition"
|
||||
on:click|stopPropagation={() => { show = false; showSettings.set(true); }}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-3.5">
|
||||
<path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{#each directTerminals as terminal}
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl {$selectedTerminalId === terminal.url
|
||||
? 'bg-gray-50 dark:bg-gray-800/50'
|
||||
: 'hover:bg-gray-50 dark:hover:bg-gray-800/50'}"
|
||||
on:click={() => selectDirect(terminal)}
|
||||
>
|
||||
<div class="flex flex-1 gap-2 items-center truncate">
|
||||
<Cloud className="size-4 shrink-0" strokeWidth="2" />
|
||||
<span class="truncate">{terminal.name || terminal.url.replace(/^https?:\/\//, '')}</span>
|
||||
</div>
|
||||
{#if $selectedTerminalId === terminal.url}
|
||||
<div class="shrink-0 text-emerald-600 dark:text-emerald-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4">
|
||||
<path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
<hr class="border-gray-100 dark:border-gray-800 my-1" />
|
||||
{/if}
|
||||
|
||||
<!-- System terminals -->
|
||||
<div class="flex items-center justify-between px-3 py-1">
|
||||
<span class="text-[10px] font-medium text-gray-400 dark:text-gray-500 uppercase tracking-wider">
|
||||
{$i18n.t('System')}
|
||||
</span>
|
||||
{#if $user?.role === 'admin'}
|
||||
<Tooltip content={$i18n.t('Add Terminal')} placement="top">
|
||||
<button
|
||||
type="button"
|
||||
class="p-0.5 rounded-md text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition"
|
||||
on:click|stopPropagation={() => { show = false; goto('/admin/settings/integrations'); }}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-3.5">
|
||||
<path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#each systemTerminals as terminal}
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl {$selectedTerminalId === terminal.id
|
||||
? 'bg-gray-50 dark:bg-gray-800/50'
|
||||
: 'hover:bg-gray-50 dark:hover:bg-gray-800/50'}"
|
||||
on:click={() => selectSystem(terminal)}
|
||||
>
|
||||
<div class="flex flex-1 gap-2 items-center truncate">
|
||||
<Cloud className="size-4 shrink-0" strokeWidth="2" />
|
||||
<span class="truncate">{terminal.name || $i18n.t('Terminal')}</span>
|
||||
</div>
|
||||
{#if $selectedTerminalId === terminal.id}
|
||||
<div class="shrink-0 text-emerald-600 dark:text-emerald-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4">
|
||||
<path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</DropdownMenu.Content>
|
||||
</div>
|
||||
</Dropdown>
|
||||
Reference in New Issue
Block a user