mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
Fix UX problems sharing chats and using Nut without being logged in (#87)
This commit is contained in:
parent
ac1858b84e
commit
8fb01c5586
@ -32,7 +32,8 @@ interface BaseChatProps {
|
|||||||
scrollRef?: RefCallback<HTMLDivElement> | undefined;
|
scrollRef?: RefCallback<HTMLDivElement> | undefined;
|
||||||
showChat?: boolean;
|
showChat?: boolean;
|
||||||
chatStarted?: boolean;
|
chatStarted?: boolean;
|
||||||
isStreaming?: boolean;
|
hasPendingMessage?: boolean;
|
||||||
|
pendingMessageStatus?: string;
|
||||||
messages?: Message[];
|
messages?: Message[];
|
||||||
enhancingPrompt?: boolean;
|
enhancingPrompt?: boolean;
|
||||||
promptEnhanced?: boolean;
|
promptEnhanced?: boolean;
|
||||||
@ -61,7 +62,8 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
scrollRef,
|
scrollRef,
|
||||||
showChat = true,
|
showChat = true,
|
||||||
chatStarted = false,
|
chatStarted = false,
|
||||||
isStreaming = false,
|
hasPendingMessage = false,
|
||||||
|
pendingMessageStatus = '',
|
||||||
input = '',
|
input = '',
|
||||||
handleInputChange,
|
handleInputChange,
|
||||||
|
|
||||||
@ -211,7 +213,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const showApproveChange = (() => {
|
const showApproveChange = (() => {
|
||||||
if (isStreaming) {
|
if (hasPendingMessage) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,7 +290,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (isStreaming) {
|
if (hasPendingMessage) {
|
||||||
handleStop?.();
|
handleStop?.();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -316,10 +318,10 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
{() => (
|
{() => (
|
||||||
<SendButton
|
<SendButton
|
||||||
show={(isStreaming || input.length > 0 || uploadedFiles.length > 0) && chatStarted}
|
show={(hasPendingMessage || input.length > 0 || uploadedFiles.length > 0) && chatStarted}
|
||||||
isStreaming={isStreaming}
|
hasPendingMessage={hasPendingMessage}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
if (isStreaming) {
|
if (hasPendingMessage) {
|
||||||
handleStop?.();
|
handleStop?.();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -341,7 +343,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
isListening={isListening}
|
isListening={isListening}
|
||||||
onStart={startListening}
|
onStart={startListening}
|
||||||
onStop={stopListening}
|
onStop={stopListening}
|
||||||
disabled={isStreaming}
|
disabled={hasPendingMessage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{input.length > 3 ? (
|
{input.length > 3 ? (
|
||||||
@ -386,7 +388,8 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
ref={messageRef}
|
ref={messageRef}
|
||||||
className="flex flex-col w-full flex-1 max-w-chat pb-6 mx-auto z-1"
|
className="flex flex-col w-full flex-1 max-w-chat pb-6 mx-auto z-1"
|
||||||
messages={messages}
|
messages={messages}
|
||||||
isStreaming={isStreaming}
|
hasPendingMessage={hasPendingMessage}
|
||||||
|
pendingMessageStatus={pendingMessageStatus}
|
||||||
onRewind={onRewind}
|
onRewind={onRewind}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
@ -470,7 +473,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
{!chatStarted && <div className="flex justify-center gap-2">{ImportButtons(importChat)}</div>}
|
{!chatStarted && <div className="flex justify-center gap-2">{ImportButtons(importChat)}</div>}
|
||||||
{!chatStarted &&
|
{!chatStarted &&
|
||||||
ExamplePrompts((event, messageInput) => {
|
ExamplePrompts((event, messageInput) => {
|
||||||
if (isStreaming) {
|
if (hasPendingMessage) {
|
||||||
handleStop?.();
|
handleStop?.();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -7,14 +7,13 @@ 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 { useChatHistory } from '~/lib/persistence';
|
import { currentChatId, handleChatTitleUpdate, useChatHistory } 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';
|
||||||
import { BaseChat } from './BaseChat';
|
import { BaseChat } from './BaseChat';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import { useSearchParams } from '@remix-run/react';
|
import { useSearchParams } from '@remix-run/react';
|
||||||
import { createSampler } from '~/utils/sampler';
|
|
||||||
import {
|
import {
|
||||||
simulationAddData,
|
simulationAddData,
|
||||||
simulationFinishData,
|
simulationFinishData,
|
||||||
@ -33,6 +32,7 @@ import type { RejectChangeData } from './ApproveChange';
|
|||||||
import { assert, generateRandomId } from '~/lib/replay/ReplayProtocolClient';
|
import { assert, generateRandomId } from '~/lib/replay/ReplayProtocolClient';
|
||||||
import { getMessagesRepositoryId, getPreviousRepositoryId, type Message } from '~/lib/persistence/message';
|
import { getMessagesRepositoryId, getPreviousRepositoryId, type Message } from '~/lib/persistence/message';
|
||||||
import { useAuthStatus } from '~/lib/stores/auth';
|
import { useAuthStatus } from '~/lib/stores/auth';
|
||||||
|
import { debounce } from '~/utils/debounce';
|
||||||
|
|
||||||
const toastAnimation = cssTransition({
|
const toastAnimation = cssTransition({
|
||||||
enter: 'animated fadeInRight',
|
enter: 'animated fadeInRight',
|
||||||
@ -111,24 +111,9 @@ export function Chat() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const processSampledMessages = createSampler(
|
|
||||||
(options: {
|
|
||||||
messages: Message[];
|
|
||||||
initialMessages: Message[];
|
|
||||||
storeMessageHistory: (messages: Message[]) => Promise<void>;
|
|
||||||
}) => {
|
|
||||||
const { messages, initialMessages, storeMessageHistory } = options;
|
|
||||||
|
|
||||||
if (messages.length > initialMessages.length) {
|
|
||||||
storeMessageHistory(messages).catch((error) => toast.error(error.message));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
50,
|
|
||||||
);
|
|
||||||
|
|
||||||
interface ChatProps {
|
interface ChatProps {
|
||||||
initialMessages: Message[];
|
initialMessages: Message[];
|
||||||
storeMessageHistory: (messages: Message[]) => Promise<void>;
|
storeMessageHistory: (messages: Message[]) => void;
|
||||||
importChat: (description: string, messages: Message[]) => Promise<void>;
|
importChat: (description: string, messages: Message[]) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,8 +141,10 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory, importChat
|
|||||||
* This is set when the user has triggered a chat message and the response hasn't finished
|
* This is set when the user has triggered a chat message and the response hasn't finished
|
||||||
* being generated.
|
* being generated.
|
||||||
*/
|
*/
|
||||||
const [activeChatId, setActiveChatId] = useState<string | undefined>(undefined);
|
const [pendingMessageId, setPendingMessageId] = useState<string | undefined>(undefined);
|
||||||
const isLoading = activeChatId !== undefined;
|
|
||||||
|
// Last status we heard for the pending message.
|
||||||
|
const [pendingMessageStatus, setPendingMessageStatus] = useState('');
|
||||||
|
|
||||||
const [messages, setMessages] = useState<Message[]>(initialMessages);
|
const [messages, setMessages] = useState<Message[]>(initialMessages);
|
||||||
|
|
||||||
@ -190,18 +177,14 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory, importChat
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
processSampledMessages({
|
storeMessageHistory(messages);
|
||||||
messages,
|
}, [messages]);
|
||||||
initialMessages,
|
|
||||||
storeMessageHistory,
|
|
||||||
});
|
|
||||||
}, [messages, isLoading]);
|
|
||||||
|
|
||||||
const abort = () => {
|
const abort = () => {
|
||||||
stop();
|
stop();
|
||||||
gNumAborts++;
|
gNumAborts++;
|
||||||
chatStore.setKey('aborted', true);
|
chatStore.setKey('aborted', true);
|
||||||
setActiveChatId(undefined);
|
setPendingMessageId(undefined);
|
||||||
|
|
||||||
if (gActiveChatMessageTelemetry) {
|
if (gActiveChatMessageTelemetry) {
|
||||||
gActiveChatMessageTelemetry.abort('StopButtonClicked');
|
gActiveChatMessageTelemetry.abort('StopButtonClicked');
|
||||||
@ -242,7 +225,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 || isLoading) {
|
if (_input.length === 0 || pendingMessageId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,7 +245,7 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory, importChat
|
|||||||
}
|
}
|
||||||
|
|
||||||
const chatId = generateRandomId();
|
const chatId = generateRandomId();
|
||||||
setActiveChatId(chatId);
|
setPendingMessageId(chatId);
|
||||||
|
|
||||||
const userMessage: Message = {
|
const userMessage: Message = {
|
||||||
id: `user-${chatId}`,
|
id: `user-${chatId}`,
|
||||||
@ -331,6 +314,16 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory, importChat
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onChatTitle = (title: string) => {
|
||||||
|
console.log('ChatTitle', title);
|
||||||
|
handleChatTitleUpdate(currentChatId.get() as string, title);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChatStatus = debounce((status: string) => {
|
||||||
|
console.log('ChatStatus', status);
|
||||||
|
setPendingMessageStatus(status);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
const references: ChatReference[] = [];
|
const references: ChatReference[] = [];
|
||||||
|
|
||||||
const mouseData = getCurrentMouseData();
|
const mouseData = getCurrentMouseData();
|
||||||
@ -347,7 +340,11 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory, importChat
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sendChatMessage(newMessages, references, addResponseMessage);
|
await sendChatMessage(newMessages, references, {
|
||||||
|
onResponsePart: addResponseMessage,
|
||||||
|
onTitle: onChatTitle,
|
||||||
|
onStatus: onChatStatus,
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error('Error sending message');
|
toast.error('Error sending message');
|
||||||
console.error('Error sending message', e);
|
console.error('Error sending message', e);
|
||||||
@ -360,7 +357,7 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory, importChat
|
|||||||
gActiveChatMessageTelemetry.finish();
|
gActiveChatMessageTelemetry.finish();
|
||||||
clearActiveChat();
|
clearActiveChat();
|
||||||
|
|
||||||
setActiveChatId(undefined);
|
setPendingMessageId(undefined);
|
||||||
|
|
||||||
setInput('');
|
setInput('');
|
||||||
|
|
||||||
@ -493,7 +490,8 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory, importChat
|
|||||||
input={input}
|
input={input}
|
||||||
showChat={showChat}
|
showChat={showChat}
|
||||||
chatStarted={chatStarted}
|
chatStarted={chatStarted}
|
||||||
isStreaming={isLoading}
|
hasPendingMessage={pendingMessageId !== undefined}
|
||||||
|
pendingMessageStatus={pendingMessageStatus}
|
||||||
sendMessage={sendMessage}
|
sendMessage={sendMessage}
|
||||||
messageRef={messageRef}
|
messageRef={messageRef}
|
||||||
scrollRef={scrollRef}
|
scrollRef={scrollRef}
|
||||||
|
@ -8,7 +8,7 @@ import type { Message } from '~/lib/persistence/message';
|
|||||||
|
|
||||||
interface ImportFolderButtonProps {
|
interface ImportFolderButtonProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
importChat?: (description: string, messages: Message[]) => Promise<void>;
|
importChat?: (title: string, messages: Message[]) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ className, importChat }) => {
|
export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ className, importChat }) => {
|
||||||
@ -85,7 +85,7 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
|
|||||||
const messages = createChatFromFolder(folderName, repositoryId);
|
const messages = createChatFromFolder(folderName, repositoryId);
|
||||||
|
|
||||||
if (importChat) {
|
if (importChat) {
|
||||||
await importChat(folderName, [...messages]);
|
await importChat(folderName, messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
logStore.logSystem('Folder imported successfully', {
|
logStore.logSystem('Folder imported successfully', {
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { createChatFromFolder } from '~/utils/folderImport';
|
|
||||||
import { logStore } from '~/lib/stores/logs';
|
import { logStore } from '~/lib/stores/logs';
|
||||||
import { assert } from '~/lib/replay/ReplayProtocolClient';
|
import { assert } from '~/lib/replay/ReplayProtocolClient';
|
||||||
import type { NutProblem } from '~/lib/replay/Problems';
|
import type { NutProblem } from '~/lib/replay/Problems';
|
||||||
import { getProblem } from '~/lib/replay/Problems';
|
import { getProblem } from '~/lib/replay/Problems';
|
||||||
import type { Message } from '~/lib/persistence/message';
|
import { createMessagesForRepository, type Message } from '~/lib/persistence/message';
|
||||||
|
|
||||||
interface LoadProblemButtonProps {
|
interface LoadProblemButtonProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -48,8 +47,8 @@ export async function loadProblem(
|
|||||||
const { repositoryId, title: problemTitle } = problem;
|
const { repositoryId, title: problemTitle } = problem;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const messages = createChatFromFolder('problem', repositoryId);
|
const messages = createMessagesForRepository(`Problem: ${problemTitle}`, repositoryId);
|
||||||
await importChat(`Problem: ${problemTitle}`, [...messages]);
|
await importChat(`Problem: ${problemTitle}`, messages);
|
||||||
|
|
||||||
logStore.logSystem('Problem loaded successfully', {
|
logStore.logSystem('Problem loaded successfully', {
|
||||||
problemId,
|
problemId,
|
||||||
|
@ -7,13 +7,14 @@ import { MessageContents } from './MessageContents';
|
|||||||
interface MessagesProps {
|
interface MessagesProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
isStreaming?: boolean;
|
hasPendingMessage?: boolean;
|
||||||
|
pendingMessageStatus?: string;
|
||||||
messages?: Message[];
|
messages?: Message[];
|
||||||
onRewind?: (messageId: string) => void;
|
onRewind?: (messageId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => {
|
export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => {
|
||||||
const { id, isStreaming = false, messages = [], onRewind } = props;
|
const { id, hasPendingMessage = false, pendingMessageStatus = '', messages = [], onRewind } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id={id} ref={ref} className={props.className}>
|
<div id={id} ref={ref} className={props.className}>
|
||||||
@ -30,9 +31,10 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
|
|||||||
data-testid="message"
|
data-testid="message"
|
||||||
key={index}
|
key={index}
|
||||||
className={classNames('flex gap-4 p-6 w-full rounded-[calc(0.75rem-1px)]', {
|
className={classNames('flex gap-4 p-6 w-full rounded-[calc(0.75rem-1px)]', {
|
||||||
'bg-bolt-elements-messages-background': isUserMessage || !isStreaming || (isStreaming && !isLast),
|
'bg-bolt-elements-messages-background':
|
||||||
|
isUserMessage || !hasPendingMessage || (hasPendingMessage && !isLast),
|
||||||
'bg-gradient-to-b from-bolt-elements-messages-background from-30% to-transparent':
|
'bg-gradient-to-b from-bolt-elements-messages-background from-30% to-transparent':
|
||||||
isStreaming && isLast,
|
hasPendingMessage && isLast,
|
||||||
'mt-4': !isFirst,
|
'mt-4': !isFirst,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
@ -70,8 +72,11 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
: null}
|
: null}
|
||||||
{isStreaming && (
|
{hasPendingMessage && (
|
||||||
<div className="text-center w-full text-bolt-elements-textSecondary i-svg-spinners:3-dots-fade text-4xl mt-4"></div>
|
<div className="w-full text-bolt-elements-textSecondary flex items-center">
|
||||||
|
<span className="i-svg-spinners:3-dots-fade inline-block w-[1em] h-[1em] mr-2 text-4xl"></span>
|
||||||
|
<span className="text-lg">{pendingMessageStatus ? `${pendingMessageStatus}...` : ''}</span>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -2,7 +2,7 @@ import { AnimatePresence, cubicBezier, motion } from 'framer-motion';
|
|||||||
|
|
||||||
interface SendButtonProps {
|
interface SendButtonProps {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
isStreaming?: boolean;
|
hasPendingMessage?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||||
onImagesSelected?: (images: File[]) => void;
|
onImagesSelected?: (images: File[]) => void;
|
||||||
@ -10,12 +10,12 @@ interface SendButtonProps {
|
|||||||
|
|
||||||
const customEasingFn = cubicBezier(0.4, 0, 0.2, 1);
|
const customEasingFn = cubicBezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
export const SendButton = ({ show, isStreaming, disabled, onClick }: SendButtonProps) => {
|
export const SendButton = ({ show, hasPendingMessage, disabled, onClick }: SendButtonProps) => {
|
||||||
const className =
|
const className =
|
||||||
'absolute flex justify-center items-center top-[18px] right-[22px] p-1 bg-accent-500 hover:brightness-94 color-white rounded-md w-[34px] h-[34px] transition-theme disabled:opacity-50 disabled:cursor-not-allowed';
|
'absolute flex justify-center items-center top-[18px] right-[22px] p-1 bg-accent-500 hover:brightness-94 color-white rounded-md w-[34px] h-[34px] transition-theme disabled:opacity-50 disabled:cursor-not-allowed';
|
||||||
|
|
||||||
// Determine tooltip text based on button state
|
// Determine tooltip text based on button state
|
||||||
const tooltipText = isStreaming ? 'Stop Generation' : 'Chat';
|
const tooltipText = hasPendingMessage ? 'Stop Generation' : 'Chat';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
@ -37,7 +37,11 @@ export const SendButton = ({ show, isStreaming, disabled, onClick }: SendButtonP
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="text-lg">
|
<div className="text-lg">
|
||||||
{!isStreaming ? <div className="i-ph:hand-fill"></div> : <div className="i-ph:stop-circle-bold"></div>}
|
{!hasPendingMessage ? (
|
||||||
|
<div className="i-ph:hand-fill"></div>
|
||||||
|
) : (
|
||||||
|
<div className="i-ph:stop-circle-bold"></div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -20,7 +20,6 @@ export function HistoryItem({ item, onDelete, onDuplicate }: HistoryItemProps) {
|
|||||||
useEditChatTitle({
|
useEditChatTitle({
|
||||||
initialTitle: item.title,
|
initialTitle: item.title,
|
||||||
customChatId: item.id,
|
customChatId: item.id,
|
||||||
syncWithGlobalStore: isActiveChat,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderDescriptionForm = (
|
const renderDescriptionForm = (
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import { useStore } from '@nanostores/react';
|
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, updateChatTitle } from '~/lib/persistence';
|
import { currentChatId, currentChatTitle, getChatContents, handleChatTitleUpdate } from '~/lib/persistence';
|
||||||
|
|
||||||
interface EditChatDescriptionOptions {
|
interface EditChatDescriptionOptions {
|
||||||
initialTitle?: string;
|
initialTitle?: string;
|
||||||
customChatId?: string;
|
customChatId?: string;
|
||||||
syncWithGlobalStore?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type EditChatDescriptionHook = {
|
type EditChatDescriptionHook = {
|
||||||
@ -30,13 +29,11 @@ type EditChatDescriptionHook = {
|
|||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @param {string} options.initialDescription - The current chat description.
|
* @param {string} options.initialDescription - The current chat description.
|
||||||
* @param {string} options.customChatId - Optional ID for updating the description via the sidebar.
|
* @param {string} options.customChatId - Optional ID for updating the description via the sidebar.
|
||||||
* @param {boolean} options.syncWithGlobalStore - Flag to indicate global description store synchronization.
|
|
||||||
* @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 = currentChatTitle.get()!,
|
||||||
customChatId,
|
customChatId,
|
||||||
syncWithGlobalStore,
|
|
||||||
}: EditChatDescriptionOptions): EditChatDescriptionHook {
|
}: EditChatDescriptionOptions): EditChatDescriptionHook {
|
||||||
const chatIdFromStore = useStore(currentChatId);
|
const chatIdFromStore = useStore(currentChatId);
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
@ -117,12 +114,7 @@ export function useEditChatTitle({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateChatTitle(chatId, currentTitle);
|
await handleChatTitleUpdate(chatId, currentTitle);
|
||||||
|
|
||||||
if (syncWithGlobalStore) {
|
|
||||||
currentChatTitle.set(currentTitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success('Chat title updated successfully');
|
toast.success('Chat title updated successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('Failed to update chat title: ' + (error as Error).message);
|
toast.error('Failed to update chat title: ' + (error as Error).message);
|
||||||
|
@ -10,7 +10,6 @@ export function ChatDescription() {
|
|||||||
const { editing, handleChange, handleBlur, handleSubmit, handleKeyDown, currentTitle, toggleEditMode } =
|
const { editing, handleChange, handleBlur, handleSubmit, handleKeyDown, currentTitle, toggleEditMode } =
|
||||||
useEditChatTitle({
|
useEditChatTitle({
|
||||||
initialTitle,
|
initialTitle,
|
||||||
syncWithGlobalStore: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!initialTitle) {
|
if (!initialTitle) {
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import { getSupabase } from '~/lib/supabase/client';
|
// When the user is not logged in, chats are kept in local storage.
|
||||||
|
// Otherwise, they are kept in the database. The first time the user logs in,
|
||||||
|
// any local chats are added to the database and then deleted.
|
||||||
|
|
||||||
|
import { getSupabase, getCurrentUserId } from '~/lib/supabase/client';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { getMessagesRepositoryId, type Message } from './message';
|
import { getMessagesRepositoryId, type Message } from './message';
|
||||||
|
import { assert } from '~/lib/replay/ReplayProtocolClient';
|
||||||
|
|
||||||
export interface ChatContents {
|
export interface ChatContents {
|
||||||
id: string;
|
id: string;
|
||||||
@ -22,7 +27,31 @@ function databaseRowToChatContents(d: any): ChatContents {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const localStorageKey = 'nut-chats';
|
||||||
|
|
||||||
|
function getLocalChats(): ChatContents[] {
|
||||||
|
const chatJSON = localStorage.getItem(localStorageKey);
|
||||||
|
if (!chatJSON) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return JSON.parse(chatJSON);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLocalChats(chats: ChatContents[] | undefined): void {
|
||||||
|
if (chats) {
|
||||||
|
localStorage.setItem(localStorageKey, JSON.stringify(chats));
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(localStorageKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getAllChats(): Promise<ChatContents[]> {
|
export async function getAllChats(): Promise<ChatContents[]> {
|
||||||
|
const userId = await getCurrentUserId();
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return getLocalChats();
|
||||||
|
}
|
||||||
|
|
||||||
const { data, error } = await getSupabase().from('chats').select('*');
|
const { data, error } = await getSupabase().from('chats').select('*');
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@ -32,12 +61,33 @@ export async function getAllChats(): Promise<ChatContents[]> {
|
|||||||
return data.map(databaseRowToChatContents);
|
return data.map(databaseRowToChatContents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function syncLocalChats(): Promise<void> {
|
||||||
|
const userId = await getCurrentUserId();
|
||||||
|
const localChats = getLocalChats();
|
||||||
|
|
||||||
|
if (userId && localChats.length) {
|
||||||
|
for (const chat of localChats) {
|
||||||
|
await setChatContents(chat.id, chat.title, chat.messages);
|
||||||
|
}
|
||||||
|
setLocalChats(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function setChatContents(id: string, title: string, messages: Message[]): Promise<void> {
|
export async function setChatContents(id: string, title: string, messages: Message[]): Promise<void> {
|
||||||
const { data: user } = await getSupabase().auth.getUser();
|
const userId = await getCurrentUserId();
|
||||||
const userId = user.user?.id;
|
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
throw new Error('Not logged in');
|
const localChats = getLocalChats().filter((c) => c.id != id);
|
||||||
|
localChats.push({
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
messages,
|
||||||
|
repositoryId: getMessagesRepositoryId(messages),
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
setLocalChats(localChats);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const repositoryId = getMessagesRepositoryId(messages);
|
const repositoryId = getMessagesRepositoryId(messages);
|
||||||
@ -55,7 +105,30 @@ export async function setChatContents(id: string, title: string, messages: Messa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getChatContents(id: string): Promise<ChatContents> {
|
export async function getChatPublicData(id: string): Promise<{ repositoryId: string; title: string }> {
|
||||||
|
const { data, error } = await getSupabase().rpc('get_chat_public_data', { chat_id: id });
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length != 1) {
|
||||||
|
throw new Error(`Unknown chat ${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
repositoryId: data[0].repository_id,
|
||||||
|
title: data[0].title,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getChatContents(id: string): Promise<ChatContents | undefined> {
|
||||||
|
const userId = await getCurrentUserId();
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return getLocalChats().find((c) => c.id == id);
|
||||||
|
}
|
||||||
|
|
||||||
const { data, error } = await getSupabase().from('chats').select('*').eq('id', id);
|
const { data, error } = await getSupabase().from('chats').select('*').eq('id', id);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@ -63,13 +136,21 @@ export async function getChatContents(id: string): Promise<ChatContents> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.length != 1) {
|
if (data.length != 1) {
|
||||||
throw new Error('Unexpected chat contents returned');
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return databaseRowToChatContents(data[0]);
|
return databaseRowToChatContents(data[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteById(id: string): Promise<void> {
|
export async function deleteById(id: string): Promise<void> {
|
||||||
|
const userId = await getCurrentUserId();
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
const localChats = getLocalChats().filter((c) => c.id != id);
|
||||||
|
setLocalChats(localChats);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { error } = await getSupabase().from('chats').delete().eq('id', id);
|
const { error } = await getSupabase().from('chats').delete().eq('id', id);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@ -83,8 +164,9 @@ export async function createChat(title: string, messages: Message[]): Promise<st
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export 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');
|
||||||
|
|
||||||
if (!title.trim()) {
|
if (!title.trim()) {
|
||||||
throw new Error('Title cannot be empty');
|
throw new Error('Title cannot be empty');
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
// Client messages match the format used by the Nut protocol.
|
// Client messages match the format used by the Nut protocol.
|
||||||
|
|
||||||
|
import { generateId } from '~/utils/fileUtils';
|
||||||
|
|
||||||
type MessageRole = 'user' | 'assistant';
|
type MessageRole = 'user' | 'assistant';
|
||||||
|
|
||||||
interface MessageBase {
|
interface MessageBase {
|
||||||
@ -36,3 +38,27 @@ export function getPreviousRepositoryId(messages: Message[], index: number): str
|
|||||||
export function getMessagesRepositoryId(messages: Message[]): string | undefined {
|
export function getMessagesRepositoryId(messages: Message[]): string | undefined {
|
||||||
return getPreviousRepositoryId(messages, messages.length);
|
return getPreviousRepositoryId(messages, messages.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return a couple messages for a new chat operating on a repository.
|
||||||
|
export function createMessagesForRepository(title: string, repositoryId: string): Message[] {
|
||||||
|
const filesContent = `I've copied the "${title}" chat.`;
|
||||||
|
|
||||||
|
const userMessage: Message = {
|
||||||
|
role: 'user',
|
||||||
|
id: generateId(),
|
||||||
|
content: `Copy the "${title}" chat`,
|
||||||
|
type: 'text',
|
||||||
|
};
|
||||||
|
|
||||||
|
const filesMessage: Message = {
|
||||||
|
role: 'assistant',
|
||||||
|
content: filesContent,
|
||||||
|
id: generateId(),
|
||||||
|
repositoryId,
|
||||||
|
type: 'text',
|
||||||
|
};
|
||||||
|
|
||||||
|
const messages = [userMessage, filesMessage];
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
@ -1,25 +1,26 @@
|
|||||||
import { useLoaderData, useNavigate } 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 { 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 } from './db';
|
import { createChat, getChatContents, setChatContents, getChatPublicData, databaseUpdateChatTitle } from './db';
|
||||||
import { loadProblem } from '~/components/chat/LoadProblemButton';
|
import { loadProblem } from '~/components/chat/LoadProblemButton';
|
||||||
import type { Message } from './message';
|
import { createMessagesForRepository, type Message } from './message';
|
||||||
|
import { debounce } from '~/utils/debounce';
|
||||||
|
|
||||||
|
// These must be kept in sync.
|
||||||
export const currentChatId = atom<string | undefined>(undefined);
|
export const currentChatId = atom<string | undefined>(undefined);
|
||||||
export const currentChatTitle = atom<string | undefined>(undefined);
|
export const currentChatTitle = atom<string | undefined>(undefined);
|
||||||
|
|
||||||
export function useChatHistory() {
|
export function useChatHistory() {
|
||||||
const navigate = useNavigate();
|
|
||||||
const { id: mixedId, problemId } = useLoaderData<{ id?: string; problemId?: string }>() ?? {};
|
const { id: mixedId, problemId } = useLoaderData<{ id?: string; problemId?: string }>() ?? {};
|
||||||
|
|
||||||
const [initialMessages, setInitialMessages] = useState<Message[]>([]);
|
const [initialMessages, setInitialMessages] = useState<Message[]>([]);
|
||||||
const [ready, setReady] = useState<boolean>(!mixedId && !problemId);
|
const [ready, setReady] = useState<boolean>(!mixedId && !problemId);
|
||||||
|
|
||||||
const importChat = async (description: string, messages: Message[]) => {
|
const importChat = async (title: string, messages: Message[]) => {
|
||||||
try {
|
try {
|
||||||
const newId = await createChat(description, messages);
|
const newId = await 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) {
|
||||||
@ -32,46 +33,49 @@ export function useChatHistory() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mixedId) {
|
(async () => {
|
||||||
getChatContents(mixedId)
|
try {
|
||||||
.then((chatContents) => {
|
if (mixedId) {
|
||||||
if (chatContents && chatContents.messages.length > 0) {
|
const chatContents = await getChatContents(mixedId);
|
||||||
|
if (chatContents) {
|
||||||
setInitialMessages(chatContents.messages);
|
setInitialMessages(chatContents.messages);
|
||||||
currentChatTitle.set(chatContents.title);
|
currentChatTitle.set(chatContents.title);
|
||||||
currentChatId.set(mixedId);
|
currentChatId.set(mixedId);
|
||||||
} else {
|
setReady(true);
|
||||||
navigate('/', { replace: true });
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const publicData = await getChatPublicData(mixedId);
|
||||||
|
const messages = createMessagesForRepository(publicData.title, publicData.repositoryId);
|
||||||
|
await importChat(publicData.title, messages);
|
||||||
|
} else if (problemId) {
|
||||||
|
await loadProblem(problemId, importChat);
|
||||||
setReady(true);
|
setReady(true);
|
||||||
})
|
}
|
||||||
.catch((error) => {
|
} catch (error) {
|
||||||
logStore.logError('Failed to load chat messages', error);
|
logStore.logError('Failed to load chat messages', error);
|
||||||
toast.error(error.message);
|
toast.error((error as any).message);
|
||||||
});
|
}
|
||||||
} else if (problemId) {
|
})();
|
||||||
loadProblem(problemId, importChat).then(() => setReady(true));
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ready,
|
ready,
|
||||||
initialMessages,
|
initialMessages,
|
||||||
storeMessageHistory: async (messages: Message[]) => {
|
storeMessageHistory: debounce(async (messages: Message[]) => {
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = currentChatTitle.get() ?? 'New Chat';
|
|
||||||
|
|
||||||
if (!currentChatId.get()) {
|
if (!currentChatId.get()) {
|
||||||
const id = await createChat(title, initialMessages);
|
const id = await createChat('New Chat', initialMessages);
|
||||||
currentChatId.set(id);
|
currentChatId.set(id);
|
||||||
|
currentChatTitle.set('New Chat');
|
||||||
navigateChat(id);
|
navigateChat(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
await setChatContents(currentChatId.get() as string, title, messages);
|
await setChatContents(currentChatId.get() as string, currentChatTitle.get() as string, messages);
|
||||||
},
|
}, 1000),
|
||||||
importChat,
|
importChat,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -87,3 +91,10 @@ function navigateChat(nextId: string) {
|
|||||||
|
|
||||||
window.history.replaceState({}, '', url);
|
window.history.replaceState({}, '', url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function handleChatTitleUpdate(id: string, title: string) {
|
||||||
|
await databaseUpdateChatTitle(id, title);
|
||||||
|
if (currentChatId.get() == id) {
|
||||||
|
currentChatTitle.set(title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -28,7 +28,11 @@ interface ChatReferenceElement {
|
|||||||
|
|
||||||
export type ChatReference = ChatReferenceElement;
|
export type ChatReference = ChatReferenceElement;
|
||||||
|
|
||||||
type ChatResponsePartCallback = (message: Message) => void;
|
interface ChatMessageCallbacks {
|
||||||
|
onResponsePart: (message: Message) => void;
|
||||||
|
onTitle: (title: string) => void;
|
||||||
|
onStatus: (status: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
class ChatManager {
|
class ChatManager {
|
||||||
// Empty if this chat has been destroyed.
|
// Empty if this chat has been destroyed.
|
||||||
@ -138,7 +142,7 @@ class ChatManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendChatMessage(messages: Message[], references: ChatReference[], onResponsePart: ChatResponsePartCallback) {
|
async sendChatMessage(messages: Message[], references: ChatReference[], callbacks: ChatMessageCallbacks) {
|
||||||
assert(this.client, 'Chat has been destroyed');
|
assert(this.client, 'Chat has been destroyed');
|
||||||
|
|
||||||
this._pendingMessages++;
|
this._pendingMessages++;
|
||||||
@ -150,7 +154,27 @@ class ChatManager {
|
|||||||
({ responseId: eventResponseId, message }: { responseId: string; message: Message }) => {
|
({ responseId: eventResponseId, message }: { responseId: string; message: Message }) => {
|
||||||
if (responseId == eventResponseId) {
|
if (responseId == eventResponseId) {
|
||||||
console.log('ChatResponse', chatId, message);
|
console.log('ChatResponse', chatId, message);
|
||||||
onResponsePart(message);
|
callbacks.onResponsePart(message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeTitleListener = this.client.listenForMessage(
|
||||||
|
'Nut.chatTitle',
|
||||||
|
({ responseId: eventResponseId, title }: { responseId: string; title: string }) => {
|
||||||
|
if (responseId == eventResponseId) {
|
||||||
|
console.log('ChatTitle', title);
|
||||||
|
callbacks.onTitle(title);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeStatusListener = this.client.listenForMessage(
|
||||||
|
'Nut.chatStatus',
|
||||||
|
({ responseId: eventResponseId, status }: { responseId: string; status: string }) => {
|
||||||
|
if (responseId == eventResponseId) {
|
||||||
|
console.log('ChatStatus', status);
|
||||||
|
callbacks.onStatus(status);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -167,6 +191,8 @@ class ChatManager {
|
|||||||
console.log('ChatMessageFinished', new Date().toISOString(), chatId);
|
console.log('ChatMessageFinished', new Date().toISOString(), chatId);
|
||||||
|
|
||||||
removeResponseListener();
|
removeResponseListener();
|
||||||
|
removeTitleListener();
|
||||||
|
removeStatusListener();
|
||||||
|
|
||||||
if (--this._pendingMessages == 0 && this._mustDestroyAfterChatFinishes) {
|
if (--this._pendingMessages == 0 && this._mustDestroyAfterChatFinishes) {
|
||||||
this._destroy();
|
this._destroy();
|
||||||
@ -266,7 +292,7 @@ export function getLastSimulationChatReferences(): ChatReference[] | undefined {
|
|||||||
export async function sendChatMessage(
|
export async function sendChatMessage(
|
||||||
messages: Message[],
|
messages: Message[],
|
||||||
references: ChatReference[],
|
references: ChatReference[],
|
||||||
onResponsePart: ChatResponsePartCallback,
|
callbacks: ChatMessageCallbacks,
|
||||||
) {
|
) {
|
||||||
if (!gChatManager) {
|
if (!gChatManager) {
|
||||||
gChatManager = new ChatManager();
|
gChatManager = new ChatManager();
|
||||||
@ -275,5 +301,5 @@ export async function sendChatMessage(
|
|||||||
gLastSimulationChatMessages = messages;
|
gLastSimulationChatMessages = messages;
|
||||||
gLastSimulationChatReferences = references;
|
gLastSimulationChatReferences = references;
|
||||||
|
|
||||||
await gChatManager.sendChatMessage(messages, references, onResponsePart);
|
await gChatManager.sendChatMessage(messages, references, callbacks);
|
||||||
}
|
}
|
||||||
|
@ -4,6 +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';
|
||||||
|
|
||||||
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);
|
||||||
@ -68,6 +69,8 @@ export async function initializeAuth() {
|
|||||||
sessionStore.set(null);
|
sessionStore.set(null);
|
||||||
logStore.logSystem('User signed out');
|
logStore.logSystem('User signed out');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await syncLocalChats();
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -77,6 +77,11 @@ export async function getCurrentUser(): Promise<SupabaseUser | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCurrentUserId(): Promise<string | null> {
|
||||||
|
const user = await getCurrentUser();
|
||||||
|
return user?.id || null;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getNutIsAdmin(): Promise<boolean> {
|
export async function getNutIsAdmin(): Promise<boolean> {
|
||||||
const user = await getCurrentUser();
|
const user = await getCurrentUser();
|
||||||
|
|
||||||
@ -137,8 +142,3 @@ export function getSupabase() {
|
|||||||
|
|
||||||
return supabaseClientInstance;
|
return supabaseClientInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to check if Supabase is properly initialized
|
|
||||||
export const isSupabaseInitialized = (): boolean => {
|
|
||||||
return Boolean(supabaseUrl && supabaseAnonKey);
|
|
||||||
};
|
|
||||||
|
@ -36,6 +36,7 @@ export async function getFileRepositoryContents(files: File[]): Promise<string>
|
|||||||
return await zip.generateAsync({ type: 'base64' });
|
return await zip.generateAsync({ type: 'base64' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Common up with createMessagesForRepository.
|
||||||
export function createChatFromFolder(folderName: string, repositoryId: string): Message[] {
|
export function createChatFromFolder(folderName: string, repositoryId: string): Message[] {
|
||||||
const filesContent = `I've imported the contents of the "${folderName}" folder.`;
|
const filesContent = `I've imported the contents of the "${folderName}" folder.`;
|
||||||
|
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
/**
|
|
||||||
* Creates a function that samples calls at regular intervals and captures trailing calls.
|
|
||||||
* - Drops calls that occur between sampling intervals
|
|
||||||
* - Takes one call per sampling interval if available
|
|
||||||
* - Captures the last call if no call was made during the interval
|
|
||||||
*
|
|
||||||
* @param fn The function to sample
|
|
||||||
* @param sampleInterval How often to sample calls (in ms)
|
|
||||||
* @returns The sampled function
|
|
||||||
*/
|
|
||||||
export function createSampler<T extends (...args: any[]) => any>(fn: T, sampleInterval: number): T {
|
|
||||||
let lastArgs: Parameters<T> | null = null;
|
|
||||||
let lastTime = 0;
|
|
||||||
let timeout: NodeJS.Timeout | null = null;
|
|
||||||
|
|
||||||
// Create a function with the same type as the input function
|
|
||||||
const sampled = function (this: any, ...args: Parameters<T>) {
|
|
||||||
const now = Date.now();
|
|
||||||
lastArgs = args;
|
|
||||||
|
|
||||||
// If we're within the sample interval, just store the args
|
|
||||||
if (now - lastTime < sampleInterval) {
|
|
||||||
// Set up trailing call if not already set
|
|
||||||
if (!timeout) {
|
|
||||||
timeout = setTimeout(
|
|
||||||
() => {
|
|
||||||
timeout = null;
|
|
||||||
lastTime = Date.now();
|
|
||||||
|
|
||||||
if (lastArgs) {
|
|
||||||
fn.apply(this, lastArgs);
|
|
||||||
lastArgs = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sampleInterval - (now - lastTime),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're outside the interval, execute immediately
|
|
||||||
lastTime = now;
|
|
||||||
fn.apply(this, args);
|
|
||||||
lastArgs = null;
|
|
||||||
} as T;
|
|
||||||
|
|
||||||
return sampled;
|
|
||||||
}
|
|
@ -78,4 +78,4 @@ BEGIN
|
|||||||
FROM chats p
|
FROM chats p
|
||||||
WHERE p.id = chat_id;
|
WHERE p.id = chat_id;
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||||
|
Loading…
Reference in New Issue
Block a user