浏览代码

Merge branch 'main' into release-notes-modal

Timothy Jaeryang Baek 1 年之前
父节点
当前提交
8fac9de269

+ 49 - 0
.github/workflows/build-release.yml

@@ -0,0 +1,49 @@
+name: Release
+
+on:
+  push:
+    branches:
+      - main # or whatever branch you want to use
+
+jobs:
+  release:
+    runs-on: ubuntu-latest
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v2
+
+    - name: Check for changes in package.json
+      run: |
+        git diff --cached --diff-filter=d package.json || {
+          echo "No changes to package.json"
+          exit 1
+        }
+
+    - name: Get version number from package.json
+      id: get_version
+      run: |
+        VERSION=$(jq -r '.version' package.json)
+        echo "::set-output name=version::$VERSION"
+
+    - name: Create GitHub release
+      uses: actions/github-script@v5
+      with:
+        github-token: ${{ secrets.GITHUB_TOKEN }}
+        script: |
+          const release = await github.rest.repos.createRelease({
+            owner: context.repo.owner,
+            repo: context.repo.repo,
+            tag_name: `v${{ steps.get_version.outputs.version }}`,
+            name: `v${{ steps.get_version.outputs.version }}`,
+            body: 'Automatically created new release',
+          })
+          console.log(`Created release ${release.data.html_url}`)
+
+    - name: Upload package to GitHub release
+      uses: actions/upload-artifact@v3
+      with:
+        name: package
+        path: .
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 1 - 1
README.md

@@ -11,7 +11,7 @@
 [![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s)
 [![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/tjbck)
 
-ChatGPT-Style Web Interface for Ollama 🦙
+User-friendly WebUI for LLMs, Inspired by ChatGPT
 
 ![Open WebUI Demo](./demo.gif)
 

+ 30 - 2
backend/apps/images/main.py

@@ -1,4 +1,4 @@
-import os
+import re
 import requests
 from fastapi import (
     FastAPI,
@@ -34,6 +34,7 @@ app.add_middleware(
 
 app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
 app.state.ENABLED = app.state.AUTOMATIC1111_BASE_URL != ""
+app.state.IMAGE_SIZE = "512x512"
 
 
 @app.get("/enabled", response_model=bool)
@@ -74,6 +75,33 @@ async def update_openai_url(form_data: UrlUpdateForm, user=Depends(get_admin_use
     }
 
 
+class ImageSizeUpdateForm(BaseModel):
+    size: str
+
+
+@app.get("/size")
+async def get_image_size(user=Depends(get_admin_user)):
+    return {"IMAGE_SIZE": app.state.IMAGE_SIZE}
+
+
+@app.post("/size/update")
+async def update_image_size(
+    form_data: ImageSizeUpdateForm, user=Depends(get_admin_user)
+):
+    pattern = r"^\d+x\d+$"  # Regular expression pattern
+    if re.match(pattern, form_data.size):
+        app.state.IMAGE_SIZE = form_data.size
+        return {
+            "IMAGE_SIZE": app.state.IMAGE_SIZE,
+            "status": True,
+        }
+    else:
+        raise HTTPException(
+            status_code=400,
+            detail=ERROR_MESSAGES.INCORRECT_FORMAT("  (e.g., 512x512)."),
+        )
+
+
 @app.get("/models")
 def get_models(user=Depends(get_current_user)):
     try:
@@ -140,7 +168,7 @@ def generate_image(
         if form_data.model:
             set_model_handler(form_data.model)
 
-        width, height = tuple(map(int, form_data.size.split("x")))
+        width, height = tuple(map(int, app.state.IMAGE_SIZE.split("x")))
 
         data = {
             "prompt": form_data.prompt,

+ 3 - 0
backend/constants.py

@@ -44,3 +44,6 @@ class ERROR_MESSAGES(str, Enum):
     MALICIOUS = "Unusual activities detected, please try again in a few minutes."
 
     PANDOC_NOT_INSTALLED = "Pandoc is not installed on the server. Please contact your administrator for assistance."
+    INCORRECT_FORMAT = (
+        lambda err="": f"Invalid format. Please use the correct format{err if err else ''}"
+    )

+ 67 - 0
src/lib/apis/images/index.ts

@@ -131,6 +131,73 @@ export const updateAUTOMATIC1111Url = async (token: string = '', url: string) =>
 	return res.AUTOMATIC1111_BASE_URL;
 };
 
+export const getImageSize = async (token: string = '') => {
+	let error = null;
+
+	const res = await fetch(`${IMAGES_API_BASE_URL}/size`, {
+		method: 'GET',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			...(token && { authorization: `Bearer ${token}` })
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			console.log(err);
+			if ('detail' in err) {
+				error = err.detail;
+			} else {
+				error = 'Server connection failed';
+			}
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res.IMAGE_SIZE;
+};
+
+export const updateImageSize = async (token: string = '', size: string) => {
+	let error = null;
+
+	const res = await fetch(`${IMAGES_API_BASE_URL}/size/update`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			...(token && { authorization: `Bearer ${token}` })
+		},
+		body: JSON.stringify({
+			size: size
+		})
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			console.log(err);
+			if ('detail' in err) {
+				error = err.detail;
+			} else {
+				error = 'Server connection failed';
+			}
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res.IMAGE_SIZE;
+};
+
 export const getDiffusionModels = async (token: string = '') => {
 	let error = null;
 

+ 119 - 87
src/lib/components/chat/Messages.svelte

@@ -222,6 +222,34 @@
 			scrollToBottom();
 		}, 100);
 	};
+
+	// TODO: change delete behaviour
+	// const deleteMessageAndDescendants = async (messageId: string) => {
+	// 	if (history.messages[messageId]) {
+	// 		history.messages[messageId].deleted = true;
+
+	// 		for (const childId of history.messages[messageId].childrenIds) {
+	// 			await deleteMessageAndDescendants(childId);
+	// 		}
+	// 	}
+	// };
+
+	// const triggerDeleteMessageRecursive = async (messageId: string) => {
+	// 	await deleteMessageAndDescendants(messageId);
+	// 	await updateChatById(localStorage.token, chatId, { history });
+	// 	await chats.set(await getChatList(localStorage.token));
+	// };
+
+	const messageDeleteHandler = async (messageId) => {
+		if (history.messages[messageId]) {
+			history.messages[messageId].deleted = true;
+
+			for (const childId of history.messages[messageId].childrenIds) {
+				history.messages[childId].deleted = true;
+			}
+		}
+		await updateChatById(localStorage.token, chatId, { history });
+	};
 </script>
 
 {#if messages.length == 0}
@@ -230,99 +258,103 @@
 	<div class=" pb-10">
 		{#key chatId}
 			{#each messages as message, messageIdx}
-				<div class=" w-full">
-					<div
-						class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
-							? 'max-w-full'
-							: 'max-w-3xl'} mx-auto rounded-lg group"
-					>
-						{#if message.role === 'user'}
-							<UserMessage
-								user={$user}
-								{message}
-								siblings={message.parentId !== null
-									? history.messages[message.parentId]?.childrenIds ?? []
-									: Object.values(history.messages)
-											.filter((message) => message.parentId === null)
-											.map((message) => message.id) ?? []}
-								{confirmEditMessage}
-								{showPreviousMessage}
-								{showNextMessage}
-								{copyToClipboard}
-							/>
-
-							{#if messages.length - 1 === messageIdx && processing !== ''}
-								<div class="flex my-2.5 ml-12 items-center w-fit space-x-2.5">
-									<div class=" dark:text-blue-100">
-										<svg
-											class=" w-4 h-4 translate-y-[0.5px]"
-											fill="currentColor"
-											viewBox="0 0 24 24"
-											xmlns="http://www.w3.org/2000/svg"
-											><style>
-												.spinner_qM83 {
-													animation: spinner_8HQG 1.05s infinite;
-												}
-												.spinner_oXPr {
-													animation-delay: 0.1s;
-												}
-												.spinner_ZTLf {
-													animation-delay: 0.2s;
-												}
-												@keyframes spinner_8HQG {
-													0%,
-													57.14% {
-														animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
-														transform: translate(0);
+				{#if !message.deleted}
+					<div class=" w-full">
+						<div
+							class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
+								? 'max-w-full'
+								: 'max-w-3xl'} mx-auto rounded-lg group"
+						>
+							{#if message.role === 'user'}
+								<UserMessage
+									on:delete={() => messageDeleteHandler(message.id)}
+									user={$user}
+									{message}
+									isFirstMessage={messageIdx === 0}
+									siblings={message.parentId !== null
+										? history.messages[message.parentId]?.childrenIds ?? []
+										: Object.values(history.messages)
+												.filter((message) => message.parentId === null)
+												.map((message) => message.id) ?? []}
+									{confirmEditMessage}
+									{showPreviousMessage}
+									{showNextMessage}
+									{copyToClipboard}
+								/>
+
+								{#if messages.length - 1 === messageIdx && processing !== ''}
+									<div class="flex my-2.5 ml-12 items-center w-fit space-x-2.5">
+										<div class=" dark:text-blue-100">
+											<svg
+												class=" w-4 h-4 translate-y-[0.5px]"
+												fill="currentColor"
+												viewBox="0 0 24 24"
+												xmlns="http://www.w3.org/2000/svg"
+												><style>
+													.spinner_qM83 {
+														animation: spinner_8HQG 1.05s infinite;
 													}
-													28.57% {
-														animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
-														transform: translateY(-6px);
+													.spinner_oXPr {
+														animation-delay: 0.1s;
 													}
-													100% {
-														transform: translate(0);
+													.spinner_ZTLf {
+														animation-delay: 0.2s;
 													}
-												}
-											</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
-												class="spinner_qM83 spinner_oXPr"
-												cx="12"
-												cy="12"
-												r="2.5"
-											/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg
-										>
-									</div>
-									<div class=" text-sm font-medium">
-										{processing}
+													@keyframes spinner_8HQG {
+														0%,
+														57.14% {
+															animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
+															transform: translate(0);
+														}
+														28.57% {
+															animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
+															transform: translateY(-6px);
+														}
+														100% {
+															transform: translate(0);
+														}
+													}
+												</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
+													class="spinner_qM83 spinner_oXPr"
+													cx="12"
+													cy="12"
+													r="2.5"
+												/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg
+											>
+										</div>
+										<div class=" text-sm font-medium">
+											{processing}
+										</div>
 									</div>
-								</div>
+								{/if}
+							{:else}
+								<ResponseMessage
+									{message}
+									modelfiles={selectedModelfiles}
+									siblings={history.messages[message.parentId]?.childrenIds ?? []}
+									isLastMessage={messageIdx + 1 === messages.length}
+									{confirmEditResponseMessage}
+									{showPreviousMessage}
+									{showNextMessage}
+									{rateMessage}
+									{copyToClipboard}
+									{continueGeneration}
+									{regenerateResponse}
+									on:save={async (e) => {
+										console.log('save', e);
+
+										const message = e.detail;
+										history.messages[message.id] = message;
+										await updateChatById(localStorage.token, chatId, {
+											messages: messages,
+											history: history
+										});
+									}}
+								/>
 							{/if}
-						{:else}
-							<ResponseMessage
-								{message}
-								modelfiles={selectedModelfiles}
-								siblings={history.messages[message.parentId]?.childrenIds ?? []}
-								isLastMessage={messageIdx + 1 === messages.length}
-								{confirmEditResponseMessage}
-								{showPreviousMessage}
-								{showNextMessage}
-								{rateMessage}
-								{copyToClipboard}
-								{continueGeneration}
-								{regenerateResponse}
-								on:save={async (e) => {
-									console.log('save', e);
-
-									const message = e.detail;
-									history.messages[message.id] = message;
-									await updateChatById(localStorage.token, chatId, {
-										messages: messages,
-										history: history
-									});
-								}}
-							/>
-						{/if}
+						</div>
 					</div>
-				</div>
+				{/if}
 			{/each}
 
 			{#if bottomPadding}

+ 2 - 2
src/lib/components/chat/Messages/ResponseMessage.svelte

@@ -20,6 +20,7 @@
 	import ProfileImage from './ProfileImage.svelte';
 	import Skeleton from './Skeleton.svelte';
 	import CodeBlock from './CodeBlock.svelte';
+	import Image from '$lib/components/common/Image.svelte';
 
 	export let modelfiles = [];
 	export let message;
@@ -46,7 +47,6 @@
 	let speakingIdx = null;
 
 	let loadingSpeech = false;
-
 	let generatingImage = false;
 
 	$: tokens = marked.lexer(message.content);
@@ -323,7 +323,7 @@
 						{#each message.files as file}
 							<div>
 								{#if file.type === 'image'}
-									<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
+									<Image src={file.url} />
 								{/if}
 							</div>
 						{/each}

+ 38 - 7
src/lib/components/chat/Messages/UserMessage.svelte

@@ -1,14 +1,17 @@
 <script lang="ts">
 	import dayjs from 'dayjs';
 
-	import { tick } from 'svelte';
+	import { tick, createEventDispatcher } from 'svelte';
 	import Name from './Name.svelte';
 	import ProfileImage from './ProfileImage.svelte';
 	import { modelfiles, settings } from '$lib/stores';
 
+	const dispatch = createEventDispatcher();
+
 	export let user;
 	export let message;
 	export let siblings;
+	export let isFirstMessage: boolean;
 
 	export let confirmEditMessage: Function;
 	export let showPreviousMessage: Function;
@@ -42,6 +45,10 @@
 		edit = false;
 		editedContent = '';
 	};
+
+	const deleteMessageHandler = async () => {
+		dispatch('delete', message.id);
+	};
 </script>
 
 <div class=" flex w-full">
@@ -189,11 +196,11 @@
 				<div class="w-full">
 					<pre id="user-message">{message.content}</pre>
 
-					<div class=" flex justify-start space-x-1">
+					<div class=" flex justify-start space-x-1 text-gray-700 dark:text-gray-500">
 						{#if siblings.length > 1}
 							<div class="flex self-center">
 								<button
-									class="self-center"
+									class="self-center dark:hover:text-white hover:text-black transition"
 									on:click={() => {
 										showPreviousMessage(message);
 									}}
@@ -212,12 +219,12 @@
 									</svg>
 								</button>
 
-								<div class="text-xs font-bold self-center">
+								<div class="text-xs font-bold self-center dark:text-gray-100">
 									{siblings.indexOf(message.id) + 1} / {siblings.length}
 								</div>
 
 								<button
-									class="self-center"
+									class="self-center dark:hover:text-white hover:text-black transition"
 									on:click={() => {
 										showNextMessage(message);
 									}}
@@ -239,7 +246,7 @@
 						{/if}
 
 						<button
-							class="invisible group-hover:visible p-1 rounded dark:hover:text-white transition edit-user-message-button"
+							class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
 							on:click={() => {
 								editMessageHandler();
 							}}
@@ -261,7 +268,7 @@
 						</button>
 
 						<button
-							class="invisible group-hover:visible p-1 rounded dark:hover:text-white transition"
+							class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
 							on:click={() => {
 								copyToClipboard(message.content);
 							}}
@@ -281,6 +288,30 @@
 								/>
 							</svg>
 						</button>
+
+						{#if !isFirstMessage}
+							<button
+								class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
+								on:click={() => {
+									deleteMessageHandler();
+								}}
+							>
+								<svg
+									xmlns="http://www.w3.org/2000/svg"
+									fill="none"
+									viewBox="0 0 24 24"
+									stroke-width="1.5"
+									stroke="currentColor"
+									class="w-4 h-4"
+								>
+									<path
+										stroke-linecap="round"
+										stroke-linejoin="round"
+										d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
+									/>
+								</svg>
+							</button>
+						{/if}
 					</div>
 				</div>
 			{/if}

+ 8 - 1
src/lib/components/chat/Settings/About.svelte

@@ -22,7 +22,7 @@
 			</div>
 			<div class="flex w-full">
 				<div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
-					{WEB_UI_VERSION}
+					v{WEB_UI_VERSION}
 
 					<button
 						class="mt-1 underline flex items-center space-x-1 text-xs text-gray-600 dark:text-gray-400"
@@ -57,6 +57,13 @@
 				/>
 			</a>
 
+			<a href="https://twitter.com/OpenWebUI" target="_blank">
+				<img
+					alt="X (formerly Twitter) Follow"
+					src="https://img.shields.io/twitter/follow/OpenWebUI"
+				/>
+			</a>
+
 			<a href="https://github.com/open-webui/open-webui" target="_blank">
 				<img
 					alt="Github Repo"

+ 26 - 5
src/lib/components/chat/Settings/Images.svelte

@@ -8,9 +8,11 @@
 		getDefaultDiffusionModel,
 		getDiffusionModels,
 		getImageGenerationEnabledStatus,
+		getImageSize,
 		toggleImageGenerationEnabledStatus,
 		updateAUTOMATIC1111Url,
-		updateDefaultDiffusionModel
+		updateDefaultDiffusionModel,
+		updateImageSize
 	} from '$lib/apis/images';
 	import { getBackendConfig } from '$lib/apis';
 	const dispatch = createEventDispatcher();
@@ -25,6 +27,8 @@
 	let selectedModel = '';
 	let models = [];
 
+	let imageSize = '';
+
 	const getModels = async () => {
 		models = await getDiffusionModels(localStorage.token).catch((error) => {
 			toast.error(error);
@@ -53,7 +57,6 @@
 			AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(localStorage.token);
 		}
 	};
-
 	const toggleImageGeneration = async () => {
 		if (AUTOMATIC1111_BASE_URL) {
 			enableImageGeneration = await toggleImageGenerationEnabledStatus(localStorage.token).catch(
@@ -79,6 +82,7 @@
 			AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(localStorage.token);
 
 			if (enableImageGeneration && AUTOMATIC1111_BASE_URL) {
+				imageSize = await getImageSize(localStorage.token);
 				getModels();
 			}
 		}
@@ -89,13 +93,17 @@
 	class="flex flex-col h-full justify-between space-y-3 text-sm"
 	on:submit|preventDefault={async () => {
 		loading = true;
-		const res = await updateDefaultDiffusionModel(localStorage.token, selectedModel);
+		await updateDefaultDiffusionModel(localStorage.token, selectedModel);
+		await updateImageSize(localStorage.token, imageSize).catch((error) => {
+			toast.error(error);
+			return null;
+		});
 
 		dispatch('save');
 		loading = false;
 	}}
 >
-	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
+	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[21rem]">
 		<div>
 			<div class=" mb-1 text-sm font-medium">Image Settings</div>
 
@@ -169,7 +177,7 @@
 			<hr class=" dark:border-gray-700" />
 
 			<div>
-				<div class=" mb-2.5 text-sm font-medium">Set default model</div>
+				<div class=" mb-2.5 text-sm font-medium">Set Default Model</div>
 				<div class="flex w-full">
 					<div class="flex-1 mr-2">
 						<select
@@ -189,6 +197,19 @@
 					</div>
 				</div>
 			</div>
+
+			<div>
+				<div class=" mb-2.5 text-sm font-medium">Set Image Size</div>
+				<div class="flex w-full">
+					<div class="flex-1 mr-2">
+						<input
+							class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
+							placeholder="Enter Image Size (e.g. 512x512)"
+							bind:value={imageSize}
+						/>
+					</div>
+				</div>
+			</div>
 		{/if}
 	</div>
 

+ 18 - 0
src/lib/components/common/Image.svelte

@@ -0,0 +1,18 @@
+<script lang="ts">
+	import ImagePreview from './ImagePreview.svelte';
+
+	export let src = '';
+	export let alt = '';
+
+	let showImagePreview = false;
+</script>
+
+<ImagePreview bind:show={showImagePreview} {src} {alt} />
+<button
+	on:click={() => {
+		console.log('image preview');
+		showImagePreview = true;
+	}}
+>
+	<img {src} {alt} class=" max-h-96 rounded-lg" draggable="false" />
+</button>

+ 62 - 0
src/lib/components/common/ImagePreview.svelte

@@ -0,0 +1,62 @@
+<script lang="ts">
+	export let show = false;
+	export let src = '';
+	export let alt = '';
+</script>
+
+{#if show}
+	<!-- svelte-ignore a11y-click-events-have-key-events -->
+	<!-- svelte-ignore a11y-no-static-element-interactions -->
+	<div
+		class="fixed top-0 right-0 left-0 bottom-0 bg-black text-white w-full min-h-screen h-screen flex justify-center z-50 overflow-hidden overscroll-contain"
+	>
+		<div class=" absolute left-0 w-full flex justify-between">
+			<div>
+				<button
+					class=" p-5"
+					on:click={() => {
+						show = false;
+					}}
+				>
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						fill="none"
+						viewBox="0 0 24 24"
+						stroke-width="2"
+						stroke="currentColor"
+						class="w-6 h-6"
+					>
+						<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
+					</svg>
+				</button>
+			</div>
+
+			<div>
+				<button
+					class=" p-5"
+					on:click={() => {
+						const a = document.createElement('a');
+						a.href = src;
+						a.download = 'Image.png';
+						a.click();
+					}}
+				>
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						viewBox="0 0 20 20"
+						fill="currentColor"
+						class="w-6 h-6"
+					>
+						<path
+							d="M10.75 2.75a.75.75 0 0 0-1.5 0v8.614L6.295 8.235a.75.75 0 1 0-1.09 1.03l4.25 4.5a.75.75 0 0 0 1.09 0l4.25-4.5a.75.75 0 0 0-1.09-1.03l-2.955 3.129V2.75Z"
+						/>
+						<path
+							d="M3.5 12.75a.75.75 0 0 0-1.5 0v2.5A2.75 2.75 0 0 0 4.75 18h10.5A2.75 2.75 0 0 0 18 15.25v-2.5a.75.75 0 0 0-1.5 0v2.5c0 .69-.56 1.25-1.25 1.25H4.75c-.69 0-1.25-.56-1.25-1.25v-2.5Z"
+						/>
+					</svg>
+				</button>
+			</div>
+		</div>
+		<img {src} {alt} class=" mx-auto h-full object-scale-down" />
+	</div>
+{/if}

+ 2 - 2
src/routes/(app)/+page.svelte

@@ -334,7 +334,7 @@
 						content: $settings.system
 				  }
 				: undefined,
-			...messages
+			...messages.filter(message => !message.deleted)
 		]
 			.filter((message) => message)
 			.map((message, idx, arr) => ({
@@ -540,7 +540,7 @@
 							content: $settings.system
 					  }
 					: undefined,
-				...messages
+			...messages.filter(message => !message.deleted)
 			]
 				.filter((message) => message)
 				.map((message, idx, arr) => ({

+ 2 - 2
src/routes/(app)/c/[id]/+page.svelte

@@ -348,7 +348,7 @@
 						content: $settings.system
 				  }
 				: undefined,
-			...messages
+			...messages.filter((message) => !message.deleted)
 		]
 			.filter((message) => message)
 			.map((message, idx, arr) => ({
@@ -555,7 +555,7 @@
 							content: $settings.system
 					  }
 					: undefined,
-				...messages
+				...messages.filter((message) => !message.deleted)
 			]
 				.filter((message) => message)
 				.map((message, idx, arr) => ({