| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent d73cd07 commit 76da245
5 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,7 +1,7 @@ | |||
| 1 | 1 | <script lang="ts"> | |
| 2 | 2 | import { goto } from '$app/navigation'; | |
| 3 | 3 | import { page } from '$app/state'; | |
| 4 | - import { Trash2, Pencil, X } from '@lucide/svelte'; | ||
| 4 | + import { Trash2, Pencil, Pin, X } from '@lucide/svelte'; | ||
| 5 | 5 | import { Button } from '$lib/components/ui/button'; | |
| 6 | 6 | import { DialogConfirmation } from '$lib/components/app'; | |
| 7 | 7 | import SidebarNavigationActions from './SidebarNavigationActions.svelte'; | |
@@ -52,6 +52,14 @@ | |||
| 52 | 52 | ||
| 53 | 53 | let conversationTree = $derived(buildConversationTree(filteredConversations)); | |
| 54 | 54 | ||
| 55 | + let pinnedConversations = $derived.by(() => { | ||
| 56 | + return conversationTree.filter(({ conversation }) => conversation.pinned); | ||
| 57 | + }); | ||
| 58 | + | ||
| 59 | + let unpinnedConversations = $derived.by(() => { | ||
| 60 | + return conversationTree.filter(({ conversation }) => !conversation.pinned); | ||
| 61 | + }); | ||
| 62 | + | ||
| 55 | 63 | let selectedConversationHasDescendants = $derived.by(() => { | |
| 56 | 64 | if (!selectedConversation) return false; | |
| 57 | 65 | ||
@@ -199,6 +207,41 @@ | |||
| 199 | 207 | /> | |
| 200 | 208 | </Sidebar.Header> | |
| 201 | 209 | ||
| 210 | + {#if !isSearchModeActive && pinnedConversations.length > 0} | ||
| 211 | + <Sidebar.Group class="p-0 px-4"> | ||
| 212 | + <Sidebar.GroupLabel> | ||
| 213 | + <div class="flex items-center gap-1"> | ||
| 214 | + <Pin class="h-3.5 w-3.5" /> | ||
| 215 | + <span>Pinned</span> | ||
| 216 | + </div> | ||
| 217 | + </Sidebar.GroupLabel> | ||
| 218 | + <Sidebar.GroupContent> | ||
| 219 | + <Sidebar.Menu> | ||
| 220 | + {#each pinnedConversations as { conversation, depth } (conversation.id)} | ||
| 221 | + <Sidebar.MenuItem class="mb-1 p-0"> | ||
| 222 | + <SidebarNavigationConversationItem | ||
| 223 | + conversation={{ | ||
| 224 | + id: conversation.id, | ||
| 225 | + name: conversation.name, | ||
| 226 | + lastModified: conversation.lastModified, | ||
| 227 | + currNode: conversation.currNode, | ||
| 228 | + forkedFromConversationId: conversation.forkedFromConversationId, | ||
| 229 | + pinned: conversation.pinned | ||
| 230 | + }} | ||
| 231 | + {depth} | ||
| 232 | + isActive={currentChatId === conversation.id} | ||
| 233 | + onSelect={selectConversation} | ||
| 234 | + onEdit={handleEditConversation} | ||
| 235 | + onDelete={handleDeleteConversation} | ||
| 236 | + onStop={handleStopGeneration} | ||
| 237 | + /> | ||
| 238 | + </Sidebar.MenuItem> | ||
| 239 | + {/each} | ||
| 240 | + </Sidebar.Menu> | ||
| 241 | + </Sidebar.GroupContent> | ||
| 242 | + </Sidebar.Group> | ||
| 243 | + {/if} | ||
| 244 | + | ||
| 202 | 245 | <Sidebar.Group class="mt-2 h-[calc(100vh-21rem)] space-y-2 p-0 px-3"> | |
| 203 | 246 | {#if (filteredConversations.length > 0 && isSearchModeActive) || !isSearchModeActive} | |
| 204 | 247 | <Sidebar.GroupLabel> | |
@@ -208,15 +251,16 @@ | |||
| 208 | 251 | ||
| 209 | 252 | <Sidebar.GroupContent> | |
| 210 | 253 | <Sidebar.Menu> | |
| 211 | - {#each conversationTree as { conversation, depth } (conversation.id)} | ||
| 254 | + {#each isSearchModeActive ? conversationTree : unpinnedConversations as { conversation, depth } (conversation.id)} | ||
| 212 | 255 | <Sidebar.MenuItem class="mb-1 p-0"> | |
| 213 | 256 | <SidebarNavigationConversationItem | |
| 214 | 257 | conversation={{ | |
| 215 | 258 | id: conversation.id, | |
| 216 | 259 | name: conversation.name, | |
| 217 | 260 | lastModified: conversation.lastModified, | |
| 218 | 261 | currNode: conversation.currNode, | |
| 219 | - forkedFromConversationId: conversation.forkedFromConversationId | ||
| 262 | + forkedFromConversationId: conversation.forkedFromConversationId, | ||
| 263 | + pinned: conversation.pinned | ||
| 220 | 264 | }} | |
| 221 | 265 | {depth} | |
| 222 | 266 | isActive={currentChatId === conversation.id} | |
@@ -228,7 +272,7 @@ | |||
| 228 | 272 | </Sidebar.MenuItem> | |
| 229 | 273 | {/each} | |
| 230 | 274 | ||
| 231 | - {#if conversationTree.length === 0} | ||
| 275 | + {#if (isSearchModeActive ? conversationTree : unpinnedConversations).length === 0} | ||
| 232 | 276 | <div class="px-2 py-4 text-center"> | |
| 233 | 277 | <p class="mb-4 p-4 text-sm text-muted-foreground"> | |
| 234 | 278 | {searchQuery.length > 0 | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -6,7 +6,9 @@ | |||
| 6 | 6 | Download, | |
| 7 | 7 | Loader2, | |
| 8 | 8 | Square, | |
| 9 | - GitBranch | ||
| 9 | + GitBranch, | ||
| 10 | + Pin, | ||
| 11 | + PinOff | ||
| 10 | 12 | } from '@lucide/svelte'; | |
| 11 | 13 | import { DropdownMenuActions } from '$lib/components/app'; | |
| 12 | 14 | import * as Tooltip from '$lib/components/ui/tooltip'; | |
@@ -57,6 +59,10 @@ | |||
| 57 | 59 | onStop?.(conversation.id); | |
| 58 | 60 | } | |
| 59 | 61 | ||
| 62 | + function handleTogglePin() { | ||
| 63 | + conversationsStore.toggleConversationPin(conversation.id); | ||
| 64 | + } | ||
| 65 | + | ||
| 60 | 66 | function handleGlobalEditEvent(event: Event) { | |
| 61 | 67 | const customEvent = event as CustomEvent<{ conversationId: string }>; | |
| 62 | 68 | ||
@@ -170,6 +176,14 @@ | |||
| 170 | 176 | triggerTooltip="More actions" | |
| 171 | 177 | bind:open={dropdownOpen} | |
| 172 | 178 | actions={[ | |
| 179 | + { | ||
| 180 | + icon: conversation.pinned ? PinOff : Pin, | ||
| 181 | + label: conversation.pinned ? 'Unpin' : 'Pin', | ||
| 182 | + onclick: (e: Event) => { | ||
| 183 | + e.stopPropagation(); | ||
| 184 | + handleTogglePin(); | ||
| 185 | + } | ||
| 186 | + }, | ||
| 173 | 187 | { | |
| 174 | 188 | icon: Pencil, | |
| 175 | 189 | label: 'Edit', | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -344,6 +344,22 @@ export class DatabaseService { | |||
| 344 | 344 | * | |
| 345 | 345 | */ | |
| 346 | 346 | ||
| 347 | + /** | ||
| 348 | + * Toggles the pinned status of a conversation. | ||
| 349 | + * | ||
| 350 | + * @param id - Conversation ID | ||
| 351 | + * @returns The new pinned status | ||
| 352 | + */ | ||
| 353 | + static async toggleConversationPin(id: string): Promise<boolean> { | ||
| 354 | + const conversation = await db.conversations.get(id); | ||
| 355 | + if (!conversation) { | ||
| 356 | + throw new Error(`Conversation ${id} not found`); | ||
| 357 | + } | ||
| 358 | + const newPinnedState = !conversation.pinned; | ||
| 359 | + await this.updateConversation(id, { pinned: newPinnedState }); | ||
| 360 | + return newPinnedState; | ||
| 361 | + } | ||
| 362 | + | ||
| 347 | 363 | /** | |
| 348 | 364 | * Updates the conversation's current node (active branch). | |
| 349 | 365 | * This determines which conversation path is currently being viewed. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -506,6 +506,33 @@ class ConversationsStore { | |||
| 506 | 506 | } | |
| 507 | 507 | } | |
| 508 | 508 | ||
| 509 | + /** | ||
| 510 | + * Toggles the pinned status of a conversation. | ||
| 511 | + * @param convId - The conversation ID to toggle | ||
| 512 | + * @returns The new pinned status | ||
| 513 | + */ | ||
| 514 | + async toggleConversationPin(convId: string): Promise<boolean> { | ||
| 515 | + try { | ||
| 516 | + const newPinnedState = await DatabaseService.toggleConversationPin(convId); | ||
| 517 | + | ||
| 518 | + const convIndex = this.conversations.findIndex((c) => c.id === convId); | ||
| 519 | + | ||
| 520 | + if (convIndex !== -1) { | ||
| 521 | + this.conversations[convIndex].pinned = newPinnedState; | ||
| 522 | + this.conversations = [...this.conversations]; | ||
| 523 | + } | ||
| 524 | + | ||
| 525 | + if (this.activeConversation?.id === convId) { | ||
| 526 | + this.activeConversation = { ...this.activeConversation, pinned: newPinnedState }; | ||
| 527 | + } | ||
| 528 | + | ||
| 529 | + return newPinnedState; | ||
| 530 | + } catch (error) { | ||
| 531 | + console.error('Failed to toggle conversation pin:', error); | ||
| 532 | + return false; | ||
| 533 | + } | ||
| 534 | + } | ||
| 535 | + | ||
| 509 | 536 | /** | |
| 510 | 537 | * Updates conversation title with optional confirmation dialog based on settings | |
| 511 | 538 | * @param convId - The conversation ID to update | |
@@ -1057,6 +1084,14 @@ export const isConversationsInitialized = () => conversationsStore.isInitialized | |||
| 1057 | 1084 | * Builds a flat tree of conversations with depth levels for nested forks. | |
| 1058 | 1085 | * Accepts a pre-filtered list so search filtering stays in the component. | |
| 1059 | 1086 | */ | |
| 1087 | + | ||
| 1088 | + // Pinned conversations first, then by lastModified descending | ||
| 1089 | + const comparePinnedThenRecent = (a: DatabaseConversation, b: DatabaseConversation) => { | ||
| 1090 | + if (a.pinned && !b.pinned) return -1; | ||
| 1091 | + if (!a.pinned && b.pinned) return 1; | ||
| 1092 | + return b.lastModified - a.lastModified; | ||
| 1093 | + }; | ||
| 1094 | + | ||
| 1060 | 1095 | export function buildConversationTree(convs: DatabaseConversation[]): ConversationTreeItem[] { | |
| 1061 | 1096 | const childrenByParent = new SvelteMap<string, DatabaseConversation[]>(); | |
| 1062 | 1097 | const forkIds = new SvelteSet<string>(); | |
@@ -1081,15 +1116,15 @@ export function buildConversationTree(convs: DatabaseConversation[]): Conversati | |||
| 1081 | 1116 | ||
| 1082 | 1117 | const children = childrenByParent.get(conv.id); | |
| 1083 | 1118 | if (children) { | |
| 1084 | - children.sort((a, b) => b.lastModified - a.lastModified); | ||
| 1119 | + children.sort(comparePinnedThenRecent); | ||
| 1085 | 1120 | ||
| 1086 | 1121 | for (const child of children) { | |
| 1087 | 1122 | walk(child, depth + 1); | |
| 1088 | 1123 | } | |
| 1089 | 1124 | } | |
| 1090 | 1125 | } | |
| 1091 | 1126 | ||
| 1092 | - const roots = convs.filter((c) => !forkIds.has(c.id)); | ||
| 1127 | + const roots = convs.filter((c) => !forkIds.has(c.id)).sort(comparePinnedThenRecent); | ||
| 1093 | 1128 | for (const root of roots) { | |
| 1094 | 1129 | walk(root, 0); | |
| 1095 | 1130 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -15,6 +15,7 @@ export interface DatabaseConversation { | |||
| 15 | 15 | thinkingEnabled?: boolean; | |
| 16 | 16 | reasoningEffort?: ReasoningEffort; | |
| 17 | 17 | forkedFromConversationId?: string; | |
| 18 | + pinned?: boolean; | ||
| 18 | 19 | } | |
| 19 | 20 | ||
| 20 | 21 | export interface DatabaseMessageExtraAudioFile { | |
| Back | FazBrowse Home | New Git URL |
0 commit comments