mirror of
https://github.com/open-webui/open-webui.git
synced 2026-11-15 09:31:21 +00:00
refac
This commit is contained in:
parent
563d621854
commit
09030d5dd2
5 changed files with 326 additions and 193 deletions
|
|
@ -2649,7 +2649,10 @@
|
|||
|
||||
const initNewChat = async () => {
|
||||
loading = true;
|
||||
if (!embedded && ($showTabs === 'comments' || $showTabs === 'side' || typeof $showTabs === 'object'))
|
||||
if (
|
||||
!embedded &&
|
||||
($showTabs === 'comments' || $showTabs === 'side' || typeof $showTabs === 'object')
|
||||
)
|
||||
showTabs.set(false);
|
||||
const signal = startChatLoad();
|
||||
modelDefaults = null;
|
||||
|
|
@ -3874,6 +3877,10 @@
|
|||
};
|
||||
|
||||
let terminalTabs: ChatTabs;
|
||||
let fullWidthTabs = false;
|
||||
let mainChatActive = false;
|
||||
let tabTitleEditor: import('svelte').Snippet | undefined;
|
||||
let tabActions: import('svelte').Snippet | undefined;
|
||||
|
||||
const getChatId = async (_history: typeof history) => {
|
||||
let _chatId = $chatId;
|
||||
|
|
@ -3954,7 +3961,9 @@
|
|||
messageInput?.openComments();
|
||||
return { status: 'rejected' };
|
||||
}
|
||||
const sideCommand = String(userPrompt).trim().match(/^\/(?:side|btw)(?:\s+([\s\S]*))?$/i);
|
||||
const sideCommand = String(userPrompt)
|
||||
.trim()
|
||||
.match(/^\/(?:side|btw)(?:\s+([\s\S]*))?$/i);
|
||||
if (sideCommand && !sideChatAllowed) {
|
||||
toast.error($i18n.t('You do not have permission to use side chat.'));
|
||||
return { status: 'rejected' };
|
||||
|
|
@ -5290,8 +5299,18 @@
|
|||
></div>
|
||||
{/if}
|
||||
|
||||
<div class="w-full h-full flex">
|
||||
<div class="h-full flex relative max-w-full min-w-0 flex-1 flex-col">
|
||||
<div
|
||||
class="w-full h-full min-h-0 {fullWidthTabs
|
||||
? 'grid grid-cols-1 grid-rows-[auto_minmax(0,1fr)_auto]'
|
||||
: 'flex'}"
|
||||
>
|
||||
<div
|
||||
class="h-full flex relative max-w-full min-w-0 min-h-0 flex-1 flex-col col-start-1 row-start-2"
|
||||
class:hidden={fullWidthTabs && !mainChatActive}
|
||||
id={fullWidthTabs ? 'chat-panel-main-chat-view' : undefined}
|
||||
role={fullWidthTabs ? 'tabpanel' : undefined}
|
||||
aria-labelledby={fullWidthTabs ? 'chat-panel-main-chat' : undefined}
|
||||
>
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center h-full w-full">
|
||||
<Spinner className="size-5" />
|
||||
|
|
@ -5327,6 +5346,9 @@
|
|||
</div>
|
||||
{:else}
|
||||
<Navbar
|
||||
tabbed={fullWidthTabs}
|
||||
bind:tabTitleEditor
|
||||
bind:tabActions
|
||||
{liveParticipants}
|
||||
bind:this={navbarElement}
|
||||
readOnly={isShared}
|
||||
|
|
@ -5424,7 +5446,7 @@
|
|||
}}
|
||||
bind:selectedModels
|
||||
{atSelectedModel}
|
||||
className={embedded ? 'h-full flex pt-4' : 'h-full flex pt-18'}
|
||||
className={embedded || fullWidthTabs ? 'h-full flex pt-4' : 'h-full flex pt-18'}
|
||||
{sendMessage}
|
||||
{showMessage}
|
||||
{submitMessage}
|
||||
|
|
@ -5436,7 +5458,7 @@
|
|||
{onToolCallResolved}
|
||||
allowDelete={!(generating || taskIds?.length)}
|
||||
forkHandler={$temporaryChatEnabled ? null : handleForkChat}
|
||||
topPadding={!embedded}
|
||||
topPadding={!embedded && !fullWidthTabs}
|
||||
bottomPadding={files.length > 0}
|
||||
{onSelect}
|
||||
{onInsertToNote}
|
||||
|
|
@ -5698,6 +5720,11 @@
|
|||
|
||||
{#if !embedded}
|
||||
<ChatTabs
|
||||
bind:fullWidth={fullWidthTabs}
|
||||
bind:mainChatActive
|
||||
title={$chatTitle || $i18n.t('New Chat')}
|
||||
tabTitleEditor={loading ? undefined : tabTitleEditor}
|
||||
tabActions={loading ? undefined : tabActions}
|
||||
bind:terminalContext
|
||||
bind:history
|
||||
bind:files
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@
|
|||
activateTab,
|
||||
removeTab,
|
||||
moveTab,
|
||||
mainChatTabId,
|
||||
setFullWidthTabs,
|
||||
fileTabId,
|
||||
type PanelTab
|
||||
} from './ChatTabs/tabs';
|
||||
|
|
@ -95,6 +97,25 @@
|
|||
export let canSideChat = false;
|
||||
|
||||
export let codeInterpreterEnabled = false;
|
||||
export let fullWidth = false;
|
||||
export let mainChatActive = false;
|
||||
export let title = '';
|
||||
export let tabTitleEditor: import('svelte').Snippet | undefined = undefined;
|
||||
export let tabActions: import('svelte').Snippet | undefined = undefined;
|
||||
let expanded = false;
|
||||
$: fullWidth = expanded && largeScreen && !!$showTabs;
|
||||
$: mainChatActive = fullWidth && tabs.activeId === mainChatTabId;
|
||||
$: if (fullWidth !== tabs.tabs.some((tab) => tab.id === mainChatTabId)) {
|
||||
tabs = setFullWidthTabs(tabs, fullWidth, title);
|
||||
if (fullWidth) void tick().then(() => document.getElementById(domId(mainChatTabId))?.focus());
|
||||
}
|
||||
const toggleLayout = async () => {
|
||||
if (panelTarget) releasePanel(panelTarget);
|
||||
expanded = !expanded;
|
||||
localStorage.setItem('chatTabsLayout', expanded ? 'full' : 'side');
|
||||
await tick();
|
||||
if (!fullWidth) document.getElementById('chat-tabs-layout-toggle')?.focus();
|
||||
};
|
||||
|
||||
let largeScreen = false;
|
||||
let dragged = false;
|
||||
|
|
@ -169,6 +190,7 @@
|
|||
};
|
||||
const tabIcon = (kind: PanelTab['kind']) =>
|
||||
({
|
||||
chat: 'chat-lines',
|
||||
files: 'folder',
|
||||
file: 'page',
|
||||
terminal: 'terminal',
|
||||
|
|
@ -183,14 +205,18 @@
|
|||
const domId = (id: string) => `chat-panel-${encodeURIComponent(id)}`;
|
||||
$: activeTabId = tabs.activeId;
|
||||
$: activeTab = tabs.tabs.find((tab) => tab.id === tabs.activeId);
|
||||
$: terminalContext = fileContexts[tabs.activeId] ?? activeTab?.context ?? null;
|
||||
$: contextTab =
|
||||
activeTab?.kind === 'chat'
|
||||
? tabs.tabs.find((tab) => tab.id === tabs.history.at(-1))
|
||||
: activeTab;
|
||||
$: terminalContext = (contextTab && fileContexts[contextTab.id]) ?? contextTab?.context ?? null;
|
||||
// Resolve before reactive Files-tab creation, which can run later in the same update.
|
||||
$: gitConnection =
|
||||
mounted &&
|
||||
$config?.features?.enable_tool_servers &&
|
||||
(tabs.tabs.find((tab) => tab.id === tabs.activeId)?.connection ??
|
||||
(contextTab?.connection ??
|
||||
resolveTerminalConnection(
|
||||
tabs.tabs.find((tab) => tab.id === tabs.activeId)?.terminalId ?? $selectedTerminalId,
|
||||
contextTab?.terminalId ?? $selectedTerminalId,
|
||||
$terminalServers ?? [],
|
||||
directTerminals,
|
||||
localStorage.token ?? ''
|
||||
|
|
@ -254,7 +280,7 @@
|
|||
terminalId: selectedConnection.selector,
|
||||
connection: { ...selectedConnection },
|
||||
untitled: true,
|
||||
context: activeTab?.terminalId === selectedConnection.selector ? terminalContext : undefined
|
||||
context: contextTab?.terminalId === selectedConnection.selector ? terminalContext : undefined
|
||||
});
|
||||
};
|
||||
const openFeature = (kind: 'artifacts' | 'preview' | 'voice' | 'comments' | 'side') => {
|
||||
|
|
@ -277,7 +303,9 @@
|
|||
const newTerminal = () => {
|
||||
if (!terminalEnabled || !selectedConnection) return;
|
||||
let terminalNumber = 1;
|
||||
while (tabs.tabs.some((tab) => tab.kind === 'terminal' && tab.terminalNumber === terminalNumber)) {
|
||||
while (
|
||||
tabs.tabs.some((tab) => tab.kind === 'terminal' && tab.terminalNumber === terminalNumber)
|
||||
) {
|
||||
terminalNumber++;
|
||||
}
|
||||
openPanel({
|
||||
|
|
@ -343,6 +371,7 @@
|
|||
});
|
||||
};
|
||||
const closeTab = async (id: string) => {
|
||||
if (id === mainChatTabId) return;
|
||||
if (closing.has(id)) return;
|
||||
closing.add(id);
|
||||
try {
|
||||
|
|
@ -397,7 +426,7 @@
|
|||
// Restore the DOM before Svelte reconciles its keyed tabs.
|
||||
from.insertBefore(item, nextSibling?.parentNode === from ? nextSibling : null);
|
||||
if (newDraggableIndex != null)
|
||||
tabs = moveTab(tabs, item.dataset.panelTab!, newDraggableIndex);
|
||||
tabs = moveTab(tabs, item.dataset.panelTab!, newDraggableIndex + (fullWidth ? 1 : 0));
|
||||
dragged = false;
|
||||
}
|
||||
});
|
||||
|
|
@ -492,7 +521,11 @@
|
|||
$: showPreviewTab = !!$embed && $embed.chatId === chatId;
|
||||
$: availableViews = [
|
||||
{ kind: 'files', label: $i18n.t('Files'), available: showFilesTab },
|
||||
{ kind: 'side', label: $i18n.t('Side chat'), available: canSideChat && !!chatId && hasMessages },
|
||||
{
|
||||
kind: 'side',
|
||||
label: $i18n.t('Side chat'),
|
||||
available: canSideChat && !!chatId && hasMessages
|
||||
},
|
||||
{ kind: 'artifacts', label: $i18n.t('Artifacts'), available: showArtifactsTab },
|
||||
{ kind: 'preview', label: $i18n.t('Preview'), available: showPreviewTab },
|
||||
{ kind: 'voice', label: $i18n.t('Voice'), available: $showCallOverlay },
|
||||
|
|
@ -529,7 +562,7 @@
|
|||
const hadTabs = tabs.tabs.length > 0;
|
||||
for (const tab of tabs.tabs) {
|
||||
// An open side chat owns its model and context; changing the parent must not discard it.
|
||||
if (tab.kind === 'side') continue;
|
||||
if (tab.kind === 'side' || tab.kind === 'chat') continue;
|
||||
const available = tab.terminalId
|
||||
? !!$selectedTerminalId && terminalAccessible(tab.terminalId)
|
||||
: availableViews.some((view) => view.kind === tab.kind && view.available);
|
||||
|
|
@ -591,7 +624,7 @@
|
|||
|
||||
$: if (chatId !== tabsChatId) {
|
||||
if (tabsChatId || !chatId) {
|
||||
tabs = emptyTabs();
|
||||
tabs = setFullWidthTabs(emptyTabs(), fullWidth, title);
|
||||
sideChatNumber = 0;
|
||||
hasOpenedTabs = false;
|
||||
dirtyFiles = {};
|
||||
|
|
@ -602,10 +635,12 @@
|
|||
}
|
||||
$: if ($showTabs === true && !tabs.tabs.length) {
|
||||
const options = [
|
||||
...availableViews.filter((view) => view.available).map((view) => () => {
|
||||
if (view.kind === 'files') openFiles();
|
||||
else openFeature(view.kind as 'artifacts' | 'preview' | 'voice' | 'comments' | 'side');
|
||||
}),
|
||||
...availableViews
|
||||
.filter((view) => view.available)
|
||||
.map((view) => () => {
|
||||
if (view.kind === 'files') openFiles();
|
||||
else openFeature(view.kind as 'artifacts' | 'preview' | 'voice' | 'comments' | 'side');
|
||||
}),
|
||||
...(terminalFilesAvailable ? [newFile] : []),
|
||||
...(terminalEnabled ? [newTerminal] : []),
|
||||
...(browserEnabled ? [openBrowser] : []),
|
||||
|
|
@ -623,7 +658,8 @@
|
|||
($user.permissions?.chat?.side_chat ?? false))
|
||||
) &&
|
||||
$sideChatRequest?.parentId === chatId
|
||||
) sideChatRequest.set(null);
|
||||
)
|
||||
sideChatRequest.set(null);
|
||||
$: if ($showTabs === 'files' && showFilesTab) openFiles();
|
||||
$: if ($showTabs === 'side' && chatId && hasMessages) {
|
||||
if (canSideChat) openFeature('side');
|
||||
|
|
@ -715,6 +751,8 @@
|
|||
const mediaQuery = window.matchMedia('(min-width: 768px)');
|
||||
mediaQuery.addEventListener('change', handleMediaQuery);
|
||||
handleMediaQuery(mediaQuery);
|
||||
expanded = localStorage.getItem('chatTabsLayout') === 'full';
|
||||
if (expanded && largeScreen && !$showTabs) showTabs.set(true);
|
||||
|
||||
let isDestroyed = false;
|
||||
|
||||
|
|
@ -788,11 +826,13 @@
|
|||
<div
|
||||
class:hidden={!$showTabs || !panelTarget}
|
||||
use:placePanel={$showTabs ? panelTarget : null}
|
||||
class="h-full w-full min-h-0 flex flex-col"
|
||||
class={fullWidth ? 'contents' : 'h-full w-full min-h-0 flex flex-col'}
|
||||
style:--chat-tabs-dark-background={'var(--color-gray-900)'}
|
||||
id="controls-container"
|
||||
>
|
||||
<div class="flex items-center px-2 pt-1 pb-1 gap-1 shrink-0 select-none">
|
||||
<div
|
||||
class="flex items-center px-2 pt-1 pb-1 gap-1 shrink-0 select-none col-start-1 row-start-1 min-w-0 z-10 bg-white dark:bg-gray-900"
|
||||
>
|
||||
<div class="flex items-center gap-0.5 flex-1 min-w-0 py-0.5">
|
||||
<div
|
||||
role="tablist"
|
||||
|
|
@ -809,7 +849,8 @@
|
|||
event.shiftKey ||
|
||||
Math.abs(event.deltaY) <= Math.abs(event.deltaX) ||
|
||||
strip.scrollWidth <= strip.clientWidth
|
||||
) return;
|
||||
)
|
||||
return;
|
||||
const unit = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? strip.clientWidth : 1;
|
||||
event.preventDefault();
|
||||
strip.scrollLeft += event.deltaY * unit;
|
||||
|
|
@ -817,22 +858,26 @@
|
|||
>
|
||||
{#each tabs.tabs as tab (tab.id)}
|
||||
<div
|
||||
data-panel-tab={tab.id}
|
||||
class="flex items-center gap-1.5 px-2.5 h-7 rounded-lg text-sm font-normal whitespace-nowrap shrink-0 transition {tabs.activeId ===
|
||||
role="presentation"
|
||||
data-panel-tab={tab.kind === 'chat' ? undefined : tab.id}
|
||||
on:click={() => selectTab(tab.id)}
|
||||
class="panel-tab flex cursor-pointer items-center gap-1.5 ps-2.5 pe-2 h-7 rounded-lg text-sm font-normal whitespace-nowrap shrink-0 transition {tabs.activeId ===
|
||||
tab.id
|
||||
? 'bg-gray-100/40 dark:bg-gray-800/25 text-gray-700 dark:text-gray-200'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:bg-gray-100/30 dark:hover:bg-gray-800/20 hover:text-gray-600 dark:hover:text-gray-300'}"
|
||||
>
|
||||
<button
|
||||
role="tab"
|
||||
aria-keyshortcuts="Alt+Shift+ArrowLeft Alt+Shift+ArrowRight"
|
||||
aria-keyshortcuts={tab.kind === 'chat'
|
||||
? undefined
|
||||
: 'Alt+Shift+ArrowLeft Alt+Shift+ArrowRight'}
|
||||
id={domId(tab.id)}
|
||||
aria-controls={`${domId(tab.id)}-view`}
|
||||
aria-selected={tabs.activeId === tab.id}
|
||||
tabindex={tabs.activeId === tab.id ? 0 : -1}
|
||||
title={tab.path ?? tab.label}
|
||||
title={tab.kind === 'chat' ? title : (tab.path ?? tab.label)}
|
||||
aria-label={tab.kind === 'chat' ? title : undefined}
|
||||
class="flex h-full items-center gap-1.5 min-w-0"
|
||||
on:click={() => selectTab(tab.id)}
|
||||
>
|
||||
{#if tab.kind === 'file'}
|
||||
<Icon name={fileIconName(tab.label)} size={14} strokeWidth={1.4} />
|
||||
|
|
@ -841,7 +886,11 @@
|
|||
{:else}
|
||||
<Icon name={tabIcon(tab.kind)} size={14} />
|
||||
{/if}
|
||||
<span class="max-w-30 overflow-hidden text-ellipsis">{tab.label}</span>
|
||||
{#if tab.kind !== 'chat' || !tabTitleEditor}
|
||||
<span class="max-w-30 overflow-hidden text-ellipsis"
|
||||
>{tab.kind === 'chat' ? title : tab.label}</span
|
||||
>
|
||||
{/if}
|
||||
{#if tab.kind === 'comments' && commentCount}<span class="text-gray-400"
|
||||
>{commentCount}</span
|
||||
>{/if}
|
||||
|
|
@ -850,14 +899,18 @@
|
|||
class="ms-1 size-1.5 shrink-0 rounded-full bg-gray-400"
|
||||
aria-label={$i18n.t('Unsaved changes')}
|
||||
></span>{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center w-4 h-4 rounded text-gray-400 hover:bg-gray-200 hover:text-gray-700 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
aria-label={tab.kind === 'voice'
|
||||
? $i18n.t('End call')
|
||||
: `${$i18n.t('Close')} ${tab.label}`}
|
||||
on:click={() => closeTab(tab.id)}><Icon name="xmark" size={12} /></button
|
||||
>
|
||||
{#if tab.kind === 'chat'}
|
||||
{@render tabTitleEditor?.()}
|
||||
{@render tabActions?.()}
|
||||
{:else}<button
|
||||
type="button"
|
||||
class="flex items-center justify-center w-4 h-4 rounded text-gray-400 hover:bg-gray-200 hover:text-gray-700 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
aria-label={tab.kind === 'voice'
|
||||
? $i18n.t('End call')
|
||||
: `${$i18n.t('Close')} ${tab.label}`}
|
||||
on:click|stopPropagation={() => closeTab(tab.id)}
|
||||
><Icon name="xmark" size={12} /></button
|
||||
>{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -906,6 +959,19 @@
|
|||
</Dropdown>
|
||||
{/if}
|
||||
</div>
|
||||
{#if largeScreen}
|
||||
<button
|
||||
id="chat-tabs-layout-toggle"
|
||||
type="button"
|
||||
class="flex items-center justify-center w-7 h-7 rounded-lg text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors duration-100 shrink-0"
|
||||
on:click={toggleLayout}
|
||||
aria-label={fullWidth ? $i18n.t('Use side tabs') : $i18n.t('Use full-width tabs')}
|
||||
title={fullWidth ? $i18n.t('Use side tabs') : $i18n.t('Use full-width tabs')}
|
||||
aria-pressed={fullWidth}
|
||||
>
|
||||
<Icon name={fullWidth ? 'split-horizontal' : 'expand'} size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center w-7 h-7 rounded-lg text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors duration-100 shrink-0"
|
||||
|
|
@ -913,8 +979,11 @@
|
|||
aria-label={$i18n.t('Hide tabs')}><Icon name="xmark" size={12} /></button
|
||||
>
|
||||
</div>
|
||||
<div class="relative flex-1 min-h-0">
|
||||
{#each tabs.tabs as tab (tab.id)}
|
||||
<div
|
||||
class="relative flex-1 min-h-0 min-w-0 col-start-1 row-start-2"
|
||||
class:hidden={mainChatActive}
|
||||
>
|
||||
{#each tabs.tabs.filter((tab) => tab.kind !== 'chat') as tab (tab.id)}
|
||||
<div
|
||||
id={`${domId(tab.id)}-view`}
|
||||
role="tabpanel"
|
||||
|
|
@ -1051,19 +1120,21 @@
|
|||
{$i18n.t('Open a tab using the + menu.')}
|
||||
</div>{/if}
|
||||
</div>
|
||||
{#if gitConnection && tabs.tabs.length}
|
||||
<GitBar
|
||||
bind:status={gitStatus}
|
||||
bind:diffMode={gitDiffMode}
|
||||
bind:hideWhitespace={gitHideWhitespace}
|
||||
connection={gitConnection}
|
||||
{chatId}
|
||||
directory={terminalContext?.currentDirectory ?? '.'}
|
||||
active={!!$showTabs}
|
||||
onOpenFile={(path) => openFile(path, gitConnection.selector)}
|
||||
onOpenDirectory={(path) => openFiles(gitConnection.selector, path)}
|
||||
/>
|
||||
{/if}
|
||||
<div class="col-start-1 row-start-3 min-w-0" class:hidden={mainChatActive}>
|
||||
{#if gitConnection && tabs.tabs.length}
|
||||
<GitBar
|
||||
bind:status={gitStatus}
|
||||
bind:diffMode={gitDiffMode}
|
||||
bind:hideWhitespace={gitHideWhitespace}
|
||||
connection={gitConnection}
|
||||
{chatId}
|
||||
directory={terminalContext?.currentDirectory ?? '.'}
|
||||
active={!!$showTabs}
|
||||
onOpenFile={(path) => openFile(path, gitConnection.selector)}
|
||||
onOpenDirectory={(path) => openFiles(gitConnection.selector, path)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !largeScreen}
|
||||
|
|
@ -1096,7 +1167,7 @@
|
|||
</MobileSwipePanel>
|
||||
{:else}
|
||||
<ResizableSidePanel
|
||||
open={!!$showTabs}
|
||||
open={!!$showTabs && !fullWidth}
|
||||
bind:width={controlsWidth}
|
||||
minWidth={280}
|
||||
minSiblingWidth={360}
|
||||
|
|
@ -1109,9 +1180,17 @@
|
|||
class="h-full w-full z-40 pointer-events-auto"
|
||||
></div>
|
||||
</ResizableSidePanel>
|
||||
{#if fullWidth}
|
||||
<!-- The tab strip and pane share the parent chat's grid without remounting either view. -->
|
||||
<div use:preservePanel bind:this={panelTarget} class="contents"></div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.panel-tab:not(:has(:global(button:not([role='tab'])))) {
|
||||
padding-inline: 0.625rem;
|
||||
}
|
||||
|
||||
.panel-tab-strip {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { TerminalContext } from '$lib/types';
|
|||
import type { TerminalConnection } from '$lib/apis/terminal';
|
||||
|
||||
export type PanelTabKind =
|
||||
| 'chat'
|
||||
| 'files'
|
||||
| 'file'
|
||||
| 'terminal'
|
||||
|
|
@ -31,6 +32,17 @@ export type PanelTabs = { tabs: PanelTab[]; activeId: string; history: string[]
|
|||
|
||||
export const emptyTabs = (): PanelTabs => ({ tabs: [], activeId: '', history: [] });
|
||||
|
||||
export const mainChatTabId = 'main-chat';
|
||||
|
||||
export function setFullWidthTabs(state: PanelTabs, fullWidth: boolean, label = 'Chat'): PanelTabs {
|
||||
if (!fullWidth) return removeTab(state, mainChatTabId);
|
||||
if (state.tabs.some((tab) => tab.id === mainChatTabId)) return state;
|
||||
return activateTab(
|
||||
{ ...state, tabs: [{ id: mainChatTabId, kind: 'chat', label }, ...state.tabs] },
|
||||
mainChatTabId
|
||||
);
|
||||
}
|
||||
|
||||
export function activateTab(state: PanelTabs, id: string): PanelTabs {
|
||||
if (!state.tabs.some((tab) => tab.id === id) || state.activeId === id) return state;
|
||||
return {
|
||||
|
|
@ -70,6 +82,7 @@ export function removeTab(state: PanelTabs, id: string): PanelTabs {
|
|||
|
||||
export function moveTab(state: PanelTabs, id: string, to: number): PanelTabs {
|
||||
const from = state.tabs.findIndex((tab) => tab.id === id);
|
||||
if (id === mainChatTabId || state.tabs[to]?.id === mainChatTabId) return state;
|
||||
if (from < 0 || to < 0 || to >= state.tabs.length || from === to) return state;
|
||||
const tabs = [...state.tabs];
|
||||
tabs.splice(to, 0, tabs.splice(from, 1)[0]);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { getContext, tick } from 'svelte';
|
||||
import { getContext, tick, type Snippet } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import {
|
||||
|
|
@ -56,9 +56,14 @@
|
|||
export let scrollTop = 0;
|
||||
export let scrollToTop: (() => void) | null = null;
|
||||
|
||||
export let chat;
|
||||
export let history;
|
||||
export let chat: any;
|
||||
export let history: any;
|
||||
export let title = '';
|
||||
export let tabbed = false;
|
||||
export let tabTitleEditor: Snippet | undefined = undefined;
|
||||
export let tabActions: Snippet | undefined = undefined;
|
||||
$: tabTitleEditor = confirmEdit ? renderTitleEditor : undefined;
|
||||
$: tabActions = renderChatActions;
|
||||
export let onSaveTempChat: () => {};
|
||||
export let archiveChatHandler: (id: string) => void;
|
||||
export let deleteChatHandler: (id: string) => void;
|
||||
|
|
@ -143,6 +148,146 @@
|
|||
};
|
||||
</script>
|
||||
|
||||
{#snippet renderTitleEditor()}
|
||||
<input
|
||||
bind:this={chatTitleInput}
|
||||
bind:value={chatTitle}
|
||||
class="min-w-0 flex-1 bg-transparent md:py-1 text-[0.9375rem] font-normal text-gray-700 outline-hidden dark:text-gray-300"
|
||||
aria-label={$i18n.t('Title')}
|
||||
placeholder={generating ? $i18n.t('Generating...') : ''}
|
||||
disabled={generating}
|
||||
on:keydown={(e) => {
|
||||
if (e.isComposing || e.keyCode === 229) return;
|
||||
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
editChatTitle();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
confirmEdit = false;
|
||||
}
|
||||
}}
|
||||
on:blur={editChatTitle}
|
||||
/>
|
||||
|
||||
<Tooltip content={$i18n.t('Generate')}>
|
||||
<button
|
||||
class="flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-lg text-gray-500 transition hover:bg-gray-50/40 hover:text-gray-700 disabled:cursor-not-allowed dark:text-gray-400 dark:hover:bg-gray-800/40 dark:hover:text-gray-200"
|
||||
id="generate-title-button"
|
||||
aria-label={$i18n.t('Generate')}
|
||||
disabled={generating}
|
||||
on:mousedown={(e) => e.preventDefault()}
|
||||
on:click={() => {
|
||||
generateTitleHandler();
|
||||
}}
|
||||
>
|
||||
<Sparkles strokeWidth="1.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
|
||||
{#snippet renderChatActions()}
|
||||
{#if $user?.role === 'user' ? ($user?.permissions?.chat?.temporary ?? true) && !($user?.permissions?.chat?.temporary_enforced ?? false) : true}
|
||||
{#if !chat?.id && !tabbed}
|
||||
<Tooltip content={$i18n.t(`Temporary Chat`)}>
|
||||
<button
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded-lg text-gray-500 transition hover:bg-gray-50/40 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800/40 dark:hover:text-gray-200"
|
||||
id="temporary-chat-button"
|
||||
on:click={async () => {
|
||||
if (($settings?.temporaryChatByDefault ?? false) && $temporaryChatEnabled) {
|
||||
// for proper initNewChat handling
|
||||
await temporaryChatEnabled.set(null);
|
||||
} else {
|
||||
await temporaryChatEnabled.set(!$temporaryChatEnabled);
|
||||
}
|
||||
|
||||
if ($page.url.pathname !== '/' && !$page.url.pathname.startsWith('/folders/')) {
|
||||
await goto('/');
|
||||
}
|
||||
|
||||
// add 'temporary-chat=true' to the URL
|
||||
if ($temporaryChatEnabled) {
|
||||
window.history.replaceState(window.history.state, '', '?temporary-chat=true');
|
||||
} else {
|
||||
window.history.replaceState(window.history.state, '', location.pathname);
|
||||
}
|
||||
}}
|
||||
aria-label={$i18n.t(`Temporary Chat`)}
|
||||
>
|
||||
{#if $temporaryChatEnabled}
|
||||
<ChatBubbleDottedChecked className="size-4" strokeWidth="1.5" />
|
||||
{:else}
|
||||
<ChatBubbleDotted className="size-4" strokeWidth="1.5" />
|
||||
{/if}
|
||||
</button>
|
||||
</Tooltip>
|
||||
{:else if chat?.id && $temporaryChatEnabled}
|
||||
<Tooltip content={$i18n.t(`Save Chat`)}>
|
||||
<button
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded-lg text-gray-500 transition hover:bg-gray-50/40 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800/40 dark:hover:text-gray-200"
|
||||
id="save-temporary-chat-button"
|
||||
on:click={async () => {
|
||||
onSaveTempChat();
|
||||
}}
|
||||
aria-label={$i18n.t(`Save Chat`)}
|
||||
>
|
||||
<ChatCheck className="size-4.5" strokeWidth="1.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if $mobile && !$temporaryChatEnabled && chat && chat.id}
|
||||
<Tooltip content={$i18n.t('New Chat')}>
|
||||
<button
|
||||
class="flex size-6 {$showSidebar
|
||||
? 'md:hidden'
|
||||
: ''} cursor-pointer items-center justify-center rounded-lg text-gray-500 transition hover:bg-gray-50/40 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800/40 dark:hover:text-gray-200"
|
||||
on:click={() => {
|
||||
initNewChat();
|
||||
}}
|
||||
aria-label={$i18n.t('New Chat')}
|
||||
>
|
||||
<ChatPlus className="size-4" strokeWidth="1.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if liveParticipants.length > 0}
|
||||
<ChatParticipants participants={liveParticipants} mode="presence" avatarClass="size-5" />
|
||||
{/if}
|
||||
|
||||
{#if shareEnabled && chat?.id && (Object.keys(history?.messages ?? {}).length > 0 || scrollToTop || ($artifactContents ?? []).length > 0 || (!readOnly && !$temporaryChatEnabled) || $user?.role === 'admin' || ($user?.permissions?.chat?.export ?? true))}
|
||||
<Menu
|
||||
{chat}
|
||||
{shareEnabled}
|
||||
{readOnly}
|
||||
{scrollToTop}
|
||||
shareHandler={() => {
|
||||
showShareChatModal = !showShareChatModal;
|
||||
}}
|
||||
{renameHandler}
|
||||
archiveChatHandler={() => {
|
||||
archiveChatHandler(chat.id);
|
||||
}}
|
||||
deleteChatHandler={() => {
|
||||
deleteChatHandler(chat.id);
|
||||
}}
|
||||
{moveChatHandler}
|
||||
>
|
||||
<button
|
||||
class="flex shrink-0 cursor-pointer items-center justify-center {tabbed
|
||||
? 'size-4 rounded text-gray-400 hover:bg-gray-200 hover:text-gray-700 dark:hover:bg-white/10 dark:hover:text-white'
|
||||
: 'size-6 rounded-lg text-gray-500 transition hover:bg-gray-50/40 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800/40 dark:hover:text-gray-200'}"
|
||||
id="chat-context-menu-button"
|
||||
aria-label={$i18n.t('Chat actions')}
|
||||
>
|
||||
<EllipsisHorizontal className={tabbed ? 'size-3' : 'size-4.5'} strokeWidth="1.5" />
|
||||
</button>
|
||||
</Menu>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<ShareChatModal bind:show={showShareChatModal} chatId={$chatId} />
|
||||
|
||||
<button
|
||||
|
|
@ -155,6 +300,7 @@
|
|||
/>
|
||||
|
||||
<nav
|
||||
class:hidden={tabbed}
|
||||
class="sticky top-0 z-30 w-full {$mobile
|
||||
? 'pt-1.5'
|
||||
: ''} pb-1 -mb-12 flex flex-col items-center drag-region"
|
||||
|
|
@ -195,42 +341,8 @@
|
|||
>
|
||||
{#if chat?.id}
|
||||
<div class="flex max-w-full min-w-0 items-center gap-2 mr-2">
|
||||
{#if confirmEdit}
|
||||
<input
|
||||
bind:this={chatTitleInput}
|
||||
bind:value={chatTitle}
|
||||
class="min-w-0 flex-1 bg-transparent md:py-1 text-[0.9375rem] font-normal text-gray-700 outline-hidden dark:text-gray-300"
|
||||
aria-label={$i18n.t('Title')}
|
||||
placeholder={generating ? $i18n.t('Generating...') : ''}
|
||||
disabled={generating}
|
||||
on:keydown={(e) => {
|
||||
if (e.isComposing || e.keyCode === 229) return;
|
||||
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
editChatTitle();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
confirmEdit = false;
|
||||
}
|
||||
}}
|
||||
on:blur={editChatTitle}
|
||||
/>
|
||||
|
||||
<Tooltip content={$i18n.t('Generate')}>
|
||||
<button
|
||||
class="flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-lg text-gray-500 transition hover:bg-gray-50/40 hover:text-gray-700 disabled:cursor-not-allowed dark:text-gray-400 dark:hover:bg-gray-800/40 dark:hover:text-gray-200"
|
||||
id="generate-title-button"
|
||||
aria-label={$i18n.t('Generate')}
|
||||
disabled={generating}
|
||||
on:mousedown={(e) => e.preventDefault()}
|
||||
on:click={() => {
|
||||
generateTitleHandler();
|
||||
}}
|
||||
>
|
||||
<Sparkles strokeWidth="1.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{#if confirmEdit && !tabbed}
|
||||
{@render renderTitleEditor()}
|
||||
{:else}
|
||||
<h1
|
||||
class="min-w-0 truncate md:py-1 text-left text-[0.9375rem] font-normal text-gray-700 dark:text-gray-300"
|
||||
|
|
@ -267,107 +379,7 @@
|
|||
<div class="lg:mr-1 flex flex-none items-center gap-2 self-center">
|
||||
<!-- <div class="md:hidden flex self-center w-[0.0625rem] h-5 mx-2 bg-gray-300 dark:bg-stone-700" /> -->
|
||||
|
||||
{#if $user?.role === 'user' ? ($user?.permissions?.chat?.temporary ?? true) && !($user?.permissions?.chat?.temporary_enforced ?? false) : true}
|
||||
{#if !chat?.id}
|
||||
<Tooltip content={$i18n.t(`Temporary Chat`)}>
|
||||
<button
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded-lg text-gray-500 transition hover:bg-gray-50/40 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800/40 dark:hover:text-gray-200"
|
||||
id="temporary-chat-button"
|
||||
on:click={async () => {
|
||||
if (($settings?.temporaryChatByDefault ?? false) && $temporaryChatEnabled) {
|
||||
// for proper initNewChat handling
|
||||
await temporaryChatEnabled.set(null);
|
||||
} else {
|
||||
await temporaryChatEnabled.set(!$temporaryChatEnabled);
|
||||
}
|
||||
|
||||
if ($page.url.pathname !== '/' && !$page.url.pathname.startsWith('/folders/')) {
|
||||
await goto('/');
|
||||
}
|
||||
|
||||
// add 'temporary-chat=true' to the URL
|
||||
if ($temporaryChatEnabled) {
|
||||
window.history.replaceState(window.history.state, '', '?temporary-chat=true');
|
||||
} else {
|
||||
window.history.replaceState(window.history.state, '', location.pathname);
|
||||
}
|
||||
}}
|
||||
aria-label={$i18n.t(`Temporary Chat`)}
|
||||
>
|
||||
{#if $temporaryChatEnabled}
|
||||
<ChatBubbleDottedChecked className="size-4" strokeWidth="1.5" />
|
||||
{:else}
|
||||
<ChatBubbleDotted className="size-4" strokeWidth="1.5" />
|
||||
{/if}
|
||||
</button>
|
||||
</Tooltip>
|
||||
{:else if $temporaryChatEnabled}
|
||||
<Tooltip content={$i18n.t(`Save Chat`)}>
|
||||
<button
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded-lg text-gray-500 transition hover:bg-gray-50/40 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800/40 dark:hover:text-gray-200"
|
||||
id="save-temporary-chat-button"
|
||||
on:click={async () => {
|
||||
onSaveTempChat();
|
||||
}}
|
||||
aria-label={$i18n.t(`Save Chat`)}
|
||||
>
|
||||
<ChatCheck className="size-4.5" strokeWidth="1.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if $mobile && !$temporaryChatEnabled && chat && chat.id}
|
||||
<Tooltip content={$i18n.t('New Chat')}>
|
||||
<button
|
||||
class="flex size-6 {$showSidebar
|
||||
? 'md:hidden'
|
||||
: ''} cursor-pointer items-center justify-center rounded-lg text-gray-500 transition hover:bg-gray-50/40 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800/40 dark:hover:text-gray-200"
|
||||
on:click={() => {
|
||||
initNewChat();
|
||||
}}
|
||||
aria-label={$i18n.t('New Chat')}
|
||||
>
|
||||
<ChatPlus className="size-4" strokeWidth="1.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if liveParticipants.length > 0}
|
||||
<ChatParticipants
|
||||
participants={liveParticipants}
|
||||
mode="presence"
|
||||
avatarClass="size-5"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if shareEnabled && chat?.id && (Object.keys(history?.messages ?? {}).length > 0 || scrollToTop || ($artifactContents ?? []).length > 0 || (!readOnly && !$temporaryChatEnabled) || $user?.role === 'admin' || ($user?.permissions?.chat?.export ?? true))}
|
||||
<Menu
|
||||
{chat}
|
||||
{shareEnabled}
|
||||
{readOnly}
|
||||
{scrollToTop}
|
||||
shareHandler={() => {
|
||||
showShareChatModal = !showShareChatModal;
|
||||
}}
|
||||
{renameHandler}
|
||||
archiveChatHandler={() => {
|
||||
archiveChatHandler(chat.id);
|
||||
}}
|
||||
deleteChatHandler={() => {
|
||||
deleteChatHandler(chat.id);
|
||||
}}
|
||||
{moveChatHandler}
|
||||
>
|
||||
<button
|
||||
class="flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-lg text-gray-500 transition hover:bg-gray-50/40 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800/40 dark:hover:text-gray-200"
|
||||
id="chat-context-menu-button"
|
||||
aria-label={$i18n.t('Chat actions')}
|
||||
>
|
||||
<EllipsisHorizontal className="size-4.5" strokeWidth="1.5" />
|
||||
</button>
|
||||
</Menu>
|
||||
{/if}
|
||||
{#if !tabbed}{@render renderChatActions()}{/if}
|
||||
|
||||
{#if history.currentId || $selectedTerminalId}
|
||||
<Tooltip content={$i18n.t('Tabs')}>
|
||||
|
|
|
|||
|
|
@ -4180,6 +4180,7 @@
|
|||
"Use default": "",
|
||||
"Use device vibration feedback on supported Android devices.": "",
|
||||
"Use discovered scopes": "",
|
||||
"Use full-width tabs": "",
|
||||
"Use global setting": "",
|
||||
"Use groups to organize your users and assign permissions.": "",
|
||||
"Use in prompt": "",
|
||||
|
|
@ -4187,6 +4188,7 @@
|
|||
"Use segmented retrieval for focused and relevant content extraction.": "",
|
||||
"Use segmented retrieval for focused and relevant context.": "",
|
||||
"Use segmented retrieval for focused context.": "",
|
||||
"Use side tabs": "",
|
||||
"Use these in model system prompts as {{example}}.": "",
|
||||
"Use Web Search?": "",
|
||||
"Used in native function calling mode. Workspace settings and user permissions still apply.": "Used in native function calling mode. Workspace settings and user permissions still apply.",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue