Merge pull request #483 from PuneetP16/feat/enhance-chat-description-management
Browse files
app/components/header/Header.tsx
CHANGED
|
@@ -24,17 +24,19 @@ export function Header() {
|
|
| 24 |
<span className="i-bolt:logo-text?mask w-[46px] inline-block" />
|
| 25 |
</a>
|
| 26 |
</div>
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
<
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
| 38 |
)}
|
| 39 |
</header>
|
| 40 |
);
|
|
|
|
| 24 |
<span className="i-bolt:logo-text?mask w-[46px] inline-block" />
|
| 25 |
</a>
|
| 26 |
</div>
|
| 27 |
+
{chat.started && ( // Display ChatDescription and HeaderActionButtons only when the chat has started.
|
| 28 |
+
<>
|
| 29 |
+
<span className="flex-1 px-4 truncate text-center text-bolt-elements-textPrimary">
|
| 30 |
+
<ClientOnly>{() => <ChatDescription />}</ClientOnly>
|
| 31 |
+
</span>
|
| 32 |
+
<ClientOnly>
|
| 33 |
+
{() => (
|
| 34 |
+
<div className="mr-1">
|
| 35 |
+
<HeaderActionButtons />
|
| 36 |
+
</div>
|
| 37 |
+
)}
|
| 38 |
+
</ClientOnly>
|
| 39 |
+
</>
|
| 40 |
)}
|
| 41 |
</header>
|
| 42 |
);
|
app/components/sidebar/HistoryItem.tsx
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
|
|
|
|
|
| 1 |
import * as Dialog from '@radix-ui/react-dialog';
|
| 2 |
import { type ChatHistoryItem } from '~/lib/persistence';
|
| 3 |
import WithTooltip from '~/components/ui/Tooltip';
|
|
|
|
| 4 |
|
| 5 |
interface HistoryItemProps {
|
| 6 |
item: ChatHistoryItem;
|
|
@@ -10,48 +13,115 @@ interface HistoryItemProps {
|
|
| 10 |
}
|
| 11 |
|
| 12 |
export function HistoryItem({ item, onDelete, onDuplicate, exportChat }: HistoryItemProps) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
return (
|
| 14 |
-
<div
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
onClick={(event) => {
|
| 24 |
event.preventDefault();
|
| 25 |
exportChat(item.id);
|
| 26 |
}}
|
| 27 |
-
title="Export chat"
|
| 28 |
/>
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
type="button"
|
| 34 |
-
className="i-ph:copy scale-110 mr-2 hover:text-bolt-elements-item-contentAccent"
|
| 35 |
onClick={() => onDuplicate?.(item.id)}
|
| 36 |
-
title="Duplicate chat"
|
| 37 |
/>
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
onClick={(event) => {
|
| 46 |
event.preventDefault();
|
| 47 |
onDelete?.(event);
|
| 48 |
}}
|
| 49 |
/>
|
| 50 |
-
</
|
| 51 |
-
</
|
| 52 |
</div>
|
| 53 |
-
</
|
| 54 |
-
|
| 55 |
</div>
|
| 56 |
);
|
| 57 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useParams } from '@remix-run/react';
|
| 2 |
+
import { classNames } from '~/utils/classNames';
|
| 3 |
import * as Dialog from '@radix-ui/react-dialog';
|
| 4 |
import { type ChatHistoryItem } from '~/lib/persistence';
|
| 5 |
import WithTooltip from '~/components/ui/Tooltip';
|
| 6 |
+
import { useEditChatDescription } from '~/lib/hooks';
|
| 7 |
|
| 8 |
interface HistoryItemProps {
|
| 9 |
item: ChatHistoryItem;
|
|
|
|
| 13 |
}
|
| 14 |
|
| 15 |
export function HistoryItem({ item, onDelete, onDuplicate, exportChat }: HistoryItemProps) {
|
| 16 |
+
const { id: urlId } = useParams();
|
| 17 |
+
const isActiveChat = urlId === item.urlId;
|
| 18 |
+
|
| 19 |
+
const { editing, handleChange, handleBlur, handleSubmit, handleKeyDown, currentDescription, toggleEditMode } =
|
| 20 |
+
useEditChatDescription({
|
| 21 |
+
initialDescription: item.description,
|
| 22 |
+
customChatId: item.id,
|
| 23 |
+
syncWithGlobalStore: isActiveChat,
|
| 24 |
+
});
|
| 25 |
+
|
| 26 |
+
const renderDescriptionForm = (
|
| 27 |
+
<form onSubmit={handleSubmit} className="flex-1 flex items-center">
|
| 28 |
+
<input
|
| 29 |
+
type="text"
|
| 30 |
+
className="flex-1 bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 mr-2"
|
| 31 |
+
autoFocus
|
| 32 |
+
value={currentDescription}
|
| 33 |
+
onChange={handleChange}
|
| 34 |
+
onBlur={handleBlur}
|
| 35 |
+
onKeyDown={handleKeyDown}
|
| 36 |
+
/>
|
| 37 |
+
<button
|
| 38 |
+
type="submit"
|
| 39 |
+
className="i-ph:check scale-110 hover:text-bolt-elements-item-contentAccent"
|
| 40 |
+
onMouseDown={handleSubmit}
|
| 41 |
+
/>
|
| 42 |
+
</form>
|
| 43 |
+
);
|
| 44 |
+
|
| 45 |
return (
|
| 46 |
+
<div
|
| 47 |
+
className={classNames(
|
| 48 |
+
'group rounded-md text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 overflow-hidden flex justify-between items-center px-2 py-1',
|
| 49 |
+
{ '[&&]:text-bolt-elements-textPrimary bg-bolt-elements-background-depth-3': isActiveChat },
|
| 50 |
+
)}
|
| 51 |
+
>
|
| 52 |
+
{editing ? (
|
| 53 |
+
renderDescriptionForm
|
| 54 |
+
) : (
|
| 55 |
+
<a href={`/chat/${item.urlId}`} className="flex w-full relative truncate block">
|
| 56 |
+
{currentDescription}
|
| 57 |
+
<div
|
| 58 |
+
className={classNames(
|
| 59 |
+
'absolute right-0 z-1 top-0 bottom-0 bg-gradient-to-l from-bolt-elements-background-depth-2 group-hover:from-bolt-elements-background-depth-3 box-content pl-3 to-transparent w-10 flex justify-end group-hover:w-22 group-hover:from-99%',
|
| 60 |
+
{ 'from-bolt-elements-background-depth-3 w-10 ': isActiveChat },
|
| 61 |
+
)}
|
| 62 |
+
>
|
| 63 |
+
<div className="flex items-center p-1 text-bolt-elements-textSecondary opacity-0 group-hover:opacity-100 transition-opacity">
|
| 64 |
+
<ChatActionButton
|
| 65 |
+
toolTipContent="Export chat"
|
| 66 |
+
icon="i-ph:download-simple"
|
| 67 |
onClick={(event) => {
|
| 68 |
event.preventDefault();
|
| 69 |
exportChat(item.id);
|
| 70 |
}}
|
|
|
|
| 71 |
/>
|
| 72 |
+
{onDuplicate && (
|
| 73 |
+
<ChatActionButton
|
| 74 |
+
toolTipContent="Duplicate chat"
|
| 75 |
+
icon="i-ph:copy"
|
|
|
|
|
|
|
| 76 |
onClick={() => onDuplicate?.(item.id)}
|
|
|
|
| 77 |
/>
|
| 78 |
+
)}
|
| 79 |
+
<ChatActionButton
|
| 80 |
+
toolTipContent="Rename chat"
|
| 81 |
+
icon="i-ph:pencil-fill"
|
| 82 |
+
onClick={(event) => {
|
| 83 |
+
event.preventDefault();
|
| 84 |
+
toggleEditMode();
|
| 85 |
+
}}
|
| 86 |
+
/>
|
| 87 |
+
<Dialog.Trigger asChild>
|
| 88 |
+
<ChatActionButton
|
| 89 |
+
toolTipContent="Delete chat"
|
| 90 |
+
icon="i-ph:trash"
|
| 91 |
+
className="[&&]:hover:text-bolt-elements-button-danger-text"
|
| 92 |
onClick={(event) => {
|
| 93 |
event.preventDefault();
|
| 94 |
onDelete?.(event);
|
| 95 |
}}
|
| 96 |
/>
|
| 97 |
+
</Dialog.Trigger>
|
| 98 |
+
</div>
|
| 99 |
</div>
|
| 100 |
+
</a>
|
| 101 |
+
)}
|
| 102 |
</div>
|
| 103 |
);
|
| 104 |
}
|
| 105 |
+
|
| 106 |
+
const ChatActionButton = ({
|
| 107 |
+
toolTipContent,
|
| 108 |
+
icon,
|
| 109 |
+
className,
|
| 110 |
+
onClick,
|
| 111 |
+
}: {
|
| 112 |
+
toolTipContent: string;
|
| 113 |
+
icon: string;
|
| 114 |
+
className?: string;
|
| 115 |
+
onClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
| 116 |
+
btnTitle?: string;
|
| 117 |
+
}) => {
|
| 118 |
+
return (
|
| 119 |
+
<WithTooltip tooltip={toolTipContent}>
|
| 120 |
+
<button
|
| 121 |
+
type="button"
|
| 122 |
+
className={`scale-110 mr-2 hover:text-bolt-elements-item-contentAccent ${icon} ${className ? className : ''}`}
|
| 123 |
+
onClick={onClick}
|
| 124 |
+
/>
|
| 125 |
+
</WithTooltip>
|
| 126 |
+
);
|
| 127 |
+
};
|
app/lib/hooks/index.ts
CHANGED
|
@@ -2,4 +2,5 @@ export * from './useMessageParser';
|
|
| 2 |
export * from './usePromptEnhancer';
|
| 3 |
export * from './useShortcuts';
|
| 4 |
export * from './useSnapScroll';
|
|
|
|
| 5 |
export { default } from './useViewport';
|
|
|
|
| 2 |
export * from './usePromptEnhancer';
|
| 3 |
export * from './useShortcuts';
|
| 4 |
export * from './useSnapScroll';
|
| 5 |
+
export * from './useEditChatDescription';
|
| 6 |
export { default } from './useViewport';
|
app/lib/hooks/useEditChatDescription.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useStore } from '@nanostores/react';
|
| 2 |
+
import { useCallback, useEffect, useState } from 'react';
|
| 3 |
+
import { toast } from 'react-toastify';
|
| 4 |
+
import {
|
| 5 |
+
chatId as chatIdStore,
|
| 6 |
+
description as descriptionStore,
|
| 7 |
+
db,
|
| 8 |
+
updateChatDescription,
|
| 9 |
+
getMessages,
|
| 10 |
+
} from '~/lib/persistence';
|
| 11 |
+
|
| 12 |
+
interface EditChatDescriptionOptions {
|
| 13 |
+
initialDescription?: string;
|
| 14 |
+
customChatId?: string;
|
| 15 |
+
syncWithGlobalStore?: boolean;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
type EditChatDescriptionHook = {
|
| 19 |
+
editing: boolean;
|
| 20 |
+
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
| 21 |
+
handleBlur: () => Promise<void>;
|
| 22 |
+
handleSubmit: (event: React.FormEvent) => Promise<void>;
|
| 23 |
+
handleKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => Promise<void>;
|
| 24 |
+
currentDescription: string;
|
| 25 |
+
toggleEditMode: () => void;
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
/**
|
| 29 |
+
* Hook to manage the state and behavior for editing chat descriptions.
|
| 30 |
+
*
|
| 31 |
+
* Offers functions to:
|
| 32 |
+
* - Switch between edit and view modes.
|
| 33 |
+
* - Manage input changes, blur, and form submission events.
|
| 34 |
+
* - Save updates to IndexedDB and optionally to the global application state.
|
| 35 |
+
*
|
| 36 |
+
* @param {Object} options
|
| 37 |
+
* @param {string} options.initialDescription - The current chat description.
|
| 38 |
+
* @param {string} options.customChatId - Optional ID for updating the description via the sidebar.
|
| 39 |
+
* @param {boolean} options.syncWithGlobalStore - Flag to indicate global description store synchronization.
|
| 40 |
+
* @returns {EditChatDescriptionHook} Methods and state for managing description edits.
|
| 41 |
+
*/
|
| 42 |
+
export function useEditChatDescription({
|
| 43 |
+
initialDescription = descriptionStore.get()!,
|
| 44 |
+
customChatId,
|
| 45 |
+
syncWithGlobalStore,
|
| 46 |
+
}: EditChatDescriptionOptions): EditChatDescriptionHook {
|
| 47 |
+
const chatIdFromStore = useStore(chatIdStore);
|
| 48 |
+
const [editing, setEditing] = useState(false);
|
| 49 |
+
const [currentDescription, setCurrentDescription] = useState(initialDescription);
|
| 50 |
+
|
| 51 |
+
const [chatId, setChatId] = useState<string>();
|
| 52 |
+
|
| 53 |
+
useEffect(() => {
|
| 54 |
+
setChatId(customChatId || chatIdFromStore);
|
| 55 |
+
}, [customChatId, chatIdFromStore]);
|
| 56 |
+
useEffect(() => {
|
| 57 |
+
setCurrentDescription(initialDescription);
|
| 58 |
+
}, [initialDescription]);
|
| 59 |
+
|
| 60 |
+
const toggleEditMode = useCallback(() => setEditing((prev) => !prev), []);
|
| 61 |
+
|
| 62 |
+
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
| 63 |
+
setCurrentDescription(e.target.value);
|
| 64 |
+
}, []);
|
| 65 |
+
|
| 66 |
+
const fetchLatestDescription = useCallback(async () => {
|
| 67 |
+
if (!db || !chatId) {
|
| 68 |
+
return initialDescription;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
try {
|
| 72 |
+
const chat = await getMessages(db, chatId);
|
| 73 |
+
return chat?.description || initialDescription;
|
| 74 |
+
} catch (error) {
|
| 75 |
+
console.error('Failed to fetch latest description:', error);
|
| 76 |
+
return initialDescription;
|
| 77 |
+
}
|
| 78 |
+
}, [db, chatId, initialDescription]);
|
| 79 |
+
|
| 80 |
+
const handleBlur = useCallback(async () => {
|
| 81 |
+
const latestDescription = await fetchLatestDescription();
|
| 82 |
+
setCurrentDescription(latestDescription);
|
| 83 |
+
toggleEditMode();
|
| 84 |
+
}, [fetchLatestDescription, toggleEditMode]);
|
| 85 |
+
|
| 86 |
+
const isValidDescription = useCallback((desc: string): boolean => {
|
| 87 |
+
const trimmedDesc = desc.trim();
|
| 88 |
+
|
| 89 |
+
if (trimmedDesc === initialDescription) {
|
| 90 |
+
toggleEditMode();
|
| 91 |
+
return false; // No change, skip validation
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
const lengthValid = trimmedDesc.length > 0 && trimmedDesc.length <= 100;
|
| 95 |
+
const characterValid = /^[a-zA-Z0-9\s]+$/.test(trimmedDesc);
|
| 96 |
+
|
| 97 |
+
if (!lengthValid) {
|
| 98 |
+
toast.error('Description must be between 1 and 100 characters.');
|
| 99 |
+
return false;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
if (!characterValid) {
|
| 103 |
+
toast.error('Description can only contain alphanumeric characters and spaces.');
|
| 104 |
+
return false;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
return true;
|
| 108 |
+
}, []);
|
| 109 |
+
|
| 110 |
+
const handleSubmit = useCallback(
|
| 111 |
+
async (event: React.FormEvent) => {
|
| 112 |
+
event.preventDefault();
|
| 113 |
+
|
| 114 |
+
if (!isValidDescription(currentDescription)) {
|
| 115 |
+
return;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
try {
|
| 119 |
+
if (!db) {
|
| 120 |
+
toast.error('Chat persistence is not available');
|
| 121 |
+
return;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
if (!chatId) {
|
| 125 |
+
toast.error('Chat Id is not available');
|
| 126 |
+
return;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
await updateChatDescription(db, chatId, currentDescription);
|
| 130 |
+
|
| 131 |
+
if (syncWithGlobalStore) {
|
| 132 |
+
descriptionStore.set(currentDescription);
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
toast.success('Chat description updated successfully');
|
| 136 |
+
} catch (error) {
|
| 137 |
+
toast.error('Failed to update chat description: ' + (error as Error).message);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
toggleEditMode();
|
| 141 |
+
},
|
| 142 |
+
[currentDescription, db, chatId, initialDescription, customChatId],
|
| 143 |
+
);
|
| 144 |
+
|
| 145 |
+
const handleKeyDown = useCallback(
|
| 146 |
+
async (e: React.KeyboardEvent<HTMLInputElement>) => {
|
| 147 |
+
if (e.key === 'Escape') {
|
| 148 |
+
await handleBlur();
|
| 149 |
+
}
|
| 150 |
+
},
|
| 151 |
+
[handleBlur],
|
| 152 |
+
);
|
| 153 |
+
|
| 154 |
+
return {
|
| 155 |
+
editing,
|
| 156 |
+
handleChange,
|
| 157 |
+
handleBlur,
|
| 158 |
+
handleSubmit,
|
| 159 |
+
handleKeyDown,
|
| 160 |
+
currentDescription,
|
| 161 |
+
toggleEditMode,
|
| 162 |
+
};
|
| 163 |
+
}
|
app/lib/persistence/ChatDescription.client.tsx
CHANGED
|
@@ -1,6 +1,68 @@
|
|
| 1 |
import { useStore } from '@nanostores/react';
|
| 2 |
-
import {
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
export function ChatDescription() {
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
}
|
|
|
|
| 1 |
import { useStore } from '@nanostores/react';
|
| 2 |
+
import { TooltipProvider } from '@radix-ui/react-tooltip';
|
| 3 |
+
import WithTooltip from '~/components/ui/Tooltip';
|
| 4 |
+
import { useEditChatDescription } from '~/lib/hooks';
|
| 5 |
+
import { description as descriptionStore } from '~/lib/persistence';
|
| 6 |
|
| 7 |
export function ChatDescription() {
|
| 8 |
+
const initialDescription = useStore(descriptionStore)!;
|
| 9 |
+
|
| 10 |
+
const { editing, handleChange, handleBlur, handleSubmit, handleKeyDown, currentDescription, toggleEditMode } =
|
| 11 |
+
useEditChatDescription({
|
| 12 |
+
initialDescription,
|
| 13 |
+
syncWithGlobalStore: true,
|
| 14 |
+
});
|
| 15 |
+
|
| 16 |
+
if (!initialDescription) {
|
| 17 |
+
// doing this to prevent showing edit button until chat description is set
|
| 18 |
+
return null;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
return (
|
| 22 |
+
<div className="flex items-center justify-center">
|
| 23 |
+
{editing ? (
|
| 24 |
+
<form onSubmit={handleSubmit} className="flex items-center justify-center">
|
| 25 |
+
<input
|
| 26 |
+
type="text"
|
| 27 |
+
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 mr-2 w-fit"
|
| 28 |
+
autoFocus
|
| 29 |
+
value={currentDescription}
|
| 30 |
+
onChange={handleChange}
|
| 31 |
+
onBlur={handleBlur}
|
| 32 |
+
onKeyDown={handleKeyDown}
|
| 33 |
+
style={{ width: `${Math.max(currentDescription.length * 8, 100)}px` }}
|
| 34 |
+
/>
|
| 35 |
+
<TooltipProvider>
|
| 36 |
+
<WithTooltip tooltip="Save title">
|
| 37 |
+
<div className="flex justify-between items-center p-2 rounded-md bg-bolt-elements-item-backgroundAccent">
|
| 38 |
+
<button
|
| 39 |
+
type="submit"
|
| 40 |
+
className="i-ph:check-bold scale-110 hover:text-bolt-elements-item-contentAccent"
|
| 41 |
+
onMouseDown={handleSubmit}
|
| 42 |
+
/>
|
| 43 |
+
</div>
|
| 44 |
+
</WithTooltip>
|
| 45 |
+
</TooltipProvider>
|
| 46 |
+
</form>
|
| 47 |
+
) : (
|
| 48 |
+
<>
|
| 49 |
+
{currentDescription}
|
| 50 |
+
<TooltipProvider>
|
| 51 |
+
<WithTooltip tooltip="Rename chat">
|
| 52 |
+
<div className="flex justify-between items-center p-2 rounded-md bg-bolt-elements-item-backgroundAccent ml-2">
|
| 53 |
+
<button
|
| 54 |
+
type="button"
|
| 55 |
+
className="i-ph:pencil-fill scale-110 hover:text-bolt-elements-item-contentAccent"
|
| 56 |
+
onClick={(event) => {
|
| 57 |
+
event.preventDefault();
|
| 58 |
+
toggleEditMode();
|
| 59 |
+
}}
|
| 60 |
+
/>
|
| 61 |
+
</div>
|
| 62 |
+
</WithTooltip>
|
| 63 |
+
</TooltipProvider>
|
| 64 |
+
</>
|
| 65 |
+
)}
|
| 66 |
+
</div>
|
| 67 |
+
);
|
| 68 |
}
|
app/lib/persistence/db.ts
CHANGED
|
@@ -52,17 +52,23 @@ export async function setMessages(
|
|
| 52 |
messages: Message[],
|
| 53 |
urlId?: string,
|
| 54 |
description?: string,
|
|
|
|
| 55 |
): Promise<void> {
|
| 56 |
return new Promise((resolve, reject) => {
|
| 57 |
const transaction = db.transaction('chats', 'readwrite');
|
| 58 |
const store = transaction.objectStore('chats');
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
const request = store.put({
|
| 61 |
id,
|
| 62 |
messages,
|
| 63 |
urlId,
|
| 64 |
description,
|
| 65 |
-
timestamp: new Date().toISOString(),
|
| 66 |
});
|
| 67 |
|
| 68 |
request.onsuccess = () => resolve();
|
|
@@ -212,3 +218,17 @@ export async function createChatFromMessages(
|
|
| 212 |
|
| 213 |
return newUrlId; // Return the urlId instead of id for navigation
|
| 214 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
messages: Message[],
|
| 53 |
urlId?: string,
|
| 54 |
description?: string,
|
| 55 |
+
timestamp?: string,
|
| 56 |
): Promise<void> {
|
| 57 |
return new Promise((resolve, reject) => {
|
| 58 |
const transaction = db.transaction('chats', 'readwrite');
|
| 59 |
const store = transaction.objectStore('chats');
|
| 60 |
|
| 61 |
+
if (timestamp && isNaN(Date.parse(timestamp))) {
|
| 62 |
+
reject(new Error('Invalid timestamp'));
|
| 63 |
+
return;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
const request = store.put({
|
| 67 |
id,
|
| 68 |
messages,
|
| 69 |
urlId,
|
| 70 |
description,
|
| 71 |
+
timestamp: timestamp ?? new Date().toISOString(),
|
| 72 |
});
|
| 73 |
|
| 74 |
request.onsuccess = () => resolve();
|
|
|
|
| 218 |
|
| 219 |
return newUrlId; // Return the urlId instead of id for navigation
|
| 220 |
}
|
| 221 |
+
|
| 222 |
+
export async function updateChatDescription(db: IDBDatabase, id: string, description: string): Promise<void> {
|
| 223 |
+
const chat = await getMessages(db, id);
|
| 224 |
+
|
| 225 |
+
if (!chat) {
|
| 226 |
+
throw new Error('Chat not found');
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
if (!description.trim()) {
|
| 230 |
+
throw new Error('Description cannot be empty');
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
await setMessages(db, id, chat.messages, chat.urlId, description, chat.timestamp);
|
| 234 |
+
}
|
app/lib/runtime/action-runner.ts
CHANGED
|
@@ -100,6 +100,10 @@ export class ActionRunner {
|
|
| 100 |
.catch((error) => {
|
| 101 |
console.error('Action failed:', error);
|
| 102 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
}
|
| 104 |
|
| 105 |
async #executeAction(actionId: string, isStreaming: boolean = false) {
|
|
|
|
| 100 |
.catch((error) => {
|
| 101 |
console.error('Action failed:', error);
|
| 102 |
});
|
| 103 |
+
|
| 104 |
+
await this.#currentExecutionPromise;
|
| 105 |
+
|
| 106 |
+
return;
|
| 107 |
}
|
| 108 |
|
| 109 |
async #executeAction(actionId: string, isStreaming: boolean = false) {
|