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:
parent
4f5051dee5
commit
9389fb2afc
@ -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() {
|
||||
</button>
|
||||
|
||||
{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">
|
||||
{user.email}
|
||||
</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
|
||||
onClick={handleSignOut}
|
||||
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 { classNames } from '~/utils/classNames';
|
||||
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 * as Tooltip from '@radix-ui/react-tooltip';
|
||||
|
||||
@ -20,7 +20,6 @@ import FilePreview from './FilePreview';
|
||||
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
|
||||
import { ScreenshotStateManager } from './ScreenshotStateManager';
|
||||
import type { RejectChangeData } from './ApproveChange';
|
||||
import { assert } from '~/lib/replay/ReplayProtocolClient';
|
||||
import ApproveChange from './ApproveChange';
|
||||
|
||||
export const TEXTAREA_MIN_HEIGHT = 76;
|
||||
@ -46,10 +45,8 @@ interface BaseChatProps {
|
||||
setUploadedFiles?: (files: File[]) => void;
|
||||
imageDataList?: string[];
|
||||
setImageDataList?: (dataList: string[]) => void;
|
||||
onRewind?: (messageId: string) => void;
|
||||
approveChangesMessageId?: string;
|
||||
onApproveChange?: (messageId: string) => void;
|
||||
onRejectChange?: (lastMessageId: string, data: RejectChangeData) => void;
|
||||
onRejectChange?: (messageId: string, data: RejectChangeData) => void;
|
||||
}
|
||||
|
||||
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
@ -72,8 +69,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
imageDataList = [],
|
||||
setImageDataList,
|
||||
messages,
|
||||
onRewind,
|
||||
approveChangesMessageId,
|
||||
onApproveChange,
|
||||
onRejectChange,
|
||||
},
|
||||
@ -209,30 +204,21 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
}
|
||||
};
|
||||
|
||||
const showApproveChange = (() => {
|
||||
if (hasPendingMessage) {
|
||||
return false;
|
||||
const approveChangeMessageId = (() => {
|
||||
if (hasPendingMessage || !messages) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!messages?.length) {
|
||||
return false;
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const message = messages[i];
|
||||
if (message.repositoryId && message.peanuts) {
|
||||
return message.approved ? undefined : message.id;
|
||||
}
|
||||
if (message.role == 'user') {
|
||||
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;
|
||||
return undefined;
|
||||
})();
|
||||
|
||||
let messageInput;
|
||||
@ -369,7 +355,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
Get what you want
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
@ -387,7 +373,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
messages={messages}
|
||||
hasPendingMessage={hasPendingMessage}
|
||||
pendingMessageStatus={pendingMessageStatus}
|
||||
onRewind={onRewind}
|
||||
/>
|
||||
) : null;
|
||||
}}
|
||||
@ -444,24 +429,12 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
/>
|
||||
)}
|
||||
</ClientOnly>
|
||||
{showApproveChange && (
|
||||
{approveChangeMessageId && (
|
||||
<ApproveChange
|
||||
rejectFormOpen={rejectFormOpen}
|
||||
setRejectFormOpen={setRejectFormOpen}
|
||||
onApprove={() => {
|
||||
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}
|
||||
|
||||
@ -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<File[]>([]); // Move here
|
||||
const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [approveChangesMessageId, setApproveChangesMessageId] = useState<string | undefined>(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}
|
||||
/>
|
||||
|
||||
@ -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<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => {
|
||||
const { id, hasPendingMessage = false, pendingMessageStatus = '', messages = [], onRewind } = props;
|
||||
const { id, hasPendingMessage = false, pendingMessageStatus = '', messages = [] } = props;
|
||||
|
||||
return (
|
||||
<div id={id} ref={ref} className={props.className}>
|
||||
{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<HTMLDivElement, MessagesProps>((props:
|
||||
<div className="grid grid-col-1 w-full">
|
||||
<MessageContents message={message} />
|
||||
</div>
|
||||
{previousRepositoryId && repositoryId && onRewind && (
|
||||
{repositoryId && (
|
||||
<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
|
||||
onClick={() => {
|
||||
onRewind(messageId);
|
||||
window.open(`/repository/${repositoryId}`, '_blank');
|
||||
}}
|
||||
key="i-ph:arrow-u-up-left"
|
||||
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',
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -25,7 +25,12 @@ export function Header() {
|
||||
<img src="/logo-styled.svg" alt="logo" className="w-[40px] inline-block rotate-90" />
|
||||
</a>
|
||||
<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]" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -8,6 +8,11 @@ interface MessageBase {
|
||||
id: string;
|
||||
role: MessageRole;
|
||||
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 {
|
||||
|
||||
@ -56,20 +56,28 @@ class DevelopmentServerManager {
|
||||
|
||||
let gActiveDevelopmentServer: DevelopmentServerManager | undefined;
|
||||
|
||||
export async function updateDevelopmentServer(repositoryId: string) {
|
||||
export async function updateDevelopmentServer(repositoryId: string | undefined) {
|
||||
console.log('UpdateDevelopmentServer', new Date().toISOString(), repositoryId);
|
||||
|
||||
workbenchStore.showWorkbench.set(true);
|
||||
workbenchStore.showWorkbench.set(repositoryId !== undefined);
|
||||
workbenchStore.repositoryId.set(repositoryId);
|
||||
workbenchStore.previewURL.set(undefined);
|
||||
workbenchStore.previewError.set(false);
|
||||
|
||||
if (!repositoryId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!gActiveDevelopmentServer) {
|
||||
gActiveDevelopmentServer = new DevelopmentServerManager();
|
||||
}
|
||||
|
||||
const url = await gActiveDevelopmentServer.setRepositoryContents(repositoryId);
|
||||
|
||||
if (workbenchStore.repositoryId.get() != repositoryId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (url) {
|
||||
workbenchStore.previewURL.set(url);
|
||||
} else {
|
||||
|
||||
@ -11,6 +11,7 @@ import type { Message } from '~/lib/persistence/message';
|
||||
import { database } from '~/lib/persistence/db';
|
||||
import { chatStore } from '~/lib/stores/chat';
|
||||
import { debounce } from '~/utils/debounce';
|
||||
import { getSupabase } from '~/lib/supabase/client';
|
||||
|
||||
function createRepositoryIdPacket(repositoryId: string): SimulationPacket {
|
||||
return {
|
||||
@ -64,6 +65,15 @@ class ChatManager {
|
||||
|
||||
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 };
|
||||
|
||||
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
|
||||
* and update the remote development server.
|
||||
*/
|
||||
export const simulationRepositoryUpdated = debounce((repositoryId: string) => {
|
||||
export const simulationRepositoryUpdated = debounce((repositoryId: string | undefined) => {
|
||||
startChat(repositoryId, []);
|
||||
updateDevelopmentServer(repositoryId);
|
||||
}, 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>
|
||||
|
||||
<p className="mb-6">
|
||||
Nut is an agentic app builder for reliably developing full stack apps using AI.
|
||||
When you ask Nut to build or change an app, it will do its best to get the code
|
||||
changes right the first time. Afterwards it will check the app to make sure it's
|
||||
working as expected, writing tests and fixing problems those tests uncover.
|
||||
Nut is an agentic app builder for reliably developing full stack apps using AI. When you ask Nut to build or
|
||||
change an app, it will do its best to get the code changes right the first time. Afterwards it will check
|
||||
the app to make sure it's working as expected, writing tests and fixing problems those tests uncover.
|
||||
</p>
|
||||
|
||||
<p className="mb-6">
|
||||
@ -45,9 +44,8 @@ function AboutPage() {
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Replay.io
|
||||
</a>{' '} team.
|
||||
We'd love to hear from you! Leave us some feedback at the top of the page,
|
||||
join our{' '}
|
||||
</a>{' '}
|
||||
team. We'd love to hear from you! Leave us some feedback at the top of the page, join our{' '}
|
||||
<a
|
||||
href="https://www.replay.io/discord"
|
||||
className="text-bolt-elements-accent underline hover:no-underline"
|
||||
@ -56,10 +54,11 @@ function AboutPage() {
|
||||
>
|
||||
Discord
|
||||
</a>{' '}
|
||||
or reach us at {' '}
|
||||
or reach us at{' '}
|
||||
<a href="mailto:hi@replay.io" className="text-bolt-elements-accent underline hover:no-underline">
|
||||
hi@replay.io
|
||||
</a>.
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user