* feat: improve ollama model management experience

This commit introduces several improvements to the Ollama model management modal:

- Adds a cancel button to the model pulling operation, using the existing 'x' button pattern.
- Adds a cancel button to the "Update All" models operation, allowing the user to cancel the update for the currently processing model.
- Cleans up toast notifications when updating all models. A single toast is now shown at the beginning and a summary toast at the end, preventing notification spam.
- Refactors the `ManageOllama.svelte` component to support these new cancellation features.
- Adds tooltips to all buttons in the modal to improve clarity.
- Disables buttons when their corresponding input fields are empty to prevent accidental clicks.

* fix

* i18n: improve Chinese translation

* fix: handle non‑UTF8 chars in third‑party responses without error

* German translation of new strings in i18n

* log web search queries only with level 'debug' instead of 'info'

* Tool calls now only include text and dont inlcude other content like image b64

* fix onedrive

* fix: discovery url

* fix: default permissions not being loaded

* fix: ai hallucination

* fix: non rich text input copy

* refac: rm print statements

* refac: disable direct models from model editors

* refac/fix: do not process xlsx files with azure doc intelligence

* Update pull_request_template.md

* Update generated image translation in DE-de

* added missing danish translations

* feat(onedrive): Enable search and "My Organization" pivot

* style(onedrive): Formatting fix

* feat: Implement toggling for vertical and horizontal flow layouts

This commit introduces the necessary logic and UI controls to allow users to switch the Flow component layout between vertical and horizontal orientations.

*   **`Flow.svelte` Refactoring:**
    *   Updates logic for calculating level offsets and node positions to consistently respect the current flow orientation.
    *   Adds a control panel using `<Controls>` and `<SwitchButton>` components.
    *   Provides user interface elements to easily switch the flow layout between horizontal and vertical orientations.

* build(deps): bump pydantic from 2.11.7 to 2.11.9 in /backend

Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.11.7 to 2.11.9.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/v2.11.9/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.11.7...v2.11.9)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-version: 2.11.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): bump black from 25.1.0 to 25.9.0 in /backend

Bumps [black](https://github.com/psf/black) from 25.1.0 to 25.9.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/25.1.0...25.9.0)

---
updated-dependencies:
- dependency-name: black
  dependency-version: 25.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): bump markdown from 3.8.2 to 3.9 in /backend

Bumps [markdown](https://github.com/Python-Markdown/markdown) from 3.8.2 to 3.9.
- [Release notes](https://github.com/Python-Markdown/markdown/releases)
- [Changelog](https://github.com/Python-Markdown/markdown/blob/master/docs/changelog.md)
- [Commits](https://github.com/Python-Markdown/markdown/compare/3.8.2...3.9.0)

---
updated-dependencies:
- dependency-name: markdown
  dependency-version: '3.9'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): bump chromadb from 1.0.20 to 1.1.0 in /backend

Bumps [chromadb](https://github.com/chroma-core/chroma) from 1.0.20 to 1.1.0.
- [Release notes](https://github.com/chroma-core/chroma/releases)
- [Changelog](https://github.com/chroma-core/chroma/blob/main/RELEASE_PROCESS.md)
- [Commits](https://github.com/chroma-core/chroma/compare/1.0.20...1.1.0)

---
updated-dependencies:
- dependency-name: chromadb
  dependency-version: 1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): bump opentelemetry-api from 1.36.0 to 1.37.0

Bumps [opentelemetry-api](https://github.com/open-telemetry/opentelemetry-python) from 1.36.0 to 1.37.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-python/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-python/compare/v1.36.0...v1.37.0)

---
updated-dependencies:
- dependency-name: opentelemetry-api
  dependency-version: 1.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* refac: ollama embed form data

* fix: non rich text handling

* fix: oauth client registration

* refac

* chore: dep bump

* chore: fastapi bump

* chore/refac: bump bcrypt and remove passlib

* Improving Korean Translation

* refac

* Improving Korean Translation

* feat: PWA share_target implementation

Co-Authored-By: gjveld <19951982+gjveld@users.noreply.github.com>

* refac: message input mobile detection behaviour

* feat: model_ids per folder

* Update translation.json (pt-BR)

inclusion of new translations of items that have been added

* refac

* refac

* refac

* refac

* refac/fix: temp chat

* refac

* refac: stop task

* refac/fix: azure audio escape

* refac: external tool validation

* refac/enh: start.sh additional args support

* refac

* refac: styling

* refac/fix: direct connection floating action buttons

* refac/fix: system prompt duplication

* refac/enh: openai tts additional params support

* refac

* feat: load data in parallel to accelerate page loading speed

* i18n: improve Chinese translation

* refac

* refac: model selector

* UPD: i18n es-ES Translation v0.6.33

UPD: i18n es-ES Translation v0.6.33

Updated new strings.

* refac

* improved query pref by querying only relevant columns

* refac/enh: docling params

* refac

* refac: openai additional headers support

* refac

* FEAT: Add Vega Char Visualizer Renderer

### FEAT: Add Vega Char Visualizer Renderer

Feature required in https://github.com/open-webui/open-webui/discussions/18022

Added npm vega lib to package.json
Added function for visualization renderer to src/libs/utils/index.ts
Added logic to src/lib/components/chat/Messages/CodeBlock.svelte

The treatment is similar as for mermaid diagrams.

Reference: https://vega.github.io/vega/

* refac

* chore

* refac

* FEAT: Add Vega-Lite Char Visualizer Renderer

### FEAT: Add Vega Char Visualizer Renderer

Add suport for Vega-Lite Specifications.
Vega-Lite is a "compiled" version of Vega Char Visualizer.
For be rendered with Vega it have to be compiled.
This PR add the check and compile if necessary, is a complement of recent Vega Renderer Feature added.

* refac

* refac/fix: switch

* enh/refac: url input handling

* refac

* refac: styling

* UPD: Add Validators & Error Toast for Mermaid & Vega diagrams

### UPD: Feat:  Add Validators & Error Toast for Mermaid & Vega diagrams

Description:
As many time the diagrams generated or entered have syntax errors the diagrams are not rendered due to that errors, but as there isn't any notification is difficult to know what happend.

This PR add validator and toast notification when error on Mermaid and Vega/Vega-Lite diagrams, helping the user to fix its.

* removed redundant knowledge API call

* Fix Code Format

* refac: model workspace view

* refac

* refac: knowledge

* refac: prompts

* refac: tools

* refac

* feat: attach folder

* refac: make tencentcloud-sdk-python optional

* refac/fix: oauth

* enh: ENABLE_OAUTH_EMAIL_FALLBACK

* refac/fix: folders

* Update requirements.txt

* Update pyproject.toml

* UPD: Add Validators & Error Toast for Mermaid & Vega diagrams

### UPD: Feat:  Add Validators & Error Toast for Mermaid & Vega diagrams

Description:
As many time the diagrams generated or entered have syntax errors the diagrams are not rendered due to that errors, but as there isn't any notification is difficult to know what happend.

This PR add validator and toast notification when error on Mermaid and Vega/Vega-Lite diagrams, helping the user to fix its.

Note:
Another possibility of integrating this Graph Visualizer is through its svelte component: https://github.com/vega/svelte-vega/tree/main/packages/svelte-vega

* Removed unused toast import & Code Format

* refac

* refac: external tool server view

* refac

* refac: overview

* refac: styling

* refac

* Update bug_report.yaml

* refac

* refac

* refac

* refac

* refac: oauth client fallback

* Fixed: Cannot handle batch sizes > 1 if no padding token is defined

Fixes Cannot handle batch sizes > 1 if no padding token is defined

For reranker models that do not have this defined in their config by using the eos_token_id if present as pad_token_id.

* refac: fallback to reasoning content

* fix(i18n): corrected typo in Spanish translation for "Reasoning Tags"

Typo fixed in Spanish translation file at line 1240 of `open-webui/src/lib/i18n/locales/es-ES/translation.json`:

- Incorrect: "Eriquetas de Razonamiento"
- Correct:   "Etiquetas de Razonamiento"

This improves clarity and consistency in the UI.

* refac/fix: ENABLE_STAR_SESSIONS_MIDDLEWARE

* refac/fix: redirect

* refac

* refac

* refac

* refac: web search error handling

* refac: source parsing

* refac: functions

* refac

* refac/enh: note pdf export

* refac/fix: mcp oauth2.1

* chore: format

* chore: Changelog (#17995)

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* refac

* chore: dep bump

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: silentoplayz <jacwoo21@outlook.com>
Co-authored-by: Shirasawa <764798966@qq.com>
Co-authored-by: Jan Kessler <jakessle@uni-mainz.de>
Co-authored-by: Jacob Leksan <jacob.leksan@expedient.com>
Co-authored-by: Classic298 <27028174+Classic298@users.noreply.github.com>
Co-authored-by: sinejespersen <sinejespersen@protonmail.com>
Co-authored-by: Selene Blok <selene.blok@rws.nl>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Cyp <cypher9715@naver.com>
Co-authored-by: gjveld <19951982+gjveld@users.noreply.github.com>
Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com>
Co-authored-by: _00_ <131402327+rgaricano@users.noreply.github.com>
Co-authored-by: expruc <eygabi01@gmail.com>
Co-authored-by: YetheSamartaka <55753928+YetheSamartaka@users.noreply.github.com>
Co-authored-by: Akutangulo <akutangulo@gmail.com>
This commit is contained in:
Tim Jaeryang Baek
2025-10-07 16:20:27 -05:00
committed by GitHub
co-authored by silentoplayz Shirasawa Jan Kessler Jacob Leksan Classic298 sinejespersen Selene Blok dependabot[bot] Cyp gjveld joaoback _00_ expruc YetheSamartaka Akutangulo
parent 4d7fddaf7e
commit 8d7d79d54b
156 changed files with 6119 additions and 3198 deletions
+71 -23
View File
@@ -91,6 +91,7 @@
import Sidebar from '../icons/Sidebar.svelte';
import { getFunctions } from '$lib/apis/functions';
import Image from '../common/Image.svelte';
import { updateFolderById } from '$lib/apis/folders';
export let chatIdProp = '';
@@ -219,10 +220,15 @@
}
const saveSessionSelectedModels = () => {
if (selectedModels.length === 0 || (selectedModels.length === 1 && selectedModels[0] === '')) {
const selectedModelsString = JSON.stringify(selectedModels);
if (
selectedModels.length === 0 ||
(selectedModels.length === 1 && selectedModels[0] === '') ||
sessionStorage.selectedModels === selectedModelsString
) {
return;
}
sessionStorage.selectedModels = JSON.stringify(selectedModels);
sessionStorage.selectedModels = selectedModelsString;
console.log('saveSessionSelectedModels', selectedModels, sessionStorage.selectedModels);
};
@@ -294,7 +300,7 @@
}
};
const showMessage = async (message) => {
const showMessage = async (message, ignoreSettings = false) => {
await tick();
const _chatId = JSON.parse(JSON.stringify($chatId));
@@ -320,7 +326,7 @@
await tick();
await tick();
if ($settings?.scrollOnBranchChange ?? true) {
if (($settings?.scrollOnBranchChange ?? true) || ignoreSettings) {
const messageElement = document.getElementById(`message-${message.id}`);
if (messageElement) {
messageElement.scrollIntoView({ behavior: 'smooth' });
@@ -499,12 +505,33 @@
}
};
const savedModelIds = async () => {
if (
$selectedFolder &&
selectedModels.filter((modelId) => modelId !== '').length > 0 &&
JSON.stringify($selectedFolder?.data?.model_ids) !== JSON.stringify(selectedModels)
) {
const res = await updateFolderById(localStorage.token, $selectedFolder.id, {
data: {
model_ids: selectedModels
}
});
}
};
$: if (selectedModels !== null) {
savedModelIds();
}
let pageSubscribe = null;
let showControlsSubscribe = null;
let selectedFolderSubscribe = null;
onMount(async () => {
loading = true;
console.log('mounted');
window.addEventListener('message', onMessageHandler);
$socket?.on('chat-events', chatEventHandler);
$socket?.on('events', chatEventHandler);
pageSubscribe = page.subscribe(async (p) => {
if (p.url.pathname === '/') {
@@ -548,7 +575,7 @@
} catch (e) {}
}
showControls.subscribe(async (value) => {
showControlsSubscribe = showControls.subscribe(async (value) => {
if (controlPane && !$mobile) {
try {
if (value) {
@@ -569,17 +596,32 @@
}
});
selectedFolderSubscribe = selectedFolder.subscribe(async (folder) => {
if (
folder?.data?.model_ids &&
JSON.stringify(selectedModels) !== JSON.stringify(folder.data.model_ids)
) {
selectedModels = folder.data.model_ids;
console.log('Set selectedModels from folder data:', selectedModels);
}
});
const chatInput = document.getElementById('chat-input');
chatInput?.focus();
chats.subscribe(() => {});
});
onDestroy(() => {
pageSubscribe();
chatIdUnsubscriber?.();
window.removeEventListener('message', onMessageHandler);
$socket?.off('chat-events', chatEventHandler);
try {
pageSubscribe();
showControlsSubscribe();
selectedFolderSubscribe();
chatIdUnsubscriber?.();
window.removeEventListener('message', onMessageHandler);
$socket?.off('events', chatEventHandler);
} catch (e) {
console.error(e);
}
});
// File upload functions
@@ -780,6 +822,7 @@
//////////////////////////
const initNewChat = async () => {
console.log('initNewChat');
if ($user?.role !== 'admin' && $user?.permissions?.chat?.temporary_enforced) {
await temporaryChatEnabled.set(true);
}
@@ -830,17 +873,22 @@
$models.map((m) => m.id).includes(modelId)
);
} else {
if (sessionStorage.selectedModels) {
selectedModels = JSON.parse(sessionStorage.selectedModels);
sessionStorage.removeItem('selectedModels');
if ($selectedFolder?.data?.model_ids) {
selectedModels = $selectedFolder?.data?.model_ids;
} else {
if ($settings?.models) {
selectedModels = $settings?.models;
} else if ($config?.default_models) {
console.log($config?.default_models.split(',') ?? '');
selectedModels = $config?.default_models.split(',');
if (sessionStorage.selectedModels) {
selectedModels = JSON.parse(sessionStorage.selectedModels);
sessionStorage.removeItem('selectedModels');
} else {
if ($settings?.models) {
selectedModels = $settings?.models;
} else if ($config?.default_models) {
console.log($config?.default_models.split(',') ?? '');
selectedModels = $config?.default_models.split(',');
}
}
}
selectedModels = selectedModels.filter((modelId) => availableModels.includes(modelId));
}
@@ -1499,7 +1547,7 @@
chatFiles.push(
..._files.filter((item) =>
['doc', 'text', 'file', 'note', 'chat', 'collection'].includes(item.type)
['doc', 'text', 'file', 'note', 'chat', 'folder', 'collection'].includes(item.type)
)
);
chatFiles = chatFiles.filter(
@@ -2159,8 +2207,8 @@
selectedFolder.set(null);
} else {
_chatId = 'local';
await chatId.set('local');
_chatId = `local:${$socket?.id}`; // Use socket id for temporary chat
await chatId.set(_chatId);
}
await tick();
+126 -125
View File
@@ -13,12 +13,9 @@
showEmbeds
} from '$lib/stores';
import Modal from '../common/Modal.svelte';
import Controls from './Controls/Controls.svelte';
import CallOverlay from './MessageInput/CallOverlay.svelte';
import Drawer from '../common/Drawer.svelte';
import Overview from './Overview.svelte';
import EllipsisVertical from '../icons/EllipsisVertical.svelte';
import Artifacts from './Artifacts.svelte';
import Embeds from './ChatControls/Embeds.svelte';
@@ -154,24 +151,113 @@
}
</script>
<SvelteFlowProvider>
{#if !largeScreen}
{#if $showControls}
<Drawer
show={$showControls}
onClose={() => {
showControls.set(false);
}}
{#if !largeScreen}
{#if $showControls}
<Drawer
show={$showControls}
onClose={() => {
showControls.set(false);
}}
>
<div
class=" {$showCallOverlay || $showOverview || $showArtifacts || $showEmbeds
? ' h-screen w-full'
: 'px-4 py-3'} h-full"
>
{#if $showCallOverlay}
<div
class=" h-full max-h-[100dvh] bg-white text-gray-700 dark:bg-black dark:text-gray-300 flex justify-center"
>
<CallOverlay
bind:files
{submitPrompt}
{stopResponse}
{modelId}
{chatId}
{eventTarget}
on:close={() => {
showControls.set(false);
}}
/>
</div>
{:else if $showEmbeds}
<Embeds />
{:else if $showArtifacts}
<Artifacts {history} />
{:else if $showOverview}
{#await import('./Overview.svelte') then { default: Overview }}
<Overview
{history}
onNodeClick={(e) => {
const node = e.node;
showMessage(node.data.message, true);
}}
onClose={() => {
showControls.set(false);
}}
/>
{/await}
{:else}
<Controls
on:close={() => {
showControls.set(false);
}}
{models}
bind:chatFiles
bind:params
/>
{/if}
</div>
</Drawer>
{/if}
{:else}
<!-- if $showControls -->
{#if $showControls}
<PaneResizer
class="relative flex items-center justify-center group border-l border-gray-50 dark:border-gray-850 hover:border-gray-200 dark:hover:border-gray-800 transition z-20"
id="controls-resizer"
>
<div
class=" absolute -left-1.5 -right-1.5 -top-0 -bottom-0 z-20 cursor-col-resize bg-transparent"
/>
</PaneResizer>
{/if}
<Pane
bind:pane
defaultSize={0}
onResize={(size) => {
if ($showControls && pane.isExpanded()) {
if (size < minSize) {
pane.resize(minSize);
}
if (size < minSize) {
localStorage.chatControlsSize = 0;
} else {
// save the size in pixels to localStorage
const container = document.getElementById('chat-container');
localStorage.chatControlsSize = Math.floor((size / 100) * container.clientWidth);
}
}
}}
onCollapse={() => {
showControls.set(false);
}}
collapsible={true}
class=" z-10 bg-white dark:bg-gray-850"
>
{#if $showControls}
<div class="flex max-h-full min-h-full">
<div
class=" {$showCallOverlay || $showOverview || $showArtifacts || $showEmbeds
? ' h-screen w-full'
: 'px-4 py-3'} h-full"
class="w-full {($showOverview || $showArtifacts || $showEmbeds) && !$showCallOverlay
? ' '
: 'px-4 py-3 bg-white dark:shadow-lg dark:bg-gray-850 '} z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
id="controls-container"
>
{#if $showCallOverlay}
<div
class=" h-full max-h-[100dvh] bg-white text-gray-700 dark:bg-black dark:text-gray-300 flex justify-center"
>
<div class="w-full h-full flex justify-center">
<CallOverlay
bind:files
{submitPrompt}
@@ -185,19 +271,28 @@
/>
</div>
{:else if $showEmbeds}
<Embeds />
<Embeds overlay={dragged} />
{:else if $showArtifacts}
<Artifacts {history} />
<Artifacts {history} overlay={dragged} />
{:else if $showOverview}
<Overview
{history}
on:nodeclick={(e) => {
showMessage(e.detail.node.data.message);
}}
on:close={() => {
showControls.set(false);
}}
/>
{#await import('./Overview.svelte') then { default: Overview }}
<Overview
{history}
onNodeClick={(e) => {
const node = e.node;
if (node?.data?.message?.favorite) {
history.messages[node.data.message.id].favorite = true;
} else {
history.messages[node.data.message.id].favorite = null;
}
showMessage(node.data.message, true);
}}
onClose={() => {
showControls.set(false);
}}
/>
{/await}
{:else}
<Controls
on:close={() => {
@@ -209,101 +304,7 @@
/>
{/if}
</div>
</Drawer>
</div>
{/if}
{:else}
<!-- if $showControls -->
{#if $showControls}
<PaneResizer
class="relative flex items-center justify-center group border-l border-gray-50 dark:border-gray-850 hover:border-gray-200 dark:hover:border-gray-800 transition z-20"
id="controls-resizer"
>
<div
class=" absolute -left-1.5 -right-1.5 -top-0 -bottom-0 z-20 cursor-col-resize bg-transparent"
/>
</PaneResizer>
{/if}
<Pane
bind:pane
defaultSize={0}
onResize={(size) => {
if ($showControls && pane.isExpanded()) {
if (size < minSize) {
pane.resize(minSize);
}
if (size < minSize) {
localStorage.chatControlsSize = 0;
} else {
// save the size in pixels to localStorage
const container = document.getElementById('chat-container');
localStorage.chatControlsSize = Math.floor((size / 100) * container.clientWidth);
}
}
}}
onCollapse={() => {
showControls.set(false);
}}
collapsible={true}
class=" z-10 bg-white dark:bg-gray-850"
>
{#if $showControls}
<div class="flex max-h-full min-h-full">
<div
class="w-full {($showOverview || $showArtifacts || $showEmbeds) && !$showCallOverlay
? ' '
: 'px-4 py-3 bg-white dark:shadow-lg dark:bg-gray-850 '} z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
id="controls-container"
>
{#if $showCallOverlay}
<div class="w-full h-full flex justify-center">
<CallOverlay
bind:files
{submitPrompt}
{stopResponse}
{modelId}
{chatId}
{eventTarget}
on:close={() => {
showControls.set(false);
}}
/>
</div>
{:else if $showEmbeds}
<Embeds overlay={dragged} />
{:else if $showArtifacts}
<Artifacts {history} overlay={dragged} />
{:else if $showOverview}
<Overview
{history}
on:nodeclick={(e) => {
if (e.detail.node.data.message.favorite) {
history.messages[e.detail.node.data.message.id].favorite = true;
} else {
history.messages[e.detail.node.data.message.id].favorite = null;
}
showMessage(e.detail.node.data.message);
}}
on:close={() => {
showControls.set(false);
}}
/>
{:else}
<Controls
on:close={() => {
showControls.set(false);
}}
{models}
bind:chatFiles
bind:params
/>
{/if}
</div>
</div>
{/if}
</Pane>
{/if}
</SvelteFlowProvider>
</Pane>
{/if}
@@ -13,9 +13,14 @@
class="pointer-events-auto z-20 flex justify-between items-center py-3 px-2 font-primar text-gray-900 dark:text-white"
>
<div class="flex-1 flex items-center justify-between pl-2">
<div class="flex items-center space-x-2">
<a
class="flex items-center space-x-2 hover:underline"
href={$embed?.url}
target="_blank"
rel="noopener noreferrer"
>
{$embed?.title ?? 'Embedded Content'}
</div>
</a>
</div>
<button
@@ -13,6 +13,7 @@
import LightBulb from '$lib/components/icons/LightBulb.svelte';
import Markdown from '../Messages/Markdown.svelte';
import Skeleton from '../Messages/Skeleton.svelte';
import { chatId, models, socket } from '$lib/stores';
export let id = '';
export let messageId = '';
@@ -118,6 +119,9 @@
let res;
[res, controller] = await chatCompletion(localStorage.token, {
model: model,
model_item: $models.find((m) => m.id === model),
session_id: $socket?.id,
chat_id: $chatId,
messages: [
...messages,
{
@@ -246,11 +250,11 @@
{#if responseContent === null}
{#if !floatingInput}
<div
class="flex flex-row gap-0.5 shrink-0 p-1 bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-lg shadow-xl"
class="flex flex-row shrink-0 p-0.5 bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-xl shadow-xl border border-gray-100 dark:border-gray-800"
>
{#each actions as action}
<button
class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-sm flex items-center gap-1 min-w-fit"
class="px-1.5 py-[1px] hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl flex items-center gap-1 min-w-fit transition"
on:click={async () => {
selectedText = window.getSelection().toString();
selectedAction = action;
@@ -280,7 +284,7 @@
</div>
{:else}
<div
class="py-1 flex dark:text-gray-100 bg-gray-50 dark:bg-gray-800 border border-gray-100 dark:border-gray-850 w-72 rounded-full shadow-xl"
class="py-1 flex dark:text-gray-100 bg-white dark:bg-gray-850 border border-gray-100 dark:border-gray-800 w-72 rounded-full shadow-xl"
>
<input
type="text"
@@ -295,7 +299,7 @@
}}
/>
<div class="ml-1 mr-2">
<div class="ml-1 mr-1">
<button
class="{floatingInputValue !== ''
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
@@ -321,19 +325,22 @@
</div>
{/if}
{:else}
<div class="bg-white dark:bg-gray-850 dark:text-gray-100 rounded-xl shadow-xl w-80 max-w-full">
<div
class="bg-white dark:bg-gray-850 dark:text-gray-100 rounded-3xl shadow-xl w-80 max-w-full border border-gray-100 dark:border-gray-800"
>
<div
class="bg-gray-50/50 dark:bg-gray-800 dark:text-gray-100 text-medium rounded-xl px-3.5 py-3 w-full"
class="bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-3xl px-3.5 pt-3 w-full"
>
<div class="font-medium">
<Markdown id={`${id}-float-prompt`} {content} />
</div>
</div>
<div
class="bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-xl px-3.5 py-3 w-full"
>
<div class=" max-h-80 overflow-y-auto w-full markdown-prose-xs" id="response-container">
<div class="bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-4xl w-full">
<div
class=" max-h-80 overflow-y-auto w-full markdown-prose-xs px-3.5 py-3"
id="response-container"
>
{#if !responseContent || responseContent?.trim() === ''}
<Skeleton size="sm" />
{:else}
+6 -8
View File
@@ -885,8 +885,6 @@
})
}
];
console.log(suggestions);
loaded = true;
window.setTimeout(() => {
@@ -1201,12 +1199,12 @@
floatingMenuPlacement={'top-start'}
insertPromptAsRichText={$settings?.insertPromptAsRichText ?? false}
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
(!$mobile ||
!(
'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0
))}
!$mobile &&
!(
'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0
)}
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
largeTextAsFile={($settings?.largeTextAsFile ?? false) && !shiftKey}
autocomplete={$config?.features?.enable_autocomplete_generation &&
@@ -13,6 +13,8 @@
import Database from '$lib/components/icons/Database.svelte';
import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
import Youtube from '$lib/components/icons/Youtube.svelte';
import { folders } from '$lib/stores';
import Folder from '$lib/components/icons/Folder.svelte';
const i18n = getContext('i18n');
@@ -144,14 +146,25 @@
]
: [];
items = [...collections, ...collection_files, ...legacy_collections, ...legacy_documents].map(
(item) => {
return {
...item,
...(item?.legacy || item?.meta?.legacy || item?.meta?.document ? { legacy: true } : {})
};
}
);
let folder_items = $folders.map((folder) => ({
...folder,
type: 'folder',
description: $i18n.t('Folder'),
title: folder.name
}));
items = [
...folder_items,
...collections,
...collection_files,
...legacy_collections,
...legacy_documents
].map((item) => {
return {
...item,
...(item?.legacy || item?.meta?.legacy || item?.meta?.document ? { legacy: true } : {})
};
});
fuse = new Fuse(items, {
keys: ['name', 'description']
@@ -213,6 +226,8 @@
>
{#if item?.type === 'collection'}
<Database className="size-4" />
{:else if item?.type === 'folder'}
<Folder className="size-4" />
{:else}
<DocumentPage className="size-4" />
{/if}
@@ -330,7 +330,7 @@
{#each Object.keys(tools) as toolId}
<button
class="relative flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800/50"
on:click={(e) => {
on:click={async (e) => {
if (!(tools[toolId]?.authenticated ?? true)) {
e.preventDefault();
@@ -338,9 +338,18 @@
let serverId = parts?.at(-1) ?? toolId;
const authUrl = getOAuthClientAuthorizationUrl(serverId, 'mcp');
window.open(authUrl, '_blank', 'noopener');
window.open(authUrl, '_self', 'noopener');
} else {
tools[toolId].enabled = !tools[toolId].enabled;
const state = tools[toolId].enabled;
await tick();
if (state) {
selectedToolIds = [...selectedToolIds, toolId];
} else {
selectedToolIds = selectedToolIds.filter((id) => id !== toolId);
}
}
}}
>
@@ -383,18 +392,7 @@
{/if}
<div class=" shrink-0">
<Switch
state={tools[toolId].enabled}
on:change={async (e) => {
const state = e.detail;
await tick();
if (state) {
selectedToolIds = [...selectedToolIds, toolId];
} else {
selectedToolIds = selectedToolIds.filter((id) => id !== toolId);
}
}}
/>
<Switch state={tools[toolId].enabled} />
</div>
</button>
{/each}
@@ -7,6 +7,7 @@
export let id = '';
export let sources = [];
export let readOnly = false;
let citations = [];
let showPercentage = false;
@@ -26,12 +27,18 @@
if (citations[sourceIdx]?.source?.embed_url) {
const embedUrl = citations[sourceIdx].source.embed_url;
if (embedUrl) {
showControls.set(true);
showEmbeds.set(true);
embed.set({
title: citations[sourceIdx]?.source?.name || 'Embedded Content',
url: embedUrl
});
if (readOnly) {
// Open in new tab if readOnly
window.open(embedUrl, '_blank');
return;
} else {
showControls.set(true);
showEmbeds.set(true);
embed.set({
title: citations[sourceIdx]?.source?.name || 'Embedded Content',
url: embedUrl
});
}
} else {
selectedCitation = citations[sourceIdx];
showCitationModal = true;
@@ -6,7 +6,7 @@
import PyodideWorker from '$lib/workers/pyodide.worker?worker';
import { executeCode } from '$lib/apis/utils';
import { copyToClipboard, renderMermaidDiagram } from '$lib/utils';
import { copyToClipboard, renderMermaidDiagram, renderVegaVisualization } from '$lib/utils';
import 'highlight.js/styles/github-dark.min.css';
@@ -55,6 +55,7 @@
let _token = null;
let mermaidHtml = null;
let vegaHtml = null;
let highlightedCode = null;
let executing = false;
@@ -325,7 +326,26 @@
const render = async () => {
onUpdate(token);
if (lang === 'mermaid' && (token?.raw ?? '').slice(-4).includes('```')) {
mermaidHtml = await renderMermaidDiagram(code);
try {
mermaidHtml = await renderMermaidDiagram(code);
} catch (error) {
console.error('Failed to render mermaid diagram:', error);
const errorMsg = error instanceof Error ? error.message : String(error);
toast.error($i18n.t('Failed to render diagram') + `: ${errorMsg}`);
mermaidHtml = null;
}
} else if (
(lang === 'vega' || lang === 'vega-lite') &&
(token?.raw ?? '').slice(-4).includes('```')
) {
try {
vegaHtml = await renderVegaVisualization(code);
} catch (error) {
console.error('Failed to render Vega visualization:', error);
const errorMsg = error instanceof Error ? error.message : String(error);
toast.error($i18n.t('Failed to render diagram') + `: ${errorMsg}`);
vegaHtml = null;
}
}
};
@@ -397,6 +417,16 @@
{:else}
<pre class="mermaid">{code}</pre>
{/if}
{:else if lang === 'vega' || lang === 'vega-lite'}
{#if vegaHtml}
<SvgPanZoom
className="rounded-3xl max-h-fit overflow-hidden"
svg={vegaHtml}
content={_token.text}
/>
{:else}
<pre class="vega">{code}</pre>
{/if}
{:else}
<div
class="absolute left-0 right-0 py-2.5 pr-3 text-text-300 pl-4.5 text-xs font-medium dark:text-white"
@@ -576,8 +576,6 @@
await tick();
if (buttonsContainerElement) {
console.log(buttonsContainerElement);
buttonsContainerElement.addEventListener('wheel', function (event) {
if (buttonsContainerElement.scrollWidth <= buttonsContainerElement.clientWidth) {
// If the container is not scrollable, horizontal scroll
@@ -811,6 +809,7 @@
bind:this={citationsElement}
id={message?.id}
sources={message?.sources ?? message?.citations}
{readOnly}
/>
{/if}
@@ -29,7 +29,7 @@
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[200px] rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-850 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg transition"
class="w-full max-w-[200px] rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg transition"
sideOffset={-2}
side="bottom"
align="start"
@@ -68,17 +68,20 @@
}}
>
<div class="flex items-start gap-2">
<div class="pt-3 px-1">
<span class="relative flex size-1.5 rounded-full justify-center items-center">
{#if status?.done === false}
{#if history.length > 1}
<div class="pt-3 px-1">
<span class="relative flex size-1.5 rounded-full justify-center items-center">
{#if status?.done === false}
<span
class="absolute inline-flex h-full w-full animate-ping rounded-full bg-gray-500 dark:bg-gray-300 opacity-75"
></span>
{/if}
<span
class="absolute inline-flex h-full w-full animate-ping rounded-full bg-gray-500 dark:bg-gray-300 opacity-75"
class="relative inline-flex size-1.5 rounded-full bg-gray-500 dark:bg-gray-300"
></span>
{/if}
<span class="relative inline-flex size-1.5 rounded-full bg-gray-500 dark:bg-gray-300"
></span>
</span>
</div>
</span>
</div>
{/if}
<StatusItem {status} />
</div>
</button>
+5 -1
View File
@@ -39,9 +39,13 @@
};
$: if (selectedModels.length > 0 && $models.length > 0) {
selectedModels = selectedModels.map((model) =>
const _selectedModels = selectedModels.map((model) =>
$models.map((m) => m.id).includes(model) ? model : ''
);
if (JSON.stringify(_selectedModels) !== JSON.stringify(selectedModels)) {
selectedModels = _selectedModels;
}
}
</script>
@@ -90,6 +90,26 @@
}
);
const updateFuse = () => {
if (fuse) {
fuse.setCollection(
items.map((item) => {
const _item = {
...item,
modelName: item.model?.name,
tags: (item.model?.tags ?? []).map((tag) => tag.name).join(' '),
desc: item.model?.info?.meta?.description
};
return _item;
})
);
}
};
$: if (items) {
updateFuse();
}
$: filteredItems = (
searchValue
? fuse
+1 -1
View File
@@ -248,7 +248,7 @@
</div>
</div>
{#if $temporaryChatEnabled && $chatId === 'local'}
{#if $temporaryChatEnabled && ($chatId ?? '').startsWith('local:')}
<div class=" w-full z-30 text-center">
<div class="text-xs text-gray-500">{$i18n.t('Temporary Chat')}</div>
</div>
+7 -189
View File
@@ -1,199 +1,17 @@
<script lang="ts">
import { getContext, createEventDispatcher, onDestroy } from 'svelte';
import { useSvelteFlow, useNodesInitialized, useStore } from '@xyflow/svelte';
import { useSvelteFlow, useNodesInitialized, useStore, SvelteFlowProvider } from '@xyflow/svelte';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
import { onMount, tick } from 'svelte';
import { writable } from 'svelte/store';
import { models, showOverview, theme, user } from '$lib/stores';
import '@xyflow/svelte/dist/style.css';
import CustomNode from './Overview/Node.svelte';
import Flow from './Overview/Flow.svelte';
import XMark from '../icons/XMark.svelte';
import ArrowLeft from '../icons/ArrowLeft.svelte';
const { width, height } = useStore();
const { fitView, getViewport } = useSvelteFlow();
const nodesInitialized = useNodesInitialized();
import View from './Overview/View.svelte';
export let history;
let selectedMessageId = null;
const nodes = writable([]);
const edges = writable([]);
const nodeTypes = {
custom: CustomNode
};
$: if (history) {
drawFlow();
}
$: if (history && history.currentId) {
focusNode();
}
const focusNode = async () => {
if (selectedMessageId === null) {
await fitView({ nodes: [{ id: history.currentId }] });
} else {
await fitView({ nodes: [{ id: selectedMessageId }] });
}
selectedMessageId = null;
};
const drawFlow = async () => {
const nodeList = [];
const edgeList = [];
const levelOffset = 150; // Vertical spacing between layers
const siblingOffset = 250; // Horizontal spacing between nodes at the same layer
// Map to keep track of node positions at each level
let positionMap = new Map();
// Helper function to truncate labels
function createLabel(content) {
const maxLength = 100;
return content.length > maxLength ? content.substr(0, maxLength) + '...' : content;
}
// Create nodes and map children to ensure alignment in width
let layerWidths = {}; // Track widths of each layer
Object.keys(history.messages).forEach((id) => {
const message = history.messages[id];
const level = message.parentId ? (positionMap.get(message.parentId)?.level ?? -1) + 1 : 0;
if (!layerWidths[level]) layerWidths[level] = 0;
positionMap.set(id, {
id: message.id,
level,
position: layerWidths[level]++
});
});
// Adjust positions based on siblings count to centralize vertical spacing
Object.keys(history.messages).forEach((id) => {
const pos = positionMap.get(id);
const xOffset = pos.position * siblingOffset;
const y = pos.level * levelOffset;
const x = xOffset;
nodeList.push({
id: pos.id,
type: 'custom',
data: {
user: $user,
message: history.messages[id],
model: $models.find((model) => model.id === history.messages[id].model)
},
position: { x, y }
});
// Create edges
const parentId = history.messages[id].parentId;
if (parentId) {
edgeList.push({
id: parentId + '-' + pos.id,
source: parentId,
target: pos.id,
selectable: false,
class: ' dark:fill-gray-300 fill-gray-300',
type: 'smoothstep',
animated: history.currentId === id || recurseCheckChild(id, history.currentId)
});
}
});
await edges.set([...edgeList]);
await nodes.set([...nodeList]);
};
const recurseCheckChild = (nodeId, currentId) => {
const node = history.messages[nodeId];
return (
node.childrenIds &&
node.childrenIds.some((id) => id === currentId || recurseCheckChild(id, currentId))
);
};
onMount(() => {
drawFlow();
nodesInitialized.subscribe(async (initialized) => {
if (initialized) {
await tick();
const res = await fitView({ nodes: [{ id: history.currentId }] });
}
});
width.subscribe((value) => {
if (value) {
// fitView();
fitView({ nodes: [{ id: history.currentId }] });
}
});
height.subscribe((value) => {
if (value) {
// fitView();
fitView({ nodes: [{ id: history.currentId }] });
}
});
});
onDestroy(() => {
console.log('Overview destroyed');
nodes.set([]);
edges.set([]);
});
export let onClose;
export let onNodeClick;
</script>
<div class="w-full h-full relative">
<div class=" absolute z-50 w-full flex justify-between dark:text-gray-100 px-4 py-3">
<div class="flex items-center gap-2.5">
<button
class="self-center p-0.5"
on:click={() => {
showOverview.set(false);
}}
>
<ArrowLeft className="size-3.5" />
</button>
<div class=" text-lg font-medium self-center font-primary">{$i18n.t('Chat Overview')}</div>
</div>
<button
class="self-center p-0.5"
on:click={() => {
dispatch('close');
showOverview.set(false);
}}
>
<XMark className="size-3.5" />
</button>
</div>
{#if $nodes.length > 0}
<Flow
{nodes}
{nodeTypes}
{edges}
on:nodeclick={(e) => {
console.log(e.detail.node.data);
dispatch('nodeclick', e.detail);
selectedMessageId = e.detail.node.data.message.id;
fitView({ nodes: [{ id: selectedMessageId }] });
}}
/>
{/if}
</div>
<SvelteFlowProvider>
<View {history} {onClose} {onNodeClick} />
</SvelteFlowProvider>
+20 -2
View File
@@ -4,11 +4,22 @@
const dispatch = createEventDispatcher();
import { theme } from '$lib/stores';
import { Background, Controls, SvelteFlow, BackgroundVariant } from '@xyflow/svelte';
import {
Background,
Controls,
SvelteFlow,
BackgroundVariant,
ControlButton
} from '@xyflow/svelte';
import BarsArrowUp from '$lib/components/icons/BarsArrowUp.svelte';
import Bars3BottomLeft from '$lib/components/icons/Bars3BottomLeft.svelte';
import AlignVertical from '$lib/components/icons/AlignVertical.svelte';
import AlignHorizontal from '$lib/components/icons/AlignHorizontal.svelte';
export let nodes;
export let nodeTypes;
export let edges;
export let setLayoutDirection;
</script>
<SvelteFlow
@@ -31,6 +42,13 @@
console.log('Flow initialized');
}}
>
<Controls showLock={false} />
<Controls showLock={false}>
<ControlButton on:click={() => setLayoutDirection('vertical')} title="Vertical Layout">
<AlignVertical className="size-4" />
</ControlButton>
<ControlButton on:click={() => setLayoutDirection('horizontal')} title="Horizontal Layout">
<AlignHorizontal className="size-4" />
</ControlButton>
</Controls>
<Background variant={BackgroundVariant.Dots} />
</SvelteFlow>
@@ -0,0 +1,207 @@
<script lang="ts">
import { getContext, createEventDispatcher, onDestroy } from 'svelte';
import { useSvelteFlow, useNodesInitialized, useStore } from '@xyflow/svelte';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
import { onMount, tick } from 'svelte';
import { writable } from 'svelte/store';
import { models, showOverview, theme, user } from '$lib/stores';
import '@xyflow/svelte/dist/style.css';
import CustomNode from './Node.svelte';
import Flow from './Flow.svelte';
import XMark from '../../icons/XMark.svelte';
import ArrowLeft from '../../icons/ArrowLeft.svelte';
const { width, height } = useStore();
const { fitView, getViewport } = useSvelteFlow();
const nodesInitialized = useNodesInitialized();
export let history;
export let onClose;
export let onNodeClick;
let selectedMessageId = null;
const nodes = writable([]);
const edges = writable([]);
let layoutDirection = 'vertical';
const nodeTypes = {
custom: CustomNode
};
$: if (history) {
drawFlow(layoutDirection);
}
$: if (history && history.currentId) {
focusNode();
}
const focusNode = async () => {
if (selectedMessageId === null) {
await fitView({ nodes: [{ id: history.currentId }] });
} else {
await fitView({ nodes: [{ id: selectedMessageId }] });
}
selectedMessageId = null;
};
const drawFlow = async (direction) => {
const nodeList = [];
const edgeList = [];
const levelOffset = direction === 'vertical' ? 150 : 300;
const siblingOffset = direction === 'vertical' ? 250 : 150;
// Map to keep track of node positions at each level
let positionMap = new Map();
// Helper function to truncate labels
function createLabel(content) {
const maxLength = 100;
return content.length > maxLength ? content.substr(0, maxLength) + '...' : content;
}
// Create nodes and map children to ensure alignment in width
let layerWidths = {}; // Track widths of each layer
Object.keys(history.messages).forEach((id) => {
const message = history.messages[id];
const level = message.parentId ? (positionMap.get(message.parentId)?.level ?? -1) + 1 : 0;
if (!layerWidths[level]) layerWidths[level] = 0;
positionMap.set(id, {
id: message.id,
level,
position: layerWidths[level]++
});
});
// Adjust positions based on siblings count to centralize vertical spacing
Object.keys(history.messages).forEach((id) => {
const pos = positionMap.get(id);
const x = direction === 'vertical' ? pos.position * siblingOffset : pos.level * levelOffset;
const y = direction === 'vertical' ? pos.level * levelOffset : pos.position * siblingOffset;
nodeList.push({
id: pos.id,
type: 'custom',
data: {
user: $user,
message: history.messages[id],
model: $models.find((model) => model.id === history.messages[id].model)
},
position: { x, y }
});
// Create edges
const parentId = history.messages[id].parentId;
if (parentId) {
edgeList.push({
id: parentId + '-' + pos.id,
source: parentId,
target: pos.id,
selectable: false,
class: ' dark:fill-gray-300 fill-gray-300',
type: 'smoothstep',
animated: history.currentId === id || recurseCheckChild(id, history.currentId)
});
}
});
await edges.set([...edgeList]);
await nodes.set([...nodeList]);
};
const recurseCheckChild = (nodeId, currentId) => {
const node = history.messages[nodeId];
return (
node.childrenIds &&
node.childrenIds.some((id) => id === currentId || recurseCheckChild(id, currentId))
);
};
const setLayoutDirection = (direction) => {
layoutDirection = direction;
drawFlow(layoutDirection);
};
onMount(() => {
drawFlow(layoutDirection);
nodesInitialized.subscribe(async (initialized) => {
if (initialized) {
await tick();
const res = await fitView({ nodes: [{ id: history.currentId }] });
}
});
width.subscribe((value) => {
if (value) {
// fitView();
fitView({ nodes: [{ id: history.currentId }] });
}
});
height.subscribe((value) => {
if (value) {
// fitView();
fitView({ nodes: [{ id: history.currentId }] });
}
});
});
onDestroy(() => {
console.log('Overview destroyed');
nodes.set([]);
edges.set([]);
});
</script>
<div class="w-full h-full relative">
<div class=" absolute z-50 w-full flex justify-between dark:text-gray-100 px-4 py-3">
<div class="flex items-center gap-2.5">
<button
class="self-center p-0.5"
on:click={() => {
showOverview.set(false);
}}
>
<ArrowLeft className="size-3.5" />
</button>
<div class=" text-lg font-medium self-center font-primary">{$i18n.t('Chat Overview')}</div>
</div>
<button
class="self-center p-0.5"
on:click={() => {
onClose();
showOverview.set(false);
}}
>
<XMark className="size-3.5" />
</button>
</div>
{#if $nodes.length > 0}
<Flow
{nodes}
{nodeTypes}
{edges}
{setLayoutDirection}
on:nodeclick={(e) => {
onNodeClick(e.detail);
selectedMessageId = e.detail.node.data.message.id;
fitView({ nodes: [{ id: selectedMessageId }] });
}}
/>
{/if}
</div>
+3 -6
View File
@@ -7,6 +7,9 @@
const dispatch = createEventDispatcher();
import { getChatList } from '$lib/apis/chats';
import { updateFolderById } from '$lib/apis/folders';
import {
config,
user,
@@ -25,7 +28,6 @@
import MessageInput from './MessageInput.svelte';
import FolderPlaceholder from './Placeholder/FolderPlaceholder.svelte';
import FolderTitle from './Placeholder/FolderTitle.svelte';
import { getChatList } from '$lib/apis/chats';
const i18n = getContext('i18n');
@@ -58,7 +60,6 @@
export let toolServers = [];
let models = [];
let selectedModelIdx = 0;
$: if (selectedModels.length > 0) {
@@ -66,8 +67,6 @@
}
$: models = selectedModels.map((id) => $_models.find((m) => m.id === id));
onMount(() => {});
</script>
<div class="m-auto w-full max-w-6xl px-2 @2xl:px-20 translate-y-6 py-24 text-center">
@@ -91,8 +90,6 @@
<FolderTitle
folder={$selectedFolder}
onUpdate={async (folder) => {
selectedFolder.set(folder);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
currentChatPage.set(1);
}}
@@ -11,7 +11,7 @@
import { selectedFolder } from '$lib/stores';
import { deleteFolderById, updateFolderById } from '$lib/apis/folders';
import { deleteFolderById, getFolderById, updateFolderById } from '$lib/apis/folders';
import { getChatsByFolderId } from '$lib/apis/chats';
import FolderModal from '$lib/components/layout/Sidebar/Folders/FolderModal.svelte';
@@ -61,8 +61,14 @@
}
toast.success($i18n.t('Folder updated successfully'));
selectedFolder.set(folder);
onUpdate(folder);
const _folder = await getFolderById(localStorage.token, folder.id).catch((error) => {
toast.error(`${error}`);
return null;
});
await selectedFolder.set(_folder);
onUpdate(_folder);
}
};
@@ -80,8 +86,14 @@
folder.meta = { ...folder.meta, icon: iconName };
toast.success($i18n.t('Folder updated successfully'));
selectedFolder.set(folder);
onUpdate(folder);
const _folder = await getFolderById(localStorage.token, folder.id).catch((error) => {
toast.error(`${error}`);
return null;
});
await selectedFolder.set(_folder);
onUpdate(_folder);
}
};
@@ -12,6 +12,8 @@
export let profileImageUrl;
export let user = null;
export let imageClassName = 'size-14 md:size-18';
let profileImageInputElement;
</script>
@@ -89,7 +91,7 @@
<img
src={profileImageUrl !== '' ? profileImageUrl : generateInitialsImage(user?.name)}
alt="profile"
class=" rounded-full size-14 md:size-18 object-cover"
class=" rounded-full {imageClassName} object-cover"
/>
<div class="absolute bottom-0 right-0 opacity-0 group-hover:opacity-100 transition">
@@ -7,6 +7,7 @@
import Cog6 from '$lib/components/icons/Cog6.svelte';
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import AddToolServerModal from '$lib/components/AddToolServerModal.svelte';
import WrenchAlt from '$lib/components/icons/WrenchAlt.svelte';
export let onDelete = () => {};
export let onSubmit = () => {};
@@ -41,33 +42,21 @@
/>
<div class="flex w-full gap-2 items-center">
<Tooltip
className="w-full relative"
content={$i18n.t(`WebUI will make requests to "{{url}}"`, {
url: `${connection?.url}/${connection?.path ?? 'openapi.json'}`
})}
placement="top-start"
>
<Tooltip className="w-full relative" content={''} placement="top-start">
<div class="flex w-full">
<div class="flex-1 relative">
<input
class=" outline-hidden w-full bg-transparent {!(connection?.config?.enable ?? true)
? 'opacity-50'
: ''}"
placeholder={$i18n.t('API Base URL')}
bind:value={connection.url}
autocomplete="off"
/>
<div
class="flex-1 relative flex gap-1.5 items-center {!(connection?.config?.enable ?? true)
? 'opacity-50'
: ''}"
>
<Tooltip content={connection?.type === 'mcp' ? $i18n.t('MCP') : $i18n.t('OpenAPI')}>
<WrenchAlt />
</Tooltip>
<div class=" capitalize outline-hidden w-full bg-transparent">
{connection?.info?.name ?? connection?.url}
<span class="text-gray-500">{connection?.info?.id}</span>
</div>
</div>
{#if (connection?.auth_type ?? 'bearer') === 'bearer'}
<SensitiveInput
inputClassName=" outline-hidden bg-transparent w-full"
placeholder={$i18n.t('API Key')}
bind:value={connection.key}
required={false}
/>
{/if}
</div>
</Tooltip>