490 lines
24 KiB
HTML
490 lines
24 KiB
HTML
<div x-data="chatManager()">
|
||
<div
|
||
:class="$store.chatSidebar.collapsed ? 'md:grid-cols-[3.5rem_minmax(0,1fr)]' : 'md:grid-cols-[18rem_minmax(0,1fr)]'"
|
||
class="md:grid transition-[grid-template-columns] duration-300 ease-out"
|
||
>
|
||
<!-- Desktop Sidebar - Flat rail flush with the page. Extends the full
|
||
viewport height; the transparent nav floats above its top region.
|
||
Internal padding-top clears the nav. -->
|
||
<aside
|
||
class="hidden md:flex md:flex-col md:sticky top-0 border-r border-primary-200/60"
|
||
style="height: 100dvh;"
|
||
>
|
||
<!-- Expanded Rail -->
|
||
<template x-if="!$store.chatSidebar.collapsed">
|
||
<div
|
||
class="flex flex-col h-full"
|
||
style="padding-top: var(--nav-h);"
|
||
>
|
||
<div class="px-2 py-3 flex items-center gap-1">
|
||
<button
|
||
@click="$store.chatSidebar.toggleCollapsed()"
|
||
class="p-1.5 rounded-md text-primary-500 hover:bg-primary-200 hover:text-primary-700 transition-colors cursor-pointer flex-shrink-0"
|
||
title="Collapse sidebar"
|
||
aria-label="Collapse sidebar"
|
||
>
|
||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||
</svg>
|
||
</button>
|
||
<button
|
||
@click="selectChat(null)"
|
||
class="flex-1 flex items-center gap-2 px-3 py-1.5 rounded-lg bg-primary-200/60 hover:bg-primary-200 text-primary-800 text-sm font-medium transition-colors cursor-pointer"
|
||
>
|
||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||
</svg>
|
||
<span>New conversation</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="flex-1 overflow-y-auto px-2 pb-4">
|
||
<div
|
||
x-show="chats.length === 0"
|
||
class="h-full flex flex-col justify-center text-center py-8 text-primary-500"
|
||
>
|
||
<p class="text-sm">No chats yet</p>
|
||
</div>
|
||
|
||
<template x-for="group in chatGroups" :key="group.label">
|
||
<div class="mt-2 first:mt-0">
|
||
<div
|
||
class="px-3 pt-3 pb-1 text-[10px] uppercase tracking-wider text-primary-500/70 font-medium"
|
||
x-text="group.label"
|
||
></div>
|
||
<div class="space-y-0.5">
|
||
<template x-for="chat in group.chats" :key="chat.id">
|
||
<div
|
||
@click="selectChat(chat.id)"
|
||
:class="selectedChatID === chat.id ? 'bg-primary-200' : 'hover:bg-primary-200/60'"
|
||
class="group relative px-3 py-2 rounded-lg cursor-pointer transition-colors"
|
||
:title="chat.title || chat.initial_message"
|
||
>
|
||
<div class="flex items-center justify-between gap-2">
|
||
<div
|
||
:class="selectedChatID === chat.id ? 'font-semibold' : 'font-medium'"
|
||
class="text-sm text-primary-900 truncate min-w-0 flex-1"
|
||
x-text="chat.title || chat.initial_message || 'New conversation'"
|
||
></div>
|
||
<button
|
||
@click.stop="deleteChat($event, chat.id)"
|
||
class="opacity-0 group-hover:opacity-100 focus:opacity-100 shrink-0 p-1 -mr-1 text-primary-400 hover:text-tertiary-600 hover:bg-tertiary-100 rounded transition-opacity"
|
||
title="Delete chat"
|
||
aria-label="Delete chat"
|
||
>
|
||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<div
|
||
x-show="chat.title && chat.initial_message"
|
||
class="text-xs text-primary-500/80 truncate mt-0.5"
|
||
x-text="chat.initial_message"
|
||
></div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Collapsed Icon Rail -->
|
||
<template x-if="$store.chatSidebar.collapsed">
|
||
<div
|
||
class="flex flex-col items-center py-3 gap-1"
|
||
style="padding-top: var(--nav-h);"
|
||
>
|
||
<button
|
||
@click="$store.chatSidebar.toggleCollapsed()"
|
||
class="p-2 rounded-md text-primary-500 hover:bg-primary-200 hover:text-primary-700 transition-colors cursor-pointer"
|
||
title="Expand sidebar"
|
||
aria-label="Expand sidebar"
|
||
>
|
||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||
</svg>
|
||
</button>
|
||
<button
|
||
@click="selectChat(null)"
|
||
class="p-2 rounded-md text-primary-700 hover:bg-primary-200 transition-colors cursor-pointer"
|
||
title="New conversation"
|
||
aria-label="New conversation"
|
||
>
|
||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</template>
|
||
</aside>
|
||
|
||
<!-- Main Chat Column - min-h ensures the sticky input pins to the viewport bottom
|
||
even when there are few messages (the column always fills the visible area).
|
||
The reading column (max-w-3xl) is centered within whatever space the grid gives. -->
|
||
<div
|
||
class="flex flex-col min-w-0 px-4 md:px-6"
|
||
style="min-height: 100dvh;"
|
||
>
|
||
<!-- Messages (oldest first, newest last) -->
|
||
<div
|
||
class="flex-1 flex flex-col gap-1 pb-4 w-full"
|
||
style="padding-top: var(--nav-h);"
|
||
>
|
||
<template x-for="message in currentChatMessages" :key="message.id">
|
||
<div :class="['mb-4', message.role === 'user' ? 'text-right' : 'text-left']">
|
||
<div
|
||
:class="['inline-block px-4 py-3 text-left rounded-lg max-w-[95%] md:max-w-[85%]',
|
||
message.role === 'user'
|
||
? 'bg-primary-100 text-primary-900 rounded-br-none'
|
||
: 'bg-primary-200 text-primary-900 rounded-bl-none'
|
||
]"
|
||
>
|
||
<div
|
||
x-show="message.role === 'user' && message.images && message.images.length > 0"
|
||
class="flex gap-1 mb-2 flex-wrap"
|
||
>
|
||
<template x-for="(img, imgIdx) in message.images" :key="imgIdx">
|
||
<img :src="img" class="max-w-full h-auto rounded-lg max-h-48" />
|
||
</template>
|
||
</div>
|
||
|
||
<div
|
||
x-show="message.thinking"
|
||
x-data="{ expanded: false }"
|
||
@click="expanded = !expanded"
|
||
>
|
||
<div class="cursor-pointer rounded-lg overflow-hidden bg-primary-100 hover:bg-primary-50">
|
||
<div class="flex justify-center w-full px-3 py-2 text-xs text-primary-700 flex items-center gap-2 transition-colors">
|
||
<span x-text="expanded ? '▼' : '◀'"></span>
|
||
<span class="font-medium">Reasoning</span>
|
||
<span x-text="expanded ? '▼' : '▶'"></span>
|
||
</div>
|
||
<div
|
||
x-show="expanded"
|
||
class="prose p-4 max-w-none text-sm prose-p:my-1 prose-headings:my-2 prose-ul:my-1 prose-ol:my-1 prose-pre:p-0"
|
||
x-html="renderMarkdown(message.thinking)"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
|
||
<hr x-show="message.thinking" class="my-2 border-primary-400/50" />
|
||
|
||
<div
|
||
x-show="message.role === 'assistant' && message.status === 'streaming' && !message.thinking && !message.content"
|
||
class="flex items-center gap-2 py-1"
|
||
>
|
||
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary-500 border-t-transparent"></div>
|
||
<span class="text-xs text-primary-600">Thinking...</span>
|
||
</div>
|
||
|
||
<div
|
||
x-show="message.content || message.status !== 'streaming'"
|
||
class="prose max-w-none text-sm prose-p:my-1 prose-headings:my-2 prose-ul:my-1 prose-ol:my-1 prose-pre:p-0"
|
||
x-html="renderMarkdown(message.content)"
|
||
></div>
|
||
|
||
<div class="flex items-center justify-between gap-2 mt-2">
|
||
<div class="text-[10px] opacity-60" x-text="new Date(message.created_at).toLocaleTimeString()"></div>
|
||
<div
|
||
x-show="message.role === 'assistant' && ['stopped', 'error', 'failed'].includes(message.status)"
|
||
:class="message.status === 'stopped' ? 'bg-primary-300/50 text-primary-700' : 'bg-tertiary-100 text-tertiary-700'"
|
||
class="px-2 py-0.5 rounded-full text-[10px] font-medium"
|
||
x-text="message.status === 'stopped' ? 'Stopped' : 'Error'"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
x-show="message.role === 'assistant' && message.stats"
|
||
class="flex items-center gap-1 py-2 flex-wrap justify-start text-primary-700"
|
||
>
|
||
<div
|
||
x-show="message.stats?.prompt_tokens || message.stats?.generated_tokens"
|
||
class="group relative px-2 py-0.5 text-[10px] bg-primary-300/50 rounded-full cursor-help"
|
||
>
|
||
<span x-text="`${(message.stats?.prompt_tokens || 0) + (message.stats?.generated_tokens || 0)} tokens`"></span>
|
||
<div class="invisible group-hover:visible absolute bottom-full left-1/2 -translate-x-1/2 m-2 px-2 py-1 bg-gray-900 text-white text-[10px] rounded whitespace-nowrap pointer-events-none grid grid-cols-[auto_1fr] gap-x-2">
|
||
<div x-show="message.stats?.prompt_tokens" x-text="message.stats?.prompt_tokens"></div>
|
||
<div x-show="message.stats?.prompt_tokens">prompt tokens</div>
|
||
<div x-show="message.stats?.generated_tokens" x-text="message.stats?.generated_tokens"></div>
|
||
<div x-show="message.stats?.generated_tokens">generated tokens</div>
|
||
</div>
|
||
</div>
|
||
|
||
<span
|
||
x-show="message.stats?.prompt_per_second"
|
||
class="px-2 py-0.5 text-[10px] bg-primary-300/50 rounded-full"
|
||
x-text="`${message.stats?.prompt_per_second.toFixed(1)} ppt/s`"
|
||
></span>
|
||
<span
|
||
x-show="message.stats?.generated_per_second"
|
||
class="px-2 py-0.5 text-[10px] bg-primary-300/50 rounded-full"
|
||
x-text="`${message.stats?.generated_per_second.toFixed(1)} tgt/s`"
|
||
></span>
|
||
<span
|
||
x-show="message.stats?.time_to_first_token"
|
||
class="px-2 py-0.5 text-[10px] bg-primary-300/50 rounded-full"
|
||
x-text="`${(message.stats?.time_to_first_token / 1000).toFixed(2)}s TTFT`"
|
||
></span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- Sticky Input Bar -->
|
||
<div
|
||
class="sticky bottom-0 pt-2 bg-primary-50"
|
||
style="padding-bottom: max(0.5rem, env(safe-area-inset-bottom));"
|
||
>
|
||
<form
|
||
@submit.prevent="sendMessage"
|
||
class="flex flex-col gap-2 p-3 bg-primary-50 rounded-2xl border border-primary-200"
|
||
>
|
||
<input
|
||
x-ref="fileInput"
|
||
type="file"
|
||
accept="image/png, image/jpeg, image/webp"
|
||
multiple
|
||
class="hidden"
|
||
@change="
|
||
Array.from($event.target.files).forEach(file => {
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => { selectedImages.push(e.target.result); };
|
||
reader.readAsDataURL(file);
|
||
});
|
||
$event.target.value = '';
|
||
"
|
||
/>
|
||
|
||
<div x-show="selectedImages.length > 0" class="flex gap-2 flex-wrap">
|
||
<template x-for="(img, idx) in selectedImages" :key="idx">
|
||
<div class="relative">
|
||
<img :src="img" class="w-20 h-20 object-cover rounded-lg" />
|
||
<button
|
||
type="button"
|
||
@click="selectedImages.splice(idx, 1)"
|
||
class="absolute -top-1 -right-1 w-5 h-5 bg-tertiary-700 text-white rounded-full flex items-center justify-center text-xs hover:bg-tertiary-900"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<textarea
|
||
x-model="inputMessage"
|
||
placeholder="Type your message..."
|
||
rows="2"
|
||
class="scrollbar-hide w-full px-2 py-1 bg-transparent border-0 focus:outline-none focus:ring-0 text-primary-900 text-sm resize-none overflow-y-auto max-h-60"
|
||
@keydown.enter="if (!$event.shiftKey) { $event.preventDefault(); sendMessage(); }"
|
||
@input="$el.style.height = 'auto'; $el.style.height = $el.scrollHeight + 'px'"
|
||
></textarea>
|
||
|
||
<div class="flex items-center gap-2">
|
||
<div class="relative min-w-0 flex-shrink inline-flex items-center pl-7 pr-6 py-1 bg-primary-200/70 hover:bg-primary-300 border border-primary-200 rounded-full transition-colors">
|
||
<span
|
||
class="text-primary-900 text-xs font-medium truncate pointer-events-none"
|
||
x-text="(models.find(m => m.id === selectedModel)?.name) || selectedModel || 'Select Model'"
|
||
></span>
|
||
<select
|
||
x-model="selectedModel"
|
||
class="absolute inset-0 w-full h-full opacity-0 cursor-pointer focus:outline-none"
|
||
aria-label="Select model"
|
||
>
|
||
<option value="">Select Model</option>
|
||
<template x-for="model in models" :key="model.id">
|
||
<option :value="model.id" x-text="model.name || model.id"></option>
|
||
</template>
|
||
</select>
|
||
<svg class="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-primary-500 pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||
</svg>
|
||
<svg class="absolute right-1.5 top-1/2 -translate-y-1/2 h-3 w-3 text-primary-500 pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||
</svg>
|
||
</div>
|
||
|
||
<button
|
||
type="button"
|
||
@click="thinkingEnabled = !thinkingEnabled"
|
||
:class="thinkingEnabled
|
||
? 'bg-primary-600 text-white border-primary-600 hover:bg-primary-700'
|
||
: 'bg-transparent text-primary-500 border-primary-300 hover:bg-primary-200/70'"
|
||
class="relative h-7 w-7 rounded-full border transition-colors flex items-center justify-center flex-shrink-0"
|
||
:title="thinkingEnabled ? 'Thinking on' : 'Thinking off'"
|
||
:aria-pressed="thinkingEnabled ? 'true' : 'false'"
|
||
aria-label="Toggle thinking"
|
||
>
|
||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M12 5a3 3 0 0 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 0 0 12 21z" />
|
||
<path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 0 1 12 21" />
|
||
<path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4" />
|
||
<path d="M17.599 6.5a3 3 0 0 0 .399-1.375" />
|
||
<path d="M6.003 5.125A3 3 0 0 0 6.401 6.5" />
|
||
<path d="M3.477 10.896a4 4 0 0 1 .585-.396" />
|
||
<path d="M19.938 10.5a4 4 0 0 1 .585.396" />
|
||
<path d="M6 18a4 4 0 0 1-1.967-.516" />
|
||
<path d="M19.967 17.484A4 4 0 0 1 18 18" />
|
||
</svg>
|
||
<svg
|
||
x-show="!thinkingEnabled"
|
||
class="absolute inset-0 h-full w-full text-primary-500"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
aria-hidden="true"
|
||
>
|
||
<line x1="5" y1="19" x2="19" y2="5" stroke-width="2" stroke-linecap="round" />
|
||
</svg>
|
||
</button>
|
||
|
||
<button
|
||
type="button"
|
||
@click="$refs.fileInput.click()"
|
||
class="h-7 w-7 bg-primary-200/70 text-primary-700 rounded-full transition-colors flex items-center justify-center flex-shrink-0 hover:bg-primary-300"
|
||
title="Attach Image"
|
||
aria-label="Attach Image"
|
||
>
|
||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
|
||
</svg>
|
||
</button>
|
||
|
||
<div class="ml-auto flex items-center gap-2">
|
||
<button
|
||
x-show="loading"
|
||
type="button"
|
||
@click="stopResponse()"
|
||
:disabled="!activeStreamChatID"
|
||
:class="!activeStreamChatID ? 'opacity-50 cursor-not-allowed' : 'hover:bg-tertiary-700'"
|
||
class="h-7 w-7 bg-tertiary-600 text-white rounded-full transition-colors flex items-center justify-center flex-shrink-0"
|
||
title="Stop response"
|
||
aria-label="Stop response"
|
||
>
|
||
<svg class="h-3 w-3" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path d="M6 6h12v12H6z" />
|
||
</svg>
|
||
</button>
|
||
|
||
<button
|
||
type="submit"
|
||
:disabled="(!inputMessage.trim() && selectedImages.length === 0) || loading"
|
||
:class=" ((!inputMessage.trim() && selectedImages.length === 0) || loading) ? 'opacity-50 cursor-not-allowed' : 'hover:bg-primary-600'"
|
||
class="h-7 w-7 bg-gradient-to-r from-primary-600 to-primary-500 text-white rounded-full transition-all flex items-center justify-center flex-shrink-0"
|
||
title="Send"
|
||
aria-label="Send"
|
||
>
|
||
<template x-if="loading">
|
||
<div class="h-3 w-3 animate-spin rounded-full border-2 border-white border-t-transparent"></div>
|
||
</template>
|
||
<template x-if="!loading">
|
||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
|
||
</svg>
|
||
</template>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
x-show="error"
|
||
class="bg-tertiary-50 border border-tertiary-200 rounded-lg px-4 py-2"
|
||
>
|
||
<p class="text-sm text-tertiary-700" x-text="error"></p>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Mobile Sidebar Drawer (controlled from the top nav hamburger) -->
|
||
<div
|
||
x-show="$store.chatSidebar.mobileOpen"
|
||
x-cloak
|
||
x-transition.opacity
|
||
@click="$store.chatSidebar.mobileOpen = false"
|
||
class="md:hidden fixed inset-0 z-40 bg-black/40"
|
||
></div>
|
||
<div
|
||
x-show="$store.chatSidebar.mobileOpen"
|
||
x-cloak
|
||
x-transition:enter="transform transition-transform duration-300 ease-out"
|
||
x-transition:enter-start="-translate-x-full"
|
||
x-transition:enter-end="translate-x-0"
|
||
x-transition:leave="transform transition-transform duration-300 ease-in"
|
||
x-transition:leave-start="translate-x-0"
|
||
x-transition:leave-end="-translate-x-full"
|
||
class="md:hidden fixed top-0 left-0 bottom-0 z-40 w-80 max-w-[85vw] bg-primary-50 shadow-2xl flex flex-col"
|
||
>
|
||
<div
|
||
class="px-3 pb-3 flex items-center gap-2 border-b border-primary-200/60"
|
||
style="padding-top: var(--nav-h);"
|
||
>
|
||
<button
|
||
@click="selectChat(null); $store.chatSidebar.mobileOpen = false"
|
||
class="flex-1 flex items-center gap-2 px-3 py-1.5 rounded-lg bg-primary-200/60 hover:bg-primary-200 text-primary-800 text-sm font-medium transition-colors cursor-pointer"
|
||
>
|
||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||
</svg>
|
||
<span>New conversation</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="flex-1 overflow-y-auto px-2 pb-4">
|
||
<div
|
||
x-show="chats.length === 0"
|
||
class="h-full flex flex-col justify-center text-center py-8 text-primary-500"
|
||
>
|
||
<p class="text-sm">No chats yet</p>
|
||
</div>
|
||
|
||
<template x-for="group in chatGroups" :key="group.label">
|
||
<div class="mt-2 first:mt-0">
|
||
<div
|
||
class="px-3 pt-3 pb-1 text-[10px] uppercase tracking-wider text-primary-500/70 font-medium"
|
||
x-text="group.label"
|
||
></div>
|
||
<div class="space-y-0.5">
|
||
<template x-for="chat in group.chats" :key="chat.id">
|
||
<div
|
||
@click="selectChat(chat.id); $store.chatSidebar.mobileOpen = false"
|
||
:class="selectedChatID === chat.id ? 'bg-primary-200' : 'hover:bg-primary-200/60'"
|
||
class="group relative px-3 py-2 rounded-lg cursor-pointer transition-colors"
|
||
:title="chat.title || chat.initial_message"
|
||
>
|
||
<div class="flex items-center justify-between gap-2">
|
||
<div
|
||
:class="selectedChatID === chat.id ? 'font-semibold' : 'font-medium'"
|
||
class="text-sm text-primary-900 truncate min-w-0 flex-1"
|
||
x-text="chat.title || chat.initial_message || 'New conversation'"
|
||
></div>
|
||
<button
|
||
@click.stop="deleteChat($event, chat.id)"
|
||
class="shrink-0 p-1 -mr-1 text-primary-400 hover:text-tertiary-600 hover:bg-tertiary-100 rounded transition-colors"
|
||
title="Delete chat"
|
||
aria-label="Delete chat"
|
||
>
|
||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<div
|
||
x-show="chat.title && chat.initial_message"
|
||
class="text-xs text-primary-500/80 truncate mt-0.5"
|
||
x-text="chat.initial_message"
|
||
></div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|