refac
This commit is contained in:
@@ -216,12 +216,14 @@ async def set_tool_servers_config(
|
|||||||
|
|
||||||
|
|
||||||
class TerminalServerConnection(BaseModel):
|
class TerminalServerConnection(BaseModel):
|
||||||
id: str
|
id: Optional[str] = ""
|
||||||
url: str
|
url: str
|
||||||
|
path: Optional[str] = "/openapi.json"
|
||||||
|
auth_type: Optional[str] = "bearer"
|
||||||
key: Optional[str] = ""
|
key: Optional[str] = ""
|
||||||
name: Optional[str] = ""
|
name: Optional[str] = ""
|
||||||
auth_type: Optional[str] = "bearer"
|
enabled: Optional[bool] = True
|
||||||
config: Optional[dict] = None # holds access_grants, etc.
|
config: Optional[dict] = None
|
||||||
|
|
||||||
model_config = ConfigDict(extra="allow")
|
model_config = ConfigDict(extra="allow")
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ async def list_terminal_servers(request: Request, user=Depends(get_verified_user
|
|||||||
return [
|
return [
|
||||||
{"id": connection.get("id", ""), "url": connection.get("url", ""), "name": connection.get("name", "")}
|
{"id": connection.get("id", ""), "url": connection.get("url", ""), "name": connection.get("name", "")}
|
||||||
for connection in connections
|
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 = ""
|
content = ""
|
||||||
for part in output_parts:
|
for part in output_parts:
|
||||||
if part.get("type") == "input_text":
|
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(
|
messages.append(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -900,26 +900,27 @@ async def set_terminal_servers(request: Request):
|
|||||||
"""Load and cache OpenAPI specs from all TERMINAL_SERVER_CONNECTIONS."""
|
"""Load and cache OpenAPI specs from all TERMINAL_SERVER_CONNECTIONS."""
|
||||||
connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
|
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 = []
|
server_configs = []
|
||||||
for connection in connections:
|
for connection in connections:
|
||||||
conn_id = connection.get("id", "")
|
|
||||||
if not connection.get("url"):
|
if not connection.get("url"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
auth_type = connection.get("auth_type", "bearer")
|
enabled = connection.get("enabled", True)
|
||||||
token = None
|
|
||||||
if auth_type == "bearer":
|
|
||||||
token = connection.get("key", "")
|
|
||||||
|
|
||||||
server_configs.append({
|
server_configs.append({
|
||||||
"url": connection.get("url", ""),
|
"url": connection.get("url", ""),
|
||||||
"key": token or "",
|
"key": connection.get("key", ""),
|
||||||
"auth_type": auth_type,
|
"auth_type": connection.get("auth_type", "bearer"),
|
||||||
"path": "openapi.json",
|
"path": connection.get("path", "/openapi.json"),
|
||||||
"spec_type": "url",
|
"spec_type": "url",
|
||||||
"config": {"enable": True},
|
# get_tool_servers_data reads config.enable to filter active servers
|
||||||
"info": {"id": conn_id, "name": connection.get("name", "")},
|
"config": {"enable": enabled},
|
||||||
|
"info": {
|
||||||
|
"id": connection.get("id", ""),
|
||||||
|
"name": connection.get("name", ""),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
request.app.state.TERMINAL_SERVERS = await get_tool_servers_data(server_configs)
|
request.app.state.TERMINAL_SERVERS = await get_tool_servers_data(server_configs)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
let showConnectionModal = false;
|
let showConnectionModal = false;
|
||||||
|
|
||||||
// Terminal server admin connections
|
// Terminal server admin connections
|
||||||
let terminalConnections: { id: string; url: string; key: string; name: string }[] = [];
|
let terminalConnections = [];
|
||||||
let showAddTerminalModal = false;
|
let showAddTerminalModal = false;
|
||||||
let editTerminalIdx: number | null = null;
|
let editTerminalIdx: number | null = null;
|
||||||
let showDeleteTerminalConfirm = false;
|
let showDeleteTerminalConfirm = false;
|
||||||
@@ -72,20 +72,17 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addTerminalConnection = (server: { url: string; key: string; name?: string }) => {
|
const addTerminalConnection = (server) => {
|
||||||
terminalConnections = [
|
terminalConnections = [
|
||||||
...terminalConnections,
|
...terminalConnections,
|
||||||
{ id: uuidv4(), url: server.url, key: server.key, name: server.name ?? '' }
|
{ ...server, id: server.id ?? uuidv4() }
|
||||||
];
|
];
|
||||||
saveTerminalServers();
|
saveTerminalServers();
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateTerminalConnection = (
|
const updateTerminalConnection = (idx: number, updated) => {
|
||||||
idx: number,
|
|
||||||
updated: { url: string; key: string; name?: string }
|
|
||||||
) => {
|
|
||||||
terminalConnections = terminalConnections.map((c, i) =>
|
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();
|
saveTerminalServers();
|
||||||
};
|
};
|
||||||
@@ -191,15 +188,15 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if servers.length === 0}
|
{#if servers.length === 0}
|
||||||
<div class="text-xs text-gray-400 dark:text-gray-500">
|
<div class="text-xs text-gray-400 dark:text-gray-500">
|
||||||
{$i18n.t('No tool server connections configured.')}
|
{$i18n.t('No tool server connections configured.')}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="my-1.5">
|
<div class="my-1.5">
|
||||||
<div class="text-xs text-gray-500">
|
<div class="text-xs text-gray-500">
|
||||||
{$i18n.t('Connect to your own OpenAPI compatible external tool servers.')}
|
{$i18n.t('Connect to your own OpenAPI compatible external tool servers.')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -235,7 +232,7 @@
|
|||||||
<div class="flex w-full gap-2 items-center">
|
<div class="flex w-full gap-2 items-center">
|
||||||
<Tooltip className="w-full relative" content={''} placement="top-start">
|
<Tooltip className="w-full relative" content={''} placement="top-start">
|
||||||
<div class="flex w-full">
|
<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')}>
|
<Tooltip content={$i18n.t('Terminal')}>
|
||||||
<Cloud className="size-4" strokeWidth="1.5" />
|
<Cloud className="size-4" strokeWidth="1.5" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -260,6 +257,18 @@
|
|||||||
<Cog6 />
|
<Cog6 />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -42,12 +42,12 @@
|
|||||||
functions,
|
functions,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
pinnedChats,
|
pinnedChats,
|
||||||
showEmbeds
|
showEmbeds,
|
||||||
|
selectedTerminalId
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
|
|
||||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
convertMessagesToHistory,
|
convertMessagesToHistory,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
@@ -140,13 +140,18 @@
|
|||||||
|
|
||||||
let selectedToolIds = [];
|
let selectedToolIds = [];
|
||||||
let selectedFilterIds = [];
|
let selectedFilterIds = [];
|
||||||
|
|
||||||
let imageGenerationEnabled = false;
|
let imageGenerationEnabled = false;
|
||||||
let webSearchEnabled = false;
|
let webSearchEnabled = false;
|
||||||
let codeInterpreterEnabled = 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) {
|
$: 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));
|
const missingIds = terminalIds.filter((id) => !selectedToolIds.includes(id));
|
||||||
if (missingIds.length > 0) {
|
if (missingIds.length > 0) {
|
||||||
selectedToolIds = [...selectedToolIds, ...missingIds];
|
selectedToolIds = [...selectedToolIds, ...missingIds];
|
||||||
@@ -155,7 +160,10 @@
|
|||||||
|
|
||||||
// Remove disabled terminal servers from selectedToolIds automatically
|
// Remove disabled terminal servers from selectedToolIds automatically
|
||||||
$: if (selectedToolIds.length > 0) {
|
$: 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(
|
const invalidTerminalIds = selectedToolIds.filter(
|
||||||
(id) => id.startsWith('direct_server:terminal_') && !terminalIds.includes(id)
|
(id) => id.startsWith('direct_server:terminal_') && !terminalIds.includes(id)
|
||||||
);
|
);
|
||||||
@@ -354,9 +362,12 @@
|
|||||||
selectedToolIds = selectedToolIds.filter((id) => !id.startsWith('direct_server:'));
|
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) {
|
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])];
|
selectedToolIds = [...new Set([...selectedToolIds, ...terminalIds])];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,6 +661,14 @@
|
|||||||
|
|
||||||
audioQueue.set(new AudioQueue(document.getElementById('audioElement')));
|
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) => {
|
pageSubscribe = page.subscribe(async (p) => {
|
||||||
if (p.url.pathname === '/') {
|
if (p.url.pathname === '/') {
|
||||||
await tick();
|
await tick();
|
||||||
@@ -2130,9 +2149,8 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the active terminal (first accessible backend terminal, or user-configured)
|
// Use the user-selected terminal from the dropdown
|
||||||
const activeTerminal = $terminalServers?.[0] ?? null;
|
const activeTerminalId = $selectedTerminalId ?? null;
|
||||||
const activeTerminalId = activeTerminal?.id ?? null;
|
|
||||||
|
|
||||||
const res = await generateOpenAIChatCompletion(
|
const res = await generateOpenAIChatCompletion(
|
||||||
localStorage.token,
|
localStorage.token,
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
showArtifacts,
|
showArtifacts,
|
||||||
showEmbeds,
|
showEmbeds,
|
||||||
settings,
|
settings,
|
||||||
showFileNavPath
|
showFileNavPath,
|
||||||
|
selectedTerminalId,
|
||||||
|
user
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
|
|
||||||
import { uploadFile } from '$lib/apis/files';
|
import { uploadFile } from '$lib/apis/files';
|
||||||
@@ -59,10 +61,24 @@
|
|||||||
// Tab state for Controls+Files panel
|
// Tab state for Controls+Files panel
|
||||||
let activeTab: 'controls' | 'files' | 'overview' = savedTab;
|
let activeTab: 'controls' | 'files' | 'overview' = savedTab;
|
||||||
$: savedTab = activeTab;
|
$: savedTab = activeTab;
|
||||||
$: hasTerminal = !!($settings?.terminalServers ?? []).find((s) => s.enabled)?.url
|
|
||||||
|| $terminalServers.length > 0;
|
|
||||||
$: hasMessages = history?.messages && Object.keys(history.messages).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
|
// Auto-switch to Files tab when display_file is triggered
|
||||||
$: if ($showFileNavPath) {
|
$: if ($showFileNavPath) {
|
||||||
@@ -70,6 +86,12 @@
|
|||||||
showControls.set(true);
|
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
|
// Attach a terminal file to the chat input
|
||||||
const handleTerminalAttach = async (blob: Blob, name: string, contentType: string) => {
|
const handleTerminalAttach = async (blob: Blob, name: string, contentType: string) => {
|
||||||
const tempItemId = uuidv4();
|
const tempItemId = uuidv4();
|
||||||
@@ -182,7 +204,9 @@
|
|||||||
document.addEventListener('mousedown', onMouseDown);
|
document.addEventListener('mousedown', onMouseDown);
|
||||||
document.addEventListener('mouseup', onMouseUp);
|
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 controls were persisted as open, set the pane to the saved size
|
||||||
if ($showControls && pane) {
|
if ($showControls && pane) {
|
||||||
@@ -247,15 +271,17 @@
|
|||||||
<!-- Tab bar -->
|
<!-- Tab bar -->
|
||||||
<div class="flex items-center justify-between px-2 pt-2.5 pb-2 shrink-0">
|
<div class="flex items-center justify-between px-2 pt-2.5 pb-2 shrink-0">
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<button
|
{#if showControlsTab}
|
||||||
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'controls'
|
<button
|
||||||
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
|
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'controls'
|
||||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
|
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
|
||||||
on:click={() => (activeTab = 'controls')}
|
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
|
||||||
>
|
on:click={() => (activeTab = 'controls')}
|
||||||
{$i18n.t('Controls')}
|
>
|
||||||
</button>
|
{$i18n.t('Controls')}
|
||||||
{#if hasTerminal}
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if showFilesTab}
|
||||||
<button
|
<button
|
||||||
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'files'
|
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'
|
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
|
||||||
@@ -265,7 +291,7 @@
|
|||||||
{$i18n.t('Files')}
|
{$i18n.t('Files')}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if hasMessages}
|
{#if showOverviewTab}
|
||||||
<button
|
<button
|
||||||
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'overview'
|
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'
|
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
|
||||||
@@ -310,7 +336,7 @@
|
|||||||
}}
|
}}
|
||||||
onClose={() => showControls.set(false)}
|
onClose={() => showControls.set(false)}
|
||||||
/>
|
/>
|
||||||
{:else if activeTab === 'files' && hasTerminal}
|
{:else if activeTab === 'files' && $selectedTerminalId}
|
||||||
<FileNav onAttach={handleTerminalAttach} />
|
<FileNav onAttach={handleTerminalAttach} />
|
||||||
{:else}
|
{:else}
|
||||||
<Controls embed={true} {models} bind:chatFiles bind:params />
|
<Controls embed={true} {models} bind:chatFiles bind:params />
|
||||||
@@ -383,15 +409,17 @@
|
|||||||
<!-- Tab bar -->
|
<!-- Tab bar -->
|
||||||
<div class="flex items-center justify-between px-2 pt-2.5 pb-2 shrink-0">
|
<div class="flex items-center justify-between px-2 pt-2.5 pb-2 shrink-0">
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<button
|
{#if showControlsTab}
|
||||||
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'controls'
|
<button
|
||||||
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
|
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'controls'
|
||||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
|
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
|
||||||
on:click={() => (activeTab = 'controls')}
|
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}"
|
||||||
>
|
on:click={() => (activeTab = 'controls')}
|
||||||
{$i18n.t('Controls')}
|
>
|
||||||
</button>
|
{$i18n.t('Controls')}
|
||||||
{#if hasTerminal}
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if showFilesTab}
|
||||||
<button
|
<button
|
||||||
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'files'
|
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'
|
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
|
||||||
@@ -401,7 +429,7 @@
|
|||||||
{$i18n.t('Files')}
|
{$i18n.t('Files')}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if hasMessages}
|
{#if showOverviewTab}
|
||||||
<button
|
<button
|
||||||
class="px-2.5 py-1 text-sm rounded-lg transition {activeTab === 'overview'
|
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'
|
? 'bg-gray-100 dark:bg-gray-800 font-medium text-gray-900 dark:text-white'
|
||||||
@@ -451,7 +479,7 @@
|
|||||||
}}
|
}}
|
||||||
onClose={() => showControls.set(false)}
|
onClose={() => showControls.set(false)}
|
||||||
/>
|
/>
|
||||||
{:else if activeTab === 'files' && hasTerminal}
|
{:else if activeTab === 'files' && $selectedTerminalId}
|
||||||
<FileNav onAttach={handleTerminalAttach} />
|
<FileNav onAttach={handleTerminalAttach} />
|
||||||
{:else}
|
{:else}
|
||||||
<Controls embed={true} {models} bind:chatFiles bind:params />
|
<Controls embed={true} {models} bind:chatFiles bind:params />
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { getContext, onMount, onDestroy, tick } from 'svelte';
|
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 { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
import {
|
import {
|
||||||
getCwd,
|
getCwd,
|
||||||
@@ -59,12 +59,31 @@
|
|||||||
let showDeleteConfirm = false;
|
let showDeleteConfirm = false;
|
||||||
let shiftKey = false;
|
let shiftKey = false;
|
||||||
|
|
||||||
$: firstTerminal = $terminalServers?.[0] ?? null;
|
$: firstTerminal = $selectedTerminalId
|
||||||
$: activeUserTerminal = ($settings?.terminalServers ?? []).find((s) => s.enabled);
|
? ($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 ?? '';
|
$: terminalUrl = firstTerminal?.url ?? activeUserTerminal?.url ?? '';
|
||||||
$: terminalKey = firstTerminal?.key ?? activeUserTerminal?.key ?? '';
|
$: terminalKey = isSystemTerminal ? localStorage.token : (activeUserTerminal?.key ?? '');
|
||||||
$: configured = !!terminalUrl;
|
$: 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
|
$: breadcrumbs = currentPath
|
||||||
.split('/')
|
.split('/')
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
|||||||
@@ -20,7 +20,10 @@
|
|||||||
|
|
||||||
const MD_EXTS = new Set(['md', 'markdown', 'mdx']);
|
const MD_EXTS = new Set(['md', 'markdown', 'mdx']);
|
||||||
$: isMarkdown = MD_EXTS.has(selectedFile?.split('.').pop()?.toLowerCase() ?? '');
|
$: 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;
|
let showRaw = false;
|
||||||
// Reset to preview mode when switching files
|
// Reset to preview mode when switching files
|
||||||
@@ -55,7 +58,11 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</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 -->
|
<!-- Floating action buttons -->
|
||||||
<div class="absolute top-2 right-2 z-10 flex gap-1">
|
<div class="absolute top-2 right-2 z-10 flex gap-1">
|
||||||
{#if fileImageUrl !== null}
|
{#if fileImageUrl !== null}
|
||||||
@@ -78,14 +85,40 @@
|
|||||||
>
|
>
|
||||||
{#if showRaw}
|
{#if showRaw}
|
||||||
<!-- Eye icon: switch to preview -->
|
<!-- 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">
|
<svg
|
||||||
<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" />
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
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>
|
</svg>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Code icon: switch to raw -->
|
<!-- 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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -28,8 +28,11 @@
|
|||||||
showCallOverlay,
|
showCallOverlay,
|
||||||
tools,
|
tools,
|
||||||
toolServers,
|
toolServers,
|
||||||
|
terminalServers,
|
||||||
user as _user,
|
user as _user,
|
||||||
showControls,
|
showControls,
|
||||||
|
showSettings,
|
||||||
|
selectedTerminalId,
|
||||||
TTSWorker,
|
TTSWorker,
|
||||||
temporaryChatEnabled
|
temporaryChatEnabled
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
@@ -81,8 +84,13 @@
|
|||||||
import Voice from '../icons/Voice.svelte';
|
import Voice from '../icons/Voice.svelte';
|
||||||
import Cloud from '../icons/Cloud.svelte';
|
import Cloud from '../icons/Cloud.svelte';
|
||||||
import IntegrationsMenu from './MessageInput/IntegrationsMenu.svelte';
|
import IntegrationsMenu from './MessageInput/IntegrationsMenu.svelte';
|
||||||
|
import TerminalMenu from './MessageInput/TerminalMenu.svelte';
|
||||||
import Component from '../icons/Component.svelte';
|
import Component from '../icons/Component.svelte';
|
||||||
import PlusAlt from '../icons/PlusAlt.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 CommandSuggestionList from './MessageInput/CommandSuggestionList.svelte';
|
||||||
import Knobs from '../icons/Knobs.svelte';
|
import Knobs from '../icons/Knobs.svelte';
|
||||||
@@ -124,6 +132,8 @@
|
|||||||
export let webSearchEnabled = false;
|
export let webSearchEnabled = false;
|
||||||
export let codeInterpreterEnabled = false;
|
export let codeInterpreterEnabled = false;
|
||||||
|
|
||||||
|
let showTerminalMenu = false;
|
||||||
|
|
||||||
export let messageQueue: { id: string; prompt: string; files: any[] }[] = [];
|
export let messageQueue: { id: string; prompt: string; files: any[] }[] = [];
|
||||||
export let onQueueSendNow: (id: string) => void = () => {};
|
export let onQueueSendNow: (id: string) => void = () => {};
|
||||||
export let onQueueEdit: (id: string) => void = () => {};
|
export let onQueueEdit: (id: string) => void = () => {};
|
||||||
@@ -1019,7 +1029,6 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<ToolServersModal bind:show={showTools} {selectedToolIds} />
|
<ToolServersModal bind:show={showTools} {selectedToolIds} />
|
||||||
|
|
||||||
<InputVariablesModal
|
<InputVariablesModal
|
||||||
@@ -1793,23 +1802,8 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if (!history?.currentId || history.messages[history.currentId]?.done == true) && ($_user?.role === 'admin' || ($_user?.permissions?.chat?.stt ?? true))}
|
{#if (!history?.currentId || history.messages[history.currentId]?.done == true) && ($_user?.role === 'admin' || ($_user?.permissions?.chat?.stt ?? true))}
|
||||||
<!-- Active Terminal Indicator (Always On) -->
|
<!-- Terminal Server Selector -->
|
||||||
{@const activeTerminal = ($settings?.terminalServers ?? []).find(
|
<TerminalMenu bind:show={showTerminalMenu} />
|
||||||
(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}
|
|
||||||
|
|
||||||
<!-- {$i18n.t('Record voice')} -->
|
<!-- {$i18n.t('Record voice')} -->
|
||||||
<Tooltip content={$i18n.t('Dictate')}>
|
<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