Files
vessel/frontend/src/lib/components/chat/ImagePreview.svelte
vikingowl de835b7af7 feat: initial commit - Ollama WebUI with tools, sync, and backend
Complete Ollama Web UI implementation featuring:

Frontend (SvelteKit + Svelte 5 + Tailwind CSS + Skeleton UI):
- Chat interface with streaming responses and markdown rendering
- Message tree with branching support (edit creates branches)
- Vision model support with image upload/paste
- Code syntax highlighting with Shiki
- Built-in tools: get_current_time, calculate, fetch_url
- Function model middleware (functiongemma) for tool routing
- IndexedDB storage with Dexie.js
- Context window tracking with token estimation
- Knowledge base with embeddings (RAG support)
- Keyboard shortcuts and responsive design
- Export conversations as Markdown/JSON

Backend (Go + Gin + SQLite):
- RESTful API for conversations and messages
- SQLite persistence with branching message tree
- Sync endpoints for IndexedDB ↔ SQLite synchronization
- URL proxy endpoint for CORS-bypassed web fetching
- Health check endpoint
- Docker support with host network mode

Infrastructure:
- Docker Compose for development and production
- Vite proxy configuration for Ollama and backend APIs
- Hot reload development setup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 08:11:33 +01:00

140 lines
4.2 KiB
Svelte

<script lang="ts">
/**
* ImagePreview - Display a base64 image as thumbnail with modal view
* Supports click to expand and remove functionality
*/
import { base64ToDataUrl } from '$lib/ollama/image-processor';
interface Props {
/** Base64 encoded image (without data: prefix) */
src: string;
/** Callback when remove button is clicked */
onRemove?: () => void;
/** Whether the remove button should be shown */
showRemove?: boolean;
/** Alt text for the image */
alt?: string;
}
const { src, onRemove, showRemove = true, alt = 'Image preview' }: Props = $props();
/** Modal open state */
let isModalOpen = $state(false);
/** Get the data URL for display */
const dataUrl = $derived(base64ToDataUrl(src));
/** Open the modal */
function openModal(): void {
isModalOpen = true;
}
/** Close the modal */
function closeModal(): void {
isModalOpen = false;
}
/** Handle keyboard events on modal */
function handleKeydown(event: KeyboardEvent): void {
if (event.key === 'Escape') {
closeModal();
}
}
/** Handle remove button click */
function handleRemove(event: MouseEvent): void {
event.stopPropagation();
onRemove?.();
}
</script>
<!-- Thumbnail container -->
<div class="group relative inline-block">
<!-- Thumbnail image -->
<button
type="button"
onclick={openModal}
class="relative block overflow-hidden rounded-lg border border-gray-300 bg-gray-100 transition-all hover:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:border-gray-600 dark:bg-gray-700"
>
<img
src={dataUrl}
{alt}
class="h-20 w-20 object-cover"
/>
<!-- Overlay on hover -->
<div class="absolute inset-0 flex items-center justify-center bg-black/0 transition-colors group-hover:bg-black/20">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="h-6 w-6 text-white opacity-0 drop-shadow transition-opacity group-hover:opacity-100"
>
<path d="M10 3.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM2 10a8 8 0 1 1 16 0 8 8 0 0 1-16 0Z" />
<path d="M10 6.5a.5.5 0 0 1 .5.5v2.5H13a.5.5 0 0 1 0 1h-2.5V13a.5.5 0 0 1-1 0v-2.5H7a.5.5 0 0 1 0-1h2.5V7a.5.5 0 0 1 .5-.5Z" />
</svg>
</div>
</button>
<!-- Remove button -->
{#if showRemove && onRemove}
<button
type="button"
onclick={handleRemove}
class="absolute -right-2 -top-2 flex h-6 w-6 items-center justify-center rounded-full bg-red-500 text-white opacity-0 shadow-md transition-opacity hover:bg-red-600 focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 group-hover:opacity-100"
aria-label="Remove image"
title="Remove image"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="h-4 w-4"
>
<path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" />
</svg>
</button>
{/if}
</div>
<!-- Full-size modal -->
{#if isModalOpen}
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4"
role="dialog"
tabindex="-1"
aria-modal="true"
aria-label="Image preview"
onclick={closeModal}
onkeydown={handleKeydown}
>
<!-- Close button -->
<button
type="button"
onclick={closeModal}
class="absolute right-4 top-4 flex h-10 w-10 items-center justify-center rounded-full bg-black/50 text-white transition-colors hover:bg-black/70 focus:outline-none focus:ring-2 focus:ring-white"
aria-label="Close preview"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="h-6 w-6"
>
<path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" />
</svg>
</button>
<!-- Full-size image -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<img
src={dataUrl}
{alt}
class="max-h-[90vh] max-w-[90vw] rounded-lg object-contain shadow-2xl"
onclick={(e) => e.stopPropagation()}
onkeydown={(e) => e.stopPropagation()}
/>
</div>
{/if}