Merge pull request #122 from replayio/revert-121-PRO-1364-Ux-and-codebase-improvements

Revert "Pro 1364 ux and codebase improvements"
This commit is contained in:
Strider 2025-05-28 12:01:32 -04:00 committed by GitHub
commit 776e4802bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 926 additions and 1402 deletions

View File

@ -1,14 +0,0 @@
import React from 'react';
export const Header: React.FC = () => {
return (
<header className="w-full bg-white border-b border-gray-200 px-4 py-3">
<nav className="max-w-7xl mx-auto flex items-center justify-between">
<div className="flex items-center">
<h1 className="text-xl font-semibold">Your App Name</h1>
</div>
{/* Add navigation items here as needed */}
</nav>
</header>
);
};

View File

@ -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 {

View File

@ -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<User | null>(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 <div className="w-8 h-8 rounded-full bg-gray-300 animate-pulse" />;
}
// Avatar URLs are disabled due to broken links from CORS issues.
const useAvatarURL = false;
return (
<>
{user ? (
<div className="relative">
<button
className="flex items-center justify-center w-8 h-8 rounded-full bg-green-500 text-white"
onClick={() => setShowDropdown(!showDropdown)}
>
{useAvatarURL && user.user_metadata?.avatar_url ? (
<img
src={user.user_metadata.avatar_url}
alt="User avatar"
className="w-full h-full rounded-full object-cover"
/>
) : (
<span>{user.email?.substring(0, 2).toUpperCase()}</span>
)}
</button>
{showDropdown && (
<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 bg-green-500 text-white rounded hover:bg-green-600 font-bold"
>
Sign Out
</button>
</div>
)}
</div>
) : (
<button
onClick={() => setShowSignIn(true)}
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 font-bold"
>
Sign In
</button>
)}
{showSignIn && (
<div
className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50"
onClick={() => setShowSignIn(false)}
>
<div
className="bg-bolt-elements-background-depth-1 p-6 rounded-lg max-w-md mx-auto"
onClick={(e) => e.stopPropagation()}
>
<h2 className="text-2xl font-bold mb-4 text-bolt-elements-textPrimary">Sign In</h2>
<button
type="button"
onClick={handleGoogleSignIn}
disabled={isSigningIn}
className="w-full mb-4 p-2 bg-green-500 text-white rounded hover:bg-green-600 disabled:opacity-50"
>
{isSigningIn ? 'Processing...' : 'Use Google'}
</button>
<form onSubmit={handleSignIn}>
<div className="mb-4">
<label htmlFor="email" className="block mb-1 text-bolt-elements-textPrimary">
Email
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => 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
/>
</div>
<div className="mb-4">
<label htmlFor="password" className="block mb-1 text-bolt-elements-textPrimary">
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => 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
/>
</div>
<div className="flex gap-2">
<button
type="submit"
disabled={isSigningIn}
className="flex-1 py-2 bg-green-500 text-white rounded hover:bg-green-600 disabled:opacity-50"
>
{isSigningIn ? 'Processing...' : 'Sign In'}
</button>
<button
type="button"
onClick={handleSignUp}
disabled={isSigningIn}
className="flex-1 py-2 bg-green-500 text-white rounded hover:bg-green-600 disabled:opacity-50"
>
{isSigningIn ? 'Processing...' : 'Sign Up'}
</button>
</div>
</form>
</div>
</div>
)}
</>
);
}

View File

@ -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<User | null>(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 <div className="w-8 h-8 rounded-full bg-gray-300 animate-pulse" />;
}
// Avatar URLs are disabled due to broken links from CORS issues.
const useAvatarURL = false;
return (
<>
{user ? (
<div className="relative">
<button
className="flex items-center justify-center w-8 h-8 rounded-full bg-green-500 text-white"
onClick={() => setShowDropdown(!showDropdown)}
>
{useAvatarURL && user.user_metadata?.avatar_url ? (
<img
src={user.user_metadata.avatar_url}
alt="User avatar"
className="w-full h-full rounded-full object-cover"
/>
) : (
<span>{user.email?.substring(0, 2).toUpperCase()}</span>
)}
</button>
{showDropdown && (
<div className="absolute right-0 mt-2 py-2 w-64 bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded-lg shadow-lg z-10">
<div className="px-4 py-3 text-bolt-elements-textPrimary border-b border-bolt-elements-borderColor">
<div className="text-sm text-bolt-elements-textSecondary">Signed in as</div>
<div className="font-medium truncate">{user.email}</div>
</div>
<div className="px-4 py-3 text-bolt-elements-textPrimary border-b border-bolt-elements-borderColor">
<div className="text-sm text-bolt-elements-textSecondary">Usage</div>
<div className="space-y-1">
<div className="flex justify-between items-center">
<span>Peanuts used</span>
<span className="font-medium">{usageData?.peanuts_used ?? '...'}</span>
</div>
<div className="flex justify-between items-center">
<span>Peanuts refunded</span>
<span className="font-medium">{usageData?.peanuts_refunded ?? '...'}</span>
</div>
</div>
</div>
<div className="px-2 pt-2">
<button
onClick={handleSignOut}
className="w-full px-4 py-2 text-left bg-green-500 text-white hover:bg-green-600 rounded-md transition-colors flex items-center justify-center"
>
Sign Out
</button>
</div>
</div>
)}
</div>
) : (
<button
onClick={() => {
setShowAuthModal(true);
setIsSignUp(false);
}}
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 font-bold"
>
Sign In
</button>
)}
{showAuthModal && (
<div
className="fixed inset-0 bg-black bg-opacity-50 backdrop-blur-sm flex justify-center items-center z-50"
onClick={() => setShowAuthModal(false)}
>
<div
className="bg-bolt-elements-background-depth-1 p-8 rounded-lg w-full max-w-md mx-auto border border-bolt-elements-borderColor"
onClick={(e) => e.stopPropagation()}
>
{isSignUp ? (
<SignUpForm onToggleForm={() => setIsSignUp(false)} />
) : (
<SignInForm onToggleForm={() => setIsSignUp(true)} />
)}
</div>
</div>
)}
</>
);
}

View File

@ -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 (
<>
<h2 className="text-2xl font-bold mb-6 text-bolt-elements-textPrimary text-center">Welcome Back</h2>
<button
type="button"
onClick={handleGoogleSignIn}
disabled={isProcessing}
className="w-full mb-6 p-3 flex items-center justify-center gap-2 bg-white text-gray-800 rounded-lg hover:bg-gray-100 disabled:opacity-50 border border-gray-300"
>
<GoogleIcon />
<span>{isProcessing ? 'Processing...' : 'Continue with Google'}</span>
</button>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-bolt-elements-borderColor"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-bolt-elements-background-depth-1 text-bolt-elements-textSecondary">
Or continue with email
</span>
</div>
</div>
<form onSubmit={handleSignIn}>
<div className="mb-4">
<label htmlFor="email" className="block mb-2 text-sm font-medium text-bolt-elements-textPrimary">
Email Address
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => 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
/>
</div>
<div className="mb-4">
<label htmlFor="password" className="block mb-2 text-sm font-medium text-bolt-elements-textPrimary">
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => 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
/>
</div>
<button
type="submit"
disabled={isProcessing}
className="w-full py-3 bg-green-500 text-white rounded-lg hover:bg-green-600 disabled:opacity-50 font-medium"
>
{isProcessing ? 'Processing...' : 'Sign In'}
</button>
</form>
<p className="mt-6 text-center text-bolt-elements-textSecondary">
Don't have an account?{' '}
<button onClick={onToggleForm} className="text-green-500 hover:text-green-600 font-medium bg-transparent">
Sign Up
</button>
</p>
</>
);
}

View File

@ -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 (
<>
<h2 className="text-2xl font-bold mb-6 text-bolt-elements-textPrimary text-center">Create Account</h2>
<button
type="button"
onClick={handleGoogleSignIn}
disabled={isProcessing}
className="w-full mb-6 p-3 flex items-center justify-center gap-2 bg-white text-gray-800 rounded-lg hover:bg-gray-100 disabled:opacity-50 border border-gray-300"
>
<GoogleIcon />
<span>{isProcessing ? 'Processing...' : 'Continue with Google'}</span>
</button>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-bolt-elements-borderColor"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-bolt-elements-background-depth-1 text-bolt-elements-textSecondary">
Or continue with email
</span>
</div>
</div>
<form onSubmit={handleSignUp}>
<div className="mb-4">
<label htmlFor="email" className="block mb-2 text-sm font-medium text-bolt-elements-textPrimary">
Email Address
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => 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
/>
</div>
<div className="mb-4">
<label htmlFor="password" className="block mb-2 text-sm font-medium text-bolt-elements-textPrimary">
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => 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
/>
</div>
<div className="mb-6">
<label htmlFor="confirmPassword" className="block mb-2 text-sm font-medium text-bolt-elements-textPrimary">
Confirm Password
</label>
<input
id="confirmPassword"
type="password"
value={confirmPassword}
onChange={(e) => 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
/>
</div>
<button
type="submit"
disabled={isProcessing}
className="w-full py-3 bg-green-500 text-white rounded-lg hover:bg-green-600 disabled:opacity-50 font-medium"
>
{isProcessing ? 'Processing...' : 'Create Account'}
</button>
</form>
<p className="mt-6 text-center text-bolt-elements-textSecondary">
Already have an account?{' '}
<button onClick={onToggleForm} className="text-green-500 hover:text-green-600 font-medium bg-transparent">
Sign In
</button>
</p>
</>
);
}

View File

@ -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;

View File

@ -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<HTMLTextAreaElement> | undefined;
messageRef?: RefCallback<HTMLDivElement> | undefined;
scrollRef?: RefCallback<HTMLDivElement> | undefined;
showChat?: boolean;
chatStarted?: boolean;
hasPendingMessage?: boolean;
pendingMessageStatus?: string;
messages?: Message[];
input?: string;
handleStop?: () => void;
sendMessage?: (messageInput?: string) => void;
handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => 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<HTMLDivElement, BaseChatProps>(
(
{
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<SpeechRecognition | null>(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<HTMLTextAreaElement>;
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<HTMLTextAreaElement>;
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 = (
<div
className={classNames('relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg')}
>
<textarea
ref={textareaRef}
className={classNames(
'w-full pl-4 pt-4 pr-25 outline-none resize-none text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent text-sm',
'transition-all duration-200',
'hover:border-bolt-elements-focus',
)}
onDragEnter={(e) => {
e.preventDefault();
e.currentTarget.style.border = '2px solid #1488fc';
}}
onDragOver={(e) => {
e.preventDefault();
e.currentTarget.style.border = '2px solid #1488fc';
}}
onDragLeave={(e) => {
e.preventDefault();
e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
}}
onDrop={(e) => {
e.preventDefault();
e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
const files = Array.from(e.dataTransfer.files);
files.forEach((file) => {
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
const base64Image = e.target?.result as string;
setUploadedFiles?.([...uploadedFiles, file]);
setImageDataList?.([...imageDataList, base64Image]);
};
reader.readAsDataURL(file);
}
});
}}
onKeyDown={(event) => {
if (event.key === 'Enter') {
if (event.shiftKey) {
return;
}
event.preventDefault();
if (hasPendingMessage) {
handleStop?.();
return;
}
// ignore if using input method engine
if (event.nativeEvent.isComposing) {
return;
}
handleSendMessage?.(event);
}
}}
value={input}
onChange={(event) => {
handleInputChange?.(event);
}}
onPaste={handlePaste}
style={{
minHeight: TEXTAREA_MIN_HEIGHT,
maxHeight: TEXTAREA_MAX_HEIGHT,
}}
placeholder={chatStarted ? 'How can we help you?' : 'What do you want to build?'}
translate="no"
/>
<ClientOnly>
{() => (
<SendButton
show={(hasPendingMessage || input.length > 0 || uploadedFiles.length > 0) && chatStarted}
hasPendingMessage={hasPendingMessage}
onClick={(event) => {
if (hasPendingMessage) {
handleStop?.();
return;
}
if (input.length > 0 || uploadedFiles.length > 0) {
handleSendMessage?.(event);
}
}}
/>
)}
</ClientOnly>
<div className="flex justify-between items-center text-sm p-4 pt-2">
<div className="flex gap-1 items-center">
<IconButton title="Upload file" className="transition-all" onClick={() => handleFileUpload()}>
<div className="i-ph:paperclip text-xl"></div>
</IconButton>
<SpeechRecognitionButton
isListening={isListening}
onStart={startListening}
onStop={stopListening}
disabled={hasPendingMessage}
/>
</div>
{input.length > 3 ? (
<div className="text-xs text-bolt-elements-textTertiary">
Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd> +{' '}
<kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd> a new line
</div>
) : null}
</div>
</div>
);
}
const baseChat = (
<div
ref={ref}
className={classNames(styles.BaseChat, 'relative flex h-full w-full overflow-hidden')}
data-chat-visible={showChat}
>
<ClientOnly>{() => <Menu />}</ClientOnly>
<div ref={scrollRef} className="flex flex-col lg:flex-row overflow-y-auto w-full h-full">
<div className={classNames(styles.Chat, 'flex flex-col flex-grow lg:min-w-[var(--chat-min-width)] h-full')}>
{!chatStarted && (
<div id="intro" className="mt-[16vh] max-w-chat mx-auto text-center px-4 lg:px-0">
<h1 className="text-3xl lg:text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
Get what you want
</h1>
<p className="text-md lg:text-xl mb-8 text-bolt-elements-textSecondary animate-fade-in animation-delay-200">
Write, test, and fix your app all from one prompt
</p>
</div>
)}
<div
className={classNames('px-2 sm:px-6', {
'h-full flex flex-col': chatStarted,
})}
>
<ClientOnly>
{() => {
return chatStarted ? (
<Messages
ref={messageRef}
className="flex flex-col w-full flex-1 max-w-chat pb-6 mx-auto z-1"
messages={messages}
hasPendingMessage={hasPendingMessage}
pendingMessageStatus={pendingMessageStatus}
/>
) : null;
}}
</ClientOnly>
<div
className={classNames(
'bg-bolt-elements-background-depth-2 p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt',
{
'sticky bottom-2': chatStarted,
},
)}
>
<svg className={classNames(styles.PromptEffectContainer)}>
<defs>
<linearGradient
id="line-gradient"
x1="20%"
y1="0%"
x2="-14%"
y2="10%"
gradientUnits="userSpaceOnUse"
gradientTransform="rotate(-45)"
>
<stop offset="0%" stopColor="#b44aff" stopOpacity="0%"></stop>
<stop offset="40%" stopColor="#b44aff" stopOpacity="80%"></stop>
<stop offset="50%" stopColor="#b44aff" stopOpacity="80%"></stop>
<stop offset="100%" stopColor="#b44aff" stopOpacity="0%"></stop>
</linearGradient>
<linearGradient id="shine-gradient">
<stop offset="0%" stopColor="white" stopOpacity="0%"></stop>
<stop offset="40%" stopColor="#ffffff" stopOpacity="80%"></stop>
<stop offset="50%" stopColor="#ffffff" stopOpacity="80%"></stop>
<stop offset="100%" stopColor="white" stopOpacity="0%"></stop>
</linearGradient>
</defs>
<rect className={classNames(styles.PromptEffectLine)} pathLength="100" strokeLinecap="round"></rect>
<rect className={classNames(styles.PromptShine)} x="48" y="24" width="70" height="1"></rect>
</svg>
<FilePreview
files={uploadedFiles}
imageDataList={imageDataList}
onRemove={(index) => {
setUploadedFiles?.(uploadedFiles.filter((_, i) => i !== index));
setImageDataList?.(imageDataList.filter((_, i) => i !== index));
}}
/>
<ClientOnly>
{() => (
<ScreenshotStateManager
setUploadedFiles={setUploadedFiles}
setImageDataList={setImageDataList}
uploadedFiles={uploadedFiles}
imageDataList={imageDataList}
/>
)}
</ClientOnly>
{approveChangeMessageId && (
<ApproveChange
rejectFormOpen={rejectFormOpen}
setRejectFormOpen={setRejectFormOpen}
onApprove={() => onApproveChange?.(approveChangeMessageId)}
onReject={(data) => onRejectChange?.(approveChangeMessageId, data)}
/>
)}
{!rejectFormOpen && messageInput}
</div>
</div>
{!chatStarted && (
<>
{ExamplePrompts((event: React.UIEvent, messageInput?: string) => {
if (hasPendingMessage) {
handleStop?.();
return;
}
handleSendMessage?.(event, messageInput);
})}
<div className="text-2xl lg:text-4xl font-bold text-bolt-elements-textPrimary mt-8 mb-4 animate-fade-in text-center max-w-chat mx-auto">
Arboretum
</div>
<div className="text-bolt-elements-textSecondary text-center max-w-chat mx-auto">
Browse these auto-generated apps for a place to start
</div>
<div
className="placeholder-bolt-elements-textTertiary"
style={{ display: 'flex', justifyContent: 'center', marginBottom: '1rem' }}
>
<input
type="text"
placeholder="Filter"
onKeyDown={(event) => {
if (event.key === 'Enter') {
setFilterText(pendingFilterText);
}
}}
onChange={(event) => {
setPendingFilterText(event.target.value);
}}
style={{
width: '200px',
padding: '0.5rem',
marginTop: '0.5rem',
border: '1px solid #ccc',
borderRadius: '4px',
fontSize: '0.9rem',
textAlign: 'left',
}}
/>
</div>
<ExampleLibraryApps filterText={filterText} />
</>
)}
</div>
<ClientOnly>{() => <Workbench chatStarted={chatStarted} />}</ClientOnly>
</div>
</div>
);
return <Tooltip.Provider delayDuration={200}>{baseChat}</Tooltip.Provider>;
},
);

View File

@ -1,215 +0,0 @@
/*
* @ts-nocheck
* Preventing TS checks with files presented in the video for a better presentation.
*/
import React, { type RefCallback, useCallback } from 'react';
import { ClientOnly } from 'remix-utils/client-only';
import { Menu } from '~/components/sidebar/Menu.client';
import { Workbench } from '~/components/workbench/Workbench.client';
import { classNames } from '~/utils/classNames';
import { Messages } from '~/components/chat/Messages.client';
import { type Message } from '~/lib/persistence/message';
import * as Tooltip from '@radix-ui/react-tooltip';
import { IntroSection } from '~/components/chat/BaseChat/components/IntroSection/IntroSection';
import { SearchInput } from '~/components/chat/SearchInput/SearchInput';
import { ChatPromptContainer } from '~/components/chat/BaseChat/components/ChatPromptContainer/ChatPromptContainer';
import { useSpeechRecognition } from '~/hooks/useSpeechRecognition';
import styles from './BaseChat.module.scss';
import { ExamplePrompts } from '~/components/chat/ExamplePrompts';
import { ExampleLibraryApps } from '~/components/app-library/ExampleLibraryApps';
import type { RejectChangeData } from '~/components/chat/ApproveChange';
import { type MessageInputProps } from '~/components/chat/MessageInput/MessageInput';
export const TEXTAREA_MIN_HEIGHT = 76;
interface BaseChatProps {
textareaRef?: React.RefObject<HTMLTextAreaElement>;
messageRef?: RefCallback<HTMLDivElement>;
scrollRef?: RefCallback<HTMLDivElement>;
showChat?: boolean;
chatStarted?: boolean;
hasPendingMessage?: boolean;
pendingMessageStatus?: string;
messages?: Message[];
input?: string;
handleStop?: () => void;
sendMessage?: (messageInput?: string) => void;
handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
uploadedFiles?: File[];
setUploadedFiles?: (files: File[]) => void;
imageDataList?: string[];
setImageDataList?: (dataList: string[]) => void;
onApproveChange?: (messageId: string) => void;
onRejectChange?: (messageId: string, data: RejectChangeData) => void;
}
type ExtendedMessage = Message & {
repositoryId?: string;
peanuts?: boolean;
approved?: boolean;
};
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
(
{
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 [rejectFormOpen, setRejectFormOpen] = React.useState(false);
const [filterText, setFilterText] = React.useState('');
const onTranscriptChange = useCallback(
(transcript: string) => {
if (handleInputChange) {
const syntheticEvent = {
target: { value: transcript },
} as React.ChangeEvent<HTMLTextAreaElement>;
handleInputChange(syntheticEvent);
}
},
[handleInputChange],
);
const { isListening, startListening, stopListening, abortListening } = useSpeechRecognition({
onTranscriptChange,
});
const handleSendMessage = (event: React.UIEvent, messageInput?: string) => {
if (sendMessage) {
sendMessage(messageInput);
abortListening();
if (handleInputChange) {
const syntheticEvent = {
target: { value: '' },
} as React.ChangeEvent<HTMLTextAreaElement>;
handleInputChange(syntheticEvent);
}
}
};
const approveChangeMessageId = (() => {
if (hasPendingMessage || !messages) {
return undefined;
}
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i] as ExtendedMessage;
if (message.repositoryId && message.peanuts) {
return message.approved ? undefined : message.id;
}
if (message.role === 'user') {
return undefined;
}
}
return undefined;
})();
const messageInputProps = {
textareaRef,
input,
handleInputChange,
handleSendMessage,
handleStop,
hasPendingMessage,
chatStarted,
uploadedFiles,
setUploadedFiles,
imageDataList,
setImageDataList,
isListening,
onStartListening: startListening,
onStopListening: stopListening,
minHeight: TEXTAREA_MIN_HEIGHT,
maxHeight: TEXTAREA_MAX_HEIGHT,
};
const baseChat = (
<div
ref={ref}
className={classNames(styles.BaseChat, 'relative flex h-full w-full overflow-hidden p-6')}
data-chat-visible={showChat}
>
<ClientOnly>{() => <Menu />}</ClientOnly>
<div ref={scrollRef} className="flex flex-col lg:flex-row w-full h-full">
<div className={classNames(styles.Chat, 'flex flex-col flex-grow lg:min-w-[var(--chat-min-width)] h-full')}>
{!chatStarted && <IntroSection />}
<div
className={classNames('px-2 sm:px-6', {
'h-full flex flex-col': chatStarted,
})}
>
<ClientOnly>
{() => {
return chatStarted ? (
<Messages
ref={messageRef}
className="flex flex-col w-full flex-1 max-w-chat pb-6 mx-auto z-1 overflow-y-auto"
messages={messages}
hasPendingMessage={hasPendingMessage}
pendingMessageStatus={pendingMessageStatus}
/>
) : null;
}}
</ClientOnly>
<ChatPromptContainer
chatStarted={chatStarted}
uploadedFiles={uploadedFiles}
setUploadedFiles={setUploadedFiles!}
imageDataList={imageDataList}
setImageDataList={setImageDataList!}
approveChangeMessageId={approveChangeMessageId}
rejectFormOpen={rejectFormOpen}
setRejectFormOpen={setRejectFormOpen}
onApproveChange={onApproveChange}
onRejectChange={onRejectChange}
messageInputProps={messageInputProps as MessageInputProps}
/>
</div>
{!chatStarted && (
<>
{ExamplePrompts((event: React.UIEvent, messageInput?: string) => {
if (hasPendingMessage) {
handleStop?.();
return;
}
handleSendMessage(event, messageInput);
})}
<div className="text-2xl lg:text-4xl font-bold text-bolt-elements-textPrimary mt-8 mb-4 animate-fade-in text-center max-w-chat mx-auto">
Arboretum
</div>
<div className="text-bolt-elements-textSecondary text-center max-w-chat mx-auto">
Browse these auto-generated apps for a place to start
</div>
<SearchInput onSearch={setFilterText} />
<ExampleLibraryApps filterText={filterText} />
</>
)}
</div>
<ClientOnly>{() => <Workbench chatStarted={chatStarted} />}</ClientOnly>
</div>
</div>
);
return <Tooltip.Provider delayDuration={200}>{baseChat}</Tooltip.Provider>;
},
);

View File

@ -1,101 +0,0 @@
import React from 'react';
import { classNames } from '~/utils/classNames';
import FilePreview from '~/components/chat/FilePreview';
import { ScreenshotStateManager } from '~/components/chat/ScreenshotStateManager';
import { ClientOnly } from 'remix-utils/client-only';
import ApproveChange from '~/components/chat/ApproveChange';
import { MessageInput } from '~/components/chat/MessageInput/MessageInput';
import styles from '~/components/chat/BaseChat/BaseChat.module.scss';
interface ChatPromptContainerProps {
chatStarted: boolean;
uploadedFiles: File[];
setUploadedFiles: (files: File[]) => void;
imageDataList: string[];
setImageDataList: (dataList: string[]) => void;
approveChangeMessageId?: string;
rejectFormOpen: boolean;
setRejectFormOpen: (open: boolean) => void;
onApproveChange?: (messageId: string) => void;
onRejectChange?: (messageId: string, data: any) => void;
messageInputProps: Partial<React.ComponentProps<typeof MessageInput>>;
}
export const ChatPromptContainer: React.FC<ChatPromptContainerProps> = ({
chatStarted,
uploadedFiles,
setUploadedFiles,
imageDataList,
setImageDataList,
approveChangeMessageId,
rejectFormOpen,
setRejectFormOpen,
onApproveChange,
onRejectChange,
messageInputProps,
}) => {
return (
<div
className={classNames(
'bg-bolt-elements-background-depth-2 p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt',
{
'sticky bottom-2': chatStarted,
},
)}
>
<svg className={classNames(styles.PromptEffectContainer)}>
<defs>
<linearGradient
id="line-gradient"
x1="20%"
y1="0%"
x2="-14%"
y2="10%"
gradientUnits="userSpaceOnUse"
gradientTransform="rotate(-45)"
>
<stop offset="0%" stopColor="#b44aff" stopOpacity="0%"></stop>
<stop offset="40%" stopColor="#b44aff" stopOpacity="80%"></stop>
<stop offset="50%" stopColor="#b44aff" stopOpacity="80%"></stop>
<stop offset="100%" stopColor="#b44aff" stopOpacity="0%"></stop>
</linearGradient>
<linearGradient id="shine-gradient">
<stop offset="0%" stopColor="white" stopOpacity="0%"></stop>
<stop offset="40%" stopColor="#ffffff" stopOpacity="80%"></stop>
<stop offset="50%" stopColor="#ffffff" stopOpacity="80%"></stop>
<stop offset="100%" stopColor="white" stopOpacity="0%"></stop>
</linearGradient>
</defs>
<rect className={classNames(styles.PromptEffectLine)} pathLength="100" strokeLinecap="round"></rect>
<rect className={classNames(styles.PromptShine)} x="48" y="24" width="70" height="1"></rect>
</svg>
<FilePreview
files={uploadedFiles}
imageDataList={imageDataList}
onRemove={(index) => {
setUploadedFiles(uploadedFiles.filter((_, i) => i !== index));
setImageDataList(imageDataList.filter((_, i) => i !== index));
}}
/>
<ClientOnly>
{() => (
<ScreenshotStateManager
setUploadedFiles={setUploadedFiles}
setImageDataList={setImageDataList}
uploadedFiles={uploadedFiles}
imageDataList={imageDataList}
/>
)}
</ClientOnly>
{approveChangeMessageId && (
<ApproveChange
rejectFormOpen={rejectFormOpen}
setRejectFormOpen={setRejectFormOpen}
onApprove={() => onApproveChange?.(approveChangeMessageId)}
onReject={(data) => onRejectChange?.(approveChangeMessageId, data)}
/>
)}
{!rejectFormOpen && <MessageInput {...messageInputProps} />}
</div>
);
};

View File

@ -1,14 +0,0 @@
import React from 'react';
export const IntroSection: React.FC = () => {
return (
<div id="intro" className="mt-[16vh] max-w-chat mx-auto text-center px-4 lg:px-0">
<h1 className="text-3xl lg:text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
Get what you want
</h1>
<p className="text-md lg:text-xl mb-8 text-bolt-elements-textSecondary animate-fade-in animation-delay-200">
Write, test, and fix your app all from one prompt
</p>
</div>
);
};

View File

@ -1,16 +1,22 @@
/*
* @ts-nocheck
* Preventing TS checks with files presented in the video for a better presentation.
*/
import { useStore } from '@nanostores/react';
import { useAnimate } from 'framer-motion';
import { memo, useEffect, useRef, useState } from 'react';
import { toast } from 'react-toastify';
import { cssTransition, toast, ToastContainer } from 'react-toastify';
import { useSnapScroll } from '~/lib/hooks';
import { handleChatTitleUpdate, type ResumeChatInfo } from '~/lib/persistence';
import { handleChatTitleUpdate, useChatHistory, type ResumeChatInfo } from '~/lib/persistence';
import { database } from '~/lib/persistence/chats';
import { chatStore } from '~/lib/stores/chat';
import { cubicEasingFn } from '~/utils/easings';
import { BaseChat } from '~/components/chat/BaseChat/BaseChat';
import { renderLogger } from '~/utils/logger';
import { BaseChat } from './BaseChat';
import Cookies from 'js-cookie';
import { useSearchParams } from '@remix-run/react';
import {
simulationAddData,
simulationFinishData,
simulationRepositoryUpdated,
sendChatMessage,
@ -18,20 +24,95 @@ import {
abortChatMessage,
resumeChatMessage,
} from '~/lib/replay/ChatManager';
import { getIFrameSimulationData } from '~/lib/replay/Recording';
import { getCurrentIFrame } from '~/components/workbench/Preview';
import { getCurrentMouseData } from '~/components/workbench/PointSelector';
import { anthropicNumFreeUsesCookieName, maxFreeUses } from '~/utils/freeUses';
import { ChatMessageTelemetry, pingTelemetry } from '~/lib/hooks/pingTelemetry';
import type { RejectChangeData } from '~/components/chat/ApproveChange';
import { generateRandomId } from '~/lib/replay/ReplayProtocolClient';
import type { RejectChangeData } from './ApproveChange';
import { assert, generateRandomId } from '~/lib/replay/ReplayProtocolClient';
import { getMessagesRepositoryId, getPreviousRepositoryId, type Message } from '~/lib/persistence/message';
import { useAuthStatus } from '~/lib/stores/auth';
import { debounce } from '~/utils/debounce';
import { supabaseSubmitFeedback } from '~/lib/supabase/feedback';
import { supabaseAddRefund } from '~/lib/supabase/peanuts';
import mergeResponseMessage from '~/components/chat/ChatComponent/functions/mergeResponseMessages';
import flushSimulationData from '~/components/chat/ChatComponent/functions/flushSimulation';
import getRewindMessageIndexAfterReject from '~/components/chat/ChatComponent/functions/getRewindMessageIndexAfterReject';
import flashScreen from '~/components/chat/ChatComponent/functions/flashScreen';
const toastAnimation = cssTransition({
enter: 'animated fadeInRight',
exit: 'animated fadeOutRight',
});
let gLastChatMessages: Message[] | undefined;
export function getLastChatMessages() {
return gLastChatMessages;
}
async function flushSimulationData() {
//console.log("FlushSimulationData");
const iframe = getCurrentIFrame();
if (!iframe) {
return;
}
const simulationData = await getIFrameSimulationData(iframe);
if (!simulationData.length) {
return;
}
//console.log("HaveSimulationData", simulationData.length);
// Add the simulation data to the chat.
simulationAddData(simulationData);
}
setInterval(async () => {
flushSimulationData();
}, 1000);
export function Chat() {
renderLogger.trace('Chat');
const { ready, initialMessages, resumeChat, storeMessageHistory } = useChatHistory();
return (
<>
{ready && (
<ChatImpl initialMessages={initialMessages} resumeChat={resumeChat} storeMessageHistory={storeMessageHistory} />
)}
<ToastContainer
closeButton={({ closeToast }) => {
return (
<button className="Toastify__close-button" onClick={closeToast}>
<div className="i-ph:x text-lg" />
</button>
);
}}
icon={({ type }) => {
/**
* @todo Handle more types if we need them. This may require extra color palettes.
*/
switch (type) {
case 'success': {
return <div className="i-ph:check-bold text-bolt-elements-icon-success text-2xl" />;
}
case 'error': {
return <div className="i-ph:warning-circle-bold text-bolt-elements-icon-error text-2xl" />;
}
}
return undefined;
}}
position="bottom-right"
pauseOnFocusLoss
transition={toastAnimation}
/>
</>
);
}
interface ChatProps {
initialMessages: Message[];
@ -47,13 +128,39 @@ async function clearActiveChat() {
gActiveChatMessageTelemetry = undefined;
}
let gLastChatMessages: Message[] | undefined;
export function getLastChatMessages() {
return gLastChatMessages;
function mergeResponseMessage(msg: Message, messages: Message[]): Message[] {
const lastMessage = messages[messages.length - 1];
if (lastMessage.id == msg.id) {
messages.pop();
assert(lastMessage.type == 'text', 'Last message must be a text message');
assert(msg.type == 'text', 'Message must be a text message');
messages.push({
...msg,
content: lastMessage.content + msg.content,
});
} else {
messages.push(msg);
}
return messages;
}
const ChatImplementer = memo((props: ChatProps) => {
// 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;
const textareaRef = useRef<HTMLTextAreaElement>(null);
@ -62,12 +169,22 @@ const ChatImplementer = memo((props: ChatProps) => {
const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
const [searchParams] = useSearchParams();
const { isLoggedIn } = useAuthStatus();
// Input currently in the textarea.
const [input, setInput] = useState('');
/*
* This is set when the user has triggered a chat message and the response hasn't finished
* being generated.
*/
const [pendingMessageId, setPendingMessageId] = useState<string | undefined>(undefined);
// Last status we heard for the pending message.
const [pendingMessageStatus, setPendingMessageStatus] = useState('');
// If we are listening to responses from a chat started in an earlier session,
// this will be set. This is equivalent to having a pending message but is
// handled differently.
const [resumeChat, setResumeChat] = useState<ResumeChatInfo | undefined>(initialResumeChat);
const [messages, setMessages] = useState<Message[]>(initialMessages);
@ -84,6 +201,7 @@ const ChatImplementer = memo((props: ChatProps) => {
}
}, [searchParams]);
// Load any repository in the initial messages.
useEffect(() => {
const repositoryId = getMessagesRepositoryId(initialMessages);
@ -197,6 +315,7 @@ const ChatImplementer = memo((props: ChatProps) => {
setMessages(newMessages);
// Add file cleanup here
setUploadedFiles([]);
setImageDataList([]);
@ -217,6 +336,7 @@ const ChatImplementer = memo((props: ChatProps) => {
newMessages = mergeResponseMessage(msg, [...newMessages]);
setMessages(newMessages);
// Update the repository as soon as it has changed.
const responseRepositoryId = getMessagesRepositoryId(newMessages);
if (responseRepositoryId && existingRepositoryId != responseRepositoryId) {
@ -297,6 +417,11 @@ const ChatImplementer = memo((props: ChatProps) => {
let newMessages = messages;
// The response messages we get may overlap with the ones we already have.
// Look for this and remove any existing message when we receive the first
// piece of a response message.
//
// Messages we have already received a portion of a response for.
const hasReceivedResponse = new Set<string>();
const addResponseMessage = (msg: Message) => {
@ -314,6 +439,7 @@ const ChatImplementer = memo((props: ChatProps) => {
newMessages = mergeResponseMessage(msg, [...newMessages]);
setMessages(newMessages);
// Update the repository as soon as it has changed.
const responseRepositoryId = getMessagesRepositoryId(newMessages);
if (responseRepositoryId && existingRepositoryId != responseRepositoryId) {
@ -366,6 +492,28 @@ const ChatImplementer = memo((props: ChatProps) => {
})();
}, [initialResumeChat]);
const flashScreen = async () => {
const flash = document.createElement('div');
flash.style.position = 'fixed';
flash.style.top = '0';
flash.style.left = '0';
flash.style.width = '100%';
flash.style.height = '100%';
flash.style.backgroundColor = 'rgba(0, 255, 0, 0.3)';
flash.style.zIndex = '9999';
flash.style.pointerEvents = 'none';
document.body.appendChild(flash);
// Fade out and remove after 500ms
setTimeout(() => {
flash.style.transition = 'opacity 0.5s';
flash.style.opacity = '0';
setTimeout(() => {
document.body.removeChild(flash);
}, 500);
}, 200);
};
const onApproveChange = async (messageId: string) => {
console.log('ApproveChange', messageId);
@ -391,6 +539,8 @@ const ChatImplementer = memo((props: ChatProps) => {
const onRejectChange = async (messageId: string, data: RejectChangeData) => {
console.log('RejectChange', messageId, data);
// 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);
if (messageIndex < 0) {
@ -471,5 +621,3 @@ const ChatImplementer = memo((props: ChatProps) => {
/>
);
});
export default ChatImplementer;

View File

@ -1,60 +0,0 @@
/*
* @ts-nocheck
* Preventing TS checks with files presented in the video for a better presentation.
*/
import { cssTransition, ToastContainer } from 'react-toastify';
import { useChatHistory } from '~/lib/persistence';
import { renderLogger } from '~/utils/logger';
import ChatImplementer from './components/ChatImplementer/ChatImplementer';
import flushSimulationData from './functions/flushSimulation';
const toastAnimation = cssTransition({
enter: 'animated fadeInRight',
exit: 'animated fadeOutRight',
});
setInterval(async () => {
flushSimulationData();
}, 1000);
export function Chat() {
renderLogger.trace('Chat');
const { ready, initialMessages, resumeChat, storeMessageHistory } = useChatHistory();
return (
<>
{ready && (
<ChatImplementer
initialMessages={initialMessages}
resumeChat={resumeChat}
storeMessageHistory={storeMessageHistory}
/>
)}
<ToastContainer
closeButton={({ closeToast }) => {
return (
<button className="Toastify__close-button" onClick={closeToast}>
<div className="i-ph:x text-lg" />
</button>
);
}}
icon={({ type }) => {
switch (type) {
case 'success': {
return <div className="i-ph:check-bold text-bolt-elements-icon-success text-2xl" />;
}
case 'error': {
return <div className="i-ph:warning-circle-bold text-bolt-elements-icon-error text-2xl" />;
}
}
return undefined;
}}
position="bottom-right"
pauseOnFocusLoss
transition={toastAnimation}
/>
</>
);
}

View File

@ -1,22 +0,0 @@
const flashScreen = async () => {
const flash = document.createElement('div');
flash.style.position = 'fixed';
flash.style.top = '0';
flash.style.left = '0';
flash.style.width = '100%';
flash.style.height = '100%';
flash.style.backgroundColor = 'rgba(0, 255, 0, 0.3)';
flash.style.zIndex = '9999';
flash.style.pointerEvents = 'none';
document.body.appendChild(flash);
setTimeout(() => {
flash.style.transition = 'opacity 0.5s';
flash.style.opacity = '0';
setTimeout(() => {
document.body.removeChild(flash);
}, 500);
}, 200);
};
export default flashScreen;

View File

@ -1,26 +0,0 @@
import { getIFrameSimulationData } from '~/lib/replay/Recording';
import { simulationAddData } from '~/lib/replay/ChatManager';
import { getCurrentIFrame } from '~/components/workbench/Preview';
async function flushSimulationData() {
//console.log("FlushSimulationData");
const iframe = getCurrentIFrame();
if (!iframe) {
return;
}
const simulationData = await getIFrameSimulationData(iframe);
if (!simulationData.length) {
return;
}
//console.log("HaveSimulationData", simulationData.length);
// Add the simulation data to the chat.
simulationAddData(simulationData);
}
export default flushSimulationData;

View File

@ -1,18 +0,0 @@
import type { Message } from '~/lib/persistence/message';
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 default getRewindMessageIndexAfterReject;

View File

@ -1,20 +0,0 @@
import type { Message } from '~/lib/persistence/message';
import { assert } from '~/lib/replay/ReplayProtocolClient';
function mergeResponseMessage(msg: Message, messages: Message[]): Message[] {
const lastMessage = messages[messages.length - 1];
if (lastMessage.id == msg.id) {
messages.pop();
assert(lastMessage.type == 'text', 'Last message must be a text message');
assert(msg.type == 'text', 'Message must be a text message');
messages.push({
...msg,
content: lastMessage.content + msg.content,
});
} else {
messages.push(msg);
}
return messages;
}
export default mergeResponseMessage;

View File

@ -1,206 +0,0 @@
import React from 'react';
import { ClientOnly } from 'remix-utils/client-only';
import { IconButton } from '~/components/ui/IconButton';
import { classNames } from '~/utils/classNames';
import { SendButton } from '~/components/chat/SendButton.client';
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
export interface MessageInputProps {
textareaRef?: React.RefObject<HTMLTextAreaElement>;
input?: string;
handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
handleSendMessage?: (event: React.UIEvent) => void;
handleStop?: () => void;
hasPendingMessage?: boolean;
chatStarted?: boolean;
uploadedFiles?: File[];
setUploadedFiles?: (files: File[]) => void;
imageDataList?: string[];
setImageDataList?: (dataList: string[]) => void;
isListening?: boolean;
onStartListening?: () => void;
onStopListening?: () => void;
minHeight?: number;
maxHeight?: number;
}
export const MessageInput: React.FC<MessageInputProps> = ({
textareaRef,
input = '',
handleInputChange = () => {},
handleSendMessage = () => {},
handleStop = () => {},
hasPendingMessage = false,
chatStarted = false,
uploadedFiles = [],
setUploadedFiles = () => {},
imageDataList = [],
setImageDataList = () => {},
isListening = false,
onStartListening = () => {},
onStopListening = () => {},
minHeight = 76,
maxHeight = 200,
}) => {
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;
}
}
};
return (
<div className={classNames('relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg')}>
<textarea
ref={textareaRef}
className={classNames(
'w-full pl-4 pt-4 pr-25 outline-none resize-none text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent text-sm',
'transition-all duration-200',
'hover:border-bolt-elements-focus',
)}
onDragEnter={(e) => {
e.preventDefault();
e.currentTarget.style.border = '2px solid #1488fc';
}}
onDragOver={(e) => {
e.preventDefault();
e.currentTarget.style.border = '2px solid #1488fc';
}}
onDragLeave={(e) => {
e.preventDefault();
e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
}}
onDrop={(e) => {
e.preventDefault();
e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
const files = Array.from(e.dataTransfer.files);
files.forEach((file) => {
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
const base64Image = e.target?.result as string;
setUploadedFiles([...uploadedFiles, file]);
setImageDataList([...imageDataList, base64Image]);
};
reader.readAsDataURL(file);
}
});
}}
onKeyDown={(event) => {
if (event.key === 'Enter') {
if (event.shiftKey) {
return;
}
event.preventDefault();
if (hasPendingMessage) {
handleStop();
return;
}
if (event.nativeEvent.isComposing) {
return;
}
handleSendMessage(event);
}
}}
value={input}
onChange={handleInputChange}
onPaste={handlePaste}
style={{
minHeight,
maxHeight,
}}
placeholder={chatStarted ? 'How can we help you?' : 'What do you want to build?'}
translate="no"
/>
<ClientOnly>
{() => (
<SendButton
show={(hasPendingMessage || input.length > 0 || uploadedFiles.length > 0) && chatStarted}
hasPendingMessage={hasPendingMessage}
onClick={(event) => {
if (hasPendingMessage) {
handleStop();
return;
}
if (input.length > 0 || uploadedFiles.length > 0) {
handleSendMessage(event);
}
}}
/>
)}
</ClientOnly>
<div className="flex justify-between items-center text-sm p-4 pt-2">
<div className="flex gap-1 items-center">
<IconButton title="Upload file" className="transition-all" onClick={handleFileUpload}>
<div className="i-ph:paperclip text-xl"></div>
</IconButton>
<SpeechRecognitionButton
isListening={isListening}
onStart={onStartListening}
onStop={onStopListening}
disabled={hasPendingMessage}
/>
</div>
{input.length > 3 ? (
<div className="text-xs text-bolt-elements-textTertiary">
Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd> +{' '}
<kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd> a new line
</div>
) : null}
</div>
</div>
);
};

View File

@ -1,33 +0,0 @@
import React from 'react';
interface SearchInputProps {
onSearch: (text: string) => void;
}
export const SearchInput: React.FC<SearchInputProps> = ({ onSearch }) => {
return (
<div
className="placeholder-bolt-elements-textTertiary"
style={{ display: 'flex', justifyContent: 'center', marginBottom: '1rem' }}
>
<input
type="text"
placeholder="Search"
onKeyDown={(event) => {
if (event.key === 'Enter') {
onSearch(event.currentTarget.value);
}
}}
style={{
width: '200px',
padding: '0.5rem',
marginTop: '0.5rem',
border: '1px solid #ccc',
borderRadius: '4px',
fontSize: '0.9rem',
textAlign: 'left',
}}
/>
</div>
);
};

View File

@ -2,7 +2,7 @@ import { toast } from 'react-toastify';
import ReactModal from 'react-modal';
import { useState } from 'react';
import { supabaseSubmitFeedback } from '~/lib/supabase/feedback';
import { getLastChatMessages } from '~/utils/chat/messageUtils';
import { getLastChatMessages } from '~/components/chat/Chat.client';
ReactModal.setAppElement('#root');

View File

@ -6,7 +6,7 @@ import { HeaderActionButtons } from './HeaderActionButtons.client';
import { ChatDescription } from '~/lib/persistence/ChatDescription.client';
import { Feedback } from './Feedback';
import { Suspense } from 'react';
import { ClientAuth } from '~/components/auth/ClientAuth/ClientAuth';
import { ClientAuth } from '~/components/auth/ClientAuth';
import { DeployChatButton } from './DeployChatButton';
import { DownloadButton } from './DownloadButton';
@ -38,7 +38,7 @@ export function Header() {
<div className="flex-1 flex items-center ">
{chatStarted && (
<span className="px-4 truncate text-center text-bolt-elements-textPrimary">
<span className="flex-1 px-4 truncate text-center text-bolt-elements-textPrimary">
<ClientOnly>{() => <ChatDescription />}</ClientOnly>
</span>
)}

View File

@ -1,22 +0,0 @@
export function GoogleIcon() {
return (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M18.171 8.368h-.67v-.035H10v3.333h4.709A4.998 4.998 0 0 1 5 10a5 5 0 0 1 5-5c1.275 0 2.434.48 3.317 1.266l2.357-2.357A8.295 8.295 0 0 0 10 1.667a8.334 8.334 0 1 0 8.171 6.7z"
fill="#FFC107"
/>
<path
d="M2.628 6.121l2.734 2.008a4.998 4.998 0 0 1 4.638-3.13c1.275 0 2.434.482 3.317 1.267l2.357-2.357A8.295 8.295 0 0 0 10 1.667 8.329 8.329 0 0 0 2.628 6.12z"
fill="#FF3D00"
/>
<path
d="M10 18.333a8.294 8.294 0 0 0 5.587-2.163l-2.579-2.183A4.963 4.963 0 0 1 10 15a4.998 4.998 0 0 1-4.701-3.311L2.58 13.783A8.327 8.327 0 0 0 10 18.333z"
fill="#4CAF50"
/>
<path
d="M18.171 8.368H17.5v-.034H10v3.333h4.71a5.017 5.017 0 0 1-1.703 2.321l2.58 2.182c-.182.166 2.746-2.003 2.746-6.17 0-.559-.057-1.104-.162-1.632z"
fill="#1976D2"
/>
</svg>
);
}

View File

@ -1,67 +0,0 @@
import { useState, useEffect } from 'react';
interface UseSpeechRecognitionProps {
onTranscriptChange: (transcript: string) => void;
}
export const useSpeechRecognition = ({ onTranscriptChange }: UseSpeechRecognitionProps) => {
const [isListening, setIsListening] = useState(false);
const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
const [transcript, setTranscript] = useState('');
useEffect(() => {
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);
onTranscriptChange(transcript);
};
recognition.onerror = (event) => {
console.error('Speech recognition error:', event.error);
setIsListening(false);
};
setRecognition(recognition);
}
}, [onTranscriptChange]);
const startListening = () => {
if (recognition) {
recognition.start();
setIsListening(true);
}
};
const stopListening = () => {
if (recognition) {
recognition.stop();
setIsListening(false);
}
};
const abortListening = () => {
if (recognition) {
recognition.abort();
setTranscript('');
setIsListening(false);
}
};
return {
isListening,
transcript,
startListening,
stopListening,
abortListening,
};
};

View File

@ -1,17 +0,0 @@
import React from 'react';
import { Header } from '~/components/header/Header';
import BackgroundRays from '~/components/ui/BackgroundRays';
interface PageContainerProps {
children: React.ReactNode;
}
export const PageContainer: React.FC<PageContainerProps> = ({ children }) => {
return (
<div className="h-screen w-full flex flex-col bg-bolt-elements-background-depth-1 dark:bg-black overflow-hidden">
<Header />
<BackgroundRays />
<div className="flex-1 w-full overflow-y-auto page-content">{children}</div>
</div>
);
};

View File

@ -16,12 +16,12 @@ interface MessageBase {
approved?: boolean;
}
export interface MessageText extends MessageBase {
interface MessageText extends MessageBase {
type: 'text';
content: string;
}
export interface MessageImage extends MessageBase {
interface MessageImage extends MessageBase {
type: 'image';
dataURL: string;
}

View File

@ -171,7 +171,7 @@ export default function App() {
<ClientOnly>
<ThemeProvider />
<AuthProvider data={data} />
<main className="h-full min-h-screen">{isLoading ? <div></div> : <Outlet />}</main>
<main className="">{isLoading ? <div></div> : <Outlet />}</main>
<ToastContainer position="bottom-right" theme={theme} />
<AuthModal />
</ClientOnly>

View File

@ -1,9 +1,11 @@
import { json, type MetaFunction } from '~/lib/remix-types';
import { Suspense } from 'react';
import { ClientOnly } from 'remix-utils/client-only';
import { BaseChat } from '~/components/chat/BaseChat/BaseChat';
import { Chat } from '~/components/chat/ChatComponent/Chat.client';
import { PageContainer } from '~/layout/PageContainer';
import { BaseChat } from '~/components/chat/BaseChat';
import { Chat } from '~/components/chat/Chat.client';
import { Header } from '~/components/header/Header';
import BackgroundRays from '~/components/ui/BackgroundRays';
export const meta: MetaFunction = () => {
return [{ title: 'Nut' }];
};
@ -14,10 +16,12 @@ const Nothing = () => null;
export default function Index() {
return (
<PageContainer>
<div className="flex flex-col h-full min-h-screen w-full bg-bolt-elements-background-depth-1 dark:bg-black">
<BackgroundRays />
<Header />
<Suspense fallback={<Nothing />}>
<ClientOnly fallback={<BaseChat />}>{() => <Chat />}</ClientOnly>
</Suspense>
</PageContainer>
</div>
);
}

View File

@ -1,12 +1,15 @@
import { ClientOnly } from 'remix-utils/client-only';
import { Header } from '~/components/header/Header';
import { Menu } from '~/components/sidebar/Menu.client';
import BackgroundRays from '~/components/ui/BackgroundRays';
import { TooltipProvider } from '@radix-ui/react-tooltip';
import { PageContainer } from '~/layout/PageContainer';
function AboutPage() {
return (
<TooltipProvider>
<PageContainer>
<div className="flex flex-col h-full min-h-screen w-full bg-bolt-elements-background-depth-1">
<BackgroundRays />
<Header />
<ClientOnly>{() => <Menu />}</ClientOnly>
<div className="max-w-3xl mx-auto px-6 py-12 prose dark:text-gray-200 prose-p:text-gray-700 dark:prose-p:text-gray-300 prose-a:text-bolt-elements-accent">
<h1 className="text-4xl font-bold mb-8 text-gray-900 dark:text-gray-200">About Nut</h1>
@ -58,7 +61,7 @@ function AboutPage() {
.
</p>
</div>
</PageContainer>
</div>
</TooltipProvider>
);
}

View File

@ -1,27 +0,0 @@
import type { Message, MessageImage, MessageText } from '~/lib/persistence/message';
import type { ResumeChatInfo } from '~/lib/persistence/useChatHistory';
import type { RejectChangeData } from '~/components/chat/ApproveChange';
export interface ChatProps {
initialMessages: Message[];
resumeChat: ResumeChatInfo | undefined;
storeMessageHistory: (messages: Message[]) => void;
}
export interface ChatImplProps extends ChatProps {
onApproveChange?: (messageId: string) => Promise<void>;
onRejectChange?: (messageId: string, data: RejectChangeData) => Promise<void>;
}
// Re-export types we need
export type { Message, MessageImage, MessageText, ResumeChatInfo };
export interface UserMessage extends MessageText {
role: 'user';
type: 'text';
}
export interface UserImageMessage extends MessageImage {
role: 'user';
type: 'image';
}

View File

@ -1,49 +0,0 @@
import { assert } from '~/lib/replay/ReplayProtocolClient';
import type { Message } from '~/lib/persistence/message';
let gLastChatMessages: Message[] | undefined;
export function getLastChatMessages() {
return gLastChatMessages;
}
export function setLastChatMessages(messages: Message[] | undefined) {
gLastChatMessages = messages;
}
export function mergeResponseMessage(msg: Message, messages: Message[]): Message[] {
const lastMessage = messages[messages.length - 1];
if (lastMessage.id == msg.id) {
messages.pop();
assert(lastMessage.type == 'text', 'Last message must be a text message');
assert(msg.type == 'text', 'Message must be a text message');
messages.push({
...msg,
content: lastMessage.content + msg.content,
});
} else {
messages.push(msg);
}
return messages;
}
export 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;
}

View File

@ -1,25 +0,0 @@
import { getIFrameSimulationData } from '~/lib/replay/Recording';
import { getCurrentIFrame } from '~/components/workbench/Preview';
import { simulationAddData } from '~/lib/replay/ChatManager';
export async function flushSimulationData() {
const iframe = getCurrentIFrame();
if (!iframe) {
return;
}
const simulationData = await getIFrameSimulationData(iframe);
if (!simulationData.length) {
return;
}
simulationAddData(simulationData);
}
export function setupSimulationInterval() {
setInterval(async () => {
flushSimulationData();
}, 1000);
}

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px"><path fill="#FFC107" d="M43.611,20.083H42V20H24v8h11.303c-1.649,4.657-6.08,8-11.303,8c-6.627,0-12-5.373-12-12c0-6.627,5.373-12,12-12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C12.955,4,4,12.955,4,24c0,11.045,8.955,20,20,20c11.045,0,20-8.955,20-20C44,22.659,43.862,21.35,43.611,20.083z"/><path fill="#FF3D00" d="M6.306,14.691l6.571,4.819C14.655,15.108,18.961,12,24,12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C16.318,4,9.656,8.337,6.306,14.691z"/><path fill="#4CAF50" d="M24,44c5.166,0,9.86-1.977,13.409-5.192l-6.19-5.238C29.211,35.091,26.715,36,24,36c-5.202,0-9.619-3.317-11.283-7.946l-6.522,5.025C9.505,39.556,16.227,44,24,44z"/><path fill="#1976D2" d="M43.611,20.083H42V20H24v8h11.303c-0.792,2.237-2.231,4.166-4.087,5.571c0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.238C36.971,39.205,44,34,44,24C44,22.659,43.862,21.35,43.611,20.083z"/></svg>

Before

Width:  |  Height:  |  Size: 988 B