mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-29 06:32:52 +00:00
refac
This commit is contained in:
parent
c596d40bd4
commit
08bfaa4be6
30 changed files with 1034 additions and 635 deletions
|
|
@ -14,7 +14,7 @@ from urllib.parse import urlparse
|
|||
import redis
|
||||
import requests
|
||||
from authlib.integrations.starlette_client import OAuth
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, StrictBool, TypeAdapter
|
||||
|
||||
from open_webui.env import (
|
||||
USE_SLIM,
|
||||
|
|
@ -1745,6 +1745,12 @@ except Exception as e:
|
|||
|
||||
DEFAULT_INTERFACE_SETTINGS = default_interface_settings if isinstance(default_interface_settings, dict) else {}
|
||||
|
||||
try:
|
||||
UI_VISIBILITY = TypeAdapter(dict[str, StrictBool]).validate_json(os.getenv('UI_VISIBILITY', '{}'))
|
||||
except Exception as e:
|
||||
log.exception(f'Error loading UI_VISIBILITY: {e}')
|
||||
UI_VISIBILITY = {}
|
||||
|
||||
DEFAULT_USER_ROLE = os.getenv('DEFAULT_USER_ROLE', 'pending')
|
||||
|
||||
DEFAULT_GROUP_ID = os.getenv('DEFAULT_GROUP_ID', '')
|
||||
|
|
@ -3191,6 +3197,7 @@ DEFAULT_CONFIG = {
|
|||
'ui.default_models': DEFAULT_MODELS,
|
||||
'ui.default_pinned_models': DEFAULT_PINNED_MODELS,
|
||||
'ui.default_interface_settings': DEFAULT_INTERFACE_SETTINGS,
|
||||
'ui.visibility': UI_VISIBILITY,
|
||||
'ui.i18n': {},
|
||||
'ui.prompt_suggestions': DEFAULT_PROMPT_SUGGESTIONS,
|
||||
'ui.prompt_suggestions_i18n': DEFAULT_PROMPT_SUGGESTIONS_I18N,
|
||||
|
|
|
|||
|
|
@ -2366,6 +2366,7 @@ async def get_app_config(request: Request):
|
|||
'ui.default_models',
|
||||
'ui.default_pinned_models',
|
||||
'ui.default_interface_settings',
|
||||
'ui.visibility',
|
||||
'ui.i18n',
|
||||
'ui.prompt_suggestions',
|
||||
'ui.prompt_suggestions_i18n',
|
||||
|
|
@ -2532,6 +2533,7 @@ async def get_app_config(request: Request):
|
|||
},
|
||||
'ui': {
|
||||
'default_interface_settings': config.get('ui.default_interface_settings'),
|
||||
'visibility': config.get('ui.visibility'),
|
||||
'pending_user_overlay_title': config.get('ui.pending_user_overlay_title'),
|
||||
'pending_user_overlay_content': config.get('ui.pending_user_overlay_content'),
|
||||
'response_watermark': config.get('ui.watermark'),
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ from open_webui.utils.tools import (
|
|||
set_terminal_servers,
|
||||
set_tool_servers,
|
||||
)
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic import BaseModel, ConfigDict, StrictBool
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
|
@ -127,6 +127,24 @@ async def get_config_namespace(namespace: str, user=Depends(get_admin_user)):
|
|||
return await Config.get_namespace(namespace)
|
||||
|
||||
|
||||
class InterfaceVisibilityForm(BaseModel):
|
||||
visibility: dict[str, StrictBool]
|
||||
|
||||
|
||||
@router.post('/interface/visibility', response_model=InterfaceVisibilityForm)
|
||||
async def set_interface_visibility(request: Request, form_data: InterfaceVisibilityForm, user=Depends(get_admin_user)):
|
||||
await Config.upsert({'ui.visibility': form_data.visibility})
|
||||
await publish_event(
|
||||
request,
|
||||
EVENTS.CONFIG_UPDATED,
|
||||
actor=user,
|
||||
subject_id='interface',
|
||||
subject_type='config',
|
||||
data={'keys': ['ui.visibility']},
|
||||
)
|
||||
return form_data
|
||||
|
||||
|
||||
############################
|
||||
# Connections Config
|
||||
############################
|
||||
|
|
|
|||
|
|
@ -1,5 +1,22 @@
|
|||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
import type { Banner } from '$lib/types';
|
||||
import type { Visibility } from '$lib/utils/ui-visibility';
|
||||
|
||||
export const setInterfaceVisibility = async (
|
||||
token: string,
|
||||
visibility: Visibility
|
||||
): Promise<Visibility> => {
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/interface/visibility`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ visibility })
|
||||
});
|
||||
if (!res.ok) throw (await res.json()).detail;
|
||||
return (await res.json()).visibility;
|
||||
};
|
||||
|
||||
export const importConfig = async (token: string, config: object) => {
|
||||
let error = null;
|
||||
|
|
|
|||
|
|
@ -89,6 +89,10 @@
|
|||
let keyboardIndex = 0;
|
||||
let plot: HTMLDivElement;
|
||||
let chartWidth = 0;
|
||||
let legendExpanded = false;
|
||||
$: legendColumns = Math.max(1, Math.floor((chartWidth + 16) / 256));
|
||||
$: legendLimit = legendColumns * 2 - 1;
|
||||
$: legendIds = legendExpanded ? ids : ids.slice(0, legendLimit);
|
||||
let tooltip: Instance | undefined;
|
||||
let tooltipContent: HTMLDivElement;
|
||||
let tooltipId = '';
|
||||
|
|
@ -145,22 +149,27 @@
|
|||
: [0, series.length - 1]
|
||||
)
|
||||
].filter((i) => i >= 0 && i < series.length);
|
||||
$: lanes = visibleIds.map((id) => {
|
||||
$: lanes = (view === 'activity' ? visibleIds : []).map((id) => {
|
||||
const values = series.map((point) => point.models[id] ?? 0);
|
||||
return { id, peak: Math.max(0, ...values), values };
|
||||
});
|
||||
$: stacks = series.map((point) => visibleIds.filter((id) => (point.models[id] ?? 0) > 0));
|
||||
$: stacks =
|
||||
view === 'volume'
|
||||
? series.map((point) => visibleIds.filter((id) => (point.models[id] ?? 0) > 0))
|
||||
: [];
|
||||
$: activityPositions = series.map((point) => (point.time - start) / span);
|
||||
const name = (id: string) => modelNames[id] || id;
|
||||
const number = (value: number) =>
|
||||
new Intl.NumberFormat($i18n.language, { notation: 'compact', maximumFractionDigits: 1 }).format(
|
||||
value
|
||||
);
|
||||
$: numberFormat = new Intl.NumberFormat($i18n.language, {
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 1
|
||||
});
|
||||
const number = (value: number) => numberFormat.format(value);
|
||||
const dateLabel = (date: string) =>
|
||||
dayjs(date)
|
||||
.locale($i18n.language)
|
||||
.format(granularity === 'daily' ? 'MMM D' : 'MMM D, HH:mm');
|
||||
const position = (time: number) => ((time - start + step / 2) / span) * 100;
|
||||
function navigate(event: KeyboardEvent, index: number) {
|
||||
async function navigate(event: KeyboardEvent, index: number) {
|
||||
const buttons = plot.querySelectorAll<HTMLButtonElement>('button[data-point]');
|
||||
const destinations: Record<string, number> = {
|
||||
ArrowRight: index + 1,
|
||||
|
|
@ -171,12 +180,74 @@
|
|||
if (view === 'activity') {
|
||||
destinations.ArrowDown = index + series.length;
|
||||
destinations.ArrowUp = index - series.length;
|
||||
destinations.End = lanes.length * series.length - 1;
|
||||
}
|
||||
const next = destinations[event.key];
|
||||
if (next === undefined) return;
|
||||
event.preventDefault();
|
||||
keyboardIndex = Math.max(0, Math.min(buttons.length - 1, next));
|
||||
buttons[keyboardIndex]?.focus();
|
||||
keyboardIndex = Math.max(
|
||||
0,
|
||||
Math.min((view === 'activity' ? lanes.length * series.length : buttons.length) - 1, next)
|
||||
);
|
||||
if (view === 'activity') {
|
||||
const row = Math.floor(keyboardIndex / series.length);
|
||||
await tick();
|
||||
buttons[row]?.focus();
|
||||
showTooltip({ currentTarget: buttons[row] }, keyboardIndex % series.length, lanes[row].id);
|
||||
} else buttons[keyboardIndex]?.focus();
|
||||
}
|
||||
|
||||
// One canvas and focus target per lane avoids a DOM element for every model/date pair.
|
||||
function drawActivity(
|
||||
canvas: HTMLCanvasElement,
|
||||
options: {
|
||||
lane: { id: string; peak: number; values: number[] };
|
||||
positions: number[];
|
||||
cellWidth: number;
|
||||
width: number;
|
||||
}
|
||||
) {
|
||||
function draw() {
|
||||
const { lane, positions, cellWidth } = options;
|
||||
const width = canvas.clientWidth;
|
||||
const height = canvas.clientHeight;
|
||||
const scale = window.devicePixelRatio || 1;
|
||||
canvas.width = Math.round(width * scale);
|
||||
canvas.height = Math.round(height * scale);
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context) return;
|
||||
context.scale(scale, scale);
|
||||
context.fillStyle = modelColor(lane.id);
|
||||
lane.values.forEach((value, index) => {
|
||||
if (value <= 0) return;
|
||||
context.globalAlpha = 0.25 + (0.75 * value) / lane.peak;
|
||||
context.fillRect(positions[index] * width, 0, cellWidth * width, height);
|
||||
});
|
||||
}
|
||||
draw();
|
||||
return {
|
||||
update(next: typeof options) {
|
||||
options = next;
|
||||
draw();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function hoverActivity(event: MouseEvent, id: string) {
|
||||
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
const time = start + Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width)) * span;
|
||||
let low = 0;
|
||||
let high = series.length - 1;
|
||||
while (low < high) {
|
||||
const mid = Math.ceil((low + high) / 2);
|
||||
if (series[mid].time <= time) low = mid;
|
||||
else high = mid - 1;
|
||||
}
|
||||
if (time >= series[low].time + step && time < start + span) {
|
||||
hideTooltip();
|
||||
return;
|
||||
}
|
||||
if (active !== low || activeModel !== id) showTooltip(event, low, id);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
|
|
@ -188,7 +259,7 @@
|
|||
placement: 'right-start',
|
||||
offset: [0, 12],
|
||||
popperOptions: {
|
||||
modifiers: [{ name: 'flip', options: { fallbackPlacements: ['left-start'] } }]
|
||||
modifiers: [{ name: 'flip', options: { fallbackPlacements: ['left-start', 'top'] } }]
|
||||
},
|
||||
duration: 0,
|
||||
interactive: true,
|
||||
|
|
@ -203,7 +274,9 @@
|
|||
const rect = (anchor ?? plot).getBoundingClientRect();
|
||||
const height = tooltip?.popper.offsetHeight || tooltipContent.offsetHeight;
|
||||
return new DOMRect(
|
||||
rect.x + rect.width / 2,
|
||||
rect.x +
|
||||
rect.width *
|
||||
(view === 'activity' && active !== null ? position(series[active].time) / 100 : 0.5),
|
||||
plot.getBoundingClientRect().top - height / 2,
|
||||
0,
|
||||
0
|
||||
|
|
@ -214,7 +287,7 @@
|
|||
return () => tooltip?.destroy();
|
||||
});
|
||||
async function showTooltip(
|
||||
event: MouseEvent | FocusEvent,
|
||||
event: { currentTarget: EventTarget | null },
|
||||
index: number,
|
||||
modelId: string | null = null
|
||||
) {
|
||||
|
|
@ -410,6 +483,8 @@
|
|||
{:else}
|
||||
<div class="space-y-1">
|
||||
{#each lanes as lane, row (lane.id)}
|
||||
{@const index =
|
||||
Math.floor(keyboardIndex / series.length) === row ? keyboardIndex % series.length : 0}
|
||||
<div class="grid grid-cols-[7rem_minmax(0,1fr)] gap-2 items-center">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -418,41 +493,34 @@
|
|||
aria-pressed={selectedModel === lane.id}
|
||||
on:click={() => isolate(lane.id)}>{name(lane.id)}</button
|
||||
>
|
||||
<div class="relative h-6 bg-gray-100/70 dark:bg-gray-850/40">
|
||||
{#each series as point, index (point.date)}
|
||||
{@const cellIndex = row * series.length + index}
|
||||
<button
|
||||
type="button"
|
||||
data-point
|
||||
class="absolute inset-y-0 focus-visible:outline focus-visible:outline-1 focus-visible:outline-gray-400"
|
||||
style:left={`${position(point.time)}%`}
|
||||
style:width={`${(100 * step) / span}%`}
|
||||
style:transform="translateX(-50%)"
|
||||
tabindex={keyboardIndex === cellIndex ? 0 : -1}
|
||||
aria-label={`${name(lane.id)}, ${dateLabel(point.date)}: ${lane.values[index].toLocaleString($i18n.language)} ${metricLabel}`}
|
||||
aria-describedby={active === index && activeModel === lane.id
|
||||
? tooltipId
|
||||
: undefined}
|
||||
on:mouseenter={(event) => showTooltip(event, index, lane.id)}
|
||||
on:mouseleave={leaveTooltip}
|
||||
on:focus={(event) => {
|
||||
keyboardIndex = cellIndex;
|
||||
showTooltip(event, index, lane.id);
|
||||
}}
|
||||
on:blur={hideTooltip}
|
||||
on:click={(event) => showTooltip(event, index, lane.id)}
|
||||
on:keydown={(event) => navigate(event, cellIndex)}
|
||||
>
|
||||
<span
|
||||
class="block absolute inset-0"
|
||||
style:background-color={modelColor(lane.id)}
|
||||
style:opacity={lane.values[index] > 0
|
||||
? 0.25 + (0.75 * lane.values[index]) / lane.peak
|
||||
: 0}
|
||||
></span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
data-point
|
||||
class="relative h-6 bg-gray-100/70 dark:bg-gray-850/40 focus-visible:outline focus-visible:outline-1 focus-visible:outline-gray-400"
|
||||
tabindex={Math.floor(keyboardIndex / series.length) === row ? 0 : -1}
|
||||
aria-label={`${name(lane.id)}, ${dateLabel(series[index].date)}: ${lane.values[index].toLocaleString($i18n.language)} ${metricLabel}`}
|
||||
aria-describedby={activeModel === lane.id ? tooltipId : undefined}
|
||||
on:mousemove={(event) => hoverActivity(event, lane.id)}
|
||||
on:mouseleave={leaveTooltip}
|
||||
on:focus={(event) => {
|
||||
keyboardIndex = row * series.length + index;
|
||||
showTooltip(event, index, lane.id);
|
||||
}}
|
||||
on:blur={hideTooltip}
|
||||
on:click={(event) => hoverActivity(event, lane.id)}
|
||||
on:keydown={(event) => navigate(event, row * series.length + index)}
|
||||
>
|
||||
<canvas
|
||||
class="absolute inset-0 w-full h-full pointer-events-none"
|
||||
aria-hidden="true"
|
||||
use:drawActivity={{
|
||||
lane,
|
||||
positions: activityPositions,
|
||||
cellWidth: step / span,
|
||||
width: chartWidth
|
||||
}}
|
||||
></canvas>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -489,16 +557,19 @@
|
|||
>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="mt-2 flex flex-wrap items-center gap-x-4 gap-y-1 text-[0.6875rem]">
|
||||
<div
|
||||
class="mt-2 grid items-center gap-x-4 gap-y-1 text-[0.6875rem]"
|
||||
style:grid-template-columns={`repeat(${legendColumns}, minmax(0, 1fr))`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="py-1"
|
||||
class="py-1 text-left"
|
||||
class:text-gray-900={selectedModel === null}
|
||||
class:dark:text-gray-200={selectedModel === null}
|
||||
aria-pressed={selectedModel === null}
|
||||
on:click={() => isolate(null)}>{$i18n.t('All models')}</button
|
||||
>
|
||||
{#each ids as id (id)}
|
||||
{#each legendIds as id (id)}
|
||||
<button
|
||||
type="button"
|
||||
class="flex min-w-0 max-w-full items-center gap-1.5 py-1 text-left hover:text-gray-900 dark:hover:text-gray-100"
|
||||
|
|
@ -510,7 +581,7 @@
|
|||
<span class="size-2 shrink-0" style:background-color={modelColor(id)}></span>
|
||||
<span class="max-w-40 truncate" title={name(id)}>{name(id)}</span>
|
||||
<span
|
||||
class="w-14 shrink-0 text-right tabular-nums text-gray-400 dark:text-gray-500"
|
||||
class="ml-auto w-14 shrink-0 text-right tabular-nums text-gray-400 dark:text-gray-500"
|
||||
title={(hovered ? (hoveredModels?.[id] ?? 0) : (totals[id] ?? 0)).toLocaleString(
|
||||
$i18n.language
|
||||
)}>{number(hovered ? (hoveredModels?.[id] ?? 0) : (totals[id] ?? 0))}</span
|
||||
|
|
@ -518,6 +589,15 @@
|
|||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{#if ids.length > legendLimit}
|
||||
<button
|
||||
type="button"
|
||||
class="mt-1 py-1 text-[0.6875rem] hover:text-gray-900 dark:hover:text-gray-100"
|
||||
aria-expanded={legendExpanded}
|
||||
on:click={() => (legendExpanded = !legendExpanded)}
|
||||
>{legendExpanded ? $i18n.t('Show less') : $i18n.t('Show more')}</button
|
||||
>
|
||||
{/if}
|
||||
<p class="mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-500">
|
||||
{view === 'activity'
|
||||
? $i18n.t('Intensity relative to each model’s peak. Blank means no recorded usage.')
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@
|
|||
import AdminSettingRow from './AdminSettingRow.svelte';
|
||||
import AdminSettingSection from './AdminSettingSection.svelte';
|
||||
import { config as appConfig } from '$lib/stores';
|
||||
import { setInterfaceVisibility } from '$lib/apis/configs';
|
||||
import type { Visibility } from '$lib/utils/ui-visibility';
|
||||
import VisibilitySettings from './Interface/Visibility.svelte';
|
||||
|
||||
export let saveHandler: () => void;
|
||||
|
||||
|
|
@ -58,6 +61,7 @@
|
|||
};
|
||||
let toolSearchAlwaysLoaded = '';
|
||||
let showTaskParameters = false;
|
||||
let visibility: Visibility = { ...($appConfig?.ui?.visibility ?? {}) };
|
||||
|
||||
const configuredParams = (params: Record<string, any> = {}) =>
|
||||
Object.fromEntries(
|
||||
|
|
@ -73,7 +77,7 @@
|
|||
};
|
||||
|
||||
try {
|
||||
[taskConfig, chatConfig] = await Promise.all([
|
||||
[taskConfig, chatConfig, visibility] = await Promise.all([
|
||||
updateTaskConfig(localStorage.token, taskConfigPayload),
|
||||
updateChatConfig(localStorage.token, {
|
||||
...chatConfig,
|
||||
|
|
@ -81,7 +85,8 @@
|
|||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item !== '')
|
||||
})
|
||||
}),
|
||||
setInterfaceVisibility(localStorage.token, visibility)
|
||||
]);
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
|
|
@ -94,6 +99,7 @@
|
|||
current
|
||||
? {
|
||||
...current,
|
||||
ui: { ...current.ui, visibility },
|
||||
features: {
|
||||
...current.features,
|
||||
enable_context_compaction: chatConfig.ENABLE_CONTEXT_COMPACTION,
|
||||
|
|
@ -193,7 +199,8 @@
|
|||
</h2>
|
||||
|
||||
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
|
||||
<AdminSettingSection title={$i18n.t('settings.admin.interface.sections.tasks.title')} first>
|
||||
<VisibilitySettings bind:visibility />
|
||||
<AdminSettingSection title={$i18n.t('settings.admin.interface.sections.tasks.title')}>
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { i18n as I18n } from 'i18next';
|
||||
import {
|
||||
CONTROL_GROUPS,
|
||||
CONTROL_LABELS,
|
||||
type ControlId,
|
||||
type Visibility
|
||||
} from '$lib/utils/ui-visibility';
|
||||
import ModelSettingsSection from '$lib/components/workspace/Models/ModelSettingsSection.svelte';
|
||||
import ModelSettingToggle from '$lib/components/workspace/Models/ModelSettingToggle.svelte';
|
||||
import AdminSettingSection from '../AdminSettingSection.svelte';
|
||||
import AdminSettingRow from '../AdminSettingRow.svelte';
|
||||
|
||||
const i18n: Writable<I18n> = getContext('i18n');
|
||||
export let visibility: Visibility = {};
|
||||
let expanded = false;
|
||||
const controls = Object.keys(CONTROL_LABELS) as ControlId[];
|
||||
|
||||
const setVisible = (id: ControlId, visible: boolean) => {
|
||||
visibility = { ...visibility };
|
||||
if (visible) delete visibility[id];
|
||||
else visibility[id] = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<AdminSettingSection title={$i18n.t('Visibility')} first>
|
||||
<AdminSettingRow
|
||||
let:labelId
|
||||
label={$i18n.t('settings.admin.interface.visibility.title')}
|
||||
description={$i18n.t('settings.admin.interface.visibility.description')}
|
||||
>
|
||||
<button
|
||||
id={`${labelId}-control`}
|
||||
type="button"
|
||||
class="focus-ring text-xs text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
|
||||
aria-expanded={expanded}
|
||||
aria-controls="interface-visible-controls"
|
||||
on:click={() => (expanded = !expanded)}
|
||||
>
|
||||
{expanded ? $i18n.t('Hide') : $i18n.t('Show')}
|
||||
</button>
|
||||
</AdminSettingRow>
|
||||
<div id="interface-visible-controls" hidden={!expanded}>
|
||||
<div class="space-y-0.5">
|
||||
{#each CONTROL_GROUPS as [prefix, label]}
|
||||
<ModelSettingsSection label={$i18n.t(label)}>
|
||||
<div class="grid grid-cols-1 gap-x-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each controls.filter((id) => id.startsWith(prefix)) as id}
|
||||
<ModelSettingToggle
|
||||
label={$i18n.t(CONTROL_LABELS[id])}
|
||||
checked={visibility[id] !== false}
|
||||
on:change={(event) => setVisible(id, event.detail)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</ModelSettingsSection>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</AdminSettingSection>
|
||||
|
|
@ -14,7 +14,8 @@
|
|||
selectedTerminalId,
|
||||
settings,
|
||||
terminalServers,
|
||||
terminalSkills
|
||||
terminalSkills,
|
||||
visibleControls
|
||||
} from '$lib/stores';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import ChatBubbleDotted from '$lib/components/icons/ChatBubbleDotted.svelte';
|
||||
|
|
@ -60,19 +61,19 @@
|
|||
...(canCompact && 'compact'.startsWith(query.toLowerCase())
|
||||
? [{ type: 'command', data: { id: 'compact' } }]
|
||||
: []),
|
||||
...(canFork && 'fork'.startsWith(query.toLowerCase())
|
||||
...(canFork && $visibleControls['chat.fork'] && 'fork'.startsWith(query.toLowerCase())
|
||||
? [{ type: 'command', data: { id: 'fork' } }]
|
||||
: []),
|
||||
...(canStatus && 'status'.startsWith(query.toLowerCase())
|
||||
...(canStatus && $visibleControls['chat.status'] && 'status'.startsWith(query.toLowerCase())
|
||||
? [{ type: 'command', data: { id: 'status' } }]
|
||||
: []),
|
||||
...('model'.startsWith(query.toLowerCase())
|
||||
...($visibleControls['chat.model_command'] && 'model'.startsWith(query.toLowerCase())
|
||||
? [{ type: 'command', data: { id: 'model' } }]
|
||||
: []),
|
||||
...('settings'.startsWith(query.toLowerCase())
|
||||
...($visibleControls['chat.settings_command'] && 'settings'.startsWith(query.toLowerCase())
|
||||
? [{ type: 'command', data: { id: 'settings' } }]
|
||||
: []),
|
||||
...(canCompare && 'compare'.startsWith(query.toLowerCase())
|
||||
...(canCompare && $visibleControls['chat.compare'] && 'compare'.startsWith(query.toLowerCase())
|
||||
? [{ type: 'command', data: { id: 'compare' } }]
|
||||
: []),
|
||||
...(hasChatContent &&
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { getContext, onMount, tick } from 'svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
import { config, user, tools as _tools, mobile, knowledge } from '$lib/stores';
|
||||
import { config, user, tools as _tools, mobile, knowledge, visibleControls } from '$lib/stores';
|
||||
import { getKnowledgeBases } from '$lib/apis/knowledge';
|
||||
|
||||
import { createPicker } from '$lib/utils/google-drive-picker';
|
||||
|
|
@ -214,7 +214,7 @@
|
|||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{#if visionCapableModels.length > 0}
|
||||
{#if visionCapableModels.length > 0 && $visibleControls['chat.capture']}
|
||||
<Tooltip
|
||||
content={fileUploadCapableModels.length !== selectedModels.length
|
||||
? $i18n.t('Model(s) do not support file upload')
|
||||
|
|
@ -250,90 +250,96 @@
|
|||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
role="menuitem"
|
||||
type="button"
|
||||
class="flex w-full gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left"
|
||||
on:click={async () => {
|
||||
show = false;
|
||||
await tick();
|
||||
onCompare();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
class="size-3.5 shrink-0"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect x="3" y="4" width="18" height="16" rx="2" />
|
||||
<path d="M12 4v16M5.5 12h4M14.5 12h4M16.5 10v4" />
|
||||
</svg>
|
||||
<span>{$i18n.t('Compare text')}</span>
|
||||
</button>
|
||||
|
||||
<Tooltip
|
||||
content={!webUploadEnabled
|
||||
? $i18n.t('You do not have permission to upload web content.')
|
||||
: ''}
|
||||
className="w-full"
|
||||
>
|
||||
{#if $visibleControls['chat.compare']}
|
||||
<button
|
||||
role="menuitem"
|
||||
class="flex w-full gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal select-none cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left {!webUploadEnabled
|
||||
? 'opacity-50'
|
||||
: ''}"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (webUploadEnabled) {
|
||||
showAttachWebpageModal = true;
|
||||
show = false;
|
||||
}
|
||||
class="flex w-full gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left"
|
||||
on:click={async () => {
|
||||
show = false;
|
||||
await tick();
|
||||
onCompare();
|
||||
}}
|
||||
>
|
||||
<GlobeAlt />
|
||||
<div class="line-clamp-1">{$i18n.t('Attach Webpage')}</div>
|
||||
<svg
|
||||
class="size-3.5 shrink-0"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect x="3" y="4" width="18" height="16" rx="2" />
|
||||
<path d="M12 4v16M5.5 12h4M14.5 12h4M16.5 10v4" />
|
||||
</svg>
|
||||
<span>{$i18n.t('Compare text')}</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<Tooltip
|
||||
content={fileUploadCapableModels.length !== selectedModels.length
|
||||
? $i18n.t('Model(s) do not support file upload')
|
||||
: !fileUploadEnabled
|
||||
? $i18n.t('You do not have permission to upload files.')
|
||||
{#if $visibleControls['chat.attach_webpage']}
|
||||
<Tooltip
|
||||
content={!webUploadEnabled
|
||||
? $i18n.t('You do not have permission to upload web content.')
|
||||
: ''}
|
||||
className="w-full"
|
||||
>
|
||||
<button
|
||||
role="menuitem"
|
||||
class="flex gap-2 w-full items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal select-none cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left {!fileUploadEnabled
|
||||
? 'opacity-50'
|
||||
: ''}"
|
||||
on:click={() => {
|
||||
if (fileUploadEnabled) {
|
||||
tab = 'files';
|
||||
}
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<DocumentArrowUp />
|
||||
<button
|
||||
role="menuitem"
|
||||
class="flex w-full gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal select-none cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left {!webUploadEnabled
|
||||
? 'opacity-50'
|
||||
: ''}"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (webUploadEnabled) {
|
||||
showAttachWebpageModal = true;
|
||||
show = false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<GlobeAlt />
|
||||
<div class="line-clamp-1">{$i18n.t('Attach Webpage')}</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center w-full justify-between">
|
||||
<div class="line-clamp-1">
|
||||
{$i18n.t('Attach Files')}
|
||||
{#if $visibleControls['chat.attach_files']}
|
||||
<Tooltip
|
||||
content={fileUploadCapableModels.length !== selectedModels.length
|
||||
? $i18n.t('Model(s) do not support file upload')
|
||||
: !fileUploadEnabled
|
||||
? $i18n.t('You do not have permission to upload files.')
|
||||
: ''}
|
||||
className="w-full"
|
||||
>
|
||||
<button
|
||||
role="menuitem"
|
||||
class="flex gap-2 w-full items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal select-none cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left {!fileUploadEnabled
|
||||
? 'opacity-50'
|
||||
: ''}"
|
||||
on:click={() => {
|
||||
if (fileUploadEnabled) {
|
||||
tab = 'files';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DocumentArrowUp />
|
||||
|
||||
<div class="flex items-center w-full justify-between">
|
||||
<div class="line-clamp-1">
|
||||
{$i18n.t('Attach Files')}
|
||||
</div>
|
||||
|
||||
<div class="text-gray-500">
|
||||
<ChevronRight />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<div class="text-gray-500">
|
||||
<ChevronRight />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{#if ($config?.features?.enable_notes ?? false) && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))}
|
||||
{#if ($config?.features?.enable_notes ?? false) && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true)) && $visibleControls['chat.attach_notes']}
|
||||
<Tooltip
|
||||
content={fileUploadCapableModels.length !== selectedModels.length
|
||||
? $i18n.t('Model(s) do not support file upload')
|
||||
|
|
@ -368,71 +374,75 @@
|
|||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<Tooltip
|
||||
content={fileUploadCapableModels.length !== selectedModels.length
|
||||
? $i18n.t('Model(s) do not support file upload')
|
||||
: !fileUploadEnabled
|
||||
? $i18n.t('You do not have permission to upload files.')
|
||||
: ''}
|
||||
className="w-full"
|
||||
>
|
||||
<button
|
||||
role="menuitem"
|
||||
class="flex gap-2 w-full items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left {!fileUploadEnabled
|
||||
? 'opacity-50'
|
||||
: ''}"
|
||||
on:click={() => {
|
||||
if (fileUploadEnabled) {
|
||||
tab = 'knowledge';
|
||||
}
|
||||
}}
|
||||
{#if $visibleControls['chat.attach_knowledge']}
|
||||
<Tooltip
|
||||
content={fileUploadCapableModels.length !== selectedModels.length
|
||||
? $i18n.t('Model(s) do not support file upload')
|
||||
: !fileUploadEnabled
|
||||
? $i18n.t('You do not have permission to upload files.')
|
||||
: ''}
|
||||
className="w-full"
|
||||
>
|
||||
<Database />
|
||||
<button
|
||||
role="menuitem"
|
||||
class="flex gap-2 w-full items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left {!fileUploadEnabled
|
||||
? 'opacity-50'
|
||||
: ''}"
|
||||
on:click={() => {
|
||||
if (fileUploadEnabled) {
|
||||
tab = 'knowledge';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Database />
|
||||
|
||||
<div class="flex items-center w-full justify-between">
|
||||
<div class=" line-clamp-1">
|
||||
{$i18n.t('Attach Knowledge')}
|
||||
<div class="flex items-center w-full justify-between">
|
||||
<div class=" line-clamp-1">
|
||||
{$i18n.t('Attach Knowledge')}
|
||||
</div>
|
||||
|
||||
<div class="text-gray-500">
|
||||
<ChevronRight />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<div class="text-gray-500">
|
||||
<ChevronRight />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
content={fileUploadCapableModels.length !== selectedModels.length
|
||||
? $i18n.t('Model(s) do not support file upload')
|
||||
: !fileUploadEnabled
|
||||
? $i18n.t('You do not have permission to upload files.')
|
||||
: ''}
|
||||
className="w-full"
|
||||
>
|
||||
<button
|
||||
role="menuitem"
|
||||
class="flex gap-2 w-full items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left {!fileUploadEnabled
|
||||
? 'opacity-50'
|
||||
: ''}"
|
||||
on:click={() => {
|
||||
if (fileUploadEnabled) {
|
||||
tab = 'chats';
|
||||
}
|
||||
}}
|
||||
{#if $visibleControls['chat.reference_chats']}
|
||||
<Tooltip
|
||||
content={fileUploadCapableModels.length !== selectedModels.length
|
||||
? $i18n.t('Model(s) do not support file upload')
|
||||
: !fileUploadEnabled
|
||||
? $i18n.t('You do not have permission to upload files.')
|
||||
: ''}
|
||||
className="w-full"
|
||||
>
|
||||
<ClockRotateRight />
|
||||
<button
|
||||
role="menuitem"
|
||||
class="flex gap-2 w-full items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left {!fileUploadEnabled
|
||||
? 'opacity-50'
|
||||
: ''}"
|
||||
on:click={() => {
|
||||
if (fileUploadEnabled) {
|
||||
tab = 'chats';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ClockRotateRight />
|
||||
|
||||
<div class="flex items-center w-full justify-between">
|
||||
<div class=" line-clamp-1">
|
||||
{$i18n.t('Reference Chats')}
|
||||
</div>
|
||||
<div class="flex items-center w-full justify-between">
|
||||
<div class=" line-clamp-1">
|
||||
{$i18n.t('Reference Chats')}
|
||||
</div>
|
||||
|
||||
<div class="text-gray-500">
|
||||
<ChevronRight />
|
||||
<div class="text-gray-500">
|
||||
<ChevronRight />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if fileUploadEnabled}
|
||||
{#if $config?.features?.enable_google_drive_integration}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import { getContext, onMount, tick, onDestroy } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { i18n as i18nType } from 'i18next';
|
||||
import { config, pyodideWorker as pyodideWorkerStore } from '$lib/stores';
|
||||
import { config, pyodideWorker as pyodideWorkerStore, visibleControls } from '$lib/stores';
|
||||
|
||||
import { createPyodideWorker } from '$lib/pyodide/createPyodideWorker';
|
||||
import { executeCode } from '$lib/apis/utils';
|
||||
|
|
@ -544,7 +544,7 @@
|
|||
on:click={copyCode}>{copied ? $i18n.t('Copied') : $i18n.t('Copy')}</button
|
||||
>
|
||||
|
||||
{#if preview && ['html', 'svg'].includes(lang)}
|
||||
{#if preview && ['html', 'svg'].includes(lang) && $visibleControls['chat.code_preview']}
|
||||
<button
|
||||
class="run-code-button flex h-7 shrink-0 items-center justify-center gap-1.5 rounded-lg px-2.5 text-xs font-normal text-gray-600 transition hover:bg-gray-100 hover:text-gray-900 disabled:opacity-60 dark:text-gray-400 dark:hover:bg-gray-850 dark:hover:text-white"
|
||||
on:click={previewCode}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { onMount, tick, getContext } from 'svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import { mobile, models, settings } from '$lib/stores';
|
||||
import { mobile, models, settings, visibleControls } from '$lib/stores';
|
||||
|
||||
import { generateMoACompletion } from '$lib/apis';
|
||||
import { updateChatById } from '$lib/apis/chats';
|
||||
|
|
@ -437,7 +437,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
{#if isLastMessage}
|
||||
{#if isLastMessage && $visibleControls['chat.merge']}
|
||||
<div class=" shrink-0 text-gray-600 dark:text-gray-500 mt-1">
|
||||
<Tooltip content={$i18n.t('Merge Responses')} placement="bottom">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@
|
|||
settings,
|
||||
temporaryChatEnabled,
|
||||
TTSWorker,
|
||||
user
|
||||
user,
|
||||
visibleControls
|
||||
} from '$lib/stores';
|
||||
import { synthesizeOpenAISpeech } from '$lib/apis/audio';
|
||||
import { imageGenerations } from '$lib/apis/images';
|
||||
|
|
@ -1191,7 +1192,7 @@
|
|||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if message.usage}
|
||||
{#if message.usage && $visibleControls['chat.usage']}
|
||||
{@const usage = JSON.stringify(message.usage, null, 2)
|
||||
.replace(/"([^(")"]+)":/g, '$1:')
|
||||
.slice(1, -1)
|
||||
|
|
@ -1247,7 +1248,7 @@
|
|||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if $user && message.done && forkHandler && ($user?.role === 'admin' || ($user?.permissions?.chat?.import ?? true))}
|
||||
{#if $user && message.done && forkHandler && ($user?.role === 'admin' || ($user?.permissions?.chat?.import ?? true)) && $visibleControls['chat.fork']}
|
||||
<Tooltip content={$i18n.t('Fork chat')} placement="bottom">
|
||||
<button
|
||||
aria-label={$i18n.t('Fork chat')}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
import PinSlash from '$lib/components/icons/PinSlash.svelte';
|
||||
import Link from '$lib/components/icons/Link.svelte';
|
||||
import Pencil from '$lib/components/icons/Pencil.svelte';
|
||||
import { config, pinnedModels, settings, showSettings, user } from '$lib/stores';
|
||||
import { config, pinnedModels, settings, showSettings, user, visibleControls } from '$lib/stores';
|
||||
import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
|
@ -105,32 +105,34 @@
|
|||
<hr class="border-gray-50 dark:border-gray-800/30 mx-1 my-0.5" />
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={$pinnedModels.includes(model?.id)}
|
||||
class="select-none flex h-[1.6875rem] w-full items-center gap-2 rounded-xl px-2 text-[0.8125rem] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 transition"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
{#if $visibleControls['models.pin']}
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={$pinnedModels.includes(model?.id)}
|
||||
class="select-none flex h-[1.6875rem] w-full items-center gap-2 rounded-xl px-2 text-[0.8125rem] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 transition"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
pinModelHandler(model?.id);
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
{#if $pinnedModels.includes(model?.id)}
|
||||
<PinSlash className="size-3.5" />
|
||||
{:else}
|
||||
<Pin className="size-3.5" />
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center">
|
||||
pinModelHandler(model?.id);
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
{#if $pinnedModels.includes(model?.id)}
|
||||
{$i18n.t('Hide from Sidebar')}
|
||||
<PinSlash className="size-3.5" />
|
||||
{:else}
|
||||
{$i18n.t('Keep in Sidebar')}
|
||||
<Pin className="size-3.5" />
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div class="flex items-center">
|
||||
{#if $pinnedModels.includes(model?.id)}
|
||||
{$i18n.t('Hide from Sidebar')}
|
||||
{:else}
|
||||
{$i18n.t('Keep in Sidebar')}
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@
|
|||
temporaryChatEnabled,
|
||||
settings,
|
||||
config,
|
||||
showSettings
|
||||
showSettings,
|
||||
visibleControls
|
||||
} from '$lib/stores';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { capitalizeFirstLetter, sanitizeResponseContent, splitStream } from '$lib/utils';
|
||||
|
|
@ -1277,7 +1278,7 @@
|
|||
{/each}
|
||||
</div>
|
||||
|
||||
{#if showSetDefault}
|
||||
{#if showSetDefault && $visibleControls['models.set_default']}
|
||||
<div
|
||||
class="flex shrink-0 items-center justify-end px-2 leading-none {($settings?.highContrastMode ??
|
||||
false)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
getArchivedChatList,
|
||||
unarchiveAllChats
|
||||
} from '$lib/apis/chats';
|
||||
import { chatId, showSettings, user } from '$lib/stores';
|
||||
import { chatId, showSettings, user, visibleControls } from '$lib/stores';
|
||||
import { refreshChatList, refreshSidebar } from '$lib/stores/chatList';
|
||||
import { formatNumber } from '$lib/utils';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
|
|
@ -244,23 +244,25 @@
|
|||
|
||||
<div slot="content">
|
||||
<DropdownMenu className="min-w-[10.625rem] shadow-sm">
|
||||
<button
|
||||
class="flex h-[1.6875rem] w-full cursor-pointer select-none items-center gap-2 rounded-lg bg-transparent px-2 text-xs hover:text-gray-900 disabled:cursor-default disabled:opacity-30 dark:hover:text-gray-100"
|
||||
disabled={loading || chatCount === 0}
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showUnarchiveAllConfirmDialog = true;
|
||||
}}
|
||||
>
|
||||
{#if loading}
|
||||
<Spinner className="size-3.5 shrink-0" />
|
||||
{:else}
|
||||
<UndoAction className="size-3.5 shrink-0" strokeWidth="1.5" />
|
||||
{/if}
|
||||
<div class="min-w-0 flex-1 truncate text-left">
|
||||
{$i18n.t('settings.personal.archivedChats.unarchiveAll.label')}
|
||||
</div>
|
||||
</button>
|
||||
{#if $visibleControls['chat.archive']}
|
||||
<button
|
||||
class="flex h-[1.6875rem] w-full cursor-pointer select-none items-center gap-2 rounded-lg bg-transparent px-2 text-xs hover:text-gray-900 disabled:cursor-default disabled:opacity-30 dark:hover:text-gray-100"
|
||||
disabled={loading || chatCount === 0}
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showUnarchiveAllConfirmDialog = true;
|
||||
}}
|
||||
>
|
||||
{#if loading}
|
||||
<Spinner className="size-3.5 shrink-0" />
|
||||
{:else}
|
||||
<UndoAction className="size-3.5 shrink-0" strokeWidth="1.5" />
|
||||
{/if}
|
||||
<div class="min-w-0 flex-1 truncate text-left">
|
||||
{$i18n.t('settings.personal.archivedChats.unarchiveAll.label')}
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if $user?.role === 'admin' || ($user?.permissions?.chat?.export ?? true)}
|
||||
<button
|
||||
|
|
@ -395,18 +397,20 @@
|
|||
class="flex shrink-0 items-center justify-end text-gray-500 dark:text-gray-500"
|
||||
role="cell"
|
||||
>
|
||||
<Tooltip content={$i18n.t('Unarchive Chat')}>
|
||||
<button
|
||||
class="rounded-lg p-1 text-gray-400 transition-colors hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Unarchive Chat')}
|
||||
on:click={() => {
|
||||
unarchiveHandler(chat.id);
|
||||
}}
|
||||
>
|
||||
<UndoAction className="size-3.5" strokeWidth="1.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{#if $visibleControls['chat.archive']}
|
||||
<Tooltip content={$i18n.t('Unarchive Chat')}>
|
||||
<button
|
||||
class="rounded-lg p-1 text-gray-400 transition-colors hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Unarchive Chat')}
|
||||
on:click={() => {
|
||||
unarchiveHandler(chat.id);
|
||||
}}
|
||||
>
|
||||
<UndoAction className="size-3.5" strokeWidth="1.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{#if $user?.role === 'admin' || ($user?.permissions?.chat?.delete ?? true)}
|
||||
<Tooltip content={$i18n.t('Delete Chat')}>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { toast } from 'svelte-sonner';
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
|
||||
import { user, settings, config } from '$lib/stores';
|
||||
import { user, settings, config, visibleControls } from '$lib/stores';
|
||||
import { getVoices as _getVoices } from '$lib/apis/audio';
|
||||
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
|
|
@ -188,19 +188,21 @@
|
|||
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
|
||||
<UserSettingSection title={$i18n.t('settings.personal.audio.sections.sttSettings.title')} first>
|
||||
{#if $config.audio.stt.engine !== 'web'}
|
||||
<UserSettingRow
|
||||
label={$i18n.t('settings.personal.audio.speechToTextEngine.label')}
|
||||
description={$i18n.t('settings.personal.audio.speechToTextEngine.description')}
|
||||
>
|
||||
<SettingsSelect
|
||||
bind:value={STTEngine}
|
||||
ariaLabel={$i18n.t('settings.personal.audio.speechToTextEngine.label')}
|
||||
placeholder={$i18n.t('Select an engine')}
|
||||
{#if $visibleControls['settings.speech_to_text_engine']}
|
||||
<UserSettingRow
|
||||
label={$i18n.t('settings.personal.audio.speechToTextEngine.label')}
|
||||
description={$i18n.t('settings.personal.audio.speechToTextEngine.description')}
|
||||
>
|
||||
<option value="">{$i18n.t('Default')}</option>
|
||||
<option value="web">{$i18n.t('Web API')}</option>
|
||||
</SettingsSelect>
|
||||
</UserSettingRow>
|
||||
<SettingsSelect
|
||||
bind:value={STTEngine}
|
||||
ariaLabel={$i18n.t('settings.personal.audio.speechToTextEngine.label')}
|
||||
placeholder={$i18n.t('Select an engine')}
|
||||
>
|
||||
<option value="">{$i18n.t('Default')}</option>
|
||||
<option value="web">{$i18n.t('Web API')}</option>
|
||||
</SettingsSelect>
|
||||
</UserSettingRow>
|
||||
{/if}
|
||||
|
||||
<UserSettingRow
|
||||
label={$i18n.t('settings.personal.audio.language.label')}
|
||||
|
|
@ -244,19 +246,21 @@
|
|||
</UserSettingSection>
|
||||
|
||||
<UserSettingSection title={$i18n.t('settings.personal.audio.sections.ttsSettings.title')}>
|
||||
<UserSettingRow
|
||||
label={$i18n.t('settings.personal.audio.textToSpeechEngine.label')}
|
||||
description={$i18n.t('settings.personal.audio.textToSpeechEngine.description')}
|
||||
>
|
||||
<SettingsSelect
|
||||
bind:value={TTSEngine}
|
||||
ariaLabel={$i18n.t('settings.personal.audio.textToSpeechEngine.label')}
|
||||
placeholder={$i18n.t('Select an engine')}
|
||||
{#if $visibleControls['settings.text_to_speech_engine']}
|
||||
<UserSettingRow
|
||||
label={$i18n.t('settings.personal.audio.textToSpeechEngine.label')}
|
||||
description={$i18n.t('settings.personal.audio.textToSpeechEngine.description')}
|
||||
>
|
||||
<option value="">{$i18n.t('Default')}</option>
|
||||
<option value="browser-kokoro">{$i18n.t('Kokoro.js (Browser)')}</option>
|
||||
</SettingsSelect>
|
||||
</UserSettingRow>
|
||||
<SettingsSelect
|
||||
bind:value={TTSEngine}
|
||||
ariaLabel={$i18n.t('settings.personal.audio.textToSpeechEngine.label')}
|
||||
placeholder={$i18n.t('Select an engine')}
|
||||
>
|
||||
<option value="">{$i18n.t('Default')}</option>
|
||||
<option value="browser-kokoro">{$i18n.t('Kokoro.js (Browser)')}</option>
|
||||
</SettingsSelect>
|
||||
</UserSettingRow>
|
||||
{/if}
|
||||
|
||||
{#if TTSEngine === 'browser-kokoro'}
|
||||
<UserSettingRow
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import { user } from '$lib/stores';
|
||||
import { user, visibleControls } from '$lib/stores';
|
||||
import { refreshSidebar } from '$lib/stores/chatList';
|
||||
|
||||
import { archiveAllChats, deleteAllChats, getAllChats, importChats } from '$lib/apis/chats';
|
||||
|
|
@ -215,22 +215,24 @@
|
|||
</button>
|
||||
</UserSettingRow>
|
||||
|
||||
<UserSettingRow
|
||||
let:labelId
|
||||
label={$i18n.t('settings.personal.dataControls.archiveAllChats.label')}
|
||||
description={$i18n.t('settings.personal.dataControls.archiveAllChats.description')}
|
||||
>
|
||||
<button
|
||||
id={`${labelId}-control`}
|
||||
class={actionButtonClass}
|
||||
on:click={() => {
|
||||
showArchiveConfirmDialog = true;
|
||||
}}
|
||||
type="button"
|
||||
{#if $visibleControls['chat.archive']}
|
||||
<UserSettingRow
|
||||
let:labelId
|
||||
label={$i18n.t('settings.personal.dataControls.archiveAllChats.label')}
|
||||
description={$i18n.t('settings.personal.dataControls.archiveAllChats.description')}
|
||||
>
|
||||
{$i18n.t('settings.personal.dataControls.archiveAll.label')}
|
||||
</button>
|
||||
</UserSettingRow>
|
||||
<button
|
||||
id={`${labelId}-control`}
|
||||
class={actionButtonClass}
|
||||
on:click={() => {
|
||||
showArchiveConfirmDialog = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{$i18n.t('settings.personal.dataControls.archiveAll.label')}
|
||||
</button>
|
||||
</UserSettingRow>
|
||||
{/if}
|
||||
|
||||
{#if canManageChats({ user: $user, config: null }, 'delete')}
|
||||
<UserSettingRow
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
import { getContext, tick } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { config, mobile, models, settings, user } from '$lib/stores';
|
||||
import { config, mobile, models, settings, user, visibleControls } from '$lib/stores';
|
||||
import type { SettingsModalRequest } from '$lib/stores';
|
||||
import { getUserSettings, updateUserSettings } from '$lib/apis/users';
|
||||
import { getBackendConfig, getModels as _getModels } from '$lib/apis';
|
||||
|
|
@ -417,6 +417,10 @@
|
|||
);
|
||||
}
|
||||
|
||||
if (tab.id === 'usage') {
|
||||
return $visibleControls['settings.usage'];
|
||||
}
|
||||
|
||||
if (tab.id === 'personalization') {
|
||||
return (
|
||||
$config?.features?.enable_memories &&
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
canUseWebSearch,
|
||||
isSettingsAdmin
|
||||
} from '$lib/utils/settings-access';
|
||||
import { config, settings, terminalServers, user } from '$lib/stores';
|
||||
import { config, settings, terminalServers, user, visibleControls } from '$lib/stores';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { updateUserInfo } from '$lib/apis/users';
|
||||
|
|
@ -662,32 +662,34 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class={settingRowClass}>
|
||||
<label
|
||||
id="allow-user-location-label"
|
||||
class={settingLabelClass}
|
||||
for="allow-user-location-label-control"
|
||||
>
|
||||
{$i18n.t('settings.personal.interface.allowUserLocation.label')}
|
||||
</label>
|
||||
{#if $visibleControls['settings.user_location']}
|
||||
<div>
|
||||
<div class={settingRowClass}>
|
||||
<label
|
||||
id="allow-user-location-label"
|
||||
class={settingLabelClass}
|
||||
for="allow-user-location-label-control"
|
||||
>
|
||||
{$i18n.t('settings.personal.interface.allowUserLocation.label')}
|
||||
</label>
|
||||
|
||||
<div class={settingControlClass}>
|
||||
<Switch
|
||||
ariaLabelledbyId="allow-user-location-label"
|
||||
tooltip={true}
|
||||
bind:state={userLocation}
|
||||
inherited={isDefaultSetting('userLocation')}
|
||||
on:change={() => {
|
||||
toggleUserLocation();
|
||||
}}
|
||||
/>
|
||||
<div class={settingControlClass}>
|
||||
<Switch
|
||||
ariaLabelledbyId="allow-user-location-label"
|
||||
tooltip={true}
|
||||
bind:state={userLocation}
|
||||
inherited={isDefaultSetting('userLocation')}
|
||||
on:change={() => {
|
||||
toggleUserLocation();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class={settingDescriptionClass}>
|
||||
{$i18n.t('settings.personal.interface.allowUserLocation.description')}
|
||||
</p>
|
||||
</div>
|
||||
<p class={settingDescriptionClass}>
|
||||
{$i18n.t('settings.personal.interface.allowUserLocation.description')}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<div class={settingRowClass}>
|
||||
|
|
@ -916,28 +918,34 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class={settingRowClass}>
|
||||
<label id="chat-bubble-ui-label" class={settingLabelClass} for="chat-bubble-ui-label-control">
|
||||
{$i18n.t('settings.personal.interface.chatBubbleUi.label')}
|
||||
</label>
|
||||
{#if $visibleControls['settings.chat_bubble']}
|
||||
<div>
|
||||
<div class={settingRowClass}>
|
||||
<label
|
||||
id="chat-bubble-ui-label"
|
||||
class={settingLabelClass}
|
||||
for="chat-bubble-ui-label-control"
|
||||
>
|
||||
{$i18n.t('settings.personal.interface.chatBubbleUi.label')}
|
||||
</label>
|
||||
|
||||
<div class={settingControlClass}>
|
||||
<Switch
|
||||
tooltip={true}
|
||||
ariaLabelledbyId="chat-bubble-ui-label"
|
||||
bind:state={chatBubble}
|
||||
inherited={isDefaultSetting('chatBubble')}
|
||||
on:change={() => {
|
||||
saveSettings({ chatBubble });
|
||||
}}
|
||||
/>
|
||||
<div class={settingControlClass}>
|
||||
<Switch
|
||||
tooltip={true}
|
||||
ariaLabelledbyId="chat-bubble-ui-label"
|
||||
bind:state={chatBubble}
|
||||
inherited={isDefaultSetting('chatBubble')}
|
||||
on:change={() => {
|
||||
saveSettings({ chatBubble });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class={settingDescriptionClass}>
|
||||
{$i18n.t('settings.personal.interface.chatBubbleUi.description')}
|
||||
</p>
|
||||
</div>
|
||||
<p class={settingDescriptionClass}>
|
||||
{$i18n.t('settings.personal.interface.chatBubbleUi.description')}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !chatBubble}
|
||||
<div>
|
||||
|
|
@ -968,32 +976,34 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<div class={settingRowClass}>
|
||||
<label
|
||||
id="widescreen-mode-label"
|
||||
class={settingLabelClass}
|
||||
for="widescreen-mode-label-control"
|
||||
>
|
||||
{$i18n.t('settings.personal.interface.widescreenMode.label')}
|
||||
</label>
|
||||
{#if $visibleControls['settings.widescreen']}
|
||||
<div>
|
||||
<div class={settingRowClass}>
|
||||
<label
|
||||
id="widescreen-mode-label"
|
||||
class={settingLabelClass}
|
||||
for="widescreen-mode-label-control"
|
||||
>
|
||||
{$i18n.t('settings.personal.interface.widescreenMode.label')}
|
||||
</label>
|
||||
|
||||
<div class={settingControlClass}>
|
||||
<Switch
|
||||
ariaLabelledbyId="widescreen-mode-label"
|
||||
tooltip={true}
|
||||
bind:state={widescreenMode}
|
||||
inherited={isDefaultSetting('widescreenMode')}
|
||||
on:change={() => {
|
||||
saveSettings({ widescreenMode });
|
||||
}}
|
||||
/>
|
||||
<div class={settingControlClass}>
|
||||
<Switch
|
||||
ariaLabelledbyId="widescreen-mode-label"
|
||||
tooltip={true}
|
||||
bind:state={widescreenMode}
|
||||
inherited={isDefaultSetting('widescreenMode')}
|
||||
on:change={() => {
|
||||
saveSettings({ widescreenMode });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class={settingDescriptionClass}>
|
||||
{$i18n.t('settings.personal.interface.widescreenMode.description')}
|
||||
</p>
|
||||
</div>
|
||||
<p class={settingDescriptionClass}>
|
||||
{$i18n.t('settings.personal.interface.widescreenMode.description')}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if canUseTemporaryChats({ user: $user, config: $config })}
|
||||
<div>
|
||||
|
|
@ -1615,47 +1625,49 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class={settingRowClass}>
|
||||
<label
|
||||
id="floating-action-buttons-label"
|
||||
class={settingLabelClass}
|
||||
for="floating-action-buttons-label-control"
|
||||
>
|
||||
{$i18n.t('settings.personal.interface.floatingQuickActions.label')}
|
||||
</label>
|
||||
{#if $visibleControls['settings.floating_action_buttons']}
|
||||
<div>
|
||||
<div class={settingRowClass}>
|
||||
<label
|
||||
id="floating-action-buttons-label"
|
||||
class={settingLabelClass}
|
||||
for="floating-action-buttons-label-control"
|
||||
>
|
||||
{$i18n.t('settings.personal.interface.floatingQuickActions.label')}
|
||||
</label>
|
||||
|
||||
<div class={settingControlClass}>
|
||||
{#if showFloatingActionButtons}
|
||||
<button
|
||||
class={actionButtonClass}
|
||||
type="button"
|
||||
aria-label={$i18n.t(
|
||||
'settings.personal.interface.openModalToManageFloatingQuickActions.label'
|
||||
)}
|
||||
on:click={() => {
|
||||
showManageFloatingActionButtonsModal = true;
|
||||
<div class={settingControlClass}>
|
||||
{#if showFloatingActionButtons}
|
||||
<button
|
||||
class={actionButtonClass}
|
||||
type="button"
|
||||
aria-label={$i18n.t(
|
||||
'settings.personal.interface.openModalToManageFloatingQuickActions.label'
|
||||
)}
|
||||
on:click={() => {
|
||||
showManageFloatingActionButtonsModal = true;
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Manage')}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<Switch
|
||||
ariaLabelledbyId="floating-action-buttons-label"
|
||||
tooltip={true}
|
||||
bind:state={showFloatingActionButtons}
|
||||
inherited={isDefaultSetting('showFloatingActionButtons')}
|
||||
on:change={() => {
|
||||
saveSettings({ showFloatingActionButtons });
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Manage')}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<Switch
|
||||
ariaLabelledbyId="floating-action-buttons-label"
|
||||
tooltip={true}
|
||||
bind:state={showFloatingActionButtons}
|
||||
inherited={isDefaultSetting('showFloatingActionButtons')}
|
||||
on:change={() => {
|
||||
saveSettings({ showFloatingActionButtons });
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class={settingDescriptionClass}>
|
||||
{$i18n.t('Show the floating quick-action toolbar in chat.')}
|
||||
</p>
|
||||
</div>
|
||||
<p class={settingDescriptionClass}>
|
||||
{$i18n.t('Show the floating quick-action toolbar in chat.')}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if canUseWebSearch({ user: $user, config: $config })}
|
||||
<div>
|
||||
|
|
@ -1723,28 +1735,30 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class={settingRowClass}>
|
||||
<label id="rich-input-label" class={settingLabelClass} for="rich-input-label-control">
|
||||
{$i18n.t('settings.personal.interface.richTextInputForChat.label')}
|
||||
</label>
|
||||
{#if $visibleControls['settings.rich_text_input']}
|
||||
<div>
|
||||
<div class={settingRowClass}>
|
||||
<label id="rich-input-label" class={settingLabelClass} for="rich-input-label-control">
|
||||
{$i18n.t('settings.personal.interface.richTextInputForChat.label')}
|
||||
</label>
|
||||
|
||||
<div class={settingControlClass}>
|
||||
<Switch
|
||||
tooltip={true}
|
||||
ariaLabelledbyId="rich-input-label"
|
||||
bind:state={richTextInput}
|
||||
inherited={isDefaultSetting('richTextInput')}
|
||||
on:change={() => {
|
||||
saveSettings({ richTextInput });
|
||||
}}
|
||||
/>
|
||||
<div class={settingControlClass}>
|
||||
<Switch
|
||||
tooltip={true}
|
||||
ariaLabelledbyId="rich-input-label"
|
||||
bind:state={richTextInput}
|
||||
inherited={isDefaultSetting('richTextInput')}
|
||||
on:change={() => {
|
||||
saveSettings({ richTextInput });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class={settingDescriptionClass}>
|
||||
{$i18n.t('settings.personal.interface.richTextInputForChat.description')}
|
||||
</p>
|
||||
</div>
|
||||
<p class={settingDescriptionClass}>
|
||||
{$i18n.t('settings.personal.interface.richTextInputForChat.description')}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $config?.features?.enable_autocomplete_generation}
|
||||
<div>
|
||||
|
|
@ -1831,167 +1845,179 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<div class={settingRowClass}>
|
||||
<label id="paste-large-label" class={settingLabelClass} for="paste-large-label-control">
|
||||
{$i18n.t('settings.personal.interface.pasteLargeTextAsFile.label')}
|
||||
</label>
|
||||
{#if $visibleControls['settings.large_text_as_file']}
|
||||
<div>
|
||||
<div class={settingRowClass}>
|
||||
<label id="paste-large-label" class={settingLabelClass} for="paste-large-label-control">
|
||||
{$i18n.t('settings.personal.interface.pasteLargeTextAsFile.label')}
|
||||
</label>
|
||||
|
||||
<div class={settingControlClass}>
|
||||
<Switch
|
||||
tooltip={true}
|
||||
ariaLabelledbyId="paste-large-label"
|
||||
bind:state={largeTextAsFile}
|
||||
inherited={isDefaultSetting('largeTextAsFile')}
|
||||
on:change={() => {
|
||||
saveSettings({ largeTextAsFile });
|
||||
}}
|
||||
/>
|
||||
<div class={settingControlClass}>
|
||||
<Switch
|
||||
tooltip={true}
|
||||
ariaLabelledbyId="paste-large-label"
|
||||
bind:state={largeTextAsFile}
|
||||
inherited={isDefaultSetting('largeTextAsFile')}
|
||||
on:change={() => {
|
||||
saveSettings({ largeTextAsFile });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class={settingDescriptionClass}>
|
||||
{$i18n.t('settings.personal.interface.pasteLargeTextAsFile.description')}
|
||||
</p>
|
||||
</div>
|
||||
<p class={settingDescriptionClass}>
|
||||
{$i18n.t('settings.personal.interface.pasteLargeTextAsFile.description')}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class={sectionHeadingClass}>
|
||||
{$i18n.t('settings.personal.interface.sections.artifacts.title')}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class={settingRowClass}>
|
||||
<label
|
||||
id="detect-artifacts-label"
|
||||
class={settingLabelClass}
|
||||
for="detect-artifacts-label-control"
|
||||
>
|
||||
{$i18n.t('settings.personal.interface.detectArtifactsAutomatically.label')}
|
||||
</label>
|
||||
{#if $visibleControls['settings.detect_artifacts']}
|
||||
<div>
|
||||
<div class={settingRowClass}>
|
||||
<label
|
||||
id="detect-artifacts-label"
|
||||
class={settingLabelClass}
|
||||
for="detect-artifacts-label-control"
|
||||
>
|
||||
{$i18n.t('settings.personal.interface.detectArtifactsAutomatically.label')}
|
||||
</label>
|
||||
|
||||
<div class={settingControlClass}>
|
||||
<Switch
|
||||
ariaLabelledbyId="detect-artifacts-label"
|
||||
tooltip={true}
|
||||
bind:state={detectArtifacts}
|
||||
inherited={isDefaultSetting('detectArtifacts')}
|
||||
on:change={() => {
|
||||
saveSettings({ detectArtifacts });
|
||||
}}
|
||||
/>
|
||||
<div class={settingControlClass}>
|
||||
<Switch
|
||||
ariaLabelledbyId="detect-artifacts-label"
|
||||
tooltip={true}
|
||||
bind:state={detectArtifacts}
|
||||
inherited={isDefaultSetting('detectArtifacts')}
|
||||
on:change={() => {
|
||||
saveSettings({ detectArtifacts });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class={settingDescriptionClass}>
|
||||
{$i18n.t('settings.personal.interface.detectArtifactsAutomatically.description')}
|
||||
</p>
|
||||
</div>
|
||||
<p class={settingDescriptionClass}>
|
||||
{$i18n.t('settings.personal.interface.detectArtifactsAutomatically.description')}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<div class={settingRowClass}>
|
||||
<label
|
||||
id="iframe-sandbox-allow-scripts-label"
|
||||
class={settingLabelClass}
|
||||
for="iframe-sandbox-allow-scripts-label-control"
|
||||
>
|
||||
{$i18n.t('settings.personal.interface.iframeSandboxAllowScripts.label')}
|
||||
</label>
|
||||
{#if $visibleControls['settings.iframe_sandbox_allow_scripts']}
|
||||
<div>
|
||||
<div class={settingRowClass}>
|
||||
<label
|
||||
id="iframe-sandbox-allow-scripts-label"
|
||||
class={settingLabelClass}
|
||||
for="iframe-sandbox-allow-scripts-label-control"
|
||||
>
|
||||
{$i18n.t('settings.personal.interface.iframeSandboxAllowScripts.label')}
|
||||
</label>
|
||||
|
||||
<div class={settingControlClass}>
|
||||
<Switch
|
||||
ariaLabelledbyId="iframe-sandbox-allow-scripts-label"
|
||||
tooltip={true}
|
||||
bind:state={iframeSandboxAllowScripts}
|
||||
inherited={isDefaultSetting('iframeSandboxAllowScripts')}
|
||||
on:change={() => {
|
||||
saveSettings({ iframeSandboxAllowScripts });
|
||||
}}
|
||||
/>
|
||||
<div class={settingControlClass}>
|
||||
<Switch
|
||||
ariaLabelledbyId="iframe-sandbox-allow-scripts-label"
|
||||
tooltip={true}
|
||||
bind:state={iframeSandboxAllowScripts}
|
||||
inherited={isDefaultSetting('iframeSandboxAllowScripts')}
|
||||
on:change={() => {
|
||||
saveSettings({ iframeSandboxAllowScripts });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class={settingDescriptionClass}>
|
||||
{$i18n.t('settings.personal.interface.iframeSandboxAllowScripts.description')}
|
||||
</p>
|
||||
</div>
|
||||
<p class={settingDescriptionClass}>
|
||||
{$i18n.t('settings.personal.interface.iframeSandboxAllowScripts.description')}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<div class={settingRowClass}>
|
||||
<label
|
||||
id="iframe-sandbox-allow-same-origin-label"
|
||||
class={settingLabelClass}
|
||||
for="iframe-sandbox-allow-same-origin-label-control"
|
||||
>
|
||||
{$i18n.t('settings.personal.interface.iframeSandboxAllowSameOrigin.label')}
|
||||
</label>
|
||||
{#if $visibleControls['settings.iframe_sandbox_allow_same_origin']}
|
||||
<div>
|
||||
<div class={settingRowClass}>
|
||||
<label
|
||||
id="iframe-sandbox-allow-same-origin-label"
|
||||
class={settingLabelClass}
|
||||
for="iframe-sandbox-allow-same-origin-label-control"
|
||||
>
|
||||
{$i18n.t('settings.personal.interface.iframeSandboxAllowSameOrigin.label')}
|
||||
</label>
|
||||
|
||||
<div class={settingControlClass}>
|
||||
<Switch
|
||||
ariaLabelledbyId="iframe-sandbox-allow-same-origin-label"
|
||||
tooltip={true}
|
||||
bind:state={iframeSandboxAllowSameOrigin}
|
||||
inherited={isDefaultSetting('iframeSandboxAllowSameOrigin')}
|
||||
on:change={() => {
|
||||
saveSettings({ iframeSandboxAllowSameOrigin });
|
||||
}}
|
||||
/>
|
||||
<div class={settingControlClass}>
|
||||
<Switch
|
||||
ariaLabelledbyId="iframe-sandbox-allow-same-origin-label"
|
||||
tooltip={true}
|
||||
bind:state={iframeSandboxAllowSameOrigin}
|
||||
inherited={isDefaultSetting('iframeSandboxAllowSameOrigin')}
|
||||
on:change={() => {
|
||||
saveSettings({ iframeSandboxAllowSameOrigin });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class={settingDescriptionClass}>
|
||||
{$i18n.t('settings.personal.interface.iframeSandboxAllowSameOrigin.description')}
|
||||
</p>
|
||||
</div>
|
||||
<p class={settingDescriptionClass}>
|
||||
{$i18n.t('settings.personal.interface.iframeSandboxAllowSameOrigin.description')}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<div class={settingRowClass}>
|
||||
<label
|
||||
id="iframe-sandbox-allow-forms-label"
|
||||
class={settingLabelClass}
|
||||
for="iframe-sandbox-allow-forms-label-control"
|
||||
>
|
||||
{$i18n.t('settings.personal.interface.iframeSandboxAllowForms.label')}
|
||||
</label>
|
||||
{#if $visibleControls['settings.iframe_sandbox_allow_forms']}
|
||||
<div>
|
||||
<div class={settingRowClass}>
|
||||
<label
|
||||
id="iframe-sandbox-allow-forms-label"
|
||||
class={settingLabelClass}
|
||||
for="iframe-sandbox-allow-forms-label-control"
|
||||
>
|
||||
{$i18n.t('settings.personal.interface.iframeSandboxAllowForms.label')}
|
||||
</label>
|
||||
|
||||
<div class={settingControlClass}>
|
||||
<Switch
|
||||
ariaLabelledbyId="iframe-sandbox-allow-forms-label"
|
||||
tooltip={true}
|
||||
bind:state={iframeSandboxAllowForms}
|
||||
inherited={isDefaultSetting('iframeSandboxAllowForms')}
|
||||
on:change={() => {
|
||||
saveSettings({ iframeSandboxAllowForms });
|
||||
}}
|
||||
/>
|
||||
<div class={settingControlClass}>
|
||||
<Switch
|
||||
ariaLabelledbyId="iframe-sandbox-allow-forms-label"
|
||||
tooltip={true}
|
||||
bind:state={iframeSandboxAllowForms}
|
||||
inherited={isDefaultSetting('iframeSandboxAllowForms')}
|
||||
on:change={() => {
|
||||
saveSettings({ iframeSandboxAllowForms });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class={settingDescriptionClass}>
|
||||
{$i18n.t('settings.personal.interface.iframeSandboxAllowForms.description')}
|
||||
</p>
|
||||
</div>
|
||||
<p class={settingDescriptionClass}>
|
||||
{$i18n.t('settings.personal.interface.iframeSandboxAllowForms.description')}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<div class={settingRowClass}>
|
||||
<label
|
||||
id="iframe-sandbox-allow-downloads-label"
|
||||
class={settingLabelClass}
|
||||
for="iframe-sandbox-allow-downloads-label-control"
|
||||
>
|
||||
{$i18n.t('settings.personal.interface.iframeSandboxAllowDownloads.label')}
|
||||
</label>
|
||||
{#if $visibleControls['settings.iframe_sandbox_allow_downloads']}
|
||||
<div>
|
||||
<div class={settingRowClass}>
|
||||
<label
|
||||
id="iframe-sandbox-allow-downloads-label"
|
||||
class={settingLabelClass}
|
||||
for="iframe-sandbox-allow-downloads-label-control"
|
||||
>
|
||||
{$i18n.t('settings.personal.interface.iframeSandboxAllowDownloads.label')}
|
||||
</label>
|
||||
|
||||
<div class={settingControlClass}>
|
||||
<Switch
|
||||
ariaLabelledbyId="iframe-sandbox-allow-downloads-label"
|
||||
tooltip={true}
|
||||
bind:state={iframeSandboxAllowDownloads}
|
||||
inherited={isDefaultSetting('iframeSandboxAllowDownloads')}
|
||||
on:change={() => {
|
||||
saveSettings({ iframeSandboxAllowDownloads });
|
||||
}}
|
||||
/>
|
||||
<div class={settingControlClass}>
|
||||
<Switch
|
||||
ariaLabelledbyId="iframe-sandbox-allow-downloads-label"
|
||||
tooltip={true}
|
||||
bind:state={iframeSandboxAllowDownloads}
|
||||
inherited={isDefaultSetting('iframeSandboxAllowDownloads')}
|
||||
on:change={() => {
|
||||
saveSettings({ iframeSandboxAllowDownloads });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class={settingDescriptionClass}>
|
||||
{$i18n.t('settings.personal.interface.iframeSandboxAllowDownloads.description')}
|
||||
</p>
|
||||
</div>
|
||||
<p class={settingDescriptionClass}>
|
||||
{$i18n.t('settings.personal.interface.iframeSandboxAllowDownloads.description')}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if canUseCalls({ user: $user, config: $config })}
|
||||
<div class={sectionHeadingClass}>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@
|
|||
settings,
|
||||
folders,
|
||||
showEmbeds,
|
||||
artifactContents
|
||||
artifactContents,
|
||||
visibleControls
|
||||
} from '$lib/stores';
|
||||
|
||||
import { getChatById } from '$lib/apis/chats';
|
||||
|
|
@ -389,7 +390,7 @@
|
|||
<hr class="border-gray-50/30 dark:border-gray-800/30 mx-1 my-0.5" />
|
||||
{/if}
|
||||
|
||||
{#if ($artifactContents ?? []).length > 0}
|
||||
{#if ($artifactContents ?? []).length > 0 && $visibleControls['chat.artifacts']}
|
||||
<button
|
||||
draggable="false"
|
||||
class="flex h-[1.6875rem] w-full items-center gap-2 rounded-xl px-2 text-[0.8125rem] cursor-pointer select-none hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
|
||||
|
|
@ -517,18 +518,20 @@
|
|||
</DropdownSub>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
draggable="false"
|
||||
class="flex h-[1.6875rem] w-full items-center gap-2 rounded-xl px-2 text-[0.8125rem] cursor-pointer select-none hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
|
||||
on:click={() => {
|
||||
archiveChatHandler();
|
||||
}}
|
||||
>
|
||||
<ArchiveBox className="size-3.5" strokeWidth="1.5" />
|
||||
<div class="flex items-center">
|
||||
{chat?.archived ? $i18n.t('Unarchive') : $i18n.t('Archive')}
|
||||
</div>
|
||||
</button>
|
||||
{#if $visibleControls['chat.archive']}
|
||||
<button
|
||||
draggable="false"
|
||||
class="flex h-[1.6875rem] w-full items-center gap-2 rounded-xl px-2 text-[0.8125rem] cursor-pointer select-none hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
|
||||
on:click={() => {
|
||||
archiveChatHandler();
|
||||
}}
|
||||
>
|
||||
<ArchiveBox className="size-3.5" strokeWidth="1.5" />
|
||||
<div class="flex items-center">
|
||||
{chat?.archived ? $i18n.t('Unarchive') : $i18n.t('Archive')}
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if $user?.role === 'admin' || ($user?.permissions?.chat?.delete ?? true)}
|
||||
<button
|
||||
|
|
@ -543,12 +546,14 @@
|
|||
</button>
|
||||
{/if}
|
||||
|
||||
<hr class="border-gray-50/30 dark:border-gray-800/30 mx-1 my-0.5" />
|
||||
{#if $visibleControls['chat.tags']}
|
||||
<hr class="border-gray-50/30 dark:border-gray-800/30 mx-1 my-0.5" />
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
||||
<div class="flex max-h-28 overflow-y-auto px-2 py-1" on:click={(e) => e.preventDefault()}>
|
||||
<Tags chatId={chat.id} />
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
||||
<div class="flex max-h-28 overflow-y-auto px-2 py-1" on:click={(e) => e.preventDefault()}>
|
||||
<Tags chatId={chat.id} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
config,
|
||||
isApp,
|
||||
visiblePinnedModels,
|
||||
visibleControls,
|
||||
selectedFolder,
|
||||
WEBUI_NAME,
|
||||
sidebarWidth
|
||||
|
|
@ -1867,17 +1868,19 @@
|
|||
>{$i18n.t('{{count}} selected', { count: $selectedChatIds.size })}</span
|
||||
>
|
||||
<div class="flex items-center gap-0.5">
|
||||
<Tooltip content={$i18n.t('Archive')}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={$i18n.t('Archive selected chats')}
|
||||
disabled={$selectionBusy}
|
||||
class="flex size-6 items-center justify-center rounded-md hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-900 dark:hover:text-gray-100 disabled:opacity-40"
|
||||
on:click={() => applySelectedChatAction('archive', [...$selectedChatIds])}
|
||||
>
|
||||
<ArchiveBoxIcon className="size-3.5" strokeWidth="1.7" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{#if $visibleControls['chat.archive']}
|
||||
<Tooltip content={$i18n.t('Archive')}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={$i18n.t('Archive selected chats')}
|
||||
disabled={$selectionBusy}
|
||||
class="flex size-6 items-center justify-center rounded-md hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-900 dark:hover:text-gray-100 disabled:opacity-40"
|
||||
on:click={() => applySelectedChatAction('archive', [...$selectedChatIds])}
|
||||
>
|
||||
<ArchiveBoxIcon className="size-3.5" strokeWidth="1.7" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{#if canDeleteChats}
|
||||
<Tooltip content={$i18n.t('Delete')}>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { visibleControls } from '$lib/stores';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import dayjs, { getDayjsLocale } from '$lib/dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
|
|
@ -815,18 +816,20 @@
|
|||
</div>
|
||||
{:else if shiftKey && mouseOver && !chatSelection}
|
||||
<div class=" flex items-center self-center space-x-1.5">
|
||||
<Tooltip content={$i18n.t('Archive')} className="flex items-center">
|
||||
<button
|
||||
class="flex size-5 items-center justify-center self-center dark:hover:text-white transition disabled:cursor-not-allowed"
|
||||
disabled={archiving}
|
||||
on:click={() => {
|
||||
archiveChatHandler(id);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<ArchiveBoxIcon className="size-3.5" strokeWidth="1.7" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{#if $visibleControls['chat.archive']}
|
||||
<Tooltip content={$i18n.t('Archive')} className="flex items-center">
|
||||
<button
|
||||
class="flex size-5 items-center justify-center self-center dark:hover:text-white transition disabled:cursor-not-allowed"
|
||||
disabled={archiving}
|
||||
on:click={() => {
|
||||
archiveChatHandler(id);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<ArchiveBoxIcon className="size-3.5" strokeWidth="1.7" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if $user?.role === 'admin' || ($user?.permissions?.chat?.delete ?? true)}
|
||||
<Tooltip content={$i18n.t('Delete')}>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
getChatPinnedStatusById,
|
||||
toggleChatPinnedStatusById
|
||||
} from '$lib/apis/chats';
|
||||
import { chats, folders, settings, theme, user } from '$lib/stores';
|
||||
import { chats, folders, settings, theme, user, visibleControls } from '$lib/stores';
|
||||
import { createMessagesList } from '$lib/utils';
|
||||
import { getOutputText } from '$lib/components/chat/Messages/structuredOutput';
|
||||
import ArchiveBoxIcon from '$lib/components/icons/ArchiveBox.svelte';
|
||||
|
|
@ -497,19 +497,21 @@
|
|||
</DropdownSub>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
role="menuitem"
|
||||
draggable="false"
|
||||
class="flex h-[1.6875rem] gap-2 items-center rounded-xl px-2 text-[0.8125rem] cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-900 w-full"
|
||||
on:click={() => {
|
||||
archiveChatHandler();
|
||||
}}
|
||||
>
|
||||
<ArchiveBoxIcon className="size-3.5" strokeWidth="1.7" />
|
||||
<div class="flex items-center">
|
||||
{archived ? $i18n.t('Unarchive') : $i18n.t('Archive')}
|
||||
</div>
|
||||
</button>
|
||||
{#if $visibleControls['chat.archive']}
|
||||
<button
|
||||
role="menuitem"
|
||||
draggable="false"
|
||||
class="flex h-[1.6875rem] gap-2 items-center rounded-xl px-2 text-[0.8125rem] cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-900 w-full"
|
||||
on:click={() => {
|
||||
archiveChatHandler();
|
||||
}}
|
||||
>
|
||||
<ArchiveBoxIcon className="size-3.5" strokeWidth="1.7" />
|
||||
<div class="flex items-center">
|
||||
{archived ? $i18n.t('Unarchive') : $i18n.t('Archive')}
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if $user?.role === 'admin' || ($user?.permissions?.chat?.delete ?? true)}
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { getContext } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { i18n as i18nType } from 'i18next';
|
||||
import { config } from '$lib/stores';
|
||||
import { config, visibleControls } from '$lib/stores';
|
||||
import ModelSettingToggle from './ModelSettingToggle.svelte';
|
||||
import ModelSettingsSection from './ModelSettingsSection.svelte';
|
||||
|
||||
|
|
@ -143,7 +143,7 @@
|
|||
{$i18n.t('Work')}
|
||||
-->
|
||||
<div class="grid grid-cols-1 gap-x-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each allTools as key}
|
||||
{#each allTools.filter((key) => $visibleControls[`model_editor.tools.${key}`]) as key}
|
||||
<ModelSettingToggle
|
||||
label={toolLabels[key].label}
|
||||
description={toolLabels[key].description}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { getContext } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { i18n as i18nType } from 'i18next';
|
||||
import { config } from '$lib/stores';
|
||||
import { config, visibleControls } from '$lib/stores';
|
||||
import ModelSettingsSection from './ModelSettingsSection.svelte';
|
||||
import ModelSettingToggle from './ModelSettingToggle.svelte';
|
||||
import BuiltinTools from './BuiltinTools.svelte';
|
||||
|
|
@ -72,16 +72,16 @@
|
|||
export let defaultFeatureIds: string[] = [];
|
||||
export let builtinTools: Record<string, boolean> = {};
|
||||
export let disabled = false;
|
||||
const featureKeys: Capability[] = ['web_search', 'image_generation', 'code_interpreter'];
|
||||
const featureKeys = ['web_search', 'image_generation', 'code_interpreter'] as const;
|
||||
$: hasTerminal =
|
||||
$config?.features?.enable_terminal_servers || $config?.features?.enable_direct_integrations;
|
||||
$: visibleCapabilities = (Object.keys(capabilityLabels) as Capability[]).filter(
|
||||
$: availableCapabilities = (Object.keys(capabilityLabels) as Capability[]).filter(
|
||||
(key) =>
|
||||
(key !== 'file_context' || capabilities.file_upload) &&
|
||||
(key !== 'terminal' || hasTerminal) &&
|
||||
(capabilityLabels[key].enabled ?? true)
|
||||
);
|
||||
$: enabledCapabilityIds = visibleCapabilities.filter((key) => capabilities[key]);
|
||||
$: enabledCapabilityIds = availableCapabilities.filter((key) => capabilities[key]);
|
||||
$: availableFeatures = featureKeys.filter((key) => enabledCapabilityIds.includes(key));
|
||||
$: enabledDefaultIds = availableFeatures.filter((key) => defaultFeatureIds.includes(key));
|
||||
const setDefault = (key: string, enabled: boolean) => {
|
||||
|
|
@ -110,7 +110,7 @@
|
|||
>{/if}
|
||||
</span>
|
||||
<div class="grid grid-cols-1 gap-x-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each visibleCapabilities as key}
|
||||
{#each availableCapabilities.filter((key) => $visibleControls[`model_editor.capabilities.${key}`]) as key}
|
||||
<ModelSettingToggle
|
||||
label={capabilityLabels[key].label}
|
||||
description={capabilityLabels[key].description}
|
||||
|
|
@ -129,7 +129,7 @@
|
|||
{$i18n.t('settings.admin.models.defaultFeatures.codeInterpreter.label')}
|
||||
{$i18n.t('settings.admin.models.defaultFeatures.codeInterpreter.description')}
|
||||
-->
|
||||
{#if availableFeatures.length}
|
||||
{#if availableFeatures.some((key) => $visibleControls[`model_editor.defaults.${key}`])}
|
||||
<ModelSettingsSection
|
||||
label={$i18n.t('settings.admin.models.defaultFeatures.title')}
|
||||
description={$i18n.t('Choose which available features start enabled in new chats.')}
|
||||
|
|
@ -149,7 +149,7 @@
|
|||
{$i18n.t('Start enabled in new chats')}
|
||||
</p>
|
||||
<div class="grid grid-cols-1 gap-x-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each availableFeatures as key}
|
||||
{#each availableFeatures.filter((key) => $visibleControls[`model_editor.defaults.${key}`]) as key}
|
||||
<ModelSettingToggle
|
||||
label={capabilityLabels[key].label}
|
||||
checked={defaultFeatureIds.includes(key)}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,13 @@
|
|||
import Pin from '$lib/components/icons/Pin.svelte';
|
||||
import PinSlash from '$lib/components/icons/PinSlash.svelte';
|
||||
|
||||
import { config, user as currentUser, pinnedModels, settings } from '$lib/stores';
|
||||
import {
|
||||
config,
|
||||
user as currentUser,
|
||||
pinnedModels,
|
||||
settings,
|
||||
visibleControls
|
||||
} from '$lib/stores';
|
||||
import Link from '$lib/components/icons/Link.svelte';
|
||||
import { resolveLocalizedModelName } from '$lib/utils/localizedContent';
|
||||
|
||||
|
|
@ -147,24 +153,26 @@
|
|||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="select-none flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[0.8125rem] hover:text-gray-900 dark:hover:text-gray-100"
|
||||
on:click={() => runAndClose(() => pinModelHandler(model?.id))}
|
||||
>
|
||||
{#if $pinnedModels.includes(model?.id)}
|
||||
<PinSlash />
|
||||
{:else}
|
||||
<Pin />
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center">
|
||||
{#if $visibleControls['models.pin']}
|
||||
<button
|
||||
class="select-none flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[0.8125rem] hover:text-gray-900 dark:hover:text-gray-100"
|
||||
on:click={() => runAndClose(() => pinModelHandler(model?.id))}
|
||||
>
|
||||
{#if $pinnedModels.includes(model?.id)}
|
||||
{$i18n.t('Hide from Sidebar')}
|
||||
<PinSlash />
|
||||
{:else}
|
||||
{$i18n.t('Keep in Sidebar')}
|
||||
<Pin />
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div class="flex items-center">
|
||||
{#if $pinnedModels.includes(model?.id)}
|
||||
{$i18n.t('Hide from Sidebar')}
|
||||
{:else}
|
||||
{$i18n.t('Keep in Sidebar')}
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if writeAccess}
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -3002,6 +3002,8 @@
|
|||
"settings.admin.interface.taskModel.description": "Choose fallback models for background tasks. Current Model follows the active chat model.",
|
||||
"settings.admin.interface.taskModel.label": "Task Model",
|
||||
"settings.admin.interface.taskModelParameters.label": "Task Model Parameters",
|
||||
"settings.admin.interface.visibility.description": "Choose which controls appear for users. Hiding a control keeps its current setting and underlying feature available. Administrators always see available controls.",
|
||||
"settings.admin.interface.visibility.title": "Visible controls",
|
||||
"settings.admin.interface.title": "Interface",
|
||||
"settings.admin.interface.titleGeneration.description": "Allow automatic names for new chats.",
|
||||
"settings.admin.interface.titleGeneration.label": "Title Generation",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
import {
|
||||
CONTROL_LABELS,
|
||||
isControlVisible,
|
||||
type ControlId,
|
||||
type Visibility
|
||||
} from '$lib/utils/ui-visibility';
|
||||
import { APP_NAME } from '$lib/constants';
|
||||
import { type Writable, derived, writable } from 'svelte/store';
|
||||
import type { ModelConfig } from '$lib/apis';
|
||||
|
|
@ -128,6 +134,17 @@ export const visiblePinnedModels = derived([pinnedModels, models], ([$pinnedMode
|
|||
)
|
||||
);
|
||||
|
||||
export const visibleControls = derived(
|
||||
[config, user],
|
||||
([$config, $user]) =>
|
||||
Object.fromEntries(
|
||||
(Object.keys(CONTROL_LABELS) as ControlId[]).map((id) => [
|
||||
id,
|
||||
isControlVisible(id, { config: $config, user: $user })
|
||||
])
|
||||
) as Record<ControlId, boolean>
|
||||
);
|
||||
|
||||
export const audioQueue = writable<AudioQueue | null>(null);
|
||||
export const chatRequestQueues: Writable<
|
||||
Record<string, { id: string; prompt: string; files: any[]; error?: string }[]>
|
||||
|
|
@ -420,6 +437,7 @@ type Config = {
|
|||
};
|
||||
ui?: {
|
||||
default_interface_settings?: Record<string, unknown>;
|
||||
visibility?: Visibility;
|
||||
pending_user_overlay_title?: string;
|
||||
pending_user_overlay_content?: string;
|
||||
response_watermark?: string;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { CONTROL_LABELS, isControlVisible, type ControlId } from './ui-visibility';
|
||||
import type { SettingsAccessContext } from './settings-search';
|
||||
|
||||
export const canEditSystemPrompt = ({ user }: SettingsAccessContext) =>
|
||||
|
|
@ -30,13 +31,25 @@ export const canRegenerateResponses = ({ user }: SettingsAccessContext) =>
|
|||
user?.role === 'admin' || (user?.permissions?.chat?.regenerate_response ?? true);
|
||||
export const isSettingsAdmin = ({ user }: SettingsAccessContext) => user?.role === 'admin';
|
||||
|
||||
/** Only permission-gated text needs an explicit rule. Provider/disclosure state is irrelevant. */
|
||||
/** Search follows the same permissions and visibility as the rendered controls. */
|
||||
export function canSearchSetting(
|
||||
key: string,
|
||||
tabId: string,
|
||||
context: SettingsAccessContext
|
||||
): boolean {
|
||||
const stem = key.replace(/\.(label|description|title)$/, '');
|
||||
if (
|
||||
/^settings\.personal\.(dataControls\.archiveAll(Chats)?|archivedChats\.unarchiveAll)$/.test(
|
||||
stem
|
||||
)
|
||||
)
|
||||
return isControlVisible('chat.archive', context);
|
||||
if (key.startsWith('settings.personal.')) {
|
||||
const control = (Object.keys(CONTROL_LABELS) as ControlId[]).find(
|
||||
(id) => CONTROL_LABELS[id].replace(/\.(label|title)$/, '') === stem
|
||||
);
|
||||
if (control && !isControlVisible(control, context)) return false;
|
||||
}
|
||||
if (stem === 'settings.personal.general.sections.systemPrompt')
|
||||
return canEditSystemPrompt(context);
|
||||
if (
|
||||
|
|
|
|||
97
src/lib/utils/ui-visibility.ts
Normal file
97
src/lib/utils/ui-visibility.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
// Stable control IDs describe user actions or preferences, not component locations.
|
||||
export const CONTROL_LABELS = {
|
||||
'model_editor.capabilities.vision': 'settings.admin.models.capabilities.vision.label',
|
||||
'model_editor.capabilities.file_upload': 'settings.admin.models.capabilities.fileUpload.label',
|
||||
'model_editor.capabilities.file_context': 'settings.admin.models.capabilities.fileContext.label',
|
||||
'model_editor.capabilities.web_search': 'settings.admin.models.capabilities.webSearch.label',
|
||||
'model_editor.capabilities.image_generation':
|
||||
'settings.admin.models.capabilities.imageGeneration.label',
|
||||
'model_editor.capabilities.code_interpreter':
|
||||
'settings.admin.models.capabilities.codeInterpreter.label',
|
||||
'model_editor.capabilities.terminal': 'settings.admin.models.capabilities.terminal.label',
|
||||
'model_editor.capabilities.usage': 'settings.admin.models.capabilities.usage.label',
|
||||
'model_editor.capabilities.citations': 'settings.admin.models.capabilities.citations.label',
|
||||
'model_editor.capabilities.status_updates':
|
||||
'settings.admin.models.capabilities.statusUpdates.label',
|
||||
'model_editor.capabilities.memory': 'settings.admin.models.capabilities.memory.label',
|
||||
'model_editor.capabilities.builtin_tools':
|
||||
'settings.admin.models.capabilities.builtinTools.label',
|
||||
'model_editor.defaults.web_search': 'settings.admin.models.capabilities.webSearch.label',
|
||||
'model_editor.defaults.image_generation':
|
||||
'settings.admin.models.capabilities.imageGeneration.label',
|
||||
'model_editor.defaults.code_interpreter':
|
||||
'settings.admin.models.capabilities.codeInterpreter.label',
|
||||
'model_editor.tools.time': 'settings.admin.models.builtinTools.time.label',
|
||||
'model_editor.tools.user_input': 'settings.admin.models.builtinTools.userInput.label',
|
||||
'model_editor.tools.memory': 'settings.admin.models.builtinTools.memory.label',
|
||||
'model_editor.tools.chats': 'settings.admin.models.builtinTools.chats.label',
|
||||
'model_editor.tools.notes': 'settings.admin.models.builtinTools.notes.label',
|
||||
'model_editor.tools.knowledge': 'settings.admin.models.builtinTools.knowledge.label',
|
||||
'model_editor.tools.files': 'settings.admin.models.builtinTools.files.label',
|
||||
'model_editor.tools.channels': 'settings.admin.models.builtinTools.channels.label',
|
||||
'model_editor.tools.notifications': 'settings.admin.models.builtinTools.notifications.label',
|
||||
'model_editor.tools.web_search': 'settings.admin.models.builtinTools.webSearch.label',
|
||||
'model_editor.tools.image_generation': 'settings.admin.models.builtinTools.imageGeneration.label',
|
||||
'model_editor.tools.code_interpreter': 'settings.admin.models.builtinTools.codeInterpreter.label',
|
||||
'model_editor.tools.tasks': 'settings.admin.models.builtinTools.tasks.label',
|
||||
'model_editor.tools.automations': 'settings.admin.models.builtinTools.automations.label',
|
||||
'model_editor.tools.calendar': 'settings.admin.models.builtinTools.calendar.label',
|
||||
'model_editor.tools.subagents': 'settings.admin.models.builtinTools.subagents.label',
|
||||
'model_editor.tools.skills': 'settings.admin.models.builtinTools.skills.label',
|
||||
'chat.capture': 'Capture',
|
||||
'chat.compare': 'Compare text',
|
||||
'chat.attach_webpage': 'Attach Webpage',
|
||||
'chat.attach_files': 'Attach Files',
|
||||
'chat.attach_notes': 'Attach Notes',
|
||||
'chat.attach_knowledge': 'Attach Knowledge',
|
||||
'chat.reference_chats': 'Reference Chats',
|
||||
'chat.status': '/status',
|
||||
'chat.model_command': '/model',
|
||||
'chat.settings_command': '/settings',
|
||||
'chat.usage': 'settings.admin.models.capabilities.usage.label',
|
||||
'chat.merge': 'Merge Responses',
|
||||
'chat.code_preview': 'Preview',
|
||||
'chat.fork': 'Fork chat',
|
||||
'chat.artifacts': 'Artifacts',
|
||||
'chat.archive': 'Archive',
|
||||
'chat.tags': 'Tags',
|
||||
'models.set_default': 'Set as default',
|
||||
'models.pin': 'Keep in Sidebar',
|
||||
'settings.usage': 'settings.personal.usage.title',
|
||||
'settings.speech_to_text_engine': 'settings.personal.audio.speechToTextEngine.label',
|
||||
'settings.text_to_speech_engine': 'settings.personal.audio.textToSpeechEngine.label',
|
||||
'settings.user_location': 'settings.personal.interface.allowUserLocation.label',
|
||||
'settings.chat_bubble': 'settings.personal.interface.chatBubbleUi.label',
|
||||
'settings.widescreen': 'settings.personal.interface.widescreenMode.label',
|
||||
'settings.floating_action_buttons': 'settings.personal.interface.floatingQuickActions.label',
|
||||
'settings.rich_text_input': 'settings.personal.interface.richTextInputForChat.label',
|
||||
'settings.large_text_as_file': 'settings.personal.interface.pasteLargeTextAsFile.label',
|
||||
'settings.detect_artifacts': 'settings.personal.interface.detectArtifactsAutomatically.label',
|
||||
'settings.iframe_sandbox_allow_scripts':
|
||||
'settings.personal.interface.iframeSandboxAllowScripts.label',
|
||||
'settings.iframe_sandbox_allow_same_origin':
|
||||
'settings.personal.interface.iframeSandboxAllowSameOrigin.label',
|
||||
'settings.iframe_sandbox_allow_forms':
|
||||
'settings.personal.interface.iframeSandboxAllowForms.label',
|
||||
'settings.iframe_sandbox_allow_downloads':
|
||||
'settings.personal.interface.iframeSandboxAllowDownloads.label'
|
||||
} as const;
|
||||
|
||||
export type ControlId = keyof typeof CONTROL_LABELS;
|
||||
export type Visibility = Partial<Record<ControlId, boolean>>;
|
||||
export type VisibilityContext = {
|
||||
user?: { role?: string } | null;
|
||||
config?: { ui?: { visibility?: Visibility } } | null;
|
||||
};
|
||||
|
||||
export const CONTROL_GROUPS = [
|
||||
['model_editor.capabilities.', 'settings.admin.models.capabilities.title'],
|
||||
['model_editor.defaults.', 'settings.admin.models.defaultFeatures.title'],
|
||||
['model_editor.tools.', 'settings.admin.models.builtinTools.title'],
|
||||
['chat.', 'Chat'],
|
||||
['models.', 'Models'],
|
||||
['settings.', 'Settings']
|
||||
] as const;
|
||||
|
||||
export const isControlVisible = (id: ControlId, { user, config }: VisibilityContext) =>
|
||||
user?.role === 'admin' || config?.ui?.visibility?.[id] !== false;
|
||||
Loading…
Add table
Reference in a new issue