ChatSidebar.svelte 6.0 KB

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