Timothy Jaeryang Baek 4 месяцев назад
Родитель
Сommit
b4536a691a

+ 2 - 2
src/lib/components/chat/Chat.svelte

@@ -300,7 +300,7 @@
 		}
 		}
 	};
 	};
 
 
-	const showMessage = async (message) => {
+	const showMessage = async (message, ignoreSettings = false) => {
 		await tick();
 		await tick();
 
 
 		const _chatId = JSON.parse(JSON.stringify($chatId));
 		const _chatId = JSON.parse(JSON.stringify($chatId));
@@ -326,7 +326,7 @@
 		await tick();
 		await tick();
 		await tick();
 		await tick();
 
 
-		if ($settings?.scrollOnBranchChange ?? true) {
+		if (($settings?.scrollOnBranchChange ?? true) || ignoreSettings) {
 			const messageElement = document.getElementById(`message-${message.id}`);
 			const messageElement = document.getElementById(`message-${message.id}`);
 			if (messageElement) {
 			if (messageElement) {
 				messageElement.scrollIntoView({ behavior: 'smooth' });
 				messageElement.scrollIntoView({ behavior: 'smooth' });

+ 126 - 125
src/lib/components/chat/ChatControls.svelte

@@ -13,12 +13,9 @@
 		showEmbeds
 		showEmbeds
 	} from '$lib/stores';
 	} from '$lib/stores';
 
 
-	import Modal from '../common/Modal.svelte';
 	import Controls from './Controls/Controls.svelte';
 	import Controls from './Controls/Controls.svelte';
 	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';
-	import EllipsisVertical from '../icons/EllipsisVertical.svelte';
 	import Artifacts from './Artifacts.svelte';
 	import Artifacts from './Artifacts.svelte';
 	import Embeds from './ChatControls/Embeds.svelte';
 	import Embeds from './ChatControls/Embeds.svelte';
 
 
@@ -154,24 +151,113 @@
 	}
 	}
 </script>
 </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
 				<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}
 					{#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
 							<CallOverlay
 								bind:files
 								bind:files
 								{submitPrompt}
 								{submitPrompt}
@@ -185,19 +271,28 @@
 							/>
 							/>
 						</div>
 						</div>
 					{:else if $showEmbeds}
 					{:else if $showEmbeds}
-						<Embeds />
+						<Embeds overlay={dragged} />
 					{:else if $showArtifacts}
 					{:else if $showArtifacts}
-						<Artifacts {history} />
+						<Artifacts {history} overlay={dragged} />
 					{:else if $showOverview}
 					{: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}
 					{:else}
 						<Controls
 						<Controls
 							on:close={() => {
 							on:close={() => {
@@ -209,101 +304,7 @@
 						/>
 						/>
 					{/if}
 					{/if}
 				</div>
 				</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>
+			</div>
 		{/if}
 		{/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}

+ 7 - 196
src/lib/components/chat/Overview.svelte

@@ -1,206 +1,17 @@
 <script lang="ts">
 <script lang="ts">
 	import { getContext, createEventDispatcher, onDestroy } from 'svelte';
 	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 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;
 	export let history;
 
 
-	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([]);
-	});
+	export let onClose;
+	export let onNodeClick;
 </script>
 </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}
-			{setLayoutDirection}
-			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>

+ 207 - 0
src/lib/components/chat/Overview/View.svelte

@@ -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>