mirror of
https://github.com/open-webui/open-webui
synced 2025-05-22 13:54:20 +00:00
feat: chat overview
This commit is contained in:
parent
bb087a5989
commit
d1dbb9a3be
@ -156,3 +156,7 @@ input[type='number'] {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@apply rounded-md dark:bg-gray-800 bg-gray-100 mx-0.5;
|
@apply rounded-md dark:bg-gray-800 bg-gray-100 mx-0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.svelte-flow {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
banners,
|
banners,
|
||||||
user,
|
user,
|
||||||
socket,
|
socket,
|
||||||
|
showControls,
|
||||||
showCallOverlay,
|
showCallOverlay,
|
||||||
currentChatPage,
|
currentChatPage,
|
||||||
temporaryChatEnabled
|
temporaryChatEnabled
|
||||||
@ -70,7 +71,6 @@
|
|||||||
let loaded = false;
|
let loaded = false;
|
||||||
const eventTarget = new EventTarget();
|
const eventTarget = new EventTarget();
|
||||||
|
|
||||||
let showControls = false;
|
|
||||||
let stopResponseFlag = false;
|
let stopResponseFlag = false;
|
||||||
let autoScroll = true;
|
let autoScroll = true;
|
||||||
let processing = '';
|
let processing = '';
|
||||||
@ -1703,7 +1703,6 @@
|
|||||||
{title}
|
{title}
|
||||||
bind:selectedModels
|
bind:selectedModels
|
||||||
bind:showModelSelector
|
bind:showModelSelector
|
||||||
bind:showControls
|
|
||||||
shareEnabled={messages.length > 0}
|
shareEnabled={messages.length > 0}
|
||||||
{chat}
|
{chat}
|
||||||
{initNewChat}
|
{initNewChat}
|
||||||
@ -1713,7 +1712,7 @@
|
|||||||
<div
|
<div
|
||||||
class="absolute top-[4.25rem] w-full {$showSidebar
|
class="absolute top-[4.25rem] w-full {$showSidebar
|
||||||
? 'md:max-w-[calc(100%-260px)]'
|
? 'md:max-w-[calc(100%-260px)]'
|
||||||
: ''} {showControls ? 'lg:pr-[24rem]' : ''} z-20"
|
: ''} {$showControls ? 'lg:pr-[24rem]' : ''} z-20"
|
||||||
>
|
>
|
||||||
<div class=" flex flex-col gap-1 w-full">
|
<div class=" flex flex-col gap-1 w-full">
|
||||||
{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
|
{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
|
||||||
@ -1740,7 +1739,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-col flex-auto z-10">
|
<div class="flex flex-col flex-auto z-10">
|
||||||
<div
|
<div
|
||||||
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full z-10 scrollbar-hidden {showControls
|
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full z-10 scrollbar-hidden {$showControls
|
||||||
? 'lg:pr-[24rem]'
|
? 'lg:pr-[24rem]'
|
||||||
: ''}"
|
: ''}"
|
||||||
id="messages-container"
|
id="messages-container"
|
||||||
@ -1770,7 +1769,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class={showControls ? 'lg:pr-[24rem]' : ''}>
|
<div class={$showControls ? 'lg:pr-[24rem]' : ''}>
|
||||||
<MessageInput
|
<MessageInput
|
||||||
bind:files
|
bind:files
|
||||||
bind:prompt
|
bind:prompt
|
||||||
@ -1791,7 +1790,7 @@
|
|||||||
{submitPrompt}
|
{submitPrompt}
|
||||||
{stopResponse}
|
{stopResponse}
|
||||||
on:call={() => {
|
on:call={() => {
|
||||||
showControls = true;
|
showControls.set(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -1807,7 +1806,7 @@
|
|||||||
}
|
}
|
||||||
return a;
|
return a;
|
||||||
}, [])}
|
}, [])}
|
||||||
bind:show={showControls}
|
bind:history
|
||||||
bind:chatFiles
|
bind:chatFiles
|
||||||
bind:params
|
bind:params
|
||||||
bind:files
|
bind:files
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { SvelteFlowProvider } from '@xyflow/svelte';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
|
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { mobile, showControls, showCallOverlay, showOverview } from '$lib/stores';
|
||||||
|
|
||||||
import Modal from '../common/Modal.svelte';
|
import Modal from '../common/Modal.svelte';
|
||||||
import Controls from './Controls/Controls.svelte';
|
import Controls from './Controls/Controls.svelte';
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { mobile, showCallOverlay } from '$lib/stores';
|
|
||||||
import CallOverlay from './MessageInput/CallOverlay.svelte';
|
import CallOverlay from './MessageInput/CallOverlay.svelte';
|
||||||
import Drawer from '../common/Drawer.svelte';
|
import Drawer from '../common/Drawer.svelte';
|
||||||
|
import Overview from './Overview.svelte';
|
||||||
|
|
||||||
export let show = false;
|
export let history;
|
||||||
|
|
||||||
export let models = [];
|
export let models = [];
|
||||||
|
|
||||||
export let chatId = null;
|
export let chatId = null;
|
||||||
@ -44,46 +47,13 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !largeScreen}
|
<SvelteFlowProvider>
|
||||||
{#if $showCallOverlay}
|
{#if !largeScreen}
|
||||||
<div class=" absolute w-full h-screen max-h-[100dvh] flex z-[999] overflow-hidden">
|
{#if $showCallOverlay}
|
||||||
<div
|
<div class=" absolute w-full h-screen max-h-[100dvh] flex z-[999] overflow-hidden">
|
||||||
class="absolute w-full h-screen max-h-[100dvh] bg-white text-gray-700 dark:bg-black dark:text-gray-300 flex justify-center"
|
<div
|
||||||
>
|
class="absolute w-full h-screen 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={() => {
|
|
||||||
show = false;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else if show}
|
|
||||||
<Drawer bind:show>
|
|
||||||
<div class=" px-6 py-4 h-full">
|
|
||||||
<Controls
|
|
||||||
on:close={() => {
|
|
||||||
show = false;
|
|
||||||
}}
|
|
||||||
{models}
|
|
||||||
bind:chatFiles
|
|
||||||
bind:params
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Drawer>
|
|
||||||
{/if}
|
|
||||||
{:else if show}
|
|
||||||
<div class=" absolute bottom-0 right-0 z-20 h-full pointer-events-none">
|
|
||||||
<div class="pr-4 pt-14 pb-8 w-[24rem] h-full" in:slide={{ duration: 200, axis: 'x' }}>
|
|
||||||
<div
|
|
||||||
class="w-full h-full px-5 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-50 dark:border-gray-800 rounded-xl z-50 pointer-events-auto overflow-y-auto scrollbar-hidden"
|
|
||||||
>
|
|
||||||
{#if $showCallOverlay}
|
|
||||||
<CallOverlay
|
<CallOverlay
|
||||||
bind:files
|
bind:files
|
||||||
{submitPrompt}
|
{submitPrompt}
|
||||||
@ -92,20 +62,68 @@
|
|||||||
{chatId}
|
{chatId}
|
||||||
{eventTarget}
|
{eventTarget}
|
||||||
on:close={() => {
|
on:close={() => {
|
||||||
show = false;
|
showControls.set(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else}
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if $showControls}
|
||||||
|
<Drawer
|
||||||
|
on:close={() => {
|
||||||
|
showControls.set(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" px-6 py-4 h-full">
|
||||||
<Controls
|
<Controls
|
||||||
on:close={() => {
|
on:close={() => {
|
||||||
show = false;
|
showControls.set(false);
|
||||||
}}
|
}}
|
||||||
{models}
|
{models}
|
||||||
bind:chatFiles
|
bind:chatFiles
|
||||||
bind:params
|
bind:params
|
||||||
/>
|
/>
|
||||||
{/if}
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
{/if}
|
||||||
|
{:else if $showControls}
|
||||||
|
<div class=" absolute bottom-0 right-0 z-20 h-full pointer-events-none">
|
||||||
|
<div class="pr-4 pt-14 pb-8 w-[24rem] h-full" in:slide={{ duration: 200, axis: 'x' }}>
|
||||||
|
<div
|
||||||
|
class="w-full h-full {$showOverview
|
||||||
|
? ' '
|
||||||
|
: 'px-5 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-50 dark:border-gray-800'} rounded-lg z-50 pointer-events-auto overflow-y-auto scrollbar-hidden"
|
||||||
|
>
|
||||||
|
{#if $showCallOverlay}
|
||||||
|
<CallOverlay
|
||||||
|
bind:files
|
||||||
|
{submitPrompt}
|
||||||
|
{stopResponse}
|
||||||
|
{modelId}
|
||||||
|
{chatId}
|
||||||
|
{eventTarget}
|
||||||
|
on:close={() => {
|
||||||
|
showControls.set(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else if $showOverview}
|
||||||
|
<Overview
|
||||||
|
bind:history
|
||||||
|
on:close={() => {
|
||||||
|
showControls.set(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<Controls
|
||||||
|
on:close={() => {
|
||||||
|
showControls.set(false);
|
||||||
|
}}
|
||||||
|
{models}
|
||||||
|
bind:chatFiles
|
||||||
|
bind:params
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
</SvelteFlowProvider>
|
||||||
|
@ -1,23 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { settings } from '$lib/stores';
|
import { settings } from '$lib/stores';
|
||||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
import ProfileImageBase from './ProfileImageBase.svelte';
|
||||||
|
|
||||||
export let className = 'size-8';
|
export let className = 'size-8';
|
||||||
|
export let src = '';
|
||||||
export let src = '/user.png';
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={`flex-shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}`}>
|
<div class={`flex-shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}`}>
|
||||||
<img
|
<ProfileImageBase {src} {className} />
|
||||||
crossorigin="anonymous"
|
|
||||||
src={src.startsWith(WEBUI_BASE_URL) ||
|
|
||||||
src.startsWith('https://www.gravatar.com/avatar/') ||
|
|
||||||
src.startsWith('data:') ||
|
|
||||||
src.startsWith('/')
|
|
||||||
? src
|
|
||||||
: `/user.png`}
|
|
||||||
class=" {className} object-cover rounded-full -translate-y-[1px]"
|
|
||||||
alt="profile"
|
|
||||||
draggable="false"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
21
src/lib/components/chat/Messages/ProfileImageBase.svelte
Normal file
21
src/lib/components/chat/Messages/ProfileImageBase.svelte
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
|
export let className = 'size-8';
|
||||||
|
export let src = `${WEBUI_BASE_URL}/static/favicon.png`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<img
|
||||||
|
crossorigin="anonymous"
|
||||||
|
src={src === ''
|
||||||
|
? `${WEBUI_BASE_URL}/static/favicon.png`
|
||||||
|
: src.startsWith(WEBUI_BASE_URL) ||
|
||||||
|
src.startsWith('https://www.gravatar.com/avatar/') ||
|
||||||
|
src.startsWith('data:') ||
|
||||||
|
src.startsWith('/')
|
||||||
|
? src
|
||||||
|
: `/user.png`}
|
||||||
|
class=" {className} object-cover rounded-full -translate-y-[1px]"
|
||||||
|
alt="profile"
|
||||||
|
draggable="false"
|
||||||
|
/>
|
143
src/lib/components/chat/Overview.svelte
Normal file
143
src/lib/components/chat/Overview.svelte
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
<script>
|
||||||
|
import { getContext, createEventDispatcher } from 'svelte';
|
||||||
|
import { useSvelteFlow, useNodesInitialized, useStore } from '@xyflow/svelte';
|
||||||
|
import { SvelteFlow, Controls, Background, BackgroundVariant } 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';
|
||||||
|
|
||||||
|
const { width, height } = useStore();
|
||||||
|
|
||||||
|
const { fitView, getViewport } = useSvelteFlow();
|
||||||
|
const nodesInitialized = useNodesInitialized();
|
||||||
|
|
||||||
|
export let history;
|
||||||
|
|
||||||
|
const nodes = writable([]);
|
||||||
|
const edges = writable([]);
|
||||||
|
|
||||||
|
const nodeTypes = {
|
||||||
|
custom: CustomNode
|
||||||
|
};
|
||||||
|
|
||||||
|
$: if (history) {
|
||||||
|
drawFlow();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 : 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),
|
||||||
|
label: createLabel(history.messages[id].content)
|
||||||
|
},
|
||||||
|
position: { x, y }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create edges
|
||||||
|
const parentId = history.messages[id].parentId;
|
||||||
|
if (parentId) {
|
||||||
|
edgeList.push({
|
||||||
|
id: parentId + '-' + pos.id,
|
||||||
|
source: parentId,
|
||||||
|
target: pos.id,
|
||||||
|
type: 'smoothstep',
|
||||||
|
animated: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await edges.set([...edgeList]);
|
||||||
|
await nodes.set([...nodeList]);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
nodesInitialized.subscribe(async (initialized) => {
|
||||||
|
if (initialized) {
|
||||||
|
await tick();
|
||||||
|
const res = await fitView();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
width.subscribe((value) => {
|
||||||
|
if (value) {
|
||||||
|
fitView();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
height.subscribe((value) => {
|
||||||
|
if (value) {
|
||||||
|
fitView();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full h-full relative">
|
||||||
|
<div class=" absolute z-50 w-full flex justify-between dark:text-gray-100 px-5 py-4">
|
||||||
|
<div class=" text-lg font-medium self-center font-primary">{$i18n.t('Chat Overview')}</div>
|
||||||
|
<button
|
||||||
|
class="self-center"
|
||||||
|
on:click={() => {
|
||||||
|
dispatch('close');
|
||||||
|
showOverview.set(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XMark className="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $nodes.length > 0}
|
||||||
|
<Flow {nodes} {nodeTypes} {edges} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
25
src/lib/components/chat/Overview/Flow.svelte
Normal file
25
src/lib/components/chat/Overview/Flow.svelte
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<script>
|
||||||
|
import { theme } from '$lib/stores';
|
||||||
|
import { Background, Controls, SvelteFlow, BackgroundVariant } from '@xyflow/svelte';
|
||||||
|
|
||||||
|
export let nodes;
|
||||||
|
export let nodeTypes;
|
||||||
|
export let edges;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SvelteFlow
|
||||||
|
{nodes}
|
||||||
|
{nodeTypes}
|
||||||
|
{edges}
|
||||||
|
fitView
|
||||||
|
minZoom={0.001}
|
||||||
|
colorMode={$theme.includes('dark') ? 'dark' : 'light'}
|
||||||
|
nodesDraggable={false}
|
||||||
|
on:nodeclick={(event) => console.log('on node click', event.detail.node)}
|
||||||
|
oninit={() => {
|
||||||
|
console.log('Flow initialized');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Controls showLock={false} />
|
||||||
|
<Background variant={BackgroundVariant.Dots} />
|
||||||
|
</SvelteFlow>
|
40
src/lib/components/chat/Overview/Node.svelte
Normal file
40
src/lib/components/chat/Overview/Node.svelte
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
|
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
|
||||||
|
|
||||||
|
import ProfileImageBase from '../Messages/ProfileImageBase.svelte';
|
||||||
|
|
||||||
|
type $$Props = NodeProps;
|
||||||
|
export let data: $$Props['data'];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="px-4 py-3 shadow-md rounded-xl dark:bg-black bg-white border dark:border-gray-900 w-60 h-20"
|
||||||
|
>
|
||||||
|
{#if data.message.role === 'user'}
|
||||||
|
<div class="flex w-full">
|
||||||
|
<ProfileImageBase
|
||||||
|
src={data.user?.profile_image_url ?? '/user.png'}
|
||||||
|
className={'size-5 -translate-y-[1px]'}
|
||||||
|
/>
|
||||||
|
<div class="ml-2">
|
||||||
|
<div class="text-xs font-medium">{data.user.name}</div>
|
||||||
|
<div class="text-gray-500 line-clamp-2 text-xs mt-0.5">{data.message.content}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex w-full">
|
||||||
|
<ProfileImageBase
|
||||||
|
src={data?.model?.info?.meta?.profile_image_url ?? ''}
|
||||||
|
className={'size-5 -translate-y-[1px]'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="ml-2">
|
||||||
|
<div class="text-xs font-medium">{data.model.name}</div>
|
||||||
|
<div class="text-gray-500 line-clamp-2 text-xs mt-0.5">{data.message.content}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<Handle type="target" position={Position.Top} class="w-2 rounded-full dark:bg-gray-900" />
|
||||||
|
<Handle type="source" position={Position.Bottom} class="w-2 rounded-full dark:bg-gray-900" />
|
||||||
|
</div>
|
@ -3,6 +3,8 @@
|
|||||||
import { flyAndScale } from '$lib/utils/transitions';
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
import { fade, fly, slide } from 'svelte/transition';
|
import { fade, fly, slide } from 'svelte/transition';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
export let size = 'md';
|
export let size = 'md';
|
||||||
|
|
||||||
@ -47,6 +49,10 @@
|
|||||||
document.body.style.overflow = 'unset';
|
document.body.style.overflow = 'unset';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: if (!show) {
|
||||||
|
dispatch('close');
|
||||||
|
}
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
show = false;
|
show = false;
|
||||||
if (modalElement) {
|
if (modalElement) {
|
||||||
|
19
src/lib/components/icons/Clipboard.svelte
Normal file
19
src/lib/components/icons/Clipboard.svelte
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let className = 'size-4';
|
||||||
|
export let strokeWidth = '2';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width={strokeWidth}
|
||||||
|
stroke="currentColor"
|
||||||
|
class={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184"
|
||||||
|
/>
|
||||||
|
</svg>
|
19
src/lib/components/icons/Map.svelte
Normal file
19
src/lib/components/icons/Map.svelte
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let className = 'size-4';
|
||||||
|
export let strokeWidth = '2';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width={strokeWidth}
|
||||||
|
stroke="currentColor"
|
||||||
|
class={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M9 6.75V15m6-6v8.25m.503 3.498 4.875-2.437c.381-.19.622-.58.622-1.006V4.82c0-.836-.88-1.38-1.628-1.006l-3.869 1.934c-.317.159-.69.159-1.006 0L9.503 3.252a1.125 1.125 0 0 0-1.006 0L3.622 5.689C3.24 5.88 3 6.27 3 6.695V19.18c0 .836.88 1.38 1.628 1.006l3.869-1.934c.317-.159.69-.159 1.006 0l4.994 2.497c.317.158.69.158 1.006 0Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
@ -8,7 +8,7 @@
|
|||||||
mobile,
|
mobile,
|
||||||
settings,
|
settings,
|
||||||
showArchivedChats,
|
showArchivedChats,
|
||||||
showSettings,
|
showControls,
|
||||||
showSidebar,
|
showSidebar,
|
||||||
user
|
user
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
@ -22,6 +22,7 @@
|
|||||||
import UserMenu from './Sidebar/UserMenu.svelte';
|
import UserMenu from './Sidebar/UserMenu.svelte';
|
||||||
import MenuLines from '../icons/MenuLines.svelte';
|
import MenuLines from '../icons/MenuLines.svelte';
|
||||||
import AdjustmentsHorizontal from '../icons/AdjustmentsHorizontal.svelte';
|
import AdjustmentsHorizontal from '../icons/AdjustmentsHorizontal.svelte';
|
||||||
|
import Map from '../icons/Map.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@ -31,9 +32,7 @@
|
|||||||
|
|
||||||
export let chat;
|
export let chat;
|
||||||
export let selectedModels;
|
export let selectedModels;
|
||||||
|
|
||||||
export let showModelSelector = true;
|
export let showModelSelector = true;
|
||||||
export let showControls = false;
|
|
||||||
|
|
||||||
let showShareChatModal = false;
|
let showShareChatModal = false;
|
||||||
let showDownloadChatModal = false;
|
let showDownloadChatModal = false;
|
||||||
@ -110,7 +109,7 @@
|
|||||||
<button
|
<button
|
||||||
class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showControls = !showControls;
|
showControls.set(!$showControls);
|
||||||
}}
|
}}
|
||||||
aria-label="Controls"
|
aria-label="Controls"
|
||||||
>
|
>
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
import { downloadChatAsPDF } from '$lib/apis/utils';
|
import { downloadChatAsPDF } from '$lib/apis/utils';
|
||||||
import { copyToClipboard } from '$lib/utils';
|
import { copyToClipboard } from '$lib/utils';
|
||||||
|
|
||||||
import { showSettings } from '$lib/stores';
|
import { showOverview, showControls } from '$lib/stores';
|
||||||
import { flyAndScale } from '$lib/utils/transitions';
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
|
|
||||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||||
@ -128,8 +128,9 @@
|
|||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||||
id="chat-overview-button"
|
id="chat-overview-button"
|
||||||
on:click={() => {
|
on:click={async () => {
|
||||||
shareHandler();
|
await showControls.set(true);
|
||||||
|
await showOverview.set(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Map className=" size-4" strokeWidth="1.5" />
|
<Map className=" size-4" strokeWidth="1.5" />
|
||||||
|
@ -40,6 +40,9 @@ export const showSidebar = writable(false);
|
|||||||
export const showSettings = writable(false);
|
export const showSettings = writable(false);
|
||||||
export const showArchivedChats = writable(false);
|
export const showArchivedChats = writable(false);
|
||||||
export const showChangelog = writable(false);
|
export const showChangelog = writable(false);
|
||||||
|
|
||||||
|
export const showControls = writable(false);
|
||||||
|
export const showOverview = writable(false);
|
||||||
export const showCallOverlay = writable(false);
|
export const showCallOverlay = writable(false);
|
||||||
|
|
||||||
export const temporaryChatEnabled = writable(false);
|
export const temporaryChatEnabled = writable(false);
|
||||||
|
Loading…
Reference in New Issue
Block a user