mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
Support resuming chat messages (#91)
This commit is contained in:
parent
16ee8276f9
commit
a0303c1102
@ -7,7 +7,7 @@ import { useAnimate } from 'framer-motion';
|
|||||||
import { memo, useEffect, useRef, useState } from 'react';
|
import { memo, useEffect, useRef, useState } from 'react';
|
||||||
import { cssTransition, toast, ToastContainer } from 'react-toastify';
|
import { cssTransition, toast, ToastContainer } from 'react-toastify';
|
||||||
import { useSnapScroll } from '~/lib/hooks';
|
import { useSnapScroll } from '~/lib/hooks';
|
||||||
import { currentChatId, handleChatTitleUpdate, useChatHistory } from '~/lib/persistence';
|
import { database, handleChatTitleUpdate, useChatHistory, type ResumeChatInfo } from '~/lib/persistence';
|
||||||
import { chatStore } from '~/lib/stores/chat';
|
import { chatStore } from '~/lib/stores/chat';
|
||||||
import { cubicEasingFn } from '~/utils/easings';
|
import { cubicEasingFn } from '~/utils/easings';
|
||||||
import { renderLogger } from '~/utils/logger';
|
import { renderLogger } from '~/utils/logger';
|
||||||
@ -21,6 +21,7 @@ import {
|
|||||||
sendChatMessage,
|
sendChatMessage,
|
||||||
type ChatReference,
|
type ChatReference,
|
||||||
simulationReset,
|
simulationReset,
|
||||||
|
resumeChatMessage,
|
||||||
} from '~/lib/replay/SimulationPrompt';
|
} from '~/lib/replay/SimulationPrompt';
|
||||||
import { getIFrameSimulationData } from '~/lib/replay/Recording';
|
import { getIFrameSimulationData } from '~/lib/replay/Recording';
|
||||||
import { getCurrentIFrame } from '~/components/workbench/Preview';
|
import { getCurrentIFrame } from '~/components/workbench/Preview';
|
||||||
@ -73,12 +74,17 @@ setInterval(async () => {
|
|||||||
export function Chat() {
|
export function Chat() {
|
||||||
renderLogger.trace('Chat');
|
renderLogger.trace('Chat');
|
||||||
|
|
||||||
const { ready, initialMessages, storeMessageHistory, importChat } = useChatHistory();
|
const { ready, initialMessages, resumeChat, storeMessageHistory, importChat } = useChatHistory();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{ready && (
|
{ready && (
|
||||||
<ChatImpl initialMessages={initialMessages} storeMessageHistory={storeMessageHistory} importChat={importChat} />
|
<ChatImpl
|
||||||
|
initialMessages={initialMessages}
|
||||||
|
resumeChat={resumeChat}
|
||||||
|
storeMessageHistory={storeMessageHistory}
|
||||||
|
importChat={importChat}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<ToastContainer
|
<ToastContainer
|
||||||
closeButton={({ closeToast }) => {
|
closeButton={({ closeToast }) => {
|
||||||
@ -113,6 +119,7 @@ export function Chat() {
|
|||||||
|
|
||||||
interface ChatProps {
|
interface ChatProps {
|
||||||
initialMessages: Message[];
|
initialMessages: Message[];
|
||||||
|
resumeChat: ResumeChatInfo | undefined;
|
||||||
storeMessageHistory: (messages: Message[]) => void;
|
storeMessageHistory: (messages: Message[]) => void;
|
||||||
importChat: (description: string, messages: Message[]) => Promise<void>;
|
importChat: (description: string, messages: Message[]) => Promise<void>;
|
||||||
}
|
}
|
||||||
@ -125,7 +132,25 @@ async function clearActiveChat() {
|
|||||||
gActiveChatMessageTelemetry = undefined;
|
gActiveChatMessageTelemetry = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatImpl = memo(({ initialMessages, storeMessageHistory, importChat }: ChatProps) => {
|
function mergeResponseMessage(msg: Message, messages: Message[]): Message[] {
|
||||||
|
const lastMessage = messages[messages.length - 1];
|
||||||
|
if (lastMessage.id == msg.id) {
|
||||||
|
messages.pop();
|
||||||
|
assert(lastMessage.type == 'text', 'Last message must be a text message');
|
||||||
|
assert(msg.type == 'text', 'Message must be a text message');
|
||||||
|
messages.push({
|
||||||
|
...msg,
|
||||||
|
content: lastMessage.content + msg.content,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
messages.push(msg);
|
||||||
|
}
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChatImpl = memo((props: ChatProps) => {
|
||||||
|
const { initialMessages, resumeChat: initialResumeChat, storeMessageHistory, importChat } = props;
|
||||||
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
|
const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
|
||||||
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]); // Move here
|
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]); // Move here
|
||||||
@ -146,9 +171,14 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory, importChat
|
|||||||
// Last status we heard for the pending message.
|
// Last status we heard for the pending message.
|
||||||
const [pendingMessageStatus, setPendingMessageStatus] = useState('');
|
const [pendingMessageStatus, setPendingMessageStatus] = useState('');
|
||||||
|
|
||||||
|
// If we are listening to responses from a chat started in an earlier session,
|
||||||
|
// this will be set. This is equivalent to having a pending message but is
|
||||||
|
// handled differently.
|
||||||
|
const [resumeChat, setResumeChat] = useState<ResumeChatInfo | undefined>(initialResumeChat);
|
||||||
|
|
||||||
const [messages, setMessages] = useState<Message[]>(initialMessages);
|
const [messages, setMessages] = useState<Message[]>(initialMessages);
|
||||||
|
|
||||||
const { showChat } = useStore(chatStore);
|
const showChat = useStore(chatStore.showChat);
|
||||||
|
|
||||||
const [animationScope, animate] = useAnimate();
|
const [animationScope, animate] = useAnimate();
|
||||||
|
|
||||||
@ -173,7 +203,7 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory, importChat
|
|||||||
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
|
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
chatStore.setKey('started', initialMessages.length > 0);
|
chatStore.started.set(initialMessages.length > 0);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -183,8 +213,14 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory, importChat
|
|||||||
const abort = () => {
|
const abort = () => {
|
||||||
stop();
|
stop();
|
||||||
gNumAborts++;
|
gNumAborts++;
|
||||||
chatStore.setKey('aborted', true);
|
chatStore.aborted.set(true);
|
||||||
setPendingMessageId(undefined);
|
setPendingMessageId(undefined);
|
||||||
|
setResumeChat(undefined);
|
||||||
|
|
||||||
|
const chatId = chatStore.currentChat.get()?.id;
|
||||||
|
if (chatId) {
|
||||||
|
database.updateChatLastMessage(chatId, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
if (gActiveChatMessageTelemetry) {
|
if (gActiveChatMessageTelemetry) {
|
||||||
gActiveChatMessageTelemetry.abort('StopButtonClicked');
|
gActiveChatMessageTelemetry.abort('StopButtonClicked');
|
||||||
@ -216,7 +252,7 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory, importChat
|
|||||||
animate('#intro', { opacity: 0, flex: 1 }, { duration: 0.2, ease: cubicEasingFn }),
|
animate('#intro', { opacity: 0, flex: 1 }, { duration: 0.2, ease: cubicEasingFn }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
chatStore.setKey('started', true);
|
chatStore.started.set(true);
|
||||||
|
|
||||||
setChatStarted(true);
|
setChatStarted(true);
|
||||||
};
|
};
|
||||||
@ -225,7 +261,7 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory, importChat
|
|||||||
const _input = messageInput || input;
|
const _input = messageInput || input;
|
||||||
const numAbortsAtStart = gNumAborts;
|
const numAbortsAtStart = gNumAborts;
|
||||||
|
|
||||||
if (_input.length === 0 || pendingMessageId) {
|
if (_input.length === 0 || pendingMessageId || resumeChat) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,11 +311,10 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory, importChat
|
|||||||
await flushSimulationData();
|
await flushSimulationData();
|
||||||
simulationFinishData();
|
simulationFinishData();
|
||||||
|
|
||||||
chatStore.setKey('aborted', false);
|
chatStore.aborted.set(false);
|
||||||
|
|
||||||
runAnimation();
|
runAnimation();
|
||||||
|
|
||||||
const existingRepositoryId = getMessagesRepositoryId(messages);
|
|
||||||
let updatedRepository = false;
|
let updatedRepository = false;
|
||||||
|
|
||||||
const addResponseMessage = (msg: Message) => {
|
const addResponseMessage = (msg: Message) => {
|
||||||
@ -287,22 +322,9 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory, importChat
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
newMessages = [...newMessages];
|
const existingRepositoryId = getMessagesRepositoryId(newMessages);
|
||||||
|
|
||||||
const lastMessage = newMessages[newMessages.length - 1];
|
|
||||||
|
|
||||||
if (lastMessage.id == msg.id) {
|
|
||||||
newMessages.pop();
|
|
||||||
assert(lastMessage.type == 'text', 'Last message must be a text message');
|
|
||||||
assert(msg.type == 'text', 'Message must be a text message');
|
|
||||||
newMessages.push({
|
|
||||||
...msg,
|
|
||||||
content: lastMessage.content + msg.content,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
newMessages.push(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
newMessages = mergeResponseMessage(msg, [...newMessages]);
|
||||||
setMessages(newMessages);
|
setMessages(newMessages);
|
||||||
|
|
||||||
// Update the repository as soon as it has changed.
|
// Update the repository as soon as it has changed.
|
||||||
@ -315,11 +337,22 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory, importChat
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onChatTitle = (title: string) => {
|
const onChatTitle = (title: string) => {
|
||||||
|
if (gNumAborts != numAbortsAtStart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('ChatTitle', title);
|
console.log('ChatTitle', title);
|
||||||
handleChatTitleUpdate(currentChatId.get() as string, title);
|
const currentChat = chatStore.currentChat.get();
|
||||||
|
if (currentChat) {
|
||||||
|
handleChatTitleUpdate(currentChat.id, title);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChatStatus = debounce((status: string) => {
|
const onChatStatus = debounce((status: string) => {
|
||||||
|
if (gNumAborts != numAbortsAtStart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('ChatStatus', status);
|
console.log('ChatStatus', status);
|
||||||
setPendingMessageStatus(status);
|
setPendingMessageStatus(status);
|
||||||
}, 500);
|
}, 500);
|
||||||
@ -371,6 +404,90 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory, importChat
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (!initialResumeChat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numAbortsAtStart = gNumAborts;
|
||||||
|
|
||||||
|
let newMessages = messages;
|
||||||
|
|
||||||
|
// The response messages we get may overlap with the ones we already have.
|
||||||
|
// Look for this and remove any existing message when we receive the first
|
||||||
|
// piece of a response message.
|
||||||
|
//
|
||||||
|
// Messages we have already received a portion of a response for.
|
||||||
|
const hasReceivedResponse = new Set<string>();
|
||||||
|
|
||||||
|
const addResponseMessage = (msg: Message) => {
|
||||||
|
if (gNumAborts != numAbortsAtStart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasReceivedResponse.has(msg.id)) {
|
||||||
|
hasReceivedResponse.add(msg.id);
|
||||||
|
newMessages = newMessages.filter((m) => m.id != msg.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingRepositoryId = getMessagesRepositoryId(newMessages);
|
||||||
|
|
||||||
|
newMessages = mergeResponseMessage(msg, [...newMessages]);
|
||||||
|
setMessages(newMessages);
|
||||||
|
|
||||||
|
// Update the repository as soon as it has changed.
|
||||||
|
const responseRepositoryId = getMessagesRepositoryId(newMessages);
|
||||||
|
|
||||||
|
if (responseRepositoryId && existingRepositoryId != responseRepositoryId) {
|
||||||
|
simulationRepositoryUpdated(responseRepositoryId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChatTitle = (title: string) => {
|
||||||
|
if (gNumAborts != numAbortsAtStart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('ChatTitle', title);
|
||||||
|
const currentChat = chatStore.currentChat.get();
|
||||||
|
if (currentChat) {
|
||||||
|
handleChatTitleUpdate(currentChat.id, title);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChatStatus = debounce((status: string) => {
|
||||||
|
if (gNumAborts != numAbortsAtStart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('ChatStatus', status);
|
||||||
|
setPendingMessageStatus(status);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await resumeChatMessage(initialResumeChat.protocolChatId, initialResumeChat.protocolChatResponseId, {
|
||||||
|
onResponsePart: addResponseMessage,
|
||||||
|
onTitle: onChatTitle,
|
||||||
|
onStatus: onChatStatus,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error resuming chat', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gNumAborts != numAbortsAtStart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setResumeChat(undefined);
|
||||||
|
|
||||||
|
const chatId = chatStore.currentChat.get()?.id;
|
||||||
|
if (chatId) {
|
||||||
|
database.updateChatLastMessage(chatId, null, null);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [initialResumeChat]);
|
||||||
|
|
||||||
// Rewind far enough to erase the specified message.
|
// Rewind far enough to erase the specified message.
|
||||||
const onRewind = async (messageId: string) => {
|
const onRewind = async (messageId: string) => {
|
||||||
console.log('Rewinding', messageId);
|
console.log('Rewinding', messageId);
|
||||||
@ -490,7 +607,7 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory, importChat
|
|||||||
input={input}
|
input={input}
|
||||||
showChat={showChat}
|
showChat={showChat}
|
||||||
chatStarted={chatStarted}
|
chatStarted={chatStarted}
|
||||||
hasPendingMessage={pendingMessageId !== undefined}
|
hasPendingMessage={pendingMessageId !== undefined || resumeChat !== undefined}
|
||||||
pendingMessageStatus={pendingMessageStatus}
|
pendingMessageStatus={pendingMessageStatus}
|
||||||
sendMessage={sendMessage}
|
sendMessage={sendMessage}
|
||||||
messageRef={messageRef}
|
messageRef={messageRef}
|
||||||
|
@ -4,8 +4,8 @@ import { useState } from 'react';
|
|||||||
import type { DeploySettingsDatabase } from '~/lib/replay/Deploy';
|
import type { DeploySettingsDatabase } from '~/lib/replay/Deploy';
|
||||||
import { generateRandomId } from '~/lib/replay/ReplayProtocolClient';
|
import { generateRandomId } from '~/lib/replay/ReplayProtocolClient';
|
||||||
import { workbenchStore } from '~/lib/stores/workbench';
|
import { workbenchStore } from '~/lib/stores/workbench';
|
||||||
import { databaseGetChatDeploySettings, databaseUpdateChatDeploySettings } from '~/lib/persistence/db';
|
import { chatStore } from '~/lib/stores/chat';
|
||||||
import { currentChatId } from '~/lib/persistence/useChatHistory';
|
import { database } from '~/lib/persistence/db';
|
||||||
import { deployRepository } from '~/lib/replay/Deploy';
|
import { deployRepository } from '~/lib/replay/Deploy';
|
||||||
|
|
||||||
ReactModal.setAppElement('#root');
|
ReactModal.setAppElement('#root');
|
||||||
@ -25,13 +25,13 @@ export function DeployChatButton() {
|
|||||||
const [status, setStatus] = useState<DeployStatus>(DeployStatus.NotStarted);
|
const [status, setStatus] = useState<DeployStatus>(DeployStatus.NotStarted);
|
||||||
|
|
||||||
const handleOpenModal = async () => {
|
const handleOpenModal = async () => {
|
||||||
const chatId = currentChatId.get();
|
const chatId = chatStore.currentChat.get()?.id;
|
||||||
if (!chatId) {
|
if (!chatId) {
|
||||||
toast.error('No chat open');
|
toast.error('No chat open');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingSettings = await databaseGetChatDeploySettings(chatId);
|
const existingSettings = await database.getChatDeploySettings(chatId);
|
||||||
|
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
setStatus(DeployStatus.NotStarted);
|
setStatus(DeployStatus.NotStarted);
|
||||||
@ -46,7 +46,7 @@ export function DeployChatButton() {
|
|||||||
const handleDeploy = async () => {
|
const handleDeploy = async () => {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const chatId = currentChatId.get();
|
const chatId = chatStore.currentChat.get()?.id;
|
||||||
if (!chatId) {
|
if (!chatId) {
|
||||||
setError('No chat open');
|
setError('No chat open');
|
||||||
return;
|
return;
|
||||||
@ -100,7 +100,7 @@ export function DeployChatButton() {
|
|||||||
setStatus(DeployStatus.Started);
|
setStatus(DeployStatus.Started);
|
||||||
|
|
||||||
// Write out to the database before we start trying to deploy.
|
// Write out to the database before we start trying to deploy.
|
||||||
await databaseUpdateChatDeploySettings(chatId, deploySettings);
|
await database.updateChatDeploySettings(chatId, deploySettings);
|
||||||
|
|
||||||
console.log('DeploymentStarting', repositoryId, deploySettings);
|
console.log('DeploymentStarting', repositoryId, deploySettings);
|
||||||
|
|
||||||
@ -135,7 +135,7 @@ export function DeployChatButton() {
|
|||||||
setStatus(DeployStatus.Succeeded);
|
setStatus(DeployStatus.Succeeded);
|
||||||
|
|
||||||
// Update the database with the new settings.
|
// Update the database with the new settings.
|
||||||
await databaseUpdateChatDeploySettings(chatId, newSettings);
|
await database.updateChatDeploySettings(chatId, newSettings);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -10,13 +10,13 @@ import { ClientAuth } from '~/components/auth/ClientAuth';
|
|||||||
import { DeployChatButton } from './DeployChatButton';
|
import { DeployChatButton } from './DeployChatButton';
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const chat = useStore(chatStore);
|
const chatStarted = useStore(chatStore.started);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={classNames('flex items-center justify-between p-5 border-b h-[var(--header-height)]', {
|
className={classNames('flex items-center justify-between p-5 border-b h-[var(--header-height)]', {
|
||||||
'border-transparent': !chat.started,
|
'border-transparent': !chatStarted,
|
||||||
'border-bolt-elements-borderColor': chat.started,
|
'border-bolt-elements-borderColor': chatStarted,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="flex flex-1 items-center gap-2 z-logo text-bolt-elements-textPrimary cursor-pointer">
|
<div className="flex flex-1 items-center gap-2 z-logo text-bolt-elements-textPrimary cursor-pointer">
|
||||||
@ -28,20 +28,20 @@ export function Header() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 flex items-center ">
|
<div className="flex-1 flex items-center ">
|
||||||
{chat.started && (
|
{chatStarted && (
|
||||||
<span className="flex-1 px-4 truncate text-center text-bolt-elements-textPrimary">
|
<span className="flex-1 px-4 truncate text-center text-bolt-elements-textPrimary">
|
||||||
<ClientOnly>{() => <ChatDescription />}</ClientOnly>
|
<ClientOnly>{() => <ChatDescription />}</ClientOnly>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{chat.started && (
|
{chatStarted && (
|
||||||
<span className="flex-1 px-4 truncate text-center text-bolt-elements-textPrimary">
|
<span className="flex-1 px-4 truncate text-center text-bolt-elements-textPrimary">
|
||||||
<ClientOnly>{() => <DeployChatButton />}</ClientOnly>
|
<ClientOnly>{() => <DeployChatButton />}</ClientOnly>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{chat.started && (
|
{chatStarted && (
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
{() => (
|
{() => (
|
||||||
<div className="mr-1">
|
<div className="mr-1">
|
||||||
|
@ -8,7 +8,7 @@ interface HeaderActionButtonsProps {}
|
|||||||
|
|
||||||
export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
||||||
const showWorkbench = useStore(workbenchStore.showWorkbench);
|
const showWorkbench = useStore(workbenchStore.showWorkbench);
|
||||||
const { showChat } = useStore(chatStore);
|
const showChat = useStore(chatStore.showChat);
|
||||||
|
|
||||||
const isSmallViewport = useViewport(1024);
|
const isSmallViewport = useViewport(1024);
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|||||||
disabled={!canHideChat || isSmallViewport} // expand button is disabled on mobile as it's not needed
|
disabled={!canHideChat || isSmallViewport} // expand button is disabled on mobile as it's not needed
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (canHideChat) {
|
if (canHideChat) {
|
||||||
chatStore.setKey('showChat', !showChat);
|
chatStore.showChat.set(!showChat);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -33,7 +33,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|||||||
active={showWorkbench}
|
active={showWorkbench}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (showWorkbench && !showChat) {
|
if (showWorkbench && !showChat) {
|
||||||
chatStore.setKey('showChat', true);
|
chatStore.showChat.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
workbenchStore.showWorkbench.set(!showWorkbench);
|
workbenchStore.showWorkbench.set(!showWorkbench);
|
||||||
|
@ -5,8 +5,8 @@ import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from
|
|||||||
import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
|
import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
|
||||||
import { SettingsWindow } from '~/components/settings/SettingsWindow';
|
import { SettingsWindow } from '~/components/settings/SettingsWindow';
|
||||||
import { SettingsButton } from '~/components/ui/SettingsButton';
|
import { SettingsButton } from '~/components/ui/SettingsButton';
|
||||||
import { deleteById, getAllChats, currentChatId } from '~/lib/persistence';
|
import { database, type ChatContents } from '~/lib/persistence/db';
|
||||||
import type { ChatContents } from '~/lib/persistence/db';
|
import { chatStore } from '~/lib/stores/chat';
|
||||||
import { cubicEasingFn } from '~/utils/easings';
|
import { cubicEasingFn } from '~/utils/easings';
|
||||||
import { logger } from '~/utils/logger';
|
import { logger } from '~/utils/logger';
|
||||||
import { HistoryItem } from './HistoryItem';
|
import { HistoryItem } from './HistoryItem';
|
||||||
@ -49,7 +49,8 @@ export const Menu = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const loadEntries = useCallback(() => {
|
const loadEntries = useCallback(() => {
|
||||||
getAllChats()
|
database
|
||||||
|
.getAllChats()
|
||||||
.then(setList)
|
.then(setList)
|
||||||
.catch((error) => toast.error(error.message));
|
.catch((error) => toast.error(error.message));
|
||||||
}, []);
|
}, []);
|
||||||
@ -57,11 +58,12 @@ export const Menu = () => {
|
|||||||
const deleteItem = useCallback((event: React.UIEvent, item: ChatContents) => {
|
const deleteItem = useCallback((event: React.UIEvent, item: ChatContents) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
deleteById(item.id)
|
database
|
||||||
|
.deleteChat(item.id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
loadEntries();
|
loadEntries();
|
||||||
|
|
||||||
if (currentChatId.get() === item.id) {
|
if (chatStore.currentChat.get()?.id === item.id) {
|
||||||
// hard page navigation to clear the stores
|
// hard page navigation to clear the stores
|
||||||
window.location.pathname = '/';
|
window.location.pathname = '/';
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { useStore } from '@nanostores/react';
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { currentChatId, currentChatTitle, getChatContents, handleChatTitleUpdate } from '~/lib/persistence';
|
import { chatStore } from '~/lib/stores/chat';
|
||||||
|
import { database } from '~/lib/persistence/db';
|
||||||
|
import { handleChatTitleUpdate } from '~/lib/persistence/useChatHistory';
|
||||||
|
|
||||||
interface EditChatDescriptionOptions {
|
interface EditChatDescriptionOptions {
|
||||||
initialTitle?: string;
|
initialTitle?: string;
|
||||||
@ -32,18 +33,19 @@ type EditChatDescriptionHook = {
|
|||||||
* @returns {EditChatDescriptionHook} Methods and state for managing description edits.
|
* @returns {EditChatDescriptionHook} Methods and state for managing description edits.
|
||||||
*/
|
*/
|
||||||
export function useEditChatTitle({
|
export function useEditChatTitle({
|
||||||
initialTitle = currentChatTitle.get()!,
|
initialTitle = chatStore.currentChat.get()?.title,
|
||||||
customChatId,
|
customChatId,
|
||||||
}: EditChatDescriptionOptions): EditChatDescriptionHook {
|
}: EditChatDescriptionOptions): EditChatDescriptionHook {
|
||||||
const chatIdFromStore = useStore(currentChatId);
|
const currentChat = chatStore.currentChat.get();
|
||||||
|
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [currentTitle, setCurrentTitle] = useState(initialTitle);
|
const [currentTitle, setCurrentTitle] = useState(initialTitle);
|
||||||
|
|
||||||
const [chatId, setChatId] = useState<string>();
|
const [chatId, setChatId] = useState<string>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setChatId(customChatId || chatIdFromStore);
|
setChatId(customChatId || currentChat?.id);
|
||||||
}, [customChatId, chatIdFromStore]);
|
}, [customChatId, currentChat]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentTitle(initialTitle);
|
setCurrentTitle(initialTitle);
|
||||||
}, [initialTitle]);
|
}, [initialTitle]);
|
||||||
@ -60,7 +62,7 @@ export function useEditChatTitle({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const chat = await getChatContents(chatId);
|
const chat = await database.getChatContents(chatId);
|
||||||
return chat?.title || initialTitle;
|
return chat?.title || initialTitle;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch latest description:', error);
|
console.error('Failed to fetch latest description:', error);
|
||||||
@ -104,6 +106,10 @@ export function useEditChatTitle({
|
|||||||
async (event: React.FormEvent) => {
|
async (event: React.FormEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!currentTitle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isValidTitle(currentTitle)) {
|
if (!isValidTitle(currentTitle)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -140,7 +146,7 @@ export function useEditChatTitle({
|
|||||||
handleBlur,
|
handleBlur,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
handleKeyDown,
|
handleKeyDown,
|
||||||
currentTitle,
|
currentTitle: currentTitle!,
|
||||||
toggleEditMode,
|
toggleEditMode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,11 @@ import { useStore } from '@nanostores/react';
|
|||||||
import { TooltipProvider } from '@radix-ui/react-tooltip';
|
import { TooltipProvider } from '@radix-ui/react-tooltip';
|
||||||
import WithTooltip from '~/components/ui/Tooltip';
|
import WithTooltip from '~/components/ui/Tooltip';
|
||||||
import { useEditChatTitle } from '~/lib/hooks/useEditChatDescription';
|
import { useEditChatTitle } from '~/lib/hooks/useEditChatDescription';
|
||||||
import { currentChatTitle } from '~/lib/persistence';
|
import { chatStore } from '~/lib/stores/chat';
|
||||||
|
|
||||||
export function ChatDescription() {
|
export function ChatDescription() {
|
||||||
const initialTitle = useStore(currentChatTitle)!;
|
const currentChat = useStore(chatStore.currentChat);
|
||||||
|
const initialTitle = currentChat?.title;
|
||||||
|
|
||||||
const { editing, handleChange, handleBlur, handleSubmit, handleKeyDown, currentTitle, toggleEditMode } =
|
const { editing, handleChange, handleBlur, handleSubmit, handleKeyDown, currentTitle, toggleEditMode } =
|
||||||
useEditChatTitle({
|
useEditChatTitle({
|
||||||
|
@ -15,6 +15,8 @@ export interface ChatContents {
|
|||||||
title: string;
|
title: string;
|
||||||
repositoryId: string | undefined;
|
repositoryId: string | undefined;
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
|
lastProtocolChatId: string | undefined;
|
||||||
|
lastProtocolChatResponseId: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function databaseRowToChatContents(d: any): ChatContents {
|
function databaseRowToChatContents(d: any): ChatContents {
|
||||||
@ -25,6 +27,8 @@ function databaseRowToChatContents(d: any): ChatContents {
|
|||||||
title: d.title,
|
title: d.title,
|
||||||
messages: d.messages,
|
messages: d.messages,
|
||||||
repositoryId: d.repository_id,
|
repositoryId: d.repository_id,
|
||||||
|
lastProtocolChatId: d.last_protocol_chat_id,
|
||||||
|
lastProtocolChatResponseId: d.last_protocol_chat_response_id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,7 +50,7 @@ function setLocalChats(chats: ChatContents[] | undefined): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllChats(): Promise<ChatContents[]> {
|
async function getAllChats(): Promise<ChatContents[]> {
|
||||||
const userId = await getCurrentUserId();
|
const userId = await getCurrentUserId();
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
@ -62,7 +66,7 @@ export async function getAllChats(): Promise<ChatContents[]> {
|
|||||||
return data.map(databaseRowToChatContents);
|
return data.map(databaseRowToChatContents);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function syncLocalChats(): Promise<void> {
|
async function syncLocalChats(): Promise<void> {
|
||||||
const userId = await getCurrentUserId();
|
const userId = await getCurrentUserId();
|
||||||
const localChats = getLocalChats();
|
const localChats = getLocalChats();
|
||||||
|
|
||||||
@ -70,7 +74,7 @@ export async function syncLocalChats(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
for (const chat of localChats) {
|
for (const chat of localChats) {
|
||||||
if (chat.title) {
|
if (chat.title) {
|
||||||
await setChatContents(chat.id, chat.title, chat.messages);
|
await setChatContents(chat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setLocalChats(undefined);
|
setLocalChats(undefined);
|
||||||
@ -80,31 +84,29 @@ export async function syncLocalChats(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setChatContents(id: string, title: string, messages: Message[]): Promise<void> {
|
async function setChatContents(chat: ChatContents) {
|
||||||
const userId = await getCurrentUserId();
|
const userId = await getCurrentUserId();
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
const localChats = getLocalChats().filter((c) => c.id != id);
|
const localChats = getLocalChats().filter((c) => c.id != chat.id);
|
||||||
localChats.push({
|
localChats.push({
|
||||||
id,
|
...chat,
|
||||||
title,
|
|
||||||
messages,
|
|
||||||
repositoryId: getMessagesRepositoryId(messages),
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
setLocalChats(localChats);
|
setLocalChats(localChats);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const repositoryId = getMessagesRepositoryId(messages);
|
const repositoryId = getMessagesRepositoryId(chat.messages);
|
||||||
|
|
||||||
const { error } = await getSupabase().from('chats').upsert({
|
const { error } = await getSupabase().from('chats').upsert({
|
||||||
id,
|
id: chat.id,
|
||||||
messages,
|
messages: chat.messages,
|
||||||
title,
|
title: chat.title,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
repository_id: repositoryId,
|
repository_id: repositoryId,
|
||||||
|
last_protocol_chat_id: chat.lastProtocolChatId,
|
||||||
|
last_protocol_chat_response_id: chat.lastProtocolChatResponseId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@ -112,7 +114,7 @@ export async function setChatContents(id: string, title: string, messages: Messa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getChatPublicData(id: string): Promise<{ repositoryId: string; title: string }> {
|
async function getChatPublicData(id: string): Promise<{ repositoryId: string; title: string }> {
|
||||||
const { data, error } = await getSupabase().rpc('get_chat_public_data', { chat_id: id });
|
const { data, error } = await getSupabase().rpc('get_chat_public_data', { chat_id: id });
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@ -129,7 +131,7 @@ export async function getChatPublicData(id: string): Promise<{ repositoryId: str
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getChatContents(id: string): Promise<ChatContents | undefined> {
|
async function getChatContents(id: string): Promise<ChatContents | undefined> {
|
||||||
const userId = await getCurrentUserId();
|
const userId = await getCurrentUserId();
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
@ -149,7 +151,7 @@ export async function getChatContents(id: string): Promise<ChatContents | undefi
|
|||||||
return databaseRowToChatContents(data[0]);
|
return databaseRowToChatContents(data[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteById(id: string): Promise<void> {
|
async function deleteChat(id: string): Promise<void> {
|
||||||
const userId = await getCurrentUserId();
|
const userId = await getCurrentUserId();
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
@ -165,13 +167,21 @@ export async function deleteById(id: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createChat(title: string, messages: Message[]): Promise<string> {
|
async function createChat(title: string, messages: Message[]): Promise<ChatContents> {
|
||||||
const id = uuid();
|
const contents = {
|
||||||
await setChatContents(id, title, messages);
|
id: uuid(),
|
||||||
return id;
|
title,
|
||||||
|
messages,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
repositoryId: getMessagesRepositoryId(messages),
|
||||||
|
lastProtocolChatId: undefined,
|
||||||
|
lastProtocolChatResponseId: undefined,
|
||||||
|
};
|
||||||
|
await setChatContents(contents);
|
||||||
|
return contents;
|
||||||
}
|
}
|
||||||
|
async function updateChatTitle(id: string, title: string): Promise<void> {
|
||||||
export async function databaseUpdateChatTitle(id: string, title: string): Promise<void> {
|
|
||||||
const chat = await getChatContents(id);
|
const chat = await getChatContents(id);
|
||||||
assert(chat, 'Unknown chat');
|
assert(chat, 'Unknown chat');
|
||||||
|
|
||||||
@ -179,10 +189,10 @@ export async function databaseUpdateChatTitle(id: string, title: string): Promis
|
|||||||
throw new Error('Title cannot be empty');
|
throw new Error('Title cannot be empty');
|
||||||
}
|
}
|
||||||
|
|
||||||
await setChatContents(id, title, chat.messages);
|
await setChatContents({ ...chat, title });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function databaseGetChatDeploySettings(id: string): Promise<DeploySettingsDatabase | undefined> {
|
async function getChatDeploySettings(id: string): Promise<DeploySettingsDatabase | undefined> {
|
||||||
console.log('DatabaseGetChatDeploySettingsStart', id);
|
console.log('DatabaseGetChatDeploySettingsStart', id);
|
||||||
|
|
||||||
const { data, error } = await getSupabase().from('chats').select('deploy_settings').eq('id', id);
|
const { data, error } = await getSupabase().from('chats').select('deploy_settings').eq('id', id);
|
||||||
@ -200,13 +210,39 @@ export async function databaseGetChatDeploySettings(id: string): Promise<DeployS
|
|||||||
return data[0].deploy_settings;
|
return data[0].deploy_settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function databaseUpdateChatDeploySettings(
|
async function updateChatDeploySettings(id: string, deploySettings: DeploySettingsDatabase): Promise<void> {
|
||||||
id: string,
|
|
||||||
deploySettings: DeploySettingsDatabase,
|
|
||||||
): Promise<void> {
|
|
||||||
const { error } = await getSupabase().from('chats').update({ deploy_settings: deploySettings }).eq('id', id);
|
const { error } = await getSupabase().from('chats').update({ deploy_settings: deploySettings }).eq('id', id);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
throw error;
|
console.error('DatabaseUpdateChatDeploySettingsError', id, deploySettings, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateChatLastMessage(
|
||||||
|
id: string,
|
||||||
|
protocolChatId: string | null,
|
||||||
|
protocolChatResponseId: string | null,
|
||||||
|
): Promise<void> {
|
||||||
|
const { error } = await getSupabase()
|
||||||
|
.from('chats')
|
||||||
|
.update({ last_protocol_chat_id: protocolChatId, last_protocol_chat_response_id: protocolChatResponseId })
|
||||||
|
.eq('id', id);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('DatabaseUpdateChatLastMessageError', id, protocolChatId, protocolChatResponseId, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const database = {
|
||||||
|
getAllChats,
|
||||||
|
syncLocalChats,
|
||||||
|
setChatContents,
|
||||||
|
getChatPublicData,
|
||||||
|
getChatContents,
|
||||||
|
deleteChat,
|
||||||
|
createChat,
|
||||||
|
updateChatTitle,
|
||||||
|
getChatDeploySettings,
|
||||||
|
updateChatDeploySettings,
|
||||||
|
updateChatLastMessage,
|
||||||
|
};
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
import { useLoaderData } from '@remix-run/react';
|
import { useLoaderData } from '@remix-run/react';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { atom } from 'nanostores';
|
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { logStore } from '~/lib/stores/logs'; // Import logStore
|
import { logStore } from '~/lib/stores/logs'; // Import logStore
|
||||||
import { createChat, getChatContents, setChatContents, getChatPublicData, databaseUpdateChatTitle } from './db';
|
import { chatStore } from '~/lib/stores/chat';
|
||||||
|
import { database } from './db';
|
||||||
import { loadProblem } from '~/components/chat/LoadProblemButton';
|
import { loadProblem } from '~/components/chat/LoadProblemButton';
|
||||||
import { createMessagesForRepository, type Message } from './message';
|
import { createMessagesForRepository, type Message } from './message';
|
||||||
import { debounce } from '~/utils/debounce';
|
import { debounce } from '~/utils/debounce';
|
||||||
|
|
||||||
// These must be kept in sync.
|
export interface ResumeChatInfo {
|
||||||
export const currentChatId = atom<string | undefined>(undefined);
|
protocolChatId: string;
|
||||||
export const currentChatTitle = atom<string | undefined>(undefined);
|
protocolChatResponseId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function useChatHistory() {
|
export function useChatHistory() {
|
||||||
const {
|
const {
|
||||||
@ -20,11 +21,12 @@ export function useChatHistory() {
|
|||||||
} = useLoaderData<{ id?: string; problemId?: string; repositoryId?: string }>() ?? {};
|
} = useLoaderData<{ id?: string; problemId?: string; repositoryId?: string }>() ?? {};
|
||||||
|
|
||||||
const [initialMessages, setInitialMessages] = useState<Message[]>([]);
|
const [initialMessages, setInitialMessages] = useState<Message[]>([]);
|
||||||
|
const [resumeChat, setResumeChat] = useState<ResumeChatInfo | undefined>(undefined);
|
||||||
const [ready, setReady] = useState<boolean>(!mixedId && !problemId && !repositoryId);
|
const [ready, setReady] = useState<boolean>(!mixedId && !problemId && !repositoryId);
|
||||||
|
|
||||||
const importChat = async (title: string, messages: Message[]) => {
|
const importChat = async (title: string, messages: Message[]) => {
|
||||||
try {
|
try {
|
||||||
const newId = await createChat(title, messages);
|
const newId = await database.createChat(title, messages);
|
||||||
window.location.href = `/chat/${newId}`;
|
window.location.href = `/chat/${newId}`;
|
||||||
toast.success('Chat imported successfully');
|
toast.success('Chat imported successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -43,23 +45,32 @@ export function useChatHistory() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const debouncedSetChatContents = debounce(async (messages: Message[]) => {
|
const debouncedSetChatContents = debounce(async (messages: Message[]) => {
|
||||||
await setChatContents(currentChatId.get() as string, currentChatTitle.get() as string, messages);
|
const chat = chatStore.currentChat.get();
|
||||||
|
if (!chat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await database.setChatContents({ ...chat, messages });
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
if (mixedId) {
|
if (mixedId) {
|
||||||
const chatContents = await getChatContents(mixedId);
|
const chatContents = await database.getChatContents(mixedId);
|
||||||
if (chatContents) {
|
if (chatContents) {
|
||||||
setInitialMessages(chatContents.messages);
|
setInitialMessages(chatContents.messages);
|
||||||
currentChatTitle.set(chatContents.title);
|
chatStore.currentChat.set(chatContents);
|
||||||
currentChatId.set(mixedId);
|
if (chatContents.lastProtocolChatId && chatContents.lastProtocolChatResponseId) {
|
||||||
|
setResumeChat({
|
||||||
|
protocolChatId: chatContents.lastProtocolChatId,
|
||||||
|
protocolChatResponseId: chatContents.lastProtocolChatResponseId,
|
||||||
|
});
|
||||||
|
}
|
||||||
setReady(true);
|
setReady(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicData = await getChatPublicData(mixedId);
|
const publicData = await database.getChatPublicData(mixedId);
|
||||||
const messages = createMessagesForRepository(publicData.title, publicData.repositoryId);
|
const messages = createMessagesForRepository(publicData.title, publicData.repositoryId);
|
||||||
await importChat(publicData.title, messages);
|
await importChat(publicData.title, messages);
|
||||||
} else if (problemId) {
|
} else if (problemId) {
|
||||||
@ -79,16 +90,17 @@ export function useChatHistory() {
|
|||||||
return {
|
return {
|
||||||
ready,
|
ready,
|
||||||
initialMessages,
|
initialMessages,
|
||||||
|
resumeChat,
|
||||||
storeMessageHistory: async (messages: Message[]) => {
|
storeMessageHistory: async (messages: Message[]) => {
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentChatId.get()) {
|
if (!chatStore.currentChat.get()) {
|
||||||
const id = await createChat('New Chat', initialMessages);
|
const title = 'New Chat';
|
||||||
currentChatId.set(id);
|
const chat = await database.createChat(title, initialMessages);
|
||||||
currentChatTitle.set('New Chat');
|
chatStore.currentChat.set(chat);
|
||||||
navigateChat(id);
|
navigateChat(chat.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
debouncedSetChatContents(messages);
|
debouncedSetChatContents(messages);
|
||||||
@ -110,8 +122,9 @@ function navigateChat(nextId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function handleChatTitleUpdate(id: string, title: string) {
|
export async function handleChatTitleUpdate(id: string, title: string) {
|
||||||
await databaseUpdateChatTitle(id, title);
|
await database.updateChatTitle(id, title);
|
||||||
if (currentChatId.get() == id) {
|
const currentChat = chatStore.currentChat.get();
|
||||||
currentChatTitle.set(title);
|
if (currentChat?.id == id) {
|
||||||
|
chatStore.currentChat.set({ ...currentChat, title });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,9 @@ import { simulationDataVersion } from './SimulationData';
|
|||||||
import { assert, generateRandomId, ProtocolClient } from './ReplayProtocolClient';
|
import { assert, generateRandomId, ProtocolClient } from './ReplayProtocolClient';
|
||||||
import { updateDevelopmentServer } from './DevelopmentServer';
|
import { updateDevelopmentServer } from './DevelopmentServer';
|
||||||
import type { Message } from '~/lib/persistence/message';
|
import type { Message } from '~/lib/persistence/message';
|
||||||
|
import { database } from '~/lib/persistence/db';
|
||||||
|
import { chatStore } from '~/lib/stores/chat';
|
||||||
|
import { debounce } from '~/utils/debounce';
|
||||||
|
|
||||||
function createRepositoryIdPacket(repositoryId: string): SimulationPacket {
|
function createRepositoryIdPacket(repositoryId: string): SimulationPacket {
|
||||||
return {
|
return {
|
||||||
@ -28,7 +31,7 @@ interface ChatReferenceElement {
|
|||||||
|
|
||||||
export type ChatReference = ChatReferenceElement;
|
export type ChatReference = ChatReferenceElement;
|
||||||
|
|
||||||
interface ChatMessageCallbacks {
|
export interface ChatMessageCallbacks {
|
||||||
onResponsePart: (message: Message) => void;
|
onResponsePart: (message: Message) => void;
|
||||||
onTitle: (title: string) => void;
|
onTitle: (title: string) => void;
|
||||||
onStatus: (status: string) => void;
|
onStatus: (status: string) => void;
|
||||||
@ -183,6 +186,10 @@ class ChatManager {
|
|||||||
|
|
||||||
console.log('ChatSendMessage', new Date().toISOString(), chatId, JSON.stringify({ messages, references }));
|
console.log('ChatSendMessage', new Date().toISOString(), chatId, JSON.stringify({ messages, references }));
|
||||||
|
|
||||||
|
const id = chatStore.currentChat.get()?.id;
|
||||||
|
assert(id, 'Expected chat ID');
|
||||||
|
database.updateChatLastMessage(id, chatId, responseId);
|
||||||
|
|
||||||
await this.client.sendCommand({
|
await this.client.sendCommand({
|
||||||
method: 'Nut.sendChatMessage',
|
method: 'Nut.sendChatMessage',
|
||||||
params: { chatId, responseId, messages, references },
|
params: { chatId, responseId, messages, references },
|
||||||
@ -228,10 +235,10 @@ function startChat(repositoryId: string | undefined, pageData: SimulationData) {
|
|||||||
* Called when the repository has changed. We'll start a new chat
|
* Called when the repository has changed. We'll start a new chat
|
||||||
* and update the remote development server.
|
* and update the remote development server.
|
||||||
*/
|
*/
|
||||||
export function simulationRepositoryUpdated(repositoryId: string) {
|
export const simulationRepositoryUpdated = debounce((repositoryId: string) => {
|
||||||
startChat(repositoryId, []);
|
startChat(repositoryId, []);
|
||||||
updateDevelopmentServer(repositoryId);
|
updateDevelopmentServer(repositoryId);
|
||||||
}
|
}, 500);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Called when the page gathering interaction data has been reloaded. We'll
|
* Called when the page gathering interaction data has been reloaded. We'll
|
||||||
@ -303,3 +310,36 @@ export async function sendChatMessage(
|
|||||||
|
|
||||||
await gChatManager.sendChatMessage(messages, references, callbacks);
|
await gChatManager.sendChatMessage(messages, references, callbacks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function resumeChatMessage(chatId: string, chatResponseId: string, callbacks: ChatMessageCallbacks) {
|
||||||
|
const client = new ProtocolClient();
|
||||||
|
await client.initialize();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const removeResponseListener = client.listenForMessage(
|
||||||
|
'Nut.chatResponsePart',
|
||||||
|
({ message }: { message: Message }) => {
|
||||||
|
callbacks.onResponsePart(message);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeTitleListener = client.listenForMessage('Nut.chatTitle', ({ title }: { title: string }) => {
|
||||||
|
callbacks.onTitle(title);
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeStatusListener = client.listenForMessage('Nut.chatStatus', ({ status }: { status: string }) => {
|
||||||
|
callbacks.onStatus(status);
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.sendCommand({
|
||||||
|
method: 'Nut.resumeChatMessage',
|
||||||
|
params: { chatId, responseId: chatResponseId },
|
||||||
|
});
|
||||||
|
|
||||||
|
removeResponseListener();
|
||||||
|
removeTitleListener();
|
||||||
|
removeStatusListener();
|
||||||
|
} finally {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,7 +4,7 @@ import type { User, Session } from '@supabase/supabase-js';
|
|||||||
import { logStore } from './logs';
|
import { logStore } from './logs';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { isAuthenticated } from '~/lib/supabase/client';
|
import { isAuthenticated } from '~/lib/supabase/client';
|
||||||
import { syncLocalChats } from '~/lib/persistence/db';
|
import { database } from '~/lib/persistence/db';
|
||||||
|
|
||||||
export const userStore = atom<User | null>(null);
|
export const userStore = atom<User | null>(null);
|
||||||
export const sessionStore = atom<Session | null>(null);
|
export const sessionStore = atom<Session | null>(null);
|
||||||
@ -70,7 +70,7 @@ export async function initializeAuth() {
|
|||||||
logStore.logSystem('User signed out');
|
logStore.logSystem('User signed out');
|
||||||
}
|
}
|
||||||
|
|
||||||
await syncLocalChats();
|
await database.syncLocalChats();
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
import { map } from 'nanostores';
|
import { atom } from 'nanostores';
|
||||||
|
import type { ChatContents } from '~/lib/persistence/db';
|
||||||
|
|
||||||
export const chatStore = map({
|
export class ChatStore {
|
||||||
started: false,
|
currentChat = atom<ChatContents | undefined>(undefined);
|
||||||
aborted: false,
|
|
||||||
showChat: true,
|
started = atom<boolean>(false);
|
||||||
});
|
aborted = atom<boolean>(false);
|
||||||
|
showChat = atom<boolean>(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const chatStore = new ChatStore();
|
||||||
|
@ -8,7 +8,9 @@ CREATE TABLE IF NOT EXISTS public.chats (
|
|||||||
repository_id UUID,
|
repository_id UUID,
|
||||||
messages JSONB DEFAULT '{}',
|
messages JSONB DEFAULT '{}',
|
||||||
deploy_settings JSONB DEFAULT '{}',
|
deploy_settings JSONB DEFAULT '{}',
|
||||||
deleted BOOLEAN DEFAULT FALSE
|
deleted BOOLEAN DEFAULT FALSE,
|
||||||
|
last_protocol_chat_id UUID,
|
||||||
|
last_protocol_chat_response_id TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Create updated_at trigger for chats table
|
-- Create updated_at trigger for chats table
|
||||||
|
Loading…
Reference in New Issue
Block a user