mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
Keep track of API usage in user accounts, improve approval mechanism (#95)
This commit is contained in:
@@ -11,6 +11,7 @@ export function ClientAuth() {
|
|||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [isSigningIn, setIsSigningIn] = useState(false);
|
const [isSigningIn, setIsSigningIn] = useState(false);
|
||||||
const [showDropdown, setShowDropdown] = useState(false);
|
const [showDropdown, setShowDropdown] = useState(false);
|
||||||
|
const [usageData, setUsageData] = useState<{ peanuts_used: number; peanuts_refunded: number } | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getUser() {
|
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) => {
|
const handleSignIn = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsSigningIn(true);
|
setIsSigningIn(true);
|
||||||
@@ -119,10 +144,16 @@ export function ClientAuth() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{showDropdown && (
|
{showDropdown && (
|
||||||
<div className="absolute right-0 mt-2 py-2 bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded shadow-lg z-10">
|
<div className="absolute right-0 mt-2 py-2 whitespace-nowrap bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded shadow-lg z-10">
|
||||||
<div className="px-4 py-2 text-bolt-elements-textPrimary border-b border-bolt-elements-borderColor">
|
<div className="px-4 py-2 text-bolt-elements-textPrimary border-b border-bolt-elements-borderColor">
|
||||||
{user.email}
|
{user.email}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="px-4 py-2 text-bolt-elements-textPrimary border-b border-bolt-elements-borderColor">
|
||||||
|
{`Peanuts used: ${usageData?.peanuts_used ?? '...'}`}
|
||||||
|
</div>
|
||||||
|
<div className="px-4 py-2 text-bolt-elements-textPrimary border-b border-bolt-elements-borderColor">
|
||||||
|
{`Peanuts refunded: ${usageData?.peanuts_refunded ?? '...'}`}
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleSignOut}
|
onClick={handleSignOut}
|
||||||
className="block w-full text-left px-4 py-2 text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-2"
|
className="block w-full text-left px-4 py-2 text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-2"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { IconButton } from '~/components/ui/IconButton';
|
|||||||
import { Workbench } from '~/components/workbench/Workbench.client';
|
import { Workbench } from '~/components/workbench/Workbench.client';
|
||||||
import { classNames } from '~/utils/classNames';
|
import { classNames } from '~/utils/classNames';
|
||||||
import { Messages } from './Messages.client';
|
import { Messages } from './Messages.client';
|
||||||
import { getPreviousRepositoryId, type Message } from '~/lib/persistence/message';
|
import { type Message } from '~/lib/persistence/message';
|
||||||
import { SendButton } from './SendButton.client';
|
import { SendButton } from './SendButton.client';
|
||||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||||
|
|
||||||
@@ -20,7 +20,6 @@ import FilePreview from './FilePreview';
|
|||||||
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
|
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
|
||||||
import { ScreenshotStateManager } from './ScreenshotStateManager';
|
import { ScreenshotStateManager } from './ScreenshotStateManager';
|
||||||
import type { RejectChangeData } from './ApproveChange';
|
import type { RejectChangeData } from './ApproveChange';
|
||||||
import { assert } from '~/lib/replay/ReplayProtocolClient';
|
|
||||||
import ApproveChange from './ApproveChange';
|
import ApproveChange from './ApproveChange';
|
||||||
|
|
||||||
export const TEXTAREA_MIN_HEIGHT = 76;
|
export const TEXTAREA_MIN_HEIGHT = 76;
|
||||||
@@ -46,10 +45,8 @@ interface BaseChatProps {
|
|||||||
setUploadedFiles?: (files: File[]) => void;
|
setUploadedFiles?: (files: File[]) => void;
|
||||||
imageDataList?: string[];
|
imageDataList?: string[];
|
||||||
setImageDataList?: (dataList: string[]) => void;
|
setImageDataList?: (dataList: string[]) => void;
|
||||||
onRewind?: (messageId: string) => void;
|
|
||||||
approveChangesMessageId?: string;
|
|
||||||
onApproveChange?: (messageId: string) => void;
|
onApproveChange?: (messageId: string) => void;
|
||||||
onRejectChange?: (lastMessageId: string, data: RejectChangeData) => void;
|
onRejectChange?: (messageId: string, data: RejectChangeData) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||||
@@ -72,8 +69,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
imageDataList = [],
|
imageDataList = [],
|
||||||
setImageDataList,
|
setImageDataList,
|
||||||
messages,
|
messages,
|
||||||
onRewind,
|
|
||||||
approveChangesMessageId,
|
|
||||||
onApproveChange,
|
onApproveChange,
|
||||||
onRejectChange,
|
onRejectChange,
|
||||||
},
|
},
|
||||||
@@ -209,30 +204,21 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showApproveChange = (() => {
|
const approveChangeMessageId = (() => {
|
||||||
if (hasPendingMessage) {
|
if (hasPendingMessage || !messages) {
|
||||||
return false;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!messages?.length) {
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
return false;
|
const message = messages[i];
|
||||||
|
if (message.repositoryId && message.peanuts) {
|
||||||
|
return message.approved ? undefined : message.id;
|
||||||
|
}
|
||||||
|
if (message.role == 'user') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return undefined;
|
||||||
const lastMessage = messages[messages.length - 1];
|
|
||||||
|
|
||||||
if (!lastMessage.repositoryId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!getPreviousRepositoryId(messages, messages.length - 1)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastMessage.id != approveChangesMessageId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
let messageInput;
|
let messageInput;
|
||||||
@@ -369,7 +355,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
Get what you want
|
Get what you want
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-md lg:text-xl mb-8 text-bolt-elements-textSecondary animate-fade-in animation-delay-200">
|
<p className="text-md lg:text-xl mb-8 text-bolt-elements-textSecondary animate-fade-in animation-delay-200">
|
||||||
Build, test, and fix your app all from one prompt
|
Write, test, and fix your app all from one prompt
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -387,7 +373,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
messages={messages}
|
messages={messages}
|
||||||
hasPendingMessage={hasPendingMessage}
|
hasPendingMessage={hasPendingMessage}
|
||||||
pendingMessageStatus={pendingMessageStatus}
|
pendingMessageStatus={pendingMessageStatus}
|
||||||
onRewind={onRewind}
|
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
}}
|
}}
|
||||||
@@ -444,24 +429,12 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
{showApproveChange && (
|
{approveChangeMessageId && (
|
||||||
<ApproveChange
|
<ApproveChange
|
||||||
rejectFormOpen={rejectFormOpen}
|
rejectFormOpen={rejectFormOpen}
|
||||||
setRejectFormOpen={setRejectFormOpen}
|
setRejectFormOpen={setRejectFormOpen}
|
||||||
onApprove={() => {
|
onApprove={() => onApproveChange?.(approveChangeMessageId)}
|
||||||
if (onApproveChange && messages) {
|
onReject={(data) => onRejectChange?.(approveChangeMessageId, data)}
|
||||||
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);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!rejectFormOpen && messageInput}
|
{!rejectFormOpen && messageInput}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { getMessagesRepositoryId, getPreviousRepositoryId, type Message } from '
|
|||||||
import { useAuthStatus } from '~/lib/stores/auth';
|
import { useAuthStatus } from '~/lib/stores/auth';
|
||||||
import { debounce } from '~/utils/debounce';
|
import { debounce } from '~/utils/debounce';
|
||||||
import { supabaseSubmitFeedback } from '~/lib/supabase/feedback';
|
import { supabaseSubmitFeedback } from '~/lib/supabase/feedback';
|
||||||
|
import { supabaseAddRefund } from '~/lib/supabase/peanuts';
|
||||||
|
|
||||||
const toastAnimation = cssTransition({
|
const toastAnimation = cssTransition({
|
||||||
enter: 'animated fadeInRight',
|
enter: 'animated fadeInRight',
|
||||||
@@ -142,6 +143,22 @@ function mergeResponseMessage(msg: Message, messages: Message[]): Message[] {
|
|||||||
return messages;
|
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) => {
|
export const ChatImpl = memo((props: ChatProps) => {
|
||||||
const { initialMessages, resumeChat: initialResumeChat, storeMessageHistory } = props;
|
const { initialMessages, resumeChat: initialResumeChat, storeMessageHistory } = props;
|
||||||
|
|
||||||
@@ -150,7 +167,6 @@ export const ChatImpl = memo((props: ChatProps) => {
|
|||||||
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]); // Move here
|
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]); // Move here
|
||||||
const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
|
const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [approveChangesMessageId, setApproveChangesMessageId] = useState<string | undefined>(undefined);
|
|
||||||
const { isLoggedIn } = useAuthStatus();
|
const { isLoggedIn } = useAuthStatus();
|
||||||
|
|
||||||
// Input currently in the textarea.
|
// Input currently in the textarea.
|
||||||
@@ -209,6 +225,7 @@ export const ChatImpl = memo((props: ChatProps) => {
|
|||||||
gNumAborts++;
|
gNumAborts++;
|
||||||
chatStore.aborted.set(true);
|
chatStore.aborted.set(true);
|
||||||
setPendingMessageId(undefined);
|
setPendingMessageId(undefined);
|
||||||
|
setPendingMessageStatus('');
|
||||||
setResumeChat(undefined);
|
setResumeChat(undefined);
|
||||||
|
|
||||||
const chatId = chatStore.currentChat.get()?.id;
|
const chatId = chatStore.currentChat.get()?.id;
|
||||||
@@ -390,10 +407,7 @@ export const ChatImpl = memo((props: ChatProps) => {
|
|||||||
|
|
||||||
textareaRef.current?.blur();
|
textareaRef.current?.blur();
|
||||||
|
|
||||||
if (updatedRepository) {
|
if (!updatedRepository) {
|
||||||
const lastMessage = newMessages[newMessages.length - 1];
|
|
||||||
setApproveChangesMessageId(lastMessage.id);
|
|
||||||
} else {
|
|
||||||
simulationReset();
|
simulationReset();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -482,33 +496,6 @@ export const ChatImpl = memo((props: ChatProps) => {
|
|||||||
})();
|
})();
|
||||||
}, [initialResumeChat]);
|
}, [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 flashScreen = async () => {
|
||||||
const flash = document.createElement('div');
|
const flash = document.createElement('div');
|
||||||
flash.style.position = 'fixed';
|
flash.style.position = 'fixed';
|
||||||
@@ -534,7 +521,17 @@ export const ChatImpl = memo((props: ChatProps) => {
|
|||||||
const onApproveChange = async (messageId: string) => {
|
const onApproveChange = async (messageId: string) => {
|
||||||
console.log('ApproveChange', messageId);
|
console.log('ApproveChange', messageId);
|
||||||
|
|
||||||
setApproveChangesMessageId(undefined);
|
setMessages(
|
||||||
|
messages.map((message) => {
|
||||||
|
if (message.id == messageId) {
|
||||||
|
return {
|
||||||
|
...message,
|
||||||
|
approved: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
await flashScreen();
|
await flashScreen();
|
||||||
|
|
||||||
@@ -546,23 +543,31 @@ export const ChatImpl = memo((props: ChatProps) => {
|
|||||||
const onRejectChange = async (messageId: string, data: RejectChangeData) => {
|
const onRejectChange = async (messageId: string, data: RejectChangeData) => {
|
||||||
console.log('RejectChange', messageId, data);
|
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);
|
if (messageIndex < 0) {
|
||||||
assert(message, 'Message not found');
|
toast.error('Rewind message not found');
|
||||||
assert(message == messages[messages.length - 1], 'Message must be the last message');
|
return;
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
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;
|
let shareProjectSuccess = false;
|
||||||
|
|
||||||
@@ -615,8 +620,6 @@ export const ChatImpl = memo((props: ChatProps) => {
|
|||||||
setUploadedFiles={setUploadedFiles}
|
setUploadedFiles={setUploadedFiles}
|
||||||
imageDataList={imageDataList}
|
imageDataList={imageDataList}
|
||||||
setImageDataList={setImageDataList}
|
setImageDataList={setImageDataList}
|
||||||
onRewind={onRewind}
|
|
||||||
approveChangesMessageId={approveChangesMessageId}
|
|
||||||
onApproveChange={onApproveChange}
|
onApproveChange={onApproveChange}
|
||||||
onRejectChange={onRejectChange}
|
onRejectChange={onRejectChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { Suspense } from 'react';
|
import React, { Suspense } from 'react';
|
||||||
import { classNames } from '~/utils/classNames';
|
import { classNames } from '~/utils/classNames';
|
||||||
import WithTooltip from '~/components/ui/Tooltip';
|
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';
|
import { MessageContents } from './MessageContents';
|
||||||
|
|
||||||
interface MessagesProps {
|
interface MessagesProps {
|
||||||
@@ -10,18 +10,16 @@ interface MessagesProps {
|
|||||||
hasPendingMessage?: boolean;
|
hasPendingMessage?: boolean;
|
||||||
pendingMessageStatus?: string;
|
pendingMessageStatus?: string;
|
||||||
messages?: Message[];
|
messages?: Message[];
|
||||||
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, hasPendingMessage = false, pendingMessageStatus = '', messages = [], onRewind } = props;
|
const { id, hasPendingMessage = false, pendingMessageStatus = '', messages = [] } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id={id} ref={ref} className={props.className}>
|
<div id={id} ref={ref} className={props.className}>
|
||||||
{messages.length > 0
|
{messages.length > 0
|
||||||
? messages.map((message, index) => {
|
? messages.map((message, index) => {
|
||||||
const { role, id: messageId, repositoryId } = message;
|
const { role, repositoryId } = message;
|
||||||
const previousRepositoryId = getPreviousRepositoryId(messages, index);
|
|
||||||
const isUserMessage = role === 'user';
|
const isUserMessage = role === 'user';
|
||||||
const isFirst = index === 0;
|
const isFirst = index === 0;
|
||||||
const isLast = index === messages.length - 1;
|
const isLast = index === messages.length - 1;
|
||||||
@@ -51,16 +49,15 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
|
|||||||
<div className="grid grid-col-1 w-full">
|
<div className="grid grid-col-1 w-full">
|
||||||
<MessageContents message={message} />
|
<MessageContents message={message} />
|
||||||
</div>
|
</div>
|
||||||
{previousRepositoryId && repositoryId && onRewind && (
|
{repositoryId && (
|
||||||
<div className="flex gap-2 flex-col lg:flex-row">
|
<div className="flex gap-2 flex-col lg:flex-row">
|
||||||
<WithTooltip tooltip="Undo changes in this message">
|
<WithTooltip tooltip="Start new chat from here">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onRewind(messageId);
|
window.open(`/repository/${repositoryId}`, '_blank');
|
||||||
}}
|
}}
|
||||||
key="i-ph:arrow-u-up-left"
|
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'i-ph:arrow-u-up-left',
|
'i-ph:git-fork',
|
||||||
'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors',
|
'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -25,7 +25,12 @@ export function Header() {
|
|||||||
<img src="/logo-styled.svg" alt="logo" className="w-[40px] inline-block rotate-90" />
|
<img src="/logo-styled.svg" alt="logo" className="w-[40px] inline-block rotate-90" />
|
||||||
</a>
|
</a>
|
||||||
<Feedback />
|
<Feedback />
|
||||||
<a href="https://www.replay.io/discord" className="text-bolt-elements-accent underline hover:no-underline" target="_blank" rel="noopener noreferrer">
|
<a
|
||||||
|
href="https://www.replay.io/discord"
|
||||||
|
className="text-bolt-elements-accent underline hover:no-underline"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
<div className="i-ph:discord-logo-fill text-[1.3em]" />
|
<div className="i-ph:discord-logo-fill text-[1.3em]" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ interface MessageBase {
|
|||||||
id: string;
|
id: string;
|
||||||
role: MessageRole;
|
role: MessageRole;
|
||||||
repositoryId?: string;
|
repositoryId?: string;
|
||||||
|
peanuts?: number;
|
||||||
|
|
||||||
|
// Not part of the protocol, indicates whether the user has explicitly approved
|
||||||
|
// the message. Once approved, the approve/reject UI is not shown again for the message.
|
||||||
|
approved?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MessageText extends MessageBase {
|
interface MessageText extends MessageBase {
|
||||||
|
|||||||
@@ -56,20 +56,28 @@ class DevelopmentServerManager {
|
|||||||
|
|
||||||
let gActiveDevelopmentServer: DevelopmentServerManager | undefined;
|
let gActiveDevelopmentServer: DevelopmentServerManager | undefined;
|
||||||
|
|
||||||
export async function updateDevelopmentServer(repositoryId: string) {
|
export async function updateDevelopmentServer(repositoryId: string | undefined) {
|
||||||
console.log('UpdateDevelopmentServer', new Date().toISOString(), repositoryId);
|
console.log('UpdateDevelopmentServer', new Date().toISOString(), repositoryId);
|
||||||
|
|
||||||
workbenchStore.showWorkbench.set(true);
|
workbenchStore.showWorkbench.set(repositoryId !== undefined);
|
||||||
workbenchStore.repositoryId.set(repositoryId);
|
workbenchStore.repositoryId.set(repositoryId);
|
||||||
workbenchStore.previewURL.set(undefined);
|
workbenchStore.previewURL.set(undefined);
|
||||||
workbenchStore.previewError.set(false);
|
workbenchStore.previewError.set(false);
|
||||||
|
|
||||||
|
if (!repositoryId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!gActiveDevelopmentServer) {
|
if (!gActiveDevelopmentServer) {
|
||||||
gActiveDevelopmentServer = new DevelopmentServerManager();
|
gActiveDevelopmentServer = new DevelopmentServerManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = await gActiveDevelopmentServer.setRepositoryContents(repositoryId);
|
const url = await gActiveDevelopmentServer.setRepositoryContents(repositoryId);
|
||||||
|
|
||||||
|
if (workbenchStore.repositoryId.get() != repositoryId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
workbenchStore.previewURL.set(url);
|
workbenchStore.previewURL.set(url);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type { Message } from '~/lib/persistence/message';
|
|||||||
import { database } from '~/lib/persistence/db';
|
import { database } from '~/lib/persistence/db';
|
||||||
import { chatStore } from '~/lib/stores/chat';
|
import { chatStore } from '~/lib/stores/chat';
|
||||||
import { debounce } from '~/utils/debounce';
|
import { debounce } from '~/utils/debounce';
|
||||||
|
import { getSupabase } from '~/lib/supabase/client';
|
||||||
|
|
||||||
function createRepositoryIdPacket(repositoryId: string): SimulationPacket {
|
function createRepositoryIdPacket(repositoryId: string): SimulationPacket {
|
||||||
return {
|
return {
|
||||||
@@ -64,6 +65,15 @@ class ChatManager {
|
|||||||
|
|
||||||
await this.client.initialize();
|
await this.client.initialize();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { user },
|
||||||
|
} = await getSupabase().auth.getUser();
|
||||||
|
const userId = user?.id || null;
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
await this.client.sendCommand({ method: 'Nut.setUserId', params: { userId } });
|
||||||
|
}
|
||||||
|
|
||||||
const { chatId } = (await this.client.sendCommand({ method: 'Nut.startChat', params: {} })) as { chatId: string };
|
const { chatId } = (await this.client.sendCommand({ method: 'Nut.startChat', params: {} })) as { chatId: string };
|
||||||
|
|
||||||
console.log('ChatStarted', new Date().toISOString(), chatId);
|
console.log('ChatStarted', new Date().toISOString(), chatId);
|
||||||
@@ -235,7 +245,7 @@ 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 const simulationRepositoryUpdated = debounce((repositoryId: string) => {
|
export const simulationRepositoryUpdated = debounce((repositoryId: string | undefined) => {
|
||||||
startChat(repositoryId, []);
|
startChat(repositoryId, []);
|
||||||
updateDevelopmentServer(repositoryId);
|
updateDevelopmentServer(repositoryId);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|||||||
37
app/lib/supabase/peanuts.ts
Normal file
37
app/lib/supabase/peanuts.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { getSupabase } from './client';
|
||||||
|
|
||||||
|
export async function supabaseAddRefund(peanuts: number) {
|
||||||
|
const supabase = getSupabase();
|
||||||
|
|
||||||
|
// Get the current user ID if available
|
||||||
|
const {
|
||||||
|
data: { user },
|
||||||
|
} = await supabase.auth.getUser();
|
||||||
|
const userId = user?.id || null;
|
||||||
|
|
||||||
|
const { data, error } = await supabase.from('profiles').select('peanuts_refunded').eq('id', userId).single();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('AddPeanutsRefund:ErrorFetchingData', { error });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPeanutsRefunded = data.peanuts_refunded;
|
||||||
|
if (typeof currentPeanutsRefunded !== 'number') {
|
||||||
|
console.error('AddPeanutsRefund:InvalidPeanutsRefunded', { currentPeanutsRefunded });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPeanutsRefunded = Math.round(currentPeanutsRefunded + peanuts);
|
||||||
|
|
||||||
|
// Note: this is not atomic.
|
||||||
|
// https://linear.app/replay/issue/PRO-1122/update-api-usage-atomically
|
||||||
|
const { error: updateError } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.update({ peanuts_refunded: newPeanutsRefunded })
|
||||||
|
.eq('id', userId);
|
||||||
|
|
||||||
|
if (updateError) {
|
||||||
|
console.error('AddPeanutsRefund:ErrorUpdatingData', { updateError });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,10 +15,9 @@ function AboutPage() {
|
|||||||
<h1 className="text-4xl font-bold mb-8 text-gray-900 dark:text-gray-200">About Nut</h1>
|
<h1 className="text-4xl font-bold mb-8 text-gray-900 dark:text-gray-200">About Nut</h1>
|
||||||
|
|
||||||
<p className="mb-6">
|
<p className="mb-6">
|
||||||
Nut is an agentic app builder for reliably developing full stack apps using AI.
|
Nut is an agentic app builder for reliably developing full stack apps using AI. When you ask Nut to build or
|
||||||
When you ask Nut to build or change an app, it will do its best to get the code
|
change an app, it will do its best to get the code changes right the first time. Afterwards it will check
|
||||||
changes right the first time. Afterwards it will check the app to make sure it's
|
the app to make sure it's working as expected, writing tests and fixing problems those tests uncover.
|
||||||
working as expected, writing tests and fixing problems those tests uncover.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="mb-6">
|
<p className="mb-6">
|
||||||
@@ -45,9 +44,8 @@ function AboutPage() {
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
Replay.io
|
Replay.io
|
||||||
</a>{' '} team.
|
</a>{' '}
|
||||||
We'd love to hear from you! Leave us some feedback at the top of the page,
|
team. We'd love to hear from you! Leave us some feedback at the top of the page, join our{' '}
|
||||||
join our{' '}
|
|
||||||
<a
|
<a
|
||||||
href="https://www.replay.io/discord"
|
href="https://www.replay.io/discord"
|
||||||
className="text-bolt-elements-accent underline hover:no-underline"
|
className="text-bolt-elements-accent underline hover:no-underline"
|
||||||
@@ -56,10 +54,11 @@ function AboutPage() {
|
|||||||
>
|
>
|
||||||
Discord
|
Discord
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
or reach us at {' '}
|
or reach us at{' '}
|
||||||
<a href="mailto:hi@replay.io" className="text-bolt-elements-accent underline hover:no-underline">
|
<a href="mailto:hi@replay.io" className="text-bolt-elements-accent underline hover:no-underline">
|
||||||
hi@replay.io
|
hi@replay.io
|
||||||
</a>.
|
</a>
|
||||||
|
.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ CREATE TABLE IF NOT EXISTS public.profiles (
|
|||||||
username TEXT UNIQUE,
|
username TEXT UNIQUE,
|
||||||
full_name TEXT,
|
full_name TEXT,
|
||||||
avatar_url 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
|
-- Create a trigger to update the updated_at column
|
||||||
|
|||||||
Reference in New Issue
Block a user