From d8199fa28299f790e1a28e9ad5ab5b911e5c7452 Mon Sep 17 00:00:00 2001 From: Strider Date: Fri, 30 May 2025 12:00:00 -0400 Subject: [PATCH] Revert "Revert "Revert "Pro 1364 ux and codebase improvements""" --- app/components/Header.tsx | 14 - .../ExampleLibraryApps.module.scss | 20 - app/components/auth/ClientAuth.tsx | 243 +++++++++ app/components/auth/ClientAuth/ClientAuth.tsx | 161 ------ app/components/auth/ClientAuth/SignInForm.tsx | 116 ---- app/components/auth/ClientAuth/SignUpForm.tsx | 137 ----- app/components/chat/ApproveChange.tsx | 2 +- .../chat/{BaseChat => }/BaseChat.module.scss | 0 app/components/chat/BaseChat.tsx | 497 ++++++++++++++++++ app/components/chat/BaseChat/BaseChat.tsx | 215 -------- .../ChatPromptContainer.tsx | 101 ---- .../components/IntroSection/IntroSection.tsx | 14 - .../ChatImplementer.tsx => Chat.client.tsx} | 180 ++++++- .../chat/ChatComponent/Chat.client.tsx | 60 --- .../ChatComponent/functions/flashScreen.ts | 22 - .../functions/flushSimulation.ts | 26 - .../getRewindMessageIndexAfterReject.ts | 18 - .../functions/mergeResponseMessages.ts | 20 - .../chat/MessageInput/MessageInput.tsx | 206 -------- .../chat/SearchInput/SearchInput.tsx | 33 -- app/components/header/Feedback.tsx | 2 +- app/components/header/Header.tsx | 4 +- app/components/icons/google-icon.tsx | 22 - app/hooks/useSpeechRecognition.ts | 67 --- app/layout/ContentContainer.tsx | 0 app/layout/PageContainer.tsx | 17 - app/lib/persistence/message.ts | 4 +- app/root.tsx | 2 +- app/routes/_index.tsx | 14 +- app/routes/about.tsx | 9 +- app/types/chat.ts | 27 - app/utils/chat/messageUtils.ts | 49 -- app/utils/chat/simulationUtils.ts | 25 - icons/google-icon.svg | 1 - 34 files changed, 926 insertions(+), 1402 deletions(-) delete mode 100644 app/components/Header.tsx create mode 100644 app/components/auth/ClientAuth.tsx delete mode 100644 app/components/auth/ClientAuth/ClientAuth.tsx delete mode 100644 app/components/auth/ClientAuth/SignInForm.tsx delete mode 100644 app/components/auth/ClientAuth/SignUpForm.tsx rename app/components/chat/{BaseChat => }/BaseChat.module.scss (100%) create mode 100644 app/components/chat/BaseChat.tsx delete mode 100644 app/components/chat/BaseChat/BaseChat.tsx delete mode 100644 app/components/chat/BaseChat/components/ChatPromptContainer/ChatPromptContainer.tsx delete mode 100644 app/components/chat/BaseChat/components/IntroSection/IntroSection.tsx rename app/components/chat/{ChatComponent/components/ChatImplementer/ChatImplementer.tsx => Chat.client.tsx} (70%) delete mode 100644 app/components/chat/ChatComponent/Chat.client.tsx delete mode 100644 app/components/chat/ChatComponent/functions/flashScreen.ts delete mode 100644 app/components/chat/ChatComponent/functions/flushSimulation.ts delete mode 100644 app/components/chat/ChatComponent/functions/getRewindMessageIndexAfterReject.ts delete mode 100644 app/components/chat/ChatComponent/functions/mergeResponseMessages.ts delete mode 100644 app/components/chat/MessageInput/MessageInput.tsx delete mode 100644 app/components/chat/SearchInput/SearchInput.tsx delete mode 100644 app/components/icons/google-icon.tsx delete mode 100644 app/hooks/useSpeechRecognition.ts delete mode 100644 app/layout/ContentContainer.tsx delete mode 100644 app/layout/PageContainer.tsx delete mode 100644 app/types/chat.ts delete mode 100644 app/utils/chat/messageUtils.ts delete mode 100644 app/utils/chat/simulationUtils.ts delete mode 100644 icons/google-icon.svg diff --git a/app/components/Header.tsx b/app/components/Header.tsx deleted file mode 100644 index fa488a26..00000000 --- a/app/components/Header.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; - -export const Header: React.FC = () => { - return ( -
- -
- ); -}; diff --git a/app/components/app-library/ExampleLibraryApps.module.scss b/app/components/app-library/ExampleLibraryApps.module.scss index fe4b43dd..d891aa34 100644 --- a/app/components/app-library/ExampleLibraryApps.module.scss +++ b/app/components/app-library/ExampleLibraryApps.module.scss @@ -1,26 +1,6 @@ .container { width: 100%; padding: 1rem; - max-height: calc(100vh - 4rem); - overflow-y: auto; - scrollbar-gutter: stable; - - &::-webkit-scrollbar { - width: 8px; - } - - &::-webkit-scrollbar-track { - background: transparent; - } - - &::-webkit-scrollbar-thumb { - background-color: var(--bolt-elements-borderColor); - border-radius: 4px; - } - - &::-webkit-scrollbar-thumb:hover { - background-color: var(--bolt-elements-textTertiary); - } } .grid { diff --git a/app/components/auth/ClientAuth.tsx b/app/components/auth/ClientAuth.tsx new file mode 100644 index 00000000..be8264a6 --- /dev/null +++ b/app/components/auth/ClientAuth.tsx @@ -0,0 +1,243 @@ +import { useState, useEffect } from 'react'; +import { toast } from 'react-toastify'; +import { getSupabase } from '~/lib/supabase/client'; +import type { AuthError, Session, User, AuthChangeEvent } from '@supabase/supabase-js'; + +export function ClientAuth() { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [showSignIn, setShowSignIn] = useState(false); + const [email, setEmail] = useState(''); + 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() { + try { + const { data } = await getSupabase().auth.getUser(); + setUser(data.user); + } catch (error) { + console.error('Error fetching user:', error); + } finally { + setLoading(false); + } + } + + getUser(); + + const { + data: { subscription }, + } = getSupabase().auth.onAuthStateChange((event: AuthChangeEvent, session: Session | null) => { + setUser(session?.user ?? null); + }); + + return () => { + subscription.unsubscribe(); + }; + }, []); + + 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); + + try { + const { error } = await getSupabase().auth.signInWithPassword({ email, password }); + + if (error) { + throw error; + } + + setShowSignIn(false); + toast.success('Successfully signed in!'); + } catch (error) { + const authError = error as AuthError; + toast.error(authError.message || 'Failed to sign in'); + } finally { + setIsSigningIn(false); + } + }; + + const handleSignUp = async (e: React.FormEvent) => { + e.preventDefault(); + setIsSigningIn(true); + + try { + const { error } = await getSupabase().auth.signUp({ email, password }); + + if (error) { + throw error; + } + + toast.success('Check your email for the confirmation link!'); + setShowSignIn(false); + } catch (error) { + const authError = error as AuthError; + toast.error(authError.message || 'Failed to sign up'); + } finally { + setIsSigningIn(false); + } + }; + + const handleSignOut = async () => { + await getSupabase().auth.signOut(); + setShowDropdown(false); + toast.success('Signed out successfully'); + }; + + const handleGoogleSignIn = async () => { + const { error } = await getSupabase().auth.signInWithOAuth({ + provider: 'google', + }); + console.log('GoogleSignIn', error); + }; + + if (loading) { + return
; + } + + // Avatar URLs are disabled due to broken links from CORS issues. + const useAvatarURL = false; + + return ( + <> + {user ? ( +
+ + + {showDropdown && ( +
+
+ {user.email} +
+
+ {`Peanuts used: ${usageData?.peanuts_used ?? '...'}`} +
+
+ {`Peanuts refunded: ${usageData?.peanuts_refunded ?? '...'}`} +
+ +
+ )} +
+ ) : ( + + )} + + {showSignIn && ( +
setShowSignIn(false)} + > +
e.stopPropagation()} + > +

Sign In

+ +
+
+ + setEmail(e.target.value)} + className="w-full p-2 border rounded bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary border-bolt-elements-borderColor" + required + /> +
+
+ + setPassword(e.target.value)} + className="w-full p-2 border rounded bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary border-bolt-elements-borderColor" + required + /> +
+
+ + +
+
+
+
+ )} + + ); +} diff --git a/app/components/auth/ClientAuth/ClientAuth.tsx b/app/components/auth/ClientAuth/ClientAuth.tsx deleted file mode 100644 index 06ff911f..00000000 --- a/app/components/auth/ClientAuth/ClientAuth.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { useState, useEffect } from 'react'; -import { toast } from 'react-toastify'; -import { getSupabase } from '~/lib/supabase/client'; -import type { Session, User, AuthChangeEvent } from '@supabase/supabase-js'; -import { SignInForm } from './SignInForm'; -import { SignUpForm } from './SignUpForm'; - -export function ClientAuth() { - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - const [showAuthModal, setShowAuthModal] = useState(false); - const [isSignUp, setIsSignUp] = useState(false); - const [showDropdown, setShowDropdown] = useState(false); - const [usageData, setUsageData] = useState<{ peanuts_used: number; peanuts_refunded: number } | null>(null); - - useEffect(() => { - async function getUser() { - try { - const { data } = await getSupabase().auth.getUser(); - setUser(data.user); - } catch (error) { - console.error('Error fetching user:', error); - } finally { - setLoading(false); - } - } - - getUser(); - - const { - data: { subscription }, - } = getSupabase().auth.onAuthStateChange((event: AuthChangeEvent, session: Session | null) => { - setUser(session?.user ?? null); - if (session?.user) { - setShowAuthModal(false); - } - }); - - return () => { - subscription.unsubscribe(); - }; - }, []); - - 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 handleSignOut = async () => { - await getSupabase().auth.signOut(); - setShowDropdown(false); - toast.success('Signed out successfully'); - }; - - if (loading) { - return
; - } - - // Avatar URLs are disabled due to broken links from CORS issues. - const useAvatarURL = false; - - return ( - <> - {user ? ( -
- - - {showDropdown && ( -
-
-
Signed in as
-
{user.email}
-
-
-
Usage
-
-
- Peanuts used - {usageData?.peanuts_used ?? '...'} -
-
- Peanuts refunded - {usageData?.peanuts_refunded ?? '...'} -
-
-
-
- -
-
- )} -
- ) : ( - - )} - - {showAuthModal && ( -
setShowAuthModal(false)} - > -
e.stopPropagation()} - > - {isSignUp ? ( - setIsSignUp(false)} /> - ) : ( - setIsSignUp(true)} /> - )} -
-
- )} - - ); -} diff --git a/app/components/auth/ClientAuth/SignInForm.tsx b/app/components/auth/ClientAuth/SignInForm.tsx deleted file mode 100644 index 4e4e2903..00000000 --- a/app/components/auth/ClientAuth/SignInForm.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { useState } from 'react'; -import { toast } from 'react-toastify'; -import { getSupabase } from '~/lib/supabase/client'; -import type { AuthError } from '@supabase/supabase-js'; -import { GoogleIcon } from '~/components/icons/google-icon'; - -interface SignInFormProps { - onToggleForm: () => void; -} - -export function SignInForm({ onToggleForm }: SignInFormProps) { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [isProcessing, setIsProcessing] = useState(false); - - const handleSignIn = async (e: React.FormEvent) => { - e.preventDefault(); - setIsProcessing(true); - - try { - const { error } = await getSupabase().auth.signInWithPassword({ email, password }); - - if (error) { - throw error; - } - - toast.success('Successfully signed in!'); - } catch (error) { - const authError = error as AuthError; - toast.error(authError.message || 'Failed to sign in'); - } finally { - setIsProcessing(false); - } - }; - - const handleGoogleSignIn = async () => { - const { error } = await getSupabase().auth.signInWithOAuth({ - provider: 'google', - }); - if (error) { - toast.error(error.message || 'Failed to sign in with Google'); - } - }; - - return ( - <> -

Welcome Back

- - - -
-
-
-
-
- - Or continue with email - -
-
- -
-
- - setEmail(e.target.value)} - className="w-full p-3 border rounded-lg bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary border-bolt-elements-borderColor focus:ring-2 focus:ring-green-500 focus:border-transparent" - required - /> -
- -
- - setPassword(e.target.value)} - className="w-full p-3 border rounded-lg bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary border-bolt-elements-borderColor focus:ring-2 focus:ring-green-500 focus:border-transparent" - required - /> -
- - -
- -

- Don't have an account?{' '} - -

- - ); -} diff --git a/app/components/auth/ClientAuth/SignUpForm.tsx b/app/components/auth/ClientAuth/SignUpForm.tsx deleted file mode 100644 index 2b853bd4..00000000 --- a/app/components/auth/ClientAuth/SignUpForm.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { useState } from 'react'; -import { toast } from 'react-toastify'; -import { getSupabase } from '~/lib/supabase/client'; -import type { AuthError } from '@supabase/supabase-js'; -import { GoogleIcon } from '~/components/icons/google-icon'; - -interface SignUpFormProps { - onToggleForm: () => void; -} - -export function SignUpForm({ onToggleForm }: SignUpFormProps) { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [isProcessing, setIsProcessing] = useState(false); - - const handleSignUp = async (e: React.FormEvent) => { - e.preventDefault(); - - if (password !== confirmPassword) { - toast.error('Passwords do not match'); - return; - } - - setIsProcessing(true); - - try { - const { error } = await getSupabase().auth.signUp({ email, password }); - - if (error) { - throw error; - } - - toast.success('Check your email for the confirmation link!'); - } catch (error) { - const authError = error as AuthError; - toast.error(authError.message || 'Failed to sign up'); - } finally { - setIsProcessing(false); - } - }; - - const handleGoogleSignIn = async () => { - const { error } = await getSupabase().auth.signInWithOAuth({ - provider: 'google', - }); - if (error) { - toast.error(error.message || 'Failed to sign in with Google'); - } - }; - - return ( - <> -

Create Account

- - - -
-
-
-
-
- - Or continue with email - -
-
- -
-
- - setEmail(e.target.value)} - className="w-full p-3 border rounded-lg bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary border-bolt-elements-borderColor focus:ring-2 focus:ring-green-500 focus:border-transparent" - required - /> -
- -
- - setPassword(e.target.value)} - className="w-full p-3 border rounded-lg bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary border-bolt-elements-borderColor focus:ring-2 focus:ring-green-500 focus:border-transparent" - required - /> -
- -
- - setConfirmPassword(e.target.value)} - className="w-full p-3 border rounded-lg bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary border-bolt-elements-borderColor focus:ring-2 focus:ring-green-500 focus:border-transparent" - required - /> -
- - -
- -

- Already have an account?{' '} - -

- - ); -} diff --git a/app/components/chat/ApproveChange.tsx b/app/components/chat/ApproveChange.tsx index 6906b5c5..1bdc4bf2 100644 --- a/app/components/chat/ApproveChange.tsx +++ b/app/components/chat/ApproveChange.tsx @@ -1,6 +1,6 @@ import React, { useRef, useState } from 'react'; import { classNames } from '~/utils/classNames'; -import { TEXTAREA_MIN_HEIGHT } from './BaseChat/BaseChat'; +import { TEXTAREA_MIN_HEIGHT } from './BaseChat'; export interface RejectChangeData { explanation: string; diff --git a/app/components/chat/BaseChat/BaseChat.module.scss b/app/components/chat/BaseChat.module.scss similarity index 100% rename from app/components/chat/BaseChat/BaseChat.module.scss rename to app/components/chat/BaseChat.module.scss diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx new file mode 100644 index 00000000..299c7df1 --- /dev/null +++ b/app/components/chat/BaseChat.tsx @@ -0,0 +1,497 @@ +/* + * @ts-nocheck + * Preventing TS checks with files presented in the video for a better presentation. + */ +import React, { type RefCallback, useEffect, useState } from 'react'; +import { ClientOnly } from 'remix-utils/client-only'; +import { Menu } from '~/components/sidebar/Menu.client'; +import { IconButton } from '~/components/ui/IconButton'; +import { Workbench } from '~/components/workbench/Workbench.client'; +import { classNames } from '~/utils/classNames'; +import { Messages } from './Messages.client'; +import { type Message } from '~/lib/persistence/message'; +import { SendButton } from './SendButton.client'; +import * as Tooltip from '@radix-ui/react-tooltip'; + +import styles from './BaseChat.module.scss'; +import { ExamplePrompts } from '~/components/chat/ExamplePrompts'; +import { ExampleLibraryApps } from '~/components/app-library/ExampleLibraryApps'; + +import FilePreview from './FilePreview'; +import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition'; +import { ScreenshotStateManager } from './ScreenshotStateManager'; +import type { RejectChangeData } from './ApproveChange'; +import ApproveChange from './ApproveChange'; + +export const TEXTAREA_MIN_HEIGHT = 76; + +interface BaseChatProps { + textareaRef?: React.RefObject | undefined; + messageRef?: RefCallback | undefined; + scrollRef?: RefCallback | undefined; + showChat?: boolean; + chatStarted?: boolean; + hasPendingMessage?: boolean; + pendingMessageStatus?: string; + messages?: Message[]; + input?: string; + handleStop?: () => void; + sendMessage?: (messageInput?: string) => void; + handleInputChange?: (event: React.ChangeEvent) => void; + _enhancingPrompt?: boolean; + _enhancePrompt?: () => void; + uploadedFiles?: File[]; + setUploadedFiles?: (files: File[]) => void; + imageDataList?: string[]; + setImageDataList?: (dataList: string[]) => void; + onApproveChange?: (messageId: string) => void; + onRejectChange?: (messageId: string, data: RejectChangeData) => void; +} + +export const BaseChat = React.forwardRef( + ( + { + textareaRef, + messageRef, + scrollRef, + showChat = true, + chatStarted = false, + hasPendingMessage = false, + pendingMessageStatus = '', + input = '', + handleInputChange, + + sendMessage, + handleStop, + uploadedFiles = [], + setUploadedFiles, + imageDataList = [], + setImageDataList, + messages, + onApproveChange, + onRejectChange, + }, + ref, + ) => { + const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200; + const [isListening, setIsListening] = useState(false); + const [recognition, setRecognition] = useState(null); + const [transcript, setTranscript] = useState(''); + const [rejectFormOpen, setRejectFormOpen] = useState(false); + const [pendingFilterText, setPendingFilterText] = useState(''); + const [filterText, setFilterText] = useState(''); + + useEffect(() => { + console.log(transcript); + }, [transcript]); + + useEffect(() => { + // Load API keys from cookies on component mount + + if (typeof window !== 'undefined' && ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window)) { + const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + const recognition = new SpeechRecognition(); + recognition.continuous = true; + recognition.interimResults = true; + + recognition.onresult = (event) => { + const transcript = Array.from(event.results) + .map((result) => result[0]) + .map((result) => result.transcript) + .join(''); + + setTranscript(transcript); + + if (handleInputChange) { + const syntheticEvent = { + target: { value: transcript }, + } as React.ChangeEvent; + handleInputChange(syntheticEvent); + } + }; + + recognition.onerror = (event) => { + console.error('Speech recognition error:', event.error); + setIsListening(false); + }; + + setRecognition(recognition); + } + }, []); + + const startListening = () => { + if (recognition) { + recognition.start(); + setIsListening(true); + } + }; + + const stopListening = () => { + if (recognition) { + recognition.stop(); + setIsListening(false); + } + }; + + const handleSendMessage = (event: React.UIEvent, messageInput?: string) => { + if (sendMessage) { + sendMessage(messageInput); + + if (recognition) { + recognition.abort(); // Stop current recognition + setTranscript(''); // Clear transcript + setIsListening(false); + + // Clear the input by triggering handleInputChange with empty value + if (handleInputChange) { + const syntheticEvent = { + target: { value: '' }, + } as React.ChangeEvent; + handleInputChange(syntheticEvent); + } + } + } + }; + + const handleFileUpload = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'image/*'; + + input.onchange = async (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + + if (file) { + const reader = new FileReader(); + + reader.onload = (e) => { + const base64Image = e.target?.result as string; + setUploadedFiles?.([...uploadedFiles, file]); + setImageDataList?.([...imageDataList, base64Image]); + }; + reader.readAsDataURL(file); + } + }; + + input.click(); + }; + + const handlePaste = async (e: React.ClipboardEvent) => { + const items = e.clipboardData?.items; + + if (!items) { + return; + } + + for (const item of items) { + if (item.type.startsWith('image/')) { + e.preventDefault(); + + const file = item.getAsFile(); + + if (file) { + const reader = new FileReader(); + + reader.onload = (e) => { + const base64Image = e.target?.result as string; + setUploadedFiles?.([...uploadedFiles, file]); + setImageDataList?.([...imageDataList, base64Image]); + }; + reader.readAsDataURL(file); + } + + break; + } + } + }; + + const approveChangeMessageId = (() => { + if (hasPendingMessage || !messages) { + return undefined; + } + + 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; + } + } + return undefined; + })(); + + let messageInput; + + if (!rejectFormOpen) { + messageInput = ( +
+