Skip to content

Commit

Permalink
Use simulation prompt API (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
bhackett1024 authored Jan 11, 2025
1 parent c3e1764 commit 6bc2183
Show file tree
Hide file tree
Showing 15 changed files with 377 additions and 287 deletions.
52 changes: 33 additions & 19 deletions app/components/chat/BaseChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ interface BaseChatProps {
setProvider?: (provider: ProviderInfo) => void;
providerList?: ProviderInfo[];
handleStop?: () => void;
sendMessage?: (event: React.UIEvent, messageInput?: string) => void;
sendMessage?: (event: React.UIEvent, messageInput?: string, simulation?: boolean) => void;
handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
enhancePrompt?: () => void;
importChat?: (description: string, messages: Message[]) => Promise<void>;
Expand Down Expand Up @@ -209,9 +209,9 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
}
};

const handleSendMessage = (event: React.UIEvent, messageInput?: string) => {
const handleSendMessage = (event: React.UIEvent, messageInput?: string, simulation?: boolean) => {
if (sendMessage) {
sendMessage(event, messageInput);
sendMessage(event, messageInput, simulation);

if (recognition) {
recognition.abort(); // Stop current recognition
Expand Down Expand Up @@ -402,7 +402,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
<textarea
ref={textareaRef}
className={classNames(
'w-full pl-4 pt-4 pr-16 outline-none resize-none text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent text-sm',
'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',
)}
Expand Down Expand Up @@ -471,21 +471,35 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
/>
<ClientOnly>
{() => (
<SendButton
show={input.length > 0 || isStreaming || uploadedFiles.length > 0}
isStreaming={isStreaming}
disabled={!providerList || providerList.length === 0}
onClick={(event) => {
if (isStreaming) {
handleStop?.();
return;
}

if (input.length > 0 || uploadedFiles.length > 0) {
handleSendMessage?.(event);
}
}}
/>
<>
<SendButton
show={input.length > 0 || isStreaming || uploadedFiles.length > 0}
simulation={false}
isStreaming={isStreaming}
disabled={!providerList || providerList.length === 0}
onClick={(event) => {
if (isStreaming) {
handleStop?.();
return;
}

if (input.length > 0 || uploadedFiles.length > 0) {
handleSendMessage?.(event);
}
}}
/>
<SendButton
show={(input.length > 0 || uploadedFiles.length > 0) && chatStarted}
simulation={true}
isStreaming={isStreaming}
disabled={!providerList || providerList.length === 0}
onClick={(event) => {
if (input.length > 0 || uploadedFiles.length > 0) {
handleSendMessage?.(event, undefined, true);
}
}}
/>
</>
)}
</ClientOnly>
<div className="flex justify-between items-center text-sm p-4 pt-2">
Expand Down
27 changes: 21 additions & 6 deletions app/components/chat/Chat.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ import type { ProviderInfo } from '~/types/model';
import { useSearchParams } from '@remix-run/react';
import { createSampler } from '~/utils/sampler';
import { saveProjectPrompt } from './Messages.client';
import { uint8ArrayToBase64 } from '../workbench/ReplayProtocolClient';
import { uint8ArrayToBase64 } from '~/lib/replay/ReplayProtocolClient';
import type { SimulationPromptClientData } from '~/lib/replay/SimulationPrompt';
import { getIFrameSimulationData } from '~/lib/replay/Recording';
import { getCurrentIFrame } from '../workbench/Preview';
import { getCurrentMouseData } from '../workbench/PointSelector';

const toastAnimation = cssTransition({
enter: 'animated fadeInRight',
Expand Down Expand Up @@ -241,7 +245,7 @@ export const ChatImpl = memo(
setChatStarted(true);
};

const sendMessage = async (_event: React.UIEvent, messageInput?: string) => {
const sendMessage = async (_event: React.UIEvent, messageInput?: string, simulation?: boolean) => {
const _input = messageInput || input;

if (_input.length === 0 || isLoading) {
Expand All @@ -257,6 +261,20 @@ export const ChatImpl = memo(
*/
await workbenchStore.saveAllFiles();

const { contentBase64, uniqueProjectName } = await workbenchStore.generateZipBase64();

let simulationClientData: SimulationPromptClientData | undefined;
if (simulation) {
const simulationData = await getIFrameSimulationData(getCurrentIFrame());
const mouseData = getCurrentMouseData();

simulationClientData = {
simulationData: simulationData,
repositoryContents: contentBase64,
mouseData: mouseData,
};
}

const fileModifications = workbenchStore.getFileModifcations();

chatStore.setKey('aborted', false);
Expand Down Expand Up @@ -303,7 +321,7 @@ export const ChatImpl = memo(
image: imageData,
})),
] as any, // Type assertion to bypass compiler check
});
}, { body: { simulationClientData } });
}

setInput('');
Expand All @@ -320,9 +338,6 @@ export const ChatImpl = memo(
// The project contents are associated with the last message present when
// the user message is added.
const lastMessage = messages[messages.length - 1];
const { content, uniqueProjectName } = await workbenchStore.generateZip();
const buf = await content.arrayBuffer();
const contentBase64 = uint8ArrayToBase64(new Uint8Array(buf));
saveProjectPrompt(lastMessage.id, { content: contentBase64, uniqueProjectName, input: _input });
};

Expand Down
2 changes: 1 addition & 1 deletion app/components/chat/LoadProblemButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Message } from 'ai';
import { toast } from 'react-toastify';
import { createChatFromFolder, type FileArtifact } from '~/utils/folderImport';
import { logStore } from '~/lib/stores/logs'; // Assuming logStore is imported from this location
import { sendCommandDedicatedClient } from '../workbench/ReplayProtocolClient';
import { sendCommandDedicatedClient } from '~/lib/replay/ReplayProtocolClient';
import type { BoltProblem } from './Messages.client';
import JSZip from 'jszip';

Expand Down
2 changes: 1 addition & 1 deletion app/components/chat/Messages.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { db, chatId } from '~/lib/persistence/useChatHistory';
import { forkChat } from '~/lib/persistence/db';
import { toast } from 'react-toastify';
import WithTooltip from '~/components/ui/Tooltip';
import { assert, sendCommandDedicatedClient } from "~/components/workbench/ReplayProtocolClient";
import { assert, sendCommandDedicatedClient } from "~/lib/replay/ReplayProtocolClient";
import ReactModal from 'react-modal';

ReactModal.setAppElement('#root');
Expand Down
19 changes: 16 additions & 3 deletions app/components/chat/SendButton.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AnimatePresence, cubicBezier, motion } from 'framer-motion';

interface SendButtonProps {
show: boolean;
simulation: boolean;
isStreaming?: boolean;
disabled?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
Expand All @@ -10,12 +11,24 @@ interface SendButtonProps {

const customEasingFn = cubicBezier(0.4, 0, 0.2, 1);

export const SendButton = ({ show, isStreaming, disabled, onClick }: SendButtonProps) => {
export const SendButton = ({ show, simulation, isStreaming, disabled, onClick }: SendButtonProps) => {
const className = simulation
? "absolute flex justify-center items-center top-[18px] right-[60px] p-1 bg-accent-500 hover:brightness-94 color-white rounded-md w-[34px] h-[34px] transition-theme disabled:opacity-50 disabled:cursor-not-allowed"
: "absolute flex justify-center items-center top-[18px] right-[22px] p-1 bg-accent-500 hover:brightness-94 color-white rounded-md w-[34px] h-[34px] transition-theme disabled:opacity-50 disabled:cursor-not-allowed";

// Determine tooltip text based on button state
const tooltipText = simulation
? "Start Recording"
: isStreaming
? "Stop Generation"
: "Send Message";

return (
<AnimatePresence>
{show ? (
<motion.button
className="absolute flex justify-center items-center top-[18px] right-[22px] p-1 bg-accent-500 hover:brightness-94 color-white rounded-md w-[34px] h-[34px] transition-theme disabled:opacity-50 disabled:cursor-not-allowed"
className={className}
title={tooltipText}
transition={{ ease: customEasingFn, duration: 0.17 }}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
Expand All @@ -30,7 +43,7 @@ export const SendButton = ({ show, isStreaming, disabled, onClick }: SendButtonP
}}
>
<div className="text-lg">
{!isStreaming ? <div className="i-ph:arrow-right"></div> : <div className="i-ph:stop-circle-bold"></div>}
{simulation ? <div className="i-ph:record-fill"></div> : !isStreaming ? <div className="i-ph:arrow-right"></div> : <div className="i-ph:stop-circle-bold"></div>}
</div>
</motion.button>
) : null}
Expand Down
8 changes: 6 additions & 2 deletions app/components/editor/codemirror/CodeMirrorEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { BinaryContent } from './BinaryContent';
import { getTheme, reconfigureTheme } from './cm-theme';
import { indentKeyBinding } from './indent';
import { getLanguage } from './languages';
import { removeRecordingMessageHandler } from '~/lib/replay/Recording';

const logger = createScopedLogger('CodeMirrorEditor');

Expand Down Expand Up @@ -275,6 +276,7 @@ function newEditorState(
onFileSaveRef: MutableRefObject<OnSaveCallback | undefined>,
extensions: Extension[],
) {
content = removeRecordingMessageHandler(content);
return EditorState.create({
doc: content,
extensions: [
Expand Down Expand Up @@ -381,13 +383,15 @@ function setEditorDocument(
autoFocus: boolean,
doc: TextEditorDocument,
) {
if (doc.value !== view.state.doc.toString()) {
const content = removeRecordingMessageHandler(doc.value);

if (content !== view.state.doc.toString()) {
view.dispatch({
selection: { anchor: 0 },
changes: {
from: 0,
to: view.state.doc.length,
insert: doc.value,
insert: content,
},
});
}
Expand Down
38 changes: 22 additions & 16 deletions app/components/workbench/PointSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
import { memo, useCallback, useState } from 'react';
import { getMouseData } from './Recording';
import { getMouseData, type MouseData } from '~/lib/replay/Recording';

interface PointSelectorProps {
isSelectionMode: boolean;
setIsSelectionMode: (mode: boolean) => void;
selectionPoint: { x: number; y: number } | null;
setSelectionPoint: (point: { x: number; y: number } | null) => void;
recordingSaved: boolean;
containerRef: React.RefObject<HTMLIFrameElement>;
}

let gCurrentMouseData: MouseData | undefined;

export function getCurrentMouseData() {
return gCurrentMouseData;
}

export const PointSelector = memo(
(props: PointSelectorProps) => {
const {
isSelectionMode,
recordingSaved,
setIsSelectionMode,
selectionPoint,
setSelectionPoint,
containerRef,
} = props;

const [isCapturing, setIsCapturing] = useState(false);
const [mouseData, setMouseData] = useState<MouseData | undefined>(undefined);

gCurrentMouseData = mouseData;

const handleSelectionClick = useCallback(async (event: React.MouseEvent) => {
event.preventDefault();
Expand All @@ -40,31 +47,30 @@ export const PointSelector = memo(

const mouseData = await getMouseData(containerRef.current, { x, y });
console.log("MouseData", mouseData);
setMouseData(mouseData);

setIsCapturing(false);
setIsSelectionMode(false); // Turn off selection mode after capture
}, [isSelectionMode, containerRef, setIsSelectionMode]);

if (!isSelectionMode) {
if (recordingSaved) {
if (selectionPoint) {
// Draw an overlay to prevent interactions with the iframe
// and to show the last point the user clicked (if there is one).
// and to show the last point the user clicked.
return (
<div
className="absolute inset-0"
onClick={(event) => event.preventDefault()}
>
{ selectionPoint && (
<div
style={{
position: 'absolute',
left: `${selectionPoint.x-8}px`,
top: `${selectionPoint.y-12}px`,
}}
>
&#10060;
</div>
)}
<div
style={{
position: 'absolute',
left: `${selectionPoint.x-8}px`,
top: `${selectionPoint.y-12}px`,
}}
>
&#10060;
</div>
</div>
);
} else {
Expand Down
Loading

0 comments on commit 6bc2183

Please sign in to comment.