1<script lang="ts">
2 import { goto } from '$app/navigation';
3 import { page } from '$app/state';
4 import { Trash2 } from '@lucide/svelte';
5 import { ChatSidebarConversationItem, DialogConfirmation } from '$lib/components/app';
6 import ScrollArea from '$lib/components/ui/scroll-area/scroll-area.svelte';
7 import * as Sidebar from '$lib/components/ui/sidebar';
8 import * as AlertDialog from '$lib/components/ui/alert-dialog';
9 import Input from '$lib/components/ui/input/input.svelte';
10 import { conversationsStore, conversations } from '$lib/stores/conversations.svelte';
11 import { chatStore } from '$lib/stores/chat.svelte';
12 import { getPreviewText } from '$lib/utils/text';
13 import ChatSidebarActions from './ChatSidebarActions.svelte';
14
15 const sidebar = Sidebar.useSidebar();
16
17 let currentChatId = $derived(page.params.id);
18 let isSearchModeActive = $state(false);
19 let searchQuery = $state('');
20 let showDeleteDialog = $state(false);
21 let showEditDialog = $state(false);
22 let selectedConversation = $state<DatabaseConversation | null>(null);
23 let editedName = $state('');
24 let selectedConversationNamePreview = $derived.by(() =>
25 selectedConversation ? getPreviewText(selectedConversation.name) : ''
26 );
27
28 let filteredConversations = $derived.by(() => {
29 if (searchQuery.trim().length > 0) {
30 return conversations().filter((conversation: { name: string }) =>
31 conversation.name.toLowerCase().includes(searchQuery.toLowerCase())
32 );
33 }
34
35 return conversations();
36 });
37
38 async function handleDeleteConversation(id: string) {
39 const conversation = conversations().find((conv) => conv.id === id);
40 if (conversation) {
41 selectedConversation = conversation;
42 showDeleteDialog = true;
43 }
44 }
45
46 async function handleEditConversation(id: string) {
47 const conversation = conversations().find((conv) => conv.id === id);
48 if (conversation) {
49 selectedConversation = conversation;
50 editedName = conversation.name;
51 showEditDialog = true;
52 }
53 }
54
55 function handleConfirmDelete() {
56 if (selectedConversation) {
57 showDeleteDialog = false;
58
59 setTimeout(() => {
60 conversationsStore.deleteConversation(selectedConversation.id);
61 selectedConversation = null;
62 }, 100); // Wait for animation to finish
63 }
64 }
65
66 function handleConfirmEdit() {
67 if (!editedName.trim() || !selectedConversation) return;
68
69 showEditDialog = false;
70
71 conversationsStore.updateConversationName(selectedConversation.id, editedName);
72 selectedConversation = null;
73 }
74
75 export function handleMobileSidebarItemClick() {
76 if (sidebar.isMobile) {
77 sidebar.toggle();
78 }
79 }
80
81 export function activateSearchMode() {
82 isSearchModeActive = true;
83 }
84
85 export function editActiveConversation() {
86 if (currentChatId) {
87 const activeConversation = filteredConversations.find((conv) => conv.id === currentChatId);
88
89 if (activeConversation) {
90 const event = new CustomEvent('edit-active-conversation', {
91 detail: { conversationId: currentChatId }
92 });
93 document.dispatchEvent(event);
94 }
95 }
96 }
97
98 async function selectConversation(id: string) {
99 if (isSearchModeActive) {
100 isSearchModeActive = false;
101 searchQuery = '';
102 }
103
104 await goto(`#/chat/${id}`);
105 }
106
107 function handleStopGeneration(id: string) {
108 chatStore.stopGenerationForChat(id);
109 }
110</script>
111
112<ScrollArea class="h-[100vh]">
113 <Sidebar.Header class=" top-0 z-10 gap-6 bg-sidebar/50 px-4 py-4 pb-2 backdrop-blur-lg md:sticky">
114 <a href="#/" onclick={handleMobileSidebarItemClick}>
115 <h1 class="inline-flex items-center gap-1 px-2 text-xl font-semibold">llama.cpp</h1>
116 </a>
117
118 <ChatSidebarActions {handleMobileSidebarItemClick} bind:isSearchModeActive bind:searchQuery />
119 </Sidebar.Header>
120
121 <Sidebar.Group class="mt-4 space-y-2 p-0 px-4">
122 {#if (filteredConversations.length > 0 && isSearchModeActive) || !isSearchModeActive}
123 <Sidebar.GroupLabel>
124 {isSearchModeActive ? 'Search results' : 'Conversations'}
125 </Sidebar.GroupLabel>
126 {/if}
127
128 <Sidebar.GroupContent>
129 <Sidebar.Menu>
130 {#each filteredConversations as conversation (conversation.id)}
131 <Sidebar.MenuItem class="mb-1">
132 <ChatSidebarConversationItem
133 conversation={{
134 id: conversation.id,
135 name: conversation.name,
136 lastModified: conversation.lastModified,
137 currNode: conversation.currNode
138 }}
139 {handleMobileSidebarItemClick}
140 isActive={currentChatId === conversation.id}
141 onSelect={selectConversation}
142 onEdit={handleEditConversation}
143 onDelete={handleDeleteConversation}
144 onStop={handleStopGeneration}
145 />
146 </Sidebar.MenuItem>
147 {/each}
148
149 {#if filteredConversations.length === 0}
150 <div class="px-2 py-4 text-center">
151 <p class="mb-4 p-4 text-sm text-muted-foreground">
152 {searchQuery.length > 0
153 ? 'No results found'
154 : isSearchModeActive
155 ? 'Start typing to see results'
156 : 'No conversations yet'}
157 </p>
158 </div>
159 {/if}
160 </Sidebar.Menu>
161 </Sidebar.GroupContent>
162 </Sidebar.Group>
163</ScrollArea>
164
165<DialogConfirmation
166 bind:open={showDeleteDialog}
167 title="Delete Conversation"
168 description={selectedConversation
169 ? `Are you sure you want to delete "${selectedConversationNamePreview}"? This action cannot be undone and will permanently remove all messages in this conversation.`
170 : ''}
171 confirmText="Delete"
172 cancelText="Cancel"
173 variant="destructive"
174 icon={Trash2}
175 onConfirm={handleConfirmDelete}
176 onCancel={() => {
177 showDeleteDialog = false;
178 selectedConversation = null;
179 }}
180/>
181
182<AlertDialog.Root bind:open={showEditDialog}>
183 <AlertDialog.Content>
184 <AlertDialog.Header>
185 <AlertDialog.Title>Edit Conversation Name</AlertDialog.Title>
186 <AlertDialog.Description>
187 <Input
188 class="mt-4 text-foreground"
189 onkeydown={(e) => {
190 if (e.key === 'Enter') {
191 e.preventDefault();
192 handleConfirmEdit();
193 }
194 }}
195 placeholder="Enter a new name"
196 type="text"
197 bind:value={editedName}
198 />
199 </AlertDialog.Description>
200 </AlertDialog.Header>
201 <AlertDialog.Footer>
202 <AlertDialog.Cancel
203 onclick={() => {
204 showEditDialog = false;
205 selectedConversation = null;
206 }}>Cancel</AlertDialog.Cancel
207 >
208 <AlertDialog.Action onclick={handleConfirmEdit}>Save</AlertDialog.Action>
209 </AlertDialog.Footer>
210 </AlertDialog.Content>
211</AlertDialog.Root>