diff --git a/app/components/auth/ClientAuth.tsx b/app/components/auth/ClientAuth.tsx index 88df0995..fe4971c7 100644 --- a/app/components/auth/ClientAuth.tsx +++ b/app/components/auth/ClientAuth.tsx @@ -11,6 +11,7 @@ export function ClientAuth() { const [password, setPassword] = useState(''); const [isSigningIn, setIsSigningIn] = useState(false); const [showDropdown, setShowDropdown] = useState(false); + const [usageData, setUsageData] = useState<{ peanuts_used: number; peanuts_refunded: number } | null>(null); useEffect(() => { async function getUser() { @@ -37,6 +38,30 @@ export function ClientAuth() { }; }, []); + useEffect(() => { + async function updateUsageData() { + try { + const { data, error } = await getSupabase() + .from('profiles') + .select('peanuts_used, peanuts_refunded') + .eq('id', user?.id) + .single(); + + if (error) { + throw error; + } + + setUsageData(data); + } catch (error) { + console.error('Error fetching usage data:', error); + } + } + + if (showDropdown) { + updateUsageData(); + } + }, [showDropdown]); + const handleSignIn = async (e: React.FormEvent) => { e.preventDefault(); setIsSigningIn(true); @@ -119,10 +144,16 @@ export function ClientAuth() { {showDropdown && ( -
+
{user.email}
+
+ {`Peanuts used: ${usageData?.peanuts_used ?? '...'}`} +
+
+ {`Peanuts refunded: ${usageData?.peanuts_refunded ?? '...'}`} +
)} @@ -387,7 +373,6 @@ export const BaseChat = React.forwardRef( messages={messages} hasPendingMessage={hasPendingMessage} pendingMessageStatus={pendingMessageStatus} - onRewind={onRewind} /> ) : null; }} @@ -444,24 +429,12 @@ export const BaseChat = React.forwardRef( /> )} - {showApproveChange && ( + {approveChangeMessageId && ( { - if (onApproveChange && messages) { - const lastMessage = messages[messages.length - 1]; - assert(lastMessage); - onApproveChange(lastMessage.id); - } - }} - onReject={(data) => { - if (onRejectChange && messages) { - const lastMessage = messages[messages.length - 1]; - assert(lastMessage); - onRejectChange(lastMessage.id, data); - } - }} + onApprove={() => onApproveChange?.(approveChangeMessageId)} + onReject={(data) => onRejectChange?.(approveChangeMessageId, data)} /> )} {!rejectFormOpen && messageInput} diff --git a/app/components/chat/Chat.client.tsx b/app/components/chat/Chat.client.tsx index 0ab55103..13a41adb 100644 --- a/app/components/chat/Chat.client.tsx +++ b/app/components/chat/Chat.client.tsx @@ -34,6 +34,7 @@ import { getMessagesRepositoryId, getPreviousRepositoryId, type Message } from ' import { useAuthStatus } from '~/lib/stores/auth'; import { debounce } from '~/utils/debounce'; import { supabaseSubmitFeedback } from '~/lib/supabase/feedback'; +import { supabaseAddRefund } from '~/lib/supabase/peanuts'; const toastAnimation = cssTransition({ enter: 'animated fadeInRight', @@ -142,6 +143,22 @@ function mergeResponseMessage(msg: Message, messages: Message[]): Message[] { return messages; } +// Get the index of the last message that will be present after rewinding. +// This is the last message which is either a user message or has a repository change. +function getRewindMessageIndexAfterReject(messages: Message[], messageId: string): number { + for (let i = messages.length - 1; i >= 0; i--) { + const { id, role, repositoryId } = messages[i]; + if (role == 'user') { + return i; + } + if (repositoryId && id != messageId) { + return i; + } + } + console.error('No rewind message found', messages, messageId); + return -1; +} + export const ChatImpl = memo((props: ChatProps) => { const { initialMessages, resumeChat: initialResumeChat, storeMessageHistory } = props; @@ -150,7 +167,6 @@ export const ChatImpl = memo((props: ChatProps) => { const [uploadedFiles, setUploadedFiles] = useState([]); // Move here const [imageDataList, setImageDataList] = useState([]); // Move here const [searchParams, setSearchParams] = useSearchParams(); - const [approveChangesMessageId, setApproveChangesMessageId] = useState(undefined); const { isLoggedIn } = useAuthStatus(); // Input currently in the textarea. @@ -209,6 +225,7 @@ export const ChatImpl = memo((props: ChatProps) => { gNumAborts++; chatStore.aborted.set(true); setPendingMessageId(undefined); + setPendingMessageStatus(''); setResumeChat(undefined); const chatId = chatStore.currentChat.get()?.id; @@ -390,10 +407,7 @@ export const ChatImpl = memo((props: ChatProps) => { textareaRef.current?.blur(); - if (updatedRepository) { - const lastMessage = newMessages[newMessages.length - 1]; - setApproveChangesMessageId(lastMessage.id); - } else { + if (!updatedRepository) { simulationReset(); } }; @@ -482,33 +496,6 @@ export const ChatImpl = memo((props: ChatProps) => { })(); }, [initialResumeChat]); - // Rewind far enough to erase the specified message. - const onRewind = async (messageId: string) => { - console.log('Rewinding', messageId); - - const messageIndex = messages.findIndex((message) => message.id === messageId); - - if (messageIndex < 0) { - toast.error('Rewind message not found'); - return; - } - - const previousRepositoryId = getPreviousRepositoryId(messages, messageIndex); - - if (!previousRepositoryId) { - toast.error('No repository ID found for rewind'); - return; - } - - setMessages(messages.slice(0, messageIndex)); - simulationRepositoryUpdated(previousRepositoryId); - - pingTelemetry('RewindChat', { - numMessages: messages.length, - rewindIndex: messageIndex, - }); - }; - const flashScreen = async () => { const flash = document.createElement('div'); flash.style.position = 'fixed'; @@ -534,7 +521,17 @@ export const ChatImpl = memo((props: ChatProps) => { const onApproveChange = async (messageId: string) => { console.log('ApproveChange', messageId); - setApproveChangesMessageId(undefined); + setMessages( + messages.map((message) => { + if (message.id == messageId) { + return { + ...message, + approved: true, + }; + } + return message; + }), + ); await flashScreen(); @@ -546,23 +543,31 @@ export const ChatImpl = memo((props: ChatProps) => { const onRejectChange = async (messageId: string, data: RejectChangeData) => { console.log('RejectChange', messageId, data); - setApproveChangesMessageId(undefined); + // Find the last message that will be present after rewinding. This is the + // last message which is either a user message or has a repository change. + const messageIndex = getRewindMessageIndexAfterReject(messages, messageId); - const message = messages.find((message) => message.id === messageId); - assert(message, 'Message not found'); - assert(message == messages[messages.length - 1], 'Message must be the last message'); - - // Erase all messages since the last user message. - let rewindMessageId = message.id; - - for (let i = messages.length - 2; i >= 0; i--) { - if (messages[i].role == 'user') { - break; - } - - rewindMessageId = messages[i].id; + if (messageIndex < 0) { + toast.error('Rewind message not found'); + return; } - await onRewind(rewindMessageId); + + const message = messages.find((m) => m.id == messageId); + + if (!message) { + toast.error('Message not found'); + return; + } + + if (message.peanuts) { + await supabaseAddRefund(message.peanuts); + } + + const previousRepositoryId = getPreviousRepositoryId(messages, messageIndex + 1); + + setMessages(messages.slice(0, messageIndex + 1)); + + simulationRepositoryUpdated(previousRepositoryId); let shareProjectSuccess = false; @@ -615,8 +620,6 @@ export const ChatImpl = memo((props: ChatProps) => { setUploadedFiles={setUploadedFiles} imageDataList={imageDataList} setImageDataList={setImageDataList} - onRewind={onRewind} - approveChangesMessageId={approveChangesMessageId} onApproveChange={onApproveChange} onRejectChange={onRejectChange} /> diff --git a/app/components/chat/Messages.client.tsx b/app/components/chat/Messages.client.tsx index d26e192e..5b109353 100644 --- a/app/components/chat/Messages.client.tsx +++ b/app/components/chat/Messages.client.tsx @@ -1,7 +1,7 @@ import React, { Suspense } from 'react'; import { classNames } from '~/utils/classNames'; import WithTooltip from '~/components/ui/Tooltip'; -import { getPreviousRepositoryId, type Message } from '~/lib/persistence/message'; +import type { Message } from '~/lib/persistence/message'; import { MessageContents } from './MessageContents'; interface MessagesProps { @@ -10,18 +10,16 @@ interface MessagesProps { hasPendingMessage?: boolean; pendingMessageStatus?: string; messages?: Message[]; - onRewind?: (messageId: string) => void; } export const Messages = React.forwardRef((props: MessagesProps, ref) => { - const { id, hasPendingMessage = false, pendingMessageStatus = '', messages = [], onRewind } = props; + const { id, hasPendingMessage = false, pendingMessageStatus = '', messages = [] } = props; return (
{messages.length > 0 ? messages.map((message, index) => { - const { role, id: messageId, repositoryId } = message; - const previousRepositoryId = getPreviousRepositoryId(messages, index); + const { role, repositoryId } = message; const isUserMessage = role === 'user'; const isFirst = index === 0; const isLast = index === messages.length - 1; @@ -51,16 +49,15 @@ export const Messages = React.forwardRef((props:
- {previousRepositoryId && repositoryId && onRewind && ( + {repositoryId && (
- +
diff --git a/supabase/migrations/2025035000000_create_profiles_table.sql b/supabase/migrations/2025035000000_create_profiles_table.sql index c8ece0ae..247e441c 100644 --- a/supabase/migrations/2025035000000_create_profiles_table.sql +++ b/supabase/migrations/2025035000000_create_profiles_table.sql @@ -6,7 +6,9 @@ CREATE TABLE IF NOT EXISTS public.profiles ( username TEXT UNIQUE, full_name TEXT, avatar_url TEXT, - is_admin BOOLEAN DEFAULT FALSE NOT NULL + is_admin BOOLEAN DEFAULT FALSE NOT NULL, + peanuts_used INTEGER DEFAULT 0 NOT NULL, + peanuts_refunded INTEGER DEFAULT 0 NOT NULL ); -- Create a trigger to update the updated_at column