From 4331b476b9112e14cd737a2b18cfad6088dea9a0 Mon Sep 17 00:00:00 2001 From: Strider Date: Fri, 30 May 2025 11:56:06 -0400 Subject: [PATCH] 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 +- app/components/chat/BaseChat.tsx | 497 ------------------ .../chat/{ => BaseChat}/BaseChat.module.scss | 0 app/components/chat/BaseChat/BaseChat.tsx | 215 ++++++++ .../ChatPromptContainer.tsx | 101 ++++ .../components/IntroSection/IntroSection.tsx | 14 + .../chat/ChatComponent/Chat.client.tsx | 60 +++ .../ChatImplementer/ChatImplementer.tsx} | 180 +------ .../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, 1402 insertions(+), 926 deletions(-) create mode 100644 app/components/Header.tsx delete mode 100644 app/components/auth/ClientAuth.tsx create mode 100644 app/components/auth/ClientAuth/ClientAuth.tsx create mode 100644 app/components/auth/ClientAuth/SignInForm.tsx create mode 100644 app/components/auth/ClientAuth/SignUpForm.tsx delete mode 100644 app/components/chat/BaseChat.tsx rename app/components/chat/{ => BaseChat}/BaseChat.module.scss (100%) create mode 100644 app/components/chat/BaseChat/BaseChat.tsx create mode 100644 app/components/chat/BaseChat/components/ChatPromptContainer/ChatPromptContainer.tsx create mode 100644 app/components/chat/BaseChat/components/IntroSection/IntroSection.tsx create mode 100644 app/components/chat/ChatComponent/Chat.client.tsx rename app/components/chat/{Chat.client.tsx => ChatComponent/components/ChatImplementer/ChatImplementer.tsx} (70%) create mode 100644 app/components/chat/ChatComponent/functions/flashScreen.ts create mode 100644 app/components/chat/ChatComponent/functions/flushSimulation.ts create mode 100644 app/components/chat/ChatComponent/functions/getRewindMessageIndexAfterReject.ts create mode 100644 app/components/chat/ChatComponent/functions/mergeResponseMessages.ts create mode 100644 app/components/chat/MessageInput/MessageInput.tsx create mode 100644 app/components/chat/SearchInput/SearchInput.tsx create mode 100644 app/components/icons/google-icon.tsx create mode 100644 app/hooks/useSpeechRecognition.ts create mode 100644 app/layout/ContentContainer.tsx create mode 100644 app/layout/PageContainer.tsx create mode 100644 app/types/chat.ts create mode 100644 app/utils/chat/messageUtils.ts create mode 100644 app/utils/chat/simulationUtils.ts create mode 100644 icons/google-icon.svg diff --git a/app/components/Header.tsx b/app/components/Header.tsx new file mode 100644 index 00000000..fa488a26 --- /dev/null +++ b/app/components/Header.tsx @@ -0,0 +1,14 @@ +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 d891aa34..fe4b43dd 100644 --- a/app/components/app-library/ExampleLibraryApps.module.scss +++ b/app/components/app-library/ExampleLibraryApps.module.scss @@ -1,6 +1,26 @@ .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 deleted file mode 100644 index be8264a6..00000000 --- a/app/components/auth/ClientAuth.tsx +++ /dev/null @@ -1,243 +0,0 @@ -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 new file mode 100644 index 00000000..06ff911f --- /dev/null +++ b/app/components/auth/ClientAuth/ClientAuth.tsx @@ -0,0 +1,161 @@ +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 new file mode 100644 index 00000000..4e4e2903 --- /dev/null +++ b/app/components/auth/ClientAuth/SignInForm.tsx @@ -0,0 +1,116 @@ +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 new file mode 100644 index 00000000..2b853bd4 --- /dev/null +++ b/app/components/auth/ClientAuth/SignUpForm.tsx @@ -0,0 +1,137 @@ +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 1bdc4bf2..6906b5c5 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'; +import { TEXTAREA_MIN_HEIGHT } from './BaseChat/BaseChat'; export interface RejectChangeData { explanation: string; diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx deleted file mode 100644 index 299c7df1..00000000 --- a/app/components/chat/BaseChat.tsx +++ /dev/null @@ -1,497 +0,0 @@ -/* - * @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 = ( -
-