Skip to content

Commit f0f6f20

Browse files
committed
feat: Update conversation title based on current first message branch
1 parent aef0339 commit f0f6f20

File tree

4 files changed

+193
-3
lines changed

4 files changed

+193
-3
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<script lang="ts">
2+
import * as AlertDialog from '$lib/components/ui/alert-dialog';
3+
import { Button } from '$lib/components/ui/button';
4+
5+
interface Props {
6+
open: boolean;
7+
currentTitle: string;
8+
newTitle: string;
9+
onConfirm: () => void;
10+
onCancel: () => void;
11+
}
12+
13+
let { open = $bindable(), currentTitle, newTitle, onConfirm, onCancel }: Props = $props();
14+
</script>
15+
16+
<AlertDialog.Root bind:open>
17+
<AlertDialog.Content>
18+
<AlertDialog.Header>
19+
<AlertDialog.Title>Update Conversation Title?</AlertDialog.Title>
20+
<AlertDialog.Description>
21+
Do you want to update the conversation title to match the first message content?
22+
</AlertDialog.Description>
23+
</AlertDialog.Header>
24+
25+
<div class="space-y-4 pt-2 pb-6">
26+
<div class="space-y-2">
27+
<p class="text-sm font-medium text-muted-foreground">Current title:</p>
28+
<p class="text-sm bg-muted/50 p-3 rounded-md font-medium">{currentTitle}</p>
29+
</div>
30+
<div class="space-y-2">
31+
<p class="text-sm font-medium text-muted-foreground">New title would be:</p>
32+
<p class="text-sm bg-muted/50 p-3 rounded-md font-medium">{newTitle}</p>
33+
</div>
34+
</div>
35+
36+
<AlertDialog.Footer>
37+
<Button variant="outline" onclick={onCancel}>
38+
Keep Current Title
39+
</Button>
40+
<Button onclick={onConfirm}>
41+
Update Title
42+
</Button>
43+
</AlertDialog.Footer>
44+
</AlertDialog.Content>
45+
</AlertDialog.Root>

tools/server/webui/src/lib/components/app/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export { default as ChatSidebar } from './chat/ChatSidebar/ChatSidebar.svelte';
2727
export { default as ChatSidebarConversationItem } from './chat/ChatSidebar/ChatSidebarConversationItem.svelte';
2828
export { default as ChatSidebarSearch } from './chat/ChatSidebar/ChatSidebarSearch.svelte';
2929

30+
export { default as ConversationTitleUpdateDialog } from './ConversationTitleUpdateDialog.svelte';
31+
3032
export { default as KeyboardShortcutInfo } from './KeyboardShortcutInfo.svelte';
3133

3234
export { default as MarkdownContent } from './MarkdownContent.svelte';

tools/server/webui/src/lib/stores/chat.svelte.ts

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ class ChatStore {
5757
maxContextError = $state<{ message: string; estimatedTokens: number; maxContext: number } | null>(
5858
null
5959
);
60+
titleUpdateConfirmationCallback?: (currentTitle: string, newTitle: string) => Promise<boolean>;
6061

6162
constructor() {
6263
if (browser) {
@@ -622,9 +623,22 @@ class ChatStore {
622623
return;
623624
}
624625

626+
const allMessages = await DatabaseStore.getConversationMessages(this.activeConversation.id);
627+
const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null);
628+
const isFirstUserMessage = rootMessage && messageToUpdate.parent === rootMessage.id && messageToUpdate.role === 'user';
629+
625630
this.updateMessageAtIndex(messageIndex, { content: newContent });
626631
await DatabaseStore.updateMessage(messageId, { content: newContent });
627632

633+
// If this is the first user message, update the conversation title with confirmation if needed
634+
if (isFirstUserMessage && newContent.trim()) {
635+
await this.updateConversationTitleWithConfirmation(
636+
this.activeConversation.id,
637+
newContent.trim(),
638+
this.titleUpdateConfirmationCallback
639+
);
640+
}
641+
628642
const messagesToRemove = this.activeMessages.slice(messageIndex + 1);
629643
for (const message of messagesToRemove) {
630644
await DatabaseStore.deleteMessage(message.id);
@@ -725,6 +739,7 @@ class ChatStore {
725739
}
726740
}
727741

742+
728743
/**
729744
* Updates the name of a conversation
730745
* @param convId - The conversation ID to update
@@ -748,6 +763,45 @@ class ChatStore {
748763
}
749764
}
750765

766+
/**
767+
* Sets the callback function for title update confirmations
768+
* @param callback - Function to call when confirmation is needed
769+
*/
770+
setTitleUpdateConfirmationCallback(
771+
callback: (currentTitle: string, newTitle: string) => Promise<boolean>
772+
): void {
773+
this.titleUpdateConfirmationCallback = callback;
774+
}
775+
776+
/**
777+
* Updates conversation title with confirmation dialog
778+
* @param convId - The conversation ID to update
779+
* @param newTitle - The new title content
780+
* @param onConfirmationNeeded - Callback when user confirmation is needed
781+
* @returns Promise<boolean> - True if title was updated, false if cancelled
782+
*/
783+
async updateConversationTitleWithConfirmation(
784+
convId: string,
785+
newTitle: string,
786+
onConfirmationNeeded?: (currentTitle: string, newTitle: string) => Promise<boolean>
787+
): Promise<boolean> {
788+
try {
789+
if (onConfirmationNeeded) {
790+
const conversation = await DatabaseStore.getConversation(convId);
791+
if (!conversation) return false;
792+
793+
const shouldUpdate = await onConfirmationNeeded(conversation.name, newTitle);
794+
if (!shouldUpdate) return false;
795+
}
796+
797+
await this.updateConversationName(convId, newTitle);
798+
return true;
799+
} catch (error) {
800+
console.error('Failed to update conversation title with confirmation:', error);
801+
return false;
802+
}
803+
}
804+
751805
/**
752806
* Deletes a conversation and all its messages
753807
* @param convId - The conversation ID to delete
@@ -915,9 +969,40 @@ class ChatStore {
915969
async navigateToSibling(siblingId: string): Promise<void> {
916970
if (!this.activeConversation) return;
917971

972+
// Get the current first user message before navigation
973+
const allMessages = await DatabaseStore.getConversationMessages(this.activeConversation.id);
974+
const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null);
975+
const currentFirstUserMessage = this.activeMessages.find(
976+
(m) => m.role === 'user' && m.parent === rootMessage?.id
977+
);
978+
918979
await DatabaseStore.updateCurrentNode(this.activeConversation.id, siblingId);
919980
this.activeConversation.currNode = siblingId;
920981
await this.refreshActiveMessages();
982+
983+
// Only show title dialog if we're navigating between different first user message siblings
984+
if (rootMessage && this.activeMessages.length > 0) {
985+
// Find the first user message in the new active path
986+
const newFirstUserMessage = this.activeMessages.find(
987+
(m) => m.role === 'user' && m.parent === rootMessage.id
988+
);
989+
990+
// Only show dialog if:
991+
// 1. We have a new first user message
992+
// 2. It's different from the previous one (different ID or content)
993+
// 3. The new message has content
994+
if (newFirstUserMessage &&
995+
newFirstUserMessage.content.trim() &&
996+
(!currentFirstUserMessage ||
997+
newFirstUserMessage.id !== currentFirstUserMessage.id ||
998+
newFirstUserMessage.content.trim() !== currentFirstUserMessage.content.trim())) {
999+
await this.updateConversationTitleWithConfirmation(
1000+
this.activeConversation.id,
1001+
newFirstUserMessage.content.trim(),
1002+
this.titleUpdateConfirmationCallback
1003+
);
1004+
}
1005+
}
9211006
}
9221007
/**
9231008
* Edits a message by creating a new branch with the edited content
@@ -940,10 +1025,15 @@ class ChatStore {
9401025
return;
9411026
}
9421027

1028+
// Check if this is the first user message in the conversation
1029+
// First user message is one that has the root message as its parent
1030+
const allMessages = await DatabaseStore.getConversationMessages(this.activeConversation.id);
1031+
const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null);
1032+
const isFirstUserMessage = rootMessage && messageToEdit.parent === rootMessage.id && messageToEdit.role === 'user';
1033+
9431034
let parentId = messageToEdit.parent;
9441035

9451036
if (parentId === undefined || parentId === null) {
946-
const allMessages = await DatabaseStore.getConversationMessages(this.activeConversation.id);
9471037
const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null);
9481038
if (rootMessage) {
9491039
parentId = rootMessage.id;
@@ -970,6 +1060,16 @@ class ChatStore {
9701060
await DatabaseStore.updateCurrentNode(this.activeConversation.id, newMessage.id);
9711061
this.activeConversation.currNode = newMessage.id;
9721062
this.updateConversationTimestamp();
1063+
1064+
// If this is the first user message, update the conversation title with confirmation if needed
1065+
if (isFirstUserMessage && newContent.trim()) {
1066+
await this.updateConversationTitleWithConfirmation(
1067+
this.activeConversation.id,
1068+
newContent.trim(),
1069+
this.titleUpdateConfirmationCallback
1070+
);
1071+
}
1072+
9731073
await this.refreshActiveMessages();
9741074

9751075
if (messageToEdit.role === 'user') {
@@ -1118,6 +1218,7 @@ export const regenerateMessageWithBranching =
11181218
export const deleteMessage = chatStore.deleteMessage.bind(chatStore);
11191219
export const getDeletionInfo = chatStore.getDeletionInfo.bind(chatStore);
11201220
export const updateConversationName = chatStore.updateConversationName.bind(chatStore);
1221+
export const setTitleUpdateConfirmationCallback = chatStore.setTitleUpdateConfirmationCallback.bind(chatStore);
11211222

11221223
export function stopGeneration() {
11231224
chatStore.stopGeneration();

tools/server/webui/src/routes/+layout.svelte

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<script lang="ts">
22
import '../app.css';
33
import { page } from '$app/state';
4-
import { ChatSidebar, MaximumContextAlertDialog } from '$lib/components/app';
5-
import { activeMessages, isLoading } from '$lib/stores/chat.svelte';
4+
import { ChatSidebar, MaximumContextAlertDialog, ConversationTitleUpdateDialog } from '$lib/components/app';
5+
import { activeMessages, isLoading, setTitleUpdateConfirmationCallback } from '$lib/stores/chat.svelte';
66
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
77
import { serverStore } from '$lib/stores/server.svelte';
88
import { ModeWatcher } from 'mode-watcher';
@@ -18,6 +18,12 @@
1818
let sidebarOpen = $state(false);
1919
let chatSidebar: any = $state();
2020
21+
// Conversation title update dialog state
22+
let titleUpdateDialogOpen = $state(false);
23+
let titleUpdateCurrentTitle = $state('');
24+
let titleUpdateNewTitle = $state('');
25+
let titleUpdateResolve: ((value: boolean) => void) | null = null;
26+
2127
$effect(() => {
2228
if (isHomeRoute && !isNewChatMode) {
2329
// Auto-collapse sidebar when navigating to home route (but not in new chat mode)
@@ -39,6 +45,34 @@
3945
serverStore.fetchServerProps();
4046
});
4147
48+
// Set up title update confirmation callback
49+
$effect(() => {
50+
setTitleUpdateConfirmationCallback(async (currentTitle: string, newTitle: string) => {
51+
return new Promise<boolean>((resolve) => {
52+
titleUpdateCurrentTitle = currentTitle;
53+
titleUpdateNewTitle = newTitle;
54+
titleUpdateResolve = resolve;
55+
titleUpdateDialogOpen = true;
56+
});
57+
});
58+
});
59+
60+
function handleTitleUpdateConfirm() {
61+
titleUpdateDialogOpen = false;
62+
if (titleUpdateResolve) {
63+
titleUpdateResolve(true);
64+
titleUpdateResolve = null;
65+
}
66+
}
67+
68+
function handleTitleUpdateCancel() {
69+
titleUpdateDialogOpen = false;
70+
if (titleUpdateResolve) {
71+
titleUpdateResolve(false);
72+
titleUpdateResolve = null;
73+
}
74+
}
75+
4276
// Global keyboard shortcuts
4377
function handleKeydown(event: KeyboardEvent) {
4478
const isCtrlOrCmd = event.ctrlKey || event.metaKey;
@@ -72,6 +106,14 @@
72106

73107
<MaximumContextAlertDialog />
74108

109+
<ConversationTitleUpdateDialog
110+
bind:open={titleUpdateDialogOpen}
111+
currentTitle={titleUpdateCurrentTitle}
112+
newTitle={titleUpdateNewTitle}
113+
onConfirm={handleTitleUpdateConfirm}
114+
onCancel={handleTitleUpdateCancel}
115+
/>
116+
75117
<Sidebar.Provider bind:open={sidebarOpen}>
76118
<div class="flex h-screen w-full">
77119
<Sidebar.Root class="h-full">

0 commit comments

Comments
 (0)