Linter fixes

This commit is contained in:
Jason Laster
2025-03-11 15:10:24 -04:00
parent 9ca3c9c977
commit 5948a48c94
35 changed files with 974 additions and 707 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,6 @@
logs
.cursor
supabase/.temp
*.log
npm-debug.log*
yarn-debug.log*

View File

@@ -22,6 +22,7 @@ const ApproveChange: React.FC<ApproveChangeProps> = ({ rejectFormOpen, setReject
if (rejectFormOpen) {
const performReject = (retry: boolean) => {
setRejectFormOpen(false);
const explanation = textareaRef.current?.value ?? '';
onReject({
explanation,
@@ -42,7 +43,7 @@ const ApproveChange: React.FC<ApproveChangeProps> = ({ rejectFormOpen, setReject
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'
'hover:border-bolt-elements-focus',
)}
onKeyDown={(event) => {
if (event.key === 'Enter') {

View File

@@ -18,9 +18,11 @@ const highlighterOptions = {
const shellHighlighter = createAsyncSuspenseValue(async () => {
const shellHighlighterPromise: Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> =
import.meta.hot?.data.shellHighlighterPromise ?? createHighlighter(highlighterOptions);
if (import.meta.hot) {
import.meta.hot.data.shellHighlighterPromise = shellHighlighterPromise;
}
return shellHighlighterPromise;
});

View File

@@ -29,9 +29,7 @@ export const AssistantMessage = memo(({ content, annotations }: AssistantMessage
return (
<div className="overflow-hidden w-full">
{usage && (
<div
className="text-sm text-bolt-elements-textSecondary mb-2"
>
<div className="text-sm text-bolt-elements-textSecondary mb-2">
Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens})
</div>
)}

View File

@@ -225,147 +225,147 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
if (isStreaming) {
return false;
}
if (!messages?.length) {
return false;
}
const lastMessageProjectContents = getLastMessageProjectContents(messages, messages.length - 1);
if (!lastMessageProjectContents) {
return false;
}
if (lastMessageProjectContents.contentsMessageId != approveChangesMessageId) {
return false;
}
const lastMessage = messages[messages.length - 1];
if (!hasFileModifications(lastMessage.content)) {
return false;
}
return true;
})();
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)';
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();
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;
}
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();
event.preventDefault();
if (isStreaming) {
handleStop?.();
return;
}
if (isStreaming) {
handleStop?.();
return;
}
// ignore if using input method engine
if (event.nativeEvent.isComposing) {
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={(isStreaming || input.length > 0 || uploadedFiles.length > 0) && chatStarted}
isStreaming={isStreaming}
onClick={(event) => {
if (isStreaming) {
handleStop?.();
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={(isStreaming || input.length > 0 || uploadedFiles.length > 0) && chatStarted}
isStreaming={isStreaming}
onClick={(event) => {
if (isStreaming) {
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>
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={isStreaming}
/>
{chatStarted && <ClientOnly>{() => <ExportChatButton exportChat={exportChat} />}</ClientOnly>}
</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>
<SpeechRecognitionButton
isListening={isListening}
onStart={startListening}
onStop={stopListening}
disabled={isStreaming}
/>
{chatStarted && <ClientOnly>{() => <ExportChatButton exportChat={exportChat} />}</ClientOnly>}
</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

@@ -21,7 +21,15 @@ import { useSettings } from '~/lib/hooks/useSettings';
import { useSearchParams } from '@remix-run/react';
import { createSampler } from '~/utils/sampler';
import { saveProjectContents } from './Messages.client';
import { getSimulationRecording, getSimulationEnhancedPrompt, simulationAddData, simulationRepositoryUpdated, shouldUseSimulation, sendDeveloperChatMessage, type ProtocolMessage } from '~/lib/replay/SimulationPrompt';
import {
getSimulationRecording,
getSimulationEnhancedPrompt,
simulationAddData,
simulationRepositoryUpdated,
shouldUseSimulation,
sendDeveloperChatMessage,
type ProtocolMessage,
} from '~/lib/replay/SimulationPrompt';
import { getIFrameSimulationData } from '~/lib/replay/Recording';
import { getCurrentIFrame } from '../workbench/Preview';
import { getCurrentMouseData } from '../workbench/PointSelector';
@@ -56,10 +64,13 @@ async function flushSimulationData() {
//console.log("FlushSimulationData");
const iframe = getCurrentIFrame();
if (!iframe) {
return;
}
const simulationData = await getIFrameSimulationData(iframe);
if (!simulationData.length) {
return;
}
@@ -156,12 +167,15 @@ let gNumAborts = 0;
let gActiveChatMessageTelemetry: ChatMessageTelemetry | undefined;
// When files are modified during a chat message we wait until the message finishes
// before updating the simulation.
/*
* When files are modified during a chat message we wait until the message finishes
* before updating the simulation.
*/
let gUpdateSimulationAfterChatMessage = false;
async function clearActiveChat() {
gActiveChatMessageTelemetry = undefined;
if (gUpdateSimulationAfterChatMessage) {
const { contentBase64 } = await workbenchStore.generateZipBase64();
await simulationRepositoryUpdated(contentBase64);
@@ -197,8 +211,10 @@ export const ChatImpl = memo(
// 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.
/*
* This is set when the user has triggered a chat message and the response hasn't finished
* being generated.
*/
const [activeChatId, setActiveChatId] = useState<string | undefined>(undefined);
const isLoading = activeChatId !== undefined;
@@ -243,7 +259,7 @@ export const ChatImpl = memo(
setActiveChatId(undefined);
if (gActiveChatMessageTelemetry) {
gActiveChatMessageTelemetry.abort("StopButtonClicked");
gActiveChatMessageTelemetry.abort('StopButtonClicked');
clearActiveChat();
}
};
@@ -278,16 +294,17 @@ export const ChatImpl = memo(
const createRecording = async (chatId: string) => {
let recordingId, message;
try {
recordingId = await getSimulationRecording();
message = `[Recording of the bug](https://app.replay.io/recording/${recordingId})\n\n`;
} catch (e) {
console.error("Error creating recording", e);
message = "Error creating recording.";
console.error('Error creating recording', e);
message = 'Error creating recording.';
}
const recordingMessage: Message = {
id: buildMessageId("create-recording", chatId),
id: buildMessageId('create-recording', chatId),
role: 'assistant',
content: message,
};
@@ -296,25 +313,28 @@ export const ChatImpl = memo(
};
const getEnhancedPrompt = async (chatId: string, userMessage: string) => {
let enhancedPrompt, message, hadError = false;
let enhancedPrompt,
message,
hadError = false;
try {
const mouseData = getCurrentMouseData();
enhancedPrompt = await getSimulationEnhancedPrompt(messages, userMessage, mouseData);
message = `Explanation of the bug:\n\n${enhancedPrompt}`;
} catch (e) {
console.error("Error enhancing prompt", e);
message = "Error enhancing prompt.";
console.error('Error enhancing prompt', e);
message = 'Error enhancing prompt.';
hadError = true;
}
const enhancedPromptMessage: Message = {
id: buildMessageId("enhanced-prompt", chatId),
id: buildMessageId('enhanced-prompt', chatId),
role: 'assistant',
content: message,
};
return { enhancedPrompt, enhancedPromptMessage, hadError };
}
};
const sendMessage = async (messageInput?: string) => {
const _input = messageInput || input;
@@ -333,10 +353,14 @@ export const ChatImpl = memo(
if (!loginKey && !anthropicApiKey) {
const numFreeUses = +(Cookies.get(anthropicNumFreeUsesCookieName) || 0);
if (numFreeUses >= MaxFreeUses) {
toast.error('All free uses consumed. Please set a login key or Anthropic API key in the "User Info" settings.');
gActiveChatMessageTelemetry.abort("NoFreeUses");
toast.error(
'All free uses consumed. Please set a login key or Anthropic API key in the "User Info" settings.',
);
gActiveChatMessageTelemetry.abort('NoFreeUses');
clearActiveChat();
return;
}
@@ -347,7 +371,7 @@ export const ChatImpl = memo(
setActiveChatId(chatId);
const userMessage: Message = {
id: buildMessageId("user", chatId),
id: buildMessageId('user', chatId),
role: 'user',
content: [
{
@@ -378,23 +402,26 @@ export const ChatImpl = memo(
await workbenchStore.saveAllFiles();
let simulation = false;
try {
simulation = chatStarted && await shouldUseSimulation(_input);
simulation = chatStarted && (await shouldUseSimulation(_input));
} catch (e) {
console.error("Error checking simulation", e);
console.error('Error checking simulation', e);
}
if (numAbortsAtStart != gNumAborts) {
return;
}
console.log("UseSimulation", simulation);
console.log('UseSimulation', simulation);
let simulationStatus = 'NoSimulation';
let simulationStatus = "NoSimulation";
if (simulation) {
gActiveChatMessageTelemetry.startSimulation();
gLockSimulationData = true;
try {
await flushSimulationData();
@@ -407,7 +434,7 @@ export const ChatImpl = memo(
return;
}
console.log("RecordingMessage", recordingMessage);
console.log('RecordingMessage', recordingMessage);
newMessages = [...newMessages, recordingMessage];
setMessages(newMessages);
@@ -418,13 +445,13 @@ export const ChatImpl = memo(
return;
}
console.log("EnhancedPromptMessage", info.enhancedPromptMessage);
console.log('EnhancedPromptMessage', info.enhancedPromptMessage);
newMessages = [...newMessages, info.enhancedPromptMessage];
setMessages(newMessages);
simulationStatus = info.hadError ? "PromptError" : "Success";
simulationStatus = info.hadError ? 'PromptError' : 'Success';
} else {
simulationStatus = "RecordingError";
simulationStatus = 'RecordingError';
}
gActiveChatMessageTelemetry.endSimulation(simulationStatus);
@@ -441,8 +468,8 @@ export const ChatImpl = memo(
gActiveChatMessageTelemetry.sendPrompt(simulationStatus);
const responseMessageId = buildMessageId("response", chatId);
let responseMessageContent = "";
const responseMessageId = buildMessageId('response', chatId);
let responseMessageContent = '';
let hasResponseMessage = false;
const addResponseContent = (content: string) => {
@@ -453,9 +480,11 @@ export const ChatImpl = memo(
}
newMessages = [...newMessages];
if (hasResponseMessage) {
newMessages.pop();
}
newMessages.push({
id: responseMessageId,
role: 'assistant',
@@ -463,13 +492,13 @@ export const ChatImpl = memo(
});
setMessages(newMessages);
hasResponseMessage = true;
}
};
try {
await sendDeveloperChatMessage(newMessages, files, addResponseContent);
} catch (e) {
console.error("Error sending message", e);
addResponseContent("Error sending message.");
console.error('Error sending message', e);
addResponseContent('Error sending message.');
}
if (gNumAborts != numAbortsAtStart) {
@@ -502,13 +531,15 @@ export const ChatImpl = memo(
};
const onRewind = async (messageId: string, contents: string) => {
console.log("Rewinding", messageId, contents);
console.log('Rewinding', messageId, contents);
await workbenchStore.restoreProjectContentsBase64(messageId, contents);
const messageIndex = messages.findIndex((message) => message.id === messageId);
if (messageIndex >= 0) {
const newParsedMessages = { ...parsedMessages };
for (let i = messageIndex + 1; i < messages.length; i++) {
delete newParsedMessages[i];
}
@@ -516,7 +547,7 @@ export const ChatImpl = memo(
setMessages(messages.slice(0, messageIndex + 1));
}
await pingTelemetry("RewindChat", {
await pingTelemetry('RewindChat', {
numMessages: messages.length,
rewindIndex: messageIndex,
loginKey: getNutLoginKey(),
@@ -546,29 +577,35 @@ export const ChatImpl = memo(
};
const onApproveChange = async (messageId: string) => {
console.log("ApproveChange", messageId);
console.log('ApproveChange', messageId);
setApproveChangesMessageId(undefined);
await flashScreen();
await pingTelemetry("ApproveChange", {
await pingTelemetry('ApproveChange', {
numMessages: messages.length,
loginKey: getNutLoginKey(),
});
};
const onRejectChange = async (messageId: string, rewindMessageId: string, projectContents: string, data: RejectChangeData) => {
console.log("RejectChange", messageId, data);
const onRejectChange = async (
messageId: string,
rewindMessageId: string,
projectContents: string,
data: RejectChangeData,
) => {
console.log('RejectChange', messageId, data);
setApproveChangesMessageId(undefined);
const message = messages.find((message) => message.id === messageId);
const messageContents = message?.content ?? "";
const messageContents = message?.content ?? '';
await onRewind(rewindMessageId, projectContents);
let shareProjectSuccess = false;
if (data.shareProject) {
const feedbackData: any = {
explanation: data.explanation,
@@ -585,7 +622,7 @@ export const ChatImpl = memo(
sendMessage(messageContents);
}
await pingTelemetry("RejectChange", {
await pingTelemetry('RejectChange', {
retry: data.retry,
shareProject: data.shareProject,
shareProjectSuccess,

View File

@@ -22,13 +22,18 @@ export function setLastLoadedProblem(problem: BoltProblem) {
export function getLastLoadedProblem(): BoltProblem | undefined {
const problemJSON = localStorage.getItem('loadedProblem');
if (!problemJSON) {
return undefined;
}
return JSON.parse(problemJSON);
}
export async function loadProblem(problemId: string, importChat: (description: string, messages: Message[]) => Promise<void>) {
export async function loadProblem(
problemId: string,
importChat: (description: string, messages: Message[]) => Promise<void>,
) {
const problem = await getProblem(problemId);
if (!problem) {
@@ -42,7 +47,7 @@ export async function loadProblem(problemId: string, importChat: (description: s
const fileArtifacts = await extractFileArtifactsFromRepositoryContents(repositoryContents);
try {
const messages = await createChatFromFolder(fileArtifacts, [], "problem");
const messages = await createChatFromFolder(fileArtifacts, [], 'problem');
await importChat(`Problem: ${problemTitle}`, [...messages]);
logStore.logSystem('Problem loaded successfully', {
@@ -67,7 +72,7 @@ export const LoadProblemButton: React.FC<LoadProblemButtonProps> = ({ className,
const problemId = (document.getElementById('problem-input') as HTMLInputElement)?.value;
assert(importChat, "importChat is required");
assert(importChat, 'importChat is required');
await loadProblem(problemId, importChat);
setIsLoading(false);
};

View File

@@ -25,23 +25,30 @@ export function saveProjectContents(messageId: string, contents: ProjectContents
}
export function getLastMessageProjectContents(messages: Message[], index: number) {
// The message index is for the model response, and the project
// contents will be associated with the last message present when
// the user prompt was sent to the model. This could be either two
// or three messages back, depending on whether a bug explanation was added.
/*
* The message index is for the model response, and the project
* contents will be associated with the last message present when
* the user prompt was sent to the model. This could be either two
* or three messages back, depending on whether a bug explanation was added.
*/
const beforeUserMessage = messages[index - 2];
const contents = gProjectContentsByMessageId.get(beforeUserMessage?.id);
if (!contents) {
const priorMessage = messages[index - 3];
const priorContents = gProjectContentsByMessageId.get(priorMessage?.id);
if (!priorContents) {
return undefined;
}
// We still rewind to just before the user message to retain any
// explanation from the Nut API.
/*
* We still rewind to just before the user message to retain any
* explanation from the Nut API.
*/
return { rewindMessageId: beforeUserMessage.id, contentsMessageId: priorMessage.id, contents: priorContents };
}
return { rewindMessageId: beforeUserMessage.id, contentsMessageId: beforeUserMessage.id, contents };
}

View File

@@ -11,12 +11,11 @@ interface SendButtonProps {
const customEasingFn = cubicBezier(0.4, 0, 0.2, 1);
export const SendButton = ({ show, isStreaming, disabled, onClick }: SendButtonProps) => {
const 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";
const 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';
// Determine tooltip text based on button state
const tooltipText = isStreaming
? "Stop Generation"
: "Chat";
const tooltipText = isStreaming ? 'Stop Generation' : 'Chat';
return (
<AnimatePresence>
@@ -38,9 +37,7 @@ export const SendButton = ({ show, isStreaming, disabled, onClick }: SendButtonP
}}
>
<div className="text-lg">
{!isStreaming
? <div className="i-ph:hand-fill"></div>
: <div className="i-ph:stop-circle-bold"></div>}
{!isStreaming ? <div className="i-ph:hand-fill"></div> : <div className="i-ph:stop-circle-bold"></div>}
</div>
</motion.button>
) : null}

View File

@@ -1,8 +1,8 @@
import { toast } from "react-toastify";
import { toast } from 'react-toastify';
import ReactModal from 'react-modal';
import { useState } from "react";
import { submitFeedback } from "~/lib/replay/Problems";
import { getLastProjectContents, getLastChatMessages } from "../chat/Chat.client";
import { useState } from 'react';
import { submitFeedback } from '~/lib/replay/Problems';
import { getLastProjectContents, getLastChatMessages } from '../chat/Chat.client';
ReactModal.setAppElement('#root');
@@ -13,7 +13,7 @@ export function Feedback() {
const [formData, setFormData] = useState({
feedback: '',
email: '',
share: false
share: false,
});
const [submitted, setSubmitted] = useState<boolean>(false);
@@ -22,7 +22,7 @@ export function Feedback() {
setFormData({
feedback: '',
email: '',
share: false
share: false,
});
setSubmitted(false);
};
@@ -39,14 +39,14 @@ export function Feedback() {
return;
}
toast.info("Submitting feedback...");
toast.info('Submitting feedback...');
console.log("SubmitFeedback", formData);
console.log('SubmitFeedback', formData);
const feedbackData: any = {
feedback: formData.feedback,
email: formData.email,
share: formData.share
share: formData.share,
};
if (feedbackData.share) {
@@ -56,10 +56,11 @@ export function Feedback() {
}
const success = await submitFeedback(feedbackData);
if (success) {
setSubmitted(true);
}
}
};
return (
<>
@@ -81,7 +82,12 @@ export function Feedback() {
<div className="text-center mb-2">Feedback Submitted</div>
<div className="text-center">
<div className="flex justify-center gap-2 mt-4">
<button onClick={() => setIsModalOpen(false)} className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400">Close</button>
<button
onClick={() => setIsModalOpen(false)}
className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400"
>
Close
</button>
</div>
</div>
</>
@@ -94,36 +100,51 @@ export function Feedback() {
name="feedback"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 w-full border border-gray-300"
value={formData.feedback}
onChange={(e) => setFormData(prev => ({
...prev,
feedback: e.target.value
}))}
onChange={(e) =>
setFormData((prev) => ({
...prev,
feedback: e.target.value,
}))
}
/>
<div className="flex items-center">Email:</div>
<input type="text"
<input
type="text"
name="email"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 w-full border border-gray-300"
value={formData.email}
onChange={(e) => setFormData(prev => ({
...prev,
email: e.target.value
}))}
onChange={(e) =>
setFormData((prev) => ({
...prev,
email: e.target.value,
}))
}
/>
<div className="flex items-center gap-2">
<span>Share project with the Nut team:</span>
<input type="checkbox"
<input
type="checkbox"
name="share"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded border border-gray-300"
checked={formData.share}
onChange={(e) => setFormData(prev => ({
...prev,
share: e.target.checked
}))}
onChange={(e) =>
setFormData((prev) => ({
...prev,
share: e.target.checked,
}))
}
/>
</div>
<div className="flex justify-center gap-2 mt-4">
<button onClick={handleSubmitFeedback} className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">Submit</button>
<button onClick={() => setIsModalOpen(false)} className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400">Cancel</button>
<button
onClick={handleSubmitFeedback}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Submit
</button>
<button onClick={() => setIsModalOpen(false)} className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400">
Cancel
</button>
</div>
</>
)}

View File

@@ -1,9 +1,9 @@
import { toast } from "react-toastify";
import { toast } from 'react-toastify';
import ReactModal from 'react-modal';
import { useState } from "react";
import { workbenchStore } from "~/lib/stores/workbench";
import { getProblemsUsername, submitProblem } from "~/lib/replay/Problems";
import type { BoltProblemInput } from "~/lib/replay/Problems";
import { useState } from 'react';
import { workbenchStore } from '~/lib/stores/workbench';
import { getProblemsUsername, submitProblem } from '~/lib/replay/Problems';
import type { BoltProblemInput } from '~/lib/replay/Problems';
ReactModal.setAppElement('#root');
@@ -14,7 +14,7 @@ export function SaveProblem() {
const [formData, setFormData] = useState({
title: '',
description: '',
name: ''
name: '',
});
const [problemId, setProblemId] = useState<string | null>(null);
@@ -23,16 +23,16 @@ export function SaveProblem() {
setFormData({
title: '',
description: '',
name: ''
name: '',
});
setProblemId(null);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
setFormData((prev) => ({
...prev,
[name]: value
[name]: value,
}));
};
@@ -50,11 +50,12 @@ export function SaveProblem() {
return;
}
toast.info("Submitting problem...");
toast.info('Submitting problem...');
console.log("SubmitProblem", formData);
console.log('SubmitProblem', formData);
await workbenchStore.saveAllFiles();
const { contentBase64 } = await workbenchStore.generateZipBase64();
const problem: BoltProblemInput = {
@@ -66,10 +67,11 @@ export function SaveProblem() {
};
const problemId = await submitProblem(problem);
if (problemId) {
setProblemId(problemId);
}
}
};
return (
<>
@@ -91,7 +93,12 @@ export function SaveProblem() {
<div className="text-center mb-2">Problem Submitted: {problemId}</div>
<div className="text-center">
<div className="flex justify-center gap-2 mt-4">
<button onClick={() => setIsModalOpen(false)} className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400">Close</button>
<button
onClick={() => setIsModalOpen(false)}
className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400"
>
Close
</button>
</div>
</div>
</>
@@ -100,10 +107,11 @@ export function SaveProblem() {
<>
<div className="text-center">Save prompts as new problems when AI results are unsatisfactory.</div>
<div className="text-center">Problems are publicly visible and are used to improve AI performance.</div>
<div style={{ marginTop: "10px" }}>
<div style={{ marginTop: '10px' }}>
<div className="grid grid-cols-[auto_1fr] gap-4 max-w-md mx-auto">
<div className="flex items-center">Title:</div>
<input type="text"
<input
type="text"
name="title"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 w-full border border-gray-300"
value={formData.title}
@@ -111,7 +119,8 @@ export function SaveProblem() {
/>
<div className="flex items-center">Description:</div>
<input type="text"
<input
type="text"
name="description"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 w-full border border-gray-300"
value={formData.description}
@@ -119,8 +128,18 @@ export function SaveProblem() {
/>
</div>
<div className="flex justify-center gap-2 mt-4">
<button onClick={handleSubmitProblem} className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">Submit</button>
<button onClick={() => setIsModalOpen(false)} className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400">Cancel</button>
<button
onClick={handleSubmitProblem}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Submit
</button>
<button
onClick={() => setIsModalOpen(false)}
className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400"
>
Cancel
</button>
</div>
</div>
</>

View File

@@ -1,21 +1,23 @@
import { toast } from "react-toastify";
import { toast } from 'react-toastify';
import ReactModal from 'react-modal';
import { useState } from "react";
import { workbenchStore } from "~/lib/stores/workbench";
import { BoltProblemStatus, updateProblem } from "~/lib/replay/Problems";
import type { BoltProblemInput } from "~/lib/replay/Problems";
import { getLastLoadedProblem } from "../chat/LoadProblemButton";
import { getLastUserSimulationData, getLastSimulationChatMessages } from "~/lib/replay/SimulationPrompt";
import { useState } from 'react';
import { workbenchStore } from '~/lib/stores/workbench';
import { BoltProblemStatus, updateProblem } from '~/lib/replay/Problems';
import type { BoltProblemInput } from '~/lib/replay/Problems';
import { getLastLoadedProblem } from '../chat/LoadProblemButton';
import { getLastUserSimulationData, getLastSimulationChatMessages } from '~/lib/replay/SimulationPrompt';
ReactModal.setAppElement('#root');
// Component for saving input simulation and prompt information for
// the problem the current chat was loaded from.
/*
* Component for saving input simulation and prompt information for
* the problem the current chat was loaded from.
*/
export function SaveSolution() {
const [isModalOpen, setIsModalOpen] = useState(false);
const [formData, setFormData] = useState({
evaluator: ''
evaluator: '',
});
const [savedSolution, setSavedSolution] = useState<boolean>(false);
@@ -29,38 +31,43 @@ export function SaveSolution() {
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
setFormData((prev) => ({
...prev,
[name]: value
[name]: value,
}));
};
const handleSubmitSolution = async () => {
const savedProblem = getLastLoadedProblem();
if (!savedProblem) {
toast.error('No problem loaded');
return;
}
const simulationData = getLastUserSimulationData();
if (!simulationData) {
toast.error('No simulation data found');
return;
}
const messages = getLastSimulationChatMessages();
if (!messages) {
toast.error('No user prompt found');
return;
}
toast.info("Submitting solution...");
toast.info('Submitting solution...');
console.log("SubmitSolution", formData);
console.log('SubmitSolution', formData);
// The evaluator is only present when the problem has been solved.
// We still create a "solution" object even if it hasn't been
// solved quite yet, which is used for working on the problem.
/*
* The evaluator is only present when the problem has been solved.
* We still create a "solution" object even if it hasn't been
* solved quite yet, which is used for working on the problem.
*/
const evaluator = formData.evaluator.length ? formData.evaluator : undefined;
const problem: BoltProblemInput = {
@@ -80,7 +87,7 @@ export function SaveSolution() {
await updateProblem(savedProblem.problemId, problem);
setSavedSolution(true);
}
};
return (
<>
@@ -102,7 +109,12 @@ export function SaveSolution() {
<div className="text-center mb-2">Solution Saved</div>
<div className="text-center">
<div className="flex justify-center gap-2 mt-4">
<button onClick={() => setIsModalOpen(false)} className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400">Close</button>
<button
onClick={() => setIsModalOpen(false)}
className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400"
>
Close
</button>
</div>
</div>
</>
@@ -111,11 +123,14 @@ export function SaveSolution() {
<>
<div className="text-center">Save solution for loaded problem from last prompt and recording.</div>
<div className="text-center">Evaluator describes a condition the explanation must satisfy.</div>
<div className="text-center">Leave the evaluator blank if the API explanation is not right and the problem isn't solved yet.</div>
<div style={{ marginTop: "10px" }}>
<div className="text-center">
Leave the evaluator blank if the API explanation is not right and the problem isn't solved yet.
</div>
<div style={{ marginTop: '10px' }}>
<div className="grid grid-cols-[auto_1fr] gap-4 max-w-md mx-auto">
<div className="flex items-center">Evaluator:</div>
<input type="text"
<input
type="text"
name="evaluator"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 w-full border border-gray-300"
value={formData.evaluator}
@@ -123,8 +138,18 @@ export function SaveSolution() {
/>
</div>
<div className="flex justify-center gap-2 mt-4">
<button onClick={handleSubmitSolution} className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">Submit</button>
<button onClick={() => setIsModalOpen(false)} className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400">Cancel</button>
<button
onClick={handleSubmitSolution}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Submit
</button>
<button
onClick={() => setIsModalOpen(false)}
className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400"
>
Cancel
</button>
</div>
</div>
</>

View File

@@ -15,22 +15,16 @@ export function getCurrentMouseData() {
return gCurrentMouseData;
}
export const PointSelector = memo(
(props: PointSelectorProps) => {
const {
isSelectionMode,
setIsSelectionMode,
selectionPoint,
setSelectionPoint,
containerRef,
} = props;
export const PointSelector = memo((props: PointSelectorProps) => {
const { isSelectionMode, setIsSelectionMode, selectionPoint, setSelectionPoint, containerRef } = props;
const [isCapturing, setIsCapturing] = useState(false);
const [mouseData, setMouseData] = useState<MouseData | undefined>(undefined);
const [isCapturing, setIsCapturing] = useState(false);
const [mouseData, setMouseData] = useState<MouseData | undefined>(undefined);
gCurrentMouseData = mouseData;
gCurrentMouseData = mouseData;
const handleSelectionClick = useCallback(async (event: React.MouseEvent) => {
const handleSelectionClick = useCallback(
async (event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
@@ -42,57 +36,56 @@ export const PointSelector = memo(
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
setSelectionPoint({ x, y });
setIsCapturing(true);
const mouseData = await getMouseData(containerRef.current, { x, y });
console.log("MouseData", mouseData);
console.log('MouseData', mouseData);
setMouseData(mouseData);
setIsCapturing(false);
setIsSelectionMode(false); // Turn off selection mode after capture
}, [isSelectionMode, containerRef, setIsSelectionMode]);
},
[isSelectionMode, containerRef, setIsSelectionMode],
);
if (!isSelectionMode) {
if (selectionPoint) {
// Draw an overlay to prevent interactions with the iframe
// and to show the last point the user clicked.
return (
if (!isSelectionMode) {
if (selectionPoint) {
/*
* Draw an overlay to prevent interactions with the iframe
* and to show the last point the user clicked.
*/
return (
<div className="absolute inset-0" onClick={(event) => event.preventDefault()}>
<div
className="absolute inset-0"
onClick={(event) => event.preventDefault()}
style={{
position: 'absolute',
left: `${selectionPoint.x - 8}px`,
top: `${selectionPoint.y - 12}px`,
}}
>
<div
style={{
position: 'absolute',
left: `${selectionPoint.x-8}px`,
top: `${selectionPoint.y-12}px`,
}}
>
&#10060;
</div>
&#10060;
</div>
);
} else {
return null;
}
</div>
);
} else {
return null;
}
}
return (
<div
className="absolute inset-0"
onClick={handleSelectionClick}
style={{
backgroundColor: isCapturing ? 'transparent' : 'rgba(0, 0, 0, 0.1)',
userSelect: 'none',
WebkitUserSelect: 'none',
pointerEvents: 'all',
opacity: isCapturing ? 0 : 1,
zIndex: 50,
transition: 'opacity 0.1s ease-in-out',
}}
>
</div>
);
},
);
return (
<div
className="absolute inset-0"
onClick={handleSelectionClick}
style={{
backgroundColor: isCapturing ? 'transparent' : 'rgba(0, 0, 0, 0.1)',
userSelect: 'none',
WebkitUserSelect: 'none',
pointerEvents: 'all',
opacity: isCapturing ? 0 : 1,
zIndex: 50,
transition: 'opacity 0.1s ease-in-out',
}}
></div>
);
});

View File

@@ -73,14 +73,16 @@ export const Preview = memo(() => {
if (url.startsWith(baseUrl)) {
const trimmedUrl = url.slice(baseUrl.length);
if (trimmedUrl.startsWith('/')) {
return trimmedUrl;
}
return "/" + trimmedUrl;
return '/' + trimmedUrl;
}
return url;
}
};
const validateUrl = useCallback(
(value: string) => {
@@ -127,6 +129,7 @@ export const Preview = memo(() => {
simulationReloaded();
iframeRef.current.src = iframeRef.current.src;
}
setIsSelectionMode(false);
setSelectionPoint(null);
};
@@ -261,7 +264,10 @@ export const Preview = memo(() => {
<IconButton
icon="i-ph:bug-beetle"
title="Point to Bug"
onClick={() => { setSelectionPoint(null); setIsSelectionMode(!isSelectionMode); }}
onClick={() => {
setSelectionPoint(null);
setIsSelectionMode(!isSelectionMode);
}}
className={isSelectionMode ? 'bg-bolt-elements-background-depth-3' : ''}
/>
<div
@@ -279,6 +285,7 @@ export const Preview = memo(() => {
}}
onKeyDown={(event) => {
let newUrl;
if (event.key === 'Enter' && (newUrl = validateUrl(url))) {
setIframeUrl(newUrl);

View File

@@ -128,33 +128,35 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
{
description: 'Text Files',
accept: {
'text/*': ['.txt', '.js', '.ts', '.jsx', '.tsx', '.json', '.html', '.css']
}
}
]
'text/*': ['.txt', '.js', '.ts', '.jsx', '.tsx', '.json', '.html', '.css'],
},
},
],
});
const changesFile = await fileHandle.getFile();
const changesContent = await changesFile.text();
let path = "";
let contents = "";
let path = '';
let contents = '';
async function saveCurrentFile() {
if (path) {
await workbenchStore.saveFileContents("/home/project/src/" + path, contents);
await workbenchStore.saveFileContents('/home/project/src/' + path, contents);
}
}
for (const line of changesContent.split("\n")) {
for (const line of changesContent.split('\n')) {
const match = /^FILE (.*)/.exec(line);
if (match) {
await saveCurrentFile();
path = match[1];
contents = "";
contents = '';
continue;
}
contents += line + "\n";
contents += line + '\n';
}
await saveCurrentFile();
@@ -190,10 +192,7 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
<div className="ml-auto" />
{selectedView === 'code' && (
<div className="flex overflow-y-auto">
<PanelHeaderButton
className="mr-1 text-sm"
onClick={handleApplyChanges}
>
<PanelHeaderButton className="mr-1 text-sm" onClick={handleApplyChanges}>
<div className="i-ph:code" />
Apply Changes
</PanelHeaderButton>

View File

@@ -22,23 +22,23 @@ const mockImplementations = {
ensureOpenTelemetryInitialized: (_context: AppLoadContext) => {
console.log('[DEV MODE - OpenTelemetry not loaded]: Skipping initialization');
},
wrapWithSpan: <Args extends any[], T>(
opts: SpanOptions,
fn: (...args: Args) => Promise<T>
fn: (...args: Args) => Promise<T>,
): ((...args: Args) => Promise<T>) => {
// In development, just pass through the function without tracing
return fn;
},
getCurrentSpan: () => {
return null;
}
},
};
// Using a let variable so we can cache the imports in production
let otelModule: any = null;
// Helper to load the module once
const getOtelModule = async () => {
if (!otelModule && !isDevelopment()) {
@@ -46,10 +46,12 @@ const getOtelModule = async () => {
otelModule = await import('./otel');
} catch (e) {
console.error('Error loading OpenTelemetry:', e);
// Return null to indicate failure
return null;
}
}
return otelModule;
};
@@ -64,20 +66,22 @@ export function ensureOpenTelemetryInitialized(context: AppLoadContext): void {
mockImplementations.ensureOpenTelemetryInitialized(context);
return;
}
// In production, initialize (this will happen asynchronously)
if (otelModule) {
// If module is already loaded, use it directly
otelModule.ensureOpenTelemetryInitialized(context);
} else {
// Otherwise trigger the async load and initialize when ready
getOtelModule().then(module => {
if (module) {
module.ensureOpenTelemetryInitialized(context);
}
}).catch(e => {
console.error('Failed to initialize OpenTelemetry:', e);
});
getOtelModule()
.then((module) => {
if (module) {
module.ensureOpenTelemetryInitialized(context);
}
})
.catch((e) => {
console.error('Failed to initialize OpenTelemetry:', e);
});
}
}
@@ -88,27 +92,29 @@ export function ensureOpenTelemetryInitialized(context: AppLoadContext): void {
*/
export function wrapWithSpan<Args extends any[], T>(
opts: SpanOptions,
fn: (...args: Args) => Promise<T>
): ((...args: Args) => Promise<T>) {
fn: (...args: Args) => Promise<T>,
): (...args: Args) => Promise<T> {
if (isDevelopment()) {
// In development, just pass through without tracing
return fn;
}
// In production, create a wrapper function
return (...args: Args) => {
// If module is already loaded, use it directly
if (otelModule) {
return otelModule.wrapWithSpan(opts, fn)(...args);
}
// Otherwise trigger the async load for future calls
getOtelModule().then(() => {
// Module will be available for future calls
}).catch(e => {
console.error('Failed to load OpenTelemetry module:', e);
});
getOtelModule()
.then(() => {
// Module will be available for future calls
})
.catch((e) => {
console.error('Failed to load OpenTelemetry module:', e);
});
// For the current call, just use the function directly
return fn(...args);
};
@@ -124,19 +130,21 @@ export function getCurrentSpan(): any {
// In development, return null
return null;
}
// If module is already loaded, use it directly
if (otelModule) {
return otelModule.getCurrentSpan();
}
// Otherwise trigger the async load for future calls
getOtelModule().then(() => {
// Module will be available for future calls
}).catch(e => {
console.error('Failed to load OpenTelemetry module:', e);
});
getOtelModule()
.then(() => {
// Module will be available for future calls
})
.catch((e) => {
console.error('Failed to load OpenTelemetry module:', e);
});
// For the current call, return null
return null;
}
}

View File

@@ -214,16 +214,16 @@ export async function createTracer(appContext: AppLoadContext) {
try {
// Load development flag
const isDev = process.env.NODE_ENV === 'development';
// Skip initialization in development
if (isDev) {
console.warn('OpenTelemetry initialization skipped in development mode');
return undefined;
}
// Dynamically import the problematic module
const ASYNC_HOOKS_MANAGER = await loadAsyncHooksContextManager();
const exporter = new OTLPExporter({
url: 'https://api.honeycomb.io/v1/traces',
headers: {
@@ -267,10 +267,11 @@ export async function ensureOpenTelemetryInitialized(context: AppLoadContext) {
console.warn('OpenTelemetry initialization skipped in development mode');
return;
}
tracer = await createTracer(context);
} catch (e) {
console.error('Failed to initialize OpenTelemetry:', e);
// Don't throw, just log and continue - this allows the app to function without telemetry
}
}

View File

@@ -28,6 +28,7 @@ export function createAsyncSuspenseValue<T>(getValue: () => Promise<T>) {
);
record = { status: 'pending', promise };
return promise;
};
@@ -65,6 +66,7 @@ export function createAsyncSuspenseValue<T>(getValue: () => Promise<T>) {
if (record) {
return;
}
load().catch(() => {});
},
};

View File

@@ -1,12 +1,11 @@
// FIXME ping telemetry server directly instead of going through the backend.
import { getNutLoginKey } from "../replay/Problems";
import { getNutLoginKey } from '../replay/Problems';
// We do this to work around CORS insanity.
export async function pingTelemetry(event: string, data: any) {
const requestBody: any = {
event: "NutChat." + event,
event: 'NutChat.' + event,
data,
};
@@ -25,7 +24,7 @@ export class ChatMessageTelemetry {
constructor(numMessages: number) {
this.id = Math.random().toString(36).substring(2, 15);
this.numMessages = numMessages;
this.ping("StartMessage");
this.ping('StartMessage');
}
private ping(event: string, data: any = {}) {
@@ -38,22 +37,22 @@ export class ChatMessageTelemetry {
}
finish() {
this.ping("FinishMessage");
this.ping('FinishMessage');
}
abort(reason: string) {
this.ping("AbortMessage", { reason });
this.ping('AbortMessage', { reason });
}
startSimulation() {
this.ping("StartSimulation");
this.ping('StartSimulation');
}
endSimulation(status: string) {
this.ping("EndSimulation", { status });
this.ping('EndSimulation', { status });
}
sendPrompt(simulationStatus: string) {
this.ping("SendPrompt", { simulationStatus });
this.ping('SendPrompt', { simulationStatus });
}
}

View File

@@ -12,10 +12,7 @@ export function usePromptEnhancer() {
setPromptEnhanced(false);
};
const enhancePrompt = async (
input: string,
setInput: (value: string) => void
) => {
const enhancePrompt = async (input: string, setInput: (value: string) => void) => {
setEnhancingPrompt(true);
setPromptEnhanced(false);

View File

@@ -1,11 +1,11 @@
// Accessors for the API to access saved problems.
import { toast } from "react-toastify";
import { sendCommandDedicatedClient } from "./ReplayProtocolClient";
import type { ProtocolMessage } from "./SimulationPrompt";
import { toast } from 'react-toastify';
import { sendCommandDedicatedClient } from './ReplayProtocolClient';
import type { ProtocolMessage } from './SimulationPrompt';
import Cookies from 'js-cookie';
import JSZip from 'jszip';
import type { FileArtifact } from "~/utils/folderImport";
import type { FileArtifact } from '~/utils/folderImport';
export interface BoltProblemComment {
username?: string;
@@ -21,13 +21,13 @@ export interface BoltProblemSolution {
export enum BoltProblemStatus {
// Problem has been submitted but not yet reviewed.
Pending = "Pending",
Pending = 'Pending',
// Problem has been reviewed and has not been solved yet.
Unsolved = "Unsolved",
Unsolved = 'Unsolved',
// Nut automatically produces a suitable explanation for solving the problem.
Solved = "Solved",
Solved = 'Solved',
}
// Information about each problem stored in the index file.
@@ -48,21 +48,23 @@ export interface BoltProblem extends BoltProblemDescription {
solution?: BoltProblemSolution;
}
export type BoltProblemInput = Omit<BoltProblem, "problemId" | "timestamp">;
export type BoltProblemInput = Omit<BoltProblem, 'problemId' | 'timestamp'>;
export async function listAllProblems(): Promise<BoltProblemDescription[]> {
try {
const rv = await sendCommandDedicatedClient({
method: "Recording.globalExperimentalCommand",
method: 'Recording.globalExperimentalCommand',
params: {
name: "listBoltProblems",
name: 'listBoltProblems',
},
});
console.log("ListProblemsRval", rv);
console.log('ListProblemsRval', rv);
return (rv as any).rval.problems.reverse();
} catch (error) {
console.error("Error fetching problems", error);
toast.error("Failed to fetch problems");
console.error('Error fetching problems', error);
toast.error('Failed to fetch problems');
return [];
}
}
@@ -70,23 +72,26 @@ export async function listAllProblems(): Promise<BoltProblemDescription[]> {
export async function getProblem(problemId: string): Promise<BoltProblem | null> {
try {
const rv = await sendCommandDedicatedClient({
method: "Recording.globalExperimentalCommand",
method: 'Recording.globalExperimentalCommand',
params: {
name: "fetchBoltProblem",
name: 'fetchBoltProblem',
params: { problemId },
},
});
console.log("FetchProblemRval", rv);
console.log('FetchProblemRval', rv);
const problem = (rv as { rval: { problem: BoltProblem } }).rval.problem;
if ("prompt" in problem) {
if ('prompt' in problem) {
// 2/11/2025: Update obsolete data format for older problems.
problem.repositoryContents = (problem as any).prompt.content;
delete problem.prompt;
}
return problem;
} catch (error) {
console.error("Error fetching problem", error);
toast.error("Failed to fetch problem");
console.error('Error fetching problem', error);
toast.error('Failed to fetch problem');
}
return null;
}
@@ -94,17 +99,19 @@ export async function getProblem(problemId: string): Promise<BoltProblem | null>
export async function submitProblem(problem: BoltProblemInput): Promise<string | null> {
try {
const rv = await sendCommandDedicatedClient({
method: "Recording.globalExperimentalCommand",
method: 'Recording.globalExperimentalCommand',
params: {
name: "submitBoltProblem",
name: 'submitBoltProblem',
params: { problem },
},
});
console.log("SubmitProblemRval", rv);
console.log('SubmitProblemRval', rv);
return (rv as any).rval.problemId;
} catch (error) {
console.error("Error submitting problem", error);
toast.error("Failed to submit problem");
console.error('Error submitting problem', error);
toast.error('Failed to submit problem');
return null;
}
}
@@ -112,21 +119,21 @@ export async function submitProblem(problem: BoltProblemInput): Promise<string |
export async function updateProblem(problemId: string, problem: BoltProblemInput | undefined) {
try {
if (!getNutIsAdmin()) {
toast.error("Admin user required");
toast.error('Admin user required');
return;
}
const loginKey = Cookies.get(nutLoginKeyCookieName);
await sendCommandDedicatedClient({
method: "Recording.globalExperimentalCommand",
method: 'Recording.globalExperimentalCommand',
params: {
name: "updateBoltProblem",
name: 'updateBoltProblem',
params: { problemId, problem, loginKey },
},
});
} catch (error) {
console.error("Error updating problem", error);
toast.error("Failed to update problem");
console.error('Error updating problem', error);
toast.error('Failed to update problem');
}
}
@@ -150,14 +157,16 @@ interface UserInfo {
}
export async function saveNutLoginKey(key: string) {
const { rval: { userInfo } } = await sendCommandDedicatedClient({
method: "Recording.globalExperimentalCommand",
const {
rval: { userInfo },
} = (await sendCommandDedicatedClient({
method: 'Recording.globalExperimentalCommand',
params: {
name: "getUserInfo",
name: 'getUserInfo',
params: { loginKey: key },
},
}) as { rval: { userInfo: UserInfo } };
console.log("UserInfo", userInfo);
})) as { rval: { userInfo: UserInfo } };
console.log('UserInfo', userInfo);
Cookies.set(nutLoginKeyCookieName, key);
Cookies.set(nutIsAdminCookieName, userInfo.admin ? 'true' : 'false');
@@ -183,30 +192,37 @@ export async function extractFileArtifactsFromRepositoryContents(repositoryConte
await zip.loadAsync(repositoryContents, { base64: true });
const fileArtifacts: FileArtifact[] = [];
for (const [key, object] of Object.entries(zip.files)) {
if (object.dir) continue;
if (object.dir) {
continue;
}
fileArtifacts.push({
content: await object.async('text'),
path: key,
});
}
return fileArtifacts;
}
export async function submitFeedback(feedback: any) {
try {
const rv = await sendCommandDedicatedClient({
method: "Recording.globalExperimentalCommand",
method: 'Recording.globalExperimentalCommand',
params: {
name: "submitFeedback",
name: 'submitFeedback',
params: { feedback },
},
});
console.log("SubmitFeedbackRval", rv);
console.log('SubmitFeedbackRval', rv);
return true;
} catch (error) {
console.error("Error submitting feedback", error);
toast.error("Failed to submit feedback");
console.error('Error submitting feedback', error);
toast.error('Failed to submit feedback');
return false;
}
}

View File

@@ -60,6 +60,7 @@ function sendIframeRequest<K extends keyof RequestMap>(
export async function getIFrameSimulationData(iframe: HTMLIFrameElement): Promise<SimulationData> {
const buffer = await sendIframeRequest(iframe, { request: 'recording-data' });
if (!buffer) {
return [];
}
@@ -80,7 +81,8 @@ export interface MouseData {
export async function getMouseData(iframe: HTMLIFrameElement, position: { x: number; y: number }): Promise<MouseData> {
const mouseData = await sendIframeRequest(iframe, { request: 'mouse-data', payload: position });
assert(mouseData, "Expected to have mouse data");
assert(mouseData, 'Expected to have mouse data');
return mouseData;
}
@@ -101,11 +103,11 @@ function addRecordingMessageHandler(messageHandlerId: string) {
size: { width: window.innerWidth, height: window.innerHeight },
});
pushSimulationData({
kind: "locationHref",
kind: 'locationHref',
href: window.location.href,
});
pushSimulationData({
kind: "documentURL",
kind: 'documentURL',
url: window.location.href,
});
@@ -120,13 +122,13 @@ function addRecordingMessageHandler(messageHandlerId: string) {
function addNetworkResource(resource: NetworkResource) {
pushSimulationData({
kind: "resource",
kind: 'resource',
resource,
});
}
function addTextResource(info: RequestInfo, text: string, responseHeaders: Record<string, string>) {
const url = (new URL(info.url, window.location.href)).href;
const url = new URL(info.url, window.location.href).href;
addNetworkResource({
url,
requestBodyBase64: stringToBase64(info.requestBody),
@@ -138,21 +140,21 @@ function addRecordingMessageHandler(messageHandlerId: string) {
function addInteraction(interaction: UserInteraction) {
pushSimulationData({
kind: "interaction",
kind: 'interaction',
interaction,
});
}
function addIndexedDBAccess(access: IndexedDBAccess) {
pushSimulationData({
kind: "indexedDB",
kind: 'indexedDB',
access,
});
}
function addLocalStorageAccess(access: LocalStorageAccess) {
pushSimulationData({
kind: "localStorage",
kind: 'localStorage',
access,
});
}
@@ -161,6 +163,7 @@ function addRecordingMessageHandler(messageHandlerId: string) {
//console.log("GetSimulationData", simulationData.length, numSimulationPacketsSent);
const data = simulationData.slice(numSimulationPacketsSent);
numSimulationPacketsSent = simulationData.length;
return data;
}
@@ -259,9 +262,11 @@ function addRecordingMessageHandler(messageHandlerId: string) {
// Add nth-child if there are siblings
const parent = current.parentElement;
if (parent) {
const siblings = Array.from(parent.children);
const index = siblings.indexOf(current) + 1;
if (siblings.filter((el) => el.tagName === current!.tagName).length > 1) {
selector += `:nth-child(${index})`;
}
@@ -350,10 +355,12 @@ function addRecordingMessageHandler(messageHandlerId: string) {
...descriptor,
get() {
onInterceptedOperation(`Getter:${prop}`);
if (!interceptValue) {
const baseValue = (descriptor?.get as any).call(obj);
interceptValue = interceptor(baseValue);
}
return interceptValue;
},
});
@@ -417,9 +424,11 @@ function addRecordingMessageHandler(messageHandlerId: string) {
_name: 'IDBRequest',
result: (value: any, target: any) => {
const key = getRequestKeys.get(target);
if (key) {
pushIndexedDBAccess(target, 'get', key, value);
}
return value;
},
};
@@ -449,6 +458,7 @@ function addRecordingMessageHandler(messageHandlerId: string) {
headers.forEach((value, key) => {
result[key] = value;
});
return result;
}
@@ -458,24 +468,29 @@ function addRecordingMessageHandler(messageHandlerId: string) {
createFunctionProxy(v, 'json', async (promise: Promise<any>) => {
const json = await promise;
const requestInfo = responseToRequestInfo.get(response);
if (requestInfo) {
addTextResource(requestInfo, JSON.stringify(json), convertHeaders(response.headers));
}
return json;
}),
text: (v: any, response: Response) =>
createFunctionProxy(v, 'text', async (promise: Promise<any>) => {
const text = await promise;
const requestInfo = responseToRequestInfo.get(response);
if (requestInfo) {
addTextResource(requestInfo, text, convertHeaders(response.headers));
}
return text;
}),
};
function createProxy(obj: any) {
let methods;
if (obj instanceof IDBFactory) {
methods = IDBFactoryMethods;
} else if (obj instanceof IDBOpenDBRequest) {
@@ -493,25 +508,32 @@ function addRecordingMessageHandler(messageHandlerId: string) {
} else if (obj instanceof Response) {
methods = ResponseMethods;
}
assert(methods, 'Unknown object for createProxy');
const name = methods._name;
return new Proxy(obj, {
get(target, prop) {
onInterceptedOperation(`ProxyGetter:${name}.${String(prop)}`);
let value = target[prop];
if (typeof value === 'function') {
value = value.bind(target);
}
if (methods[prop]) {
value = methods[prop](value, target);
}
return value;
},
set(target, prop, value) {
onInterceptedOperation(`ProxySetter:${name}.${String(prop)}`);
target[prop] = value;
return true;
},
});
@@ -520,7 +542,9 @@ function addRecordingMessageHandler(messageHandlerId: string) {
function createFunctionProxy(fn: any, name: string, handler?: (v: any, ...args: any[]) => any) {
return (...args: any[]) => {
onInterceptedOperation(`FunctionCall:${name}`);
const v = fn(...args);
return handler ? handler(v, ...args) : createProxy(v);
};
}
@@ -529,13 +553,16 @@ function addRecordingMessageHandler(messageHandlerId: string) {
interceptProperty(window, 'localStorage', createProxy);
const baseFetch = window.fetch;
window.fetch = async (info, options) => {
const url = info instanceof Request ? info.url : info.toString();
const requestBody = typeof options?.body == 'string' ? options.body : '';
const requestInfo: RequestInfo = { url, requestBody };
try {
const rv = await baseFetch(info, options);
responseToRequestInfo.set(rv, requestInfo);
return createProxy(rv);
} catch (error) {
addNetworkResource({

View File

@@ -1,6 +1,6 @@
const replayWsServer = "wss://dispatch.replay.io";
const replayWsServer = 'wss://dispatch.replay.io';
export function assert(condition: any, message: string = "Assertion failed!"): asserts condition {
export function assert(condition: any, message: string = 'Assertion failed!'): asserts condition {
if (!condition) {
debugger;
throw new Error(message);
@@ -18,23 +18,28 @@ export function defer<T>(): { promise: Promise<T>; resolve: (value: T) => void;
resolve = _resolve;
reject = _reject;
});
return { promise, resolve: resolve!, reject: reject! };
}
export function uint8ArrayToBase64(data: Uint8Array) {
let str = "";
let str = '';
for (const byte of data) {
str += String.fromCharCode(byte);
}
return btoa(str);
}
export function stringToBase64(inputString: string) {
if (typeof inputString !== "string") {
throw new TypeError("Input must be a string.");
if (typeof inputString !== 'string') {
throw new TypeError('Input must be a string.');
}
const encoder = new TextEncoder();
const data = encoder.encode(inputString);
return uint8ArrayToBase64(data);
}
@@ -73,6 +78,7 @@ function createDeferred<T>(): Deferred<T> {
resolve = _resolve;
reject = _reject;
});
return { promise, resolve: resolve!, reject: reject! };
}
@@ -90,12 +96,12 @@ export class ProtocolClient {
this.socket = new WebSocket(replayWsServer);
this.socket.addEventListener("close", this.onSocketClose);
this.socket.addEventListener("error", this.onSocketError);
this.socket.addEventListener("open", this.onSocketOpen);
this.socket.addEventListener("message", this.onSocketMessage);
this.socket.addEventListener('close', this.onSocketClose);
this.socket.addEventListener('error', this.onSocketError);
this.socket.addEventListener('open', this.onSocketOpen);
this.socket.addEventListener('message', this.onSocketMessage);
this.listenForMessage("Recording.sessionError", (error: any) => {
this.listenForMessage('Recording.sessionError', (error: any) => {
logDebug(`Session error ${error}`);
});
}
@@ -110,6 +116,7 @@ export class ProtocolClient {
listenForMessage(method: string, callback: (params: any) => void) {
let listeners = this.eventListeners.get(method);
if (listeners == null) {
listeners = new Set([callback]);
@@ -127,7 +134,7 @@ export class ProtocolClient {
const id = this.nextMessageId++;
const { method, params, sessionId } = args;
logDebug("Sending command", { id, method, params, sessionId });
logDebug('Sending command', { id, method, params, sessionId });
const command = {
id,
@@ -140,11 +147,12 @@ export class ProtocolClient {
const deferred = createDeferred();
this.pendingCommands.set(id, deferred);
return deferred.promise;
}
onSocketClose = () => {
logDebug("Socket closed");
logDebug('Socket closed');
};
onSocketError = (error: any) => {
@@ -159,26 +167,28 @@ export class ProtocolClient {
assert(deferred, `Received message with unknown id: ${id}`);
this.pendingCommands.delete(id);
if (result) {
deferred.resolve(result);
} else if (error) {
console.error("ProtocolError", error);
console.error('ProtocolError', error);
deferred.reject(new ProtocolError(error));
} else {
deferred.reject(new Error("Channel error"));
deferred.reject(new Error('Channel error'));
}
} else if (this.eventListeners.has(method)) {
const callbacks = this.eventListeners.get(method);
if (callbacks) {
callbacks.forEach(callback => callback(params));
callbacks.forEach((callback) => callback(params));
}
} else {
logDebug("Received message without a handler", { method, params });
logDebug('Received message without a handler', { method, params });
}
};
onSocketOpen = async () => {
logDebug("Socket opened");
logDebug('Socket opened');
this.openDeferred.resolve();
};
}
@@ -187,9 +197,11 @@ export class ProtocolClient {
export async function sendCommandDedicatedClient(args: { method: string; params: any }) {
const client = new ProtocolClient();
await client.initialize();
try {
const rval = await client.sendCommand(args);
client.close();
return rval;
} finally {
client.close();

View File

@@ -8,8 +8,10 @@ interface SimulationPacketServerURL {
url: string;
}
// Simulation data specifying the contents of the repository to set up a dev server
// for static resources.
/*
* Simulation data specifying the contents of the repository to set up a dev server
* for static resources.
*/
interface SimulationPacketRepositoryContents {
kind: 'repositoryContents';
contents: string; // base64 encoded zip of the repository.
@@ -58,8 +60,10 @@ export interface UserInteraction {
// Selector of the element associated with the interaction, if any.
selector?: string;
// For mouse interactions, dimensions and position within the
// element where the event occurred.
/*
* For mouse interactions, dimensions and position within the
* element where the event occurred.
*/
width?: number;
height?: number;
x?: number;

View File

@@ -1,5 +1,7 @@
// Core logic for using simulation data from a remote recording to enhance
// the AI developer prompt.
/*
* Core logic for using simulation data from a remote recording to enhance
* the AI developer prompt.
*/
import type { Message } from 'ai';
import type { SimulationData, SimulationPacket } from './SimulationData';
@@ -13,22 +15,22 @@ import { detectProjectCommands } from '~/utils/projectCommands';
function createRepositoryContentsPacket(contents: string): SimulationPacket {
return {
kind: "repositoryContents",
kind: 'repositoryContents',
contents,
time: new Date().toISOString(),
};
}
type ProtocolMessageRole = "user" | "assistant" | "system";
type ProtocolMessageRole = 'user' | 'assistant' | 'system';
type ProtocolMessageText = {
type: "text";
type: 'text';
role: ProtocolMessageRole;
content: string;
};
type ProtocolMessageImage = {
type: "image";
type: 'image';
role: ProtocolMessageRole;
dataURL: string;
};
@@ -71,13 +73,13 @@ class ChatManager {
constructor() {
this.client = new ProtocolClient();
this.chatIdPromise = (async () => {
assert(this.client, "Chat has been destroyed");
assert(this.client, 'Chat has been destroyed');
await this.client.initialize();
const { chatId } = (await this.client.sendCommand({ method: "Nut.startChat", params: {} })) as { chatId: string };
const { chatId } = (await this.client.sendCommand({ method: 'Nut.startChat', params: {} })) as { chatId: string };
console.log("ChatStarted", new Date().toISOString(), chatId);
console.log('ChatStarted', new Date().toISOString(), chatId);
return chatId;
})();
@@ -89,14 +91,14 @@ class ChatManager {
}
async setRepositoryContents(contents: string) {
assert(this.client, "Chat has been destroyed");
assert(this.client, 'Chat has been destroyed');
this.repositoryContents = contents;
const packet = createRepositoryContentsPacket(contents);
const chatId = await this.chatIdPromise;
await this.client.sendCommand({
method: "Nut.addSimulation",
method: 'Nut.addSimulation',
params: {
chatId,
version: SimulationDataVersion,
@@ -108,85 +110,100 @@ class ChatManager {
}
async addPageData(data: SimulationData) {
assert(this.client, "Chat has been destroyed");
assert(this.repositoryContents, "Expected repository contents");
assert(this.client, 'Chat has been destroyed');
assert(this.repositoryContents, 'Expected repository contents');
this.pageData.push(...data);
// If page data comes in while we are waiting for the chat to finish
// we remember it but don't update the existing chat.
/*
* If page data comes in while we are waiting for the chat to finish
* we remember it but don't update the existing chat.
*/
if (this.simulationFinished) {
return;
}
const chatId = await this.chatIdPromise;
await this.client.sendCommand({
method: "Nut.addSimulationData",
method: 'Nut.addSimulationData',
params: { chatId, simulationData: data },
});
}
finishSimulationData(): SimulationData {
assert(this.client, "Chat has been destroyed");
assert(!this.simulationFinished, "Simulation has been finished");
assert(this.repositoryContents, "Expected repository contents");
assert(this.client, 'Chat has been destroyed');
assert(!this.simulationFinished, 'Simulation has been finished');
assert(this.repositoryContents, 'Expected repository contents');
this.recordingIdPromise = (async () => {
assert(this.client, "Chat has been destroyed");
const chatId = await this.chatIdPromise;
const { recordingId } = await this.client.sendCommand({
method: "Nut.finishSimulationData",
params: { chatId },
}) as { recordingId: string | undefined };
assert(this.client, 'Chat has been destroyed');
const chatId = await this.chatIdPromise;
const { recordingId } = (await this.client.sendCommand({
method: 'Nut.finishSimulationData',
params: { chatId },
})) as { recordingId: string | undefined };
assert(recordingId, 'Recording ID not set');
assert(recordingId, "Recording ID not set");
return recordingId;
})();
})();
const allData = [createRepositoryContentsPacket(this.repositoryContents), ...this.pageData];
this.simulationFinished = true;
return allData;
}
async sendChatMessage(messages: ProtocolMessage[], options?: ChatMessageOptions) {
assert(this.client, "Chat has been destroyed");
assert(this.client, 'Chat has been destroyed');
const responseId = `response-${generateRandomId()}`;
let response: string = "";
const removeResponseListener = this.client.listenForMessage("Nut.chatResponsePart", ({ responseId: eventResponseId, message }: { responseId: string, message: ProtocolMessage }) => {
if (responseId == eventResponseId) {
if (message.type == "text") {
response += message.content;
options?.onResponsePart?.(message.content);
let response: string = '';
const removeResponseListener = this.client.listenForMessage(
'Nut.chatResponsePart',
({ responseId: eventResponseId, message }: { responseId: string; message: ProtocolMessage }) => {
if (responseId == eventResponseId) {
if (message.type == 'text') {
response += message.content;
options?.onResponsePart?.(message.content);
}
}
}
});
},
);
const modifiedFiles: ProtocolFile[] = [];
const removeFileListener = this.client.listenForMessage("Nut.chatModifiedFile", ({ responseId: eventResponseId, file }: { responseId: string, file: ProtocolFile }) => {
if (responseId == eventResponseId) {
console.log("ChatModifiedFile", file);
modifiedFiles.push(file);
const removeFileListener = this.client.listenForMessage(
'Nut.chatModifiedFile',
({ responseId: eventResponseId, file }: { responseId: string; file: ProtocolFile }) => {
if (responseId == eventResponseId) {
console.log('ChatModifiedFile', file);
modifiedFiles.push(file);
const content = `
const content = `
<boltArtifact id="modified-file-${generateRandomId()}" title="File Changes">
<boltAction type="file" filePath="${file.path}">${file.content}</boltAction>
</boltArtifact>
`;
response += content;
options?.onResponsePart?.(content);
}
});
response += content;
options?.onResponsePart?.(content);
}
},
);
const chatId = await this.chatIdPromise;
console.log("ChatSendMessage", new Date().toISOString(), chatId, JSON.stringify({ messages, developerFiles: options?.developerFiles }));
console.log(
'ChatSendMessage',
new Date().toISOString(),
chatId,
JSON.stringify({ messages, developerFiles: options?.developerFiles }),
);
await this.client.sendCommand({
method: "Nut.sendChatMessage",
method: 'Nut.sendChatMessage',
params: { chatId, responseId, messages, chatOnly: options?.chatOnly, developerFiles: options?.developerFiles },
});
@@ -217,33 +234,39 @@ function startChat(repositoryContents: string, pageData: SimulationData) {
if (gChatManager) {
gChatManager.destroy();
}
gChatManager = new ChatManager();
gChatManager.setRepositoryContents(repositoryContents);
if (pageData.length) {
gChatManager.addPageData(pageData);
}
}
// Called when the repository contents have changed. We'll start a new chat
// with the same interaction data as any existing chat.
/*
* Called when the repository contents have changed. We'll start a new chat
* with the same interaction data as any existing chat.
*/
export async function simulationRepositoryUpdated(repositoryContents: string) {
startChat(repositoryContents, gChatManager?.pageData ?? []);
}
// Called when the page gathering interaction data has been reloaded. We'll
// start a new chat with the same repository contents as any existing chat.
/*
* Called when the page gathering interaction data has been reloaded. We'll
* start a new chat with the same repository contents as any existing chat.
*/
export async function simulationReloaded() {
assert(gChatManager, "Expected to have an active chat");
assert(gChatManager, 'Expected to have an active chat');
const repositoryContents = gChatManager.repositoryContents;
assert(repositoryContents, "Expected active chat to have repository contents");
assert(repositoryContents, 'Expected active chat to have repository contents');
startChat(repositoryContents, []);
}
export async function simulationAddData(data: SimulationData) {
assert(gChatManager, "Expected to have an active chat");
assert(gChatManager, 'Expected to have an active chat');
gChatManager.addPageData(data);
}
@@ -254,17 +277,20 @@ export function getLastUserSimulationData(): SimulationData | undefined {
}
export async function getSimulationRecording(): Promise<string> {
assert(gChatManager, "Expected to have an active chat");
assert(gChatManager, 'Expected to have an active chat');
const simulationData = gChatManager.finishSimulationData();
// The repository contents are part of the problem and excluded from the simulation data
// reported for solutions.
gLastUserSimulationData = simulationData.filter(packet => packet.kind != "repositoryContents");
/*
* The repository contents are part of the problem and excluded from the simulation data
* reported for solutions.
*/
gLastUserSimulationData = simulationData.filter((packet) => packet.kind != 'repositoryContents');
console.log("SimulationData", new Date().toISOString(), JSON.stringify(simulationData));
console.log('SimulationData', new Date().toISOString(), JSON.stringify(simulationData));
assert(gChatManager.recordingIdPromise, 'Expected recording promise');
assert(gChatManager.recordingIdPromise, "Expected recording promise");
return gChatManager.recordingIdPromise;
}
@@ -283,25 +309,26 @@ Do not describe the specific fix needed.
export async function getSimulationEnhancedPrompt(
chatMessages: Message[],
userMessage: string,
mouseData: MouseData | undefined
mouseData: MouseData | undefined,
): Promise<string> {
assert(gChatManager, "Chat not started");
assert(gChatManager.simulationFinished, "Simulation not finished");
assert(gChatManager, 'Chat not started');
assert(gChatManager.simulationFinished, 'Simulation not finished');
let system = SimulationSystemPrompt;
if (mouseData) {
system += `The user pointed to an element on the page <element selector=${JSON.stringify(mouseData.selector)} height=${mouseData.height} width=${mouseData.width} x=${mouseData.x} y=${mouseData.y} />`;
}
const messages: ProtocolMessage[] = [
{
role: "system",
type: "text",
role: 'system',
type: 'text',
content: system,
},
{
role: "user",
type: "text",
role: 'user',
type: 'text',
content: userMessage,
},
];
@@ -334,46 +361,49 @@ Here is the user message you need to evaluate: <user_message>${messageInput}</us
const messages: ProtocolMessage[] = [
{
role: "system",
type: "text",
role: 'system',
type: 'text',
content: systemPrompt,
},
{
role: "user",
type: "text",
role: 'user',
type: 'text',
content: userMessage,
},
];
const response = await gChatManager.sendChatMessage(messages, { chatOnly: true });
console.log("UseSimulationResponse", response);
console.log('UseSimulationResponse', response);
const match = /<analyze>(.*?)<\/analyze>/.exec(response);
if (match) {
return match[1] === "true";
return match[1] === 'true';
}
return false;
}
function getProtocolRule(message: Message): "user" | "assistant" | "system" {
function getProtocolRule(message: Message): 'user' | 'assistant' | 'system' {
switch (message.role) {
case "user":
return "user";
case "assistant":
case "data":
return "assistant";
case "system":
return "system";
case 'user':
return 'user';
case 'assistant':
case 'data':
return 'assistant';
case 'system':
return 'system';
}
}
function removeBoltArtifacts(text: string): string {
const OpenTag = "<boltArtifact";
const CloseTag = "</boltArtifact>";
const OpenTag = '<boltArtifact';
const CloseTag = '</boltArtifact>';
while (true) {
const openTag = text.indexOf(OpenTag);
if (openTag === -1) {
break;
}
@@ -381,59 +411,69 @@ function removeBoltArtifacts(text: string): string {
const prefix = text.substring(0, openTag);
const closeTag = text.indexOf(CloseTag, openTag + OpenTag.length);
if (closeTag === -1) {
text = prefix;
} else {
text = prefix + text.substring(closeTag + CloseTag.length);
}
}
return text;
}
function buildProtocolMessages(messages: Message[]): ProtocolMessage[] {
const rv: ProtocolMessage[] = [];
for (const msg of messages) {
const role = getProtocolRule(msg);
if (Array.isArray(msg.content)) {
for (const content of msg.content) {
switch (content.type) {
case "text":
case 'text':
rv.push({
role,
type: "text",
type: 'text',
content: removeBoltArtifacts(content.text),
});
break;
case "image":
case 'image':
rv.push({
role,
type: "image",
type: 'image',
dataURL: content.image,
});
break;
default:
console.error("Unknown message content", content);
console.error('Unknown message content', content);
}
}
} else if (typeof msg.content == "string") {
} else if (typeof msg.content == 'string') {
rv.push({
role,
type: "text",
type: 'text',
content: msg.content,
});
}
}
return rv;
}
export async function sendDeveloperChatMessage(messages: Message[], files: FileMap, onResponsePart: ChatResponsePartCallback) {
export async function sendDeveloperChatMessage(
messages: Message[],
files: FileMap,
onResponsePart: ChatResponsePartCallback,
) {
if (!gChatManager) {
gChatManager = new ChatManager();
}
const developerFiles: ProtocolFile[] = [];
for (const [path, file] of Object.entries(files)) {
if (file?.type == "file" && shouldIncludeFile(path)) {
if (file?.type == 'file' && shouldIncludeFile(path)) {
developerFiles.push({
path,
content: file.content,
@@ -443,8 +483,8 @@ export async function sendDeveloperChatMessage(messages: Message[], files: FileM
const protocolMessages = buildProtocolMessages(messages);
protocolMessages.unshift({
role: "system",
type: "text",
role: 'system',
type: 'text',
content: DeveloperSystemPrompt,
});

View File

@@ -236,8 +236,10 @@ export class StreamingMessageParser {
this._options.callbacks?.onArtifactOpen?.({ messageId, ...currentArtifact });
//const artifactFactory = this._options.artifactElement ?? createArtifactElement;
//output += artifactFactory({ messageId });
/*
* const artifactFactory = this._options.artifactElement ?? createArtifactElement;
* output += artifactFactory({ messageId });
*/
i = openTagEnd + 1;
} else {

View File

@@ -93,7 +93,7 @@ export class FilesStore {
const oldContent = this.getFile(filePath)?.content;
if (!oldContent) {
console.log("CurrentFiles", JSON.stringify(Object.keys(this.files.get())));
console.log('CurrentFiles', JSON.stringify(Object.keys(this.files.get())));
unreachable(`Cannot save unknown file ${filePath}`);
}

View File

@@ -418,6 +418,7 @@ export class WorkbenchStore {
// Generate the zip file and save it
const content = await zip.generateAsync({ type: 'blob' });
return { content, uniqueProjectName };
}
@@ -430,6 +431,7 @@ export class WorkbenchStore {
const { content, uniqueProjectName } = await this.generateZip();
const buf = await content.arrayBuffer();
const contentBase64 = uint8ArrayToBase64(new Uint8Array(buf));
return { contentBase64, uniqueProjectName };
}
@@ -441,6 +443,7 @@ export class WorkbenchStore {
// Check if any files we know about have different contents in the artifacts.
const files = this.files.get();
const fileRelativePaths = new Set<string>();
for (const [filePath, dirent] of Object.entries(files)) {
if (dirent?.type === 'file' && !dirent.isBinary) {
const relativePath = extractRelativePath(filePath);
@@ -449,7 +452,7 @@ export class WorkbenchStore {
const content = dirent.content;
const artifact = fileArtifacts.find((artifact) => artifact.path === relativePath);
const artifactContent = artifact?.content ?? "";
const artifactContent = artifact?.content ?? '';
if (content != artifactContent) {
modifiedFilePaths.add(relativePath);
@@ -467,10 +470,10 @@ export class WorkbenchStore {
const actionArtifactId = `restore-contents-artifact-id-${messageId}`;
for (const filePath of modifiedFilePaths) {
console.log("RestoreModifiedFile", filePath);
console.log('RestoreModifiedFile', filePath);
const artifact = fileArtifacts.find((artifact) => artifact.path === filePath);
const artifactContent = artifact?.content ?? "";
const artifactContent = artifact?.content ?? '';
const actionId = `restore-contents-action-${messageId}-${filePath}-${Math.random().toString()}`;
const data: ActionCallbackData = {
@@ -479,7 +482,7 @@ export class WorkbenchStore {
artifactId: actionArtifactId,
action: {
type: 'file',
filePath: filePath,
filePath,
content: artifactContent,
},
};

View File

@@ -33,9 +33,9 @@ function AboutPage() {
>
Bolt.new
</a>{' '}
for helping you develop full stack apps using AI. AI developers frequently struggle with fixing
even simple bugs when they don't know the cause, and get stuck making ineffective changes
over and over. We want to crack these tough nuts, so to speak, so you can get back to building.
for helping you develop full stack apps using AI. AI developers frequently struggle with fixing even simple
bugs when they don't know the cause, and get stuck making ineffective changes over and over. We want to
crack these tough nuts, so to speak, so you can get back to building.
</p>
<p className="mb-6">
@@ -49,16 +49,15 @@ function AboutPage() {
Replay.io
</a>{' '}
recording of your app and whatever you did to produce the bug. The recording captures all the runtime
behavior of your app, which is analyzed to explain the bug's root cause.
This explanation is given to the AI developer so it has context to write a good fix.
behavior of your app, which is analyzed to explain the bug's root cause. This explanation is given to the AI
developer so it has context to write a good fix.
</p>
<p className="mb-6">
Nut.new is already pretty good at fixing problems, and we're working to make it better.
We want it to reliably fix anything you're seeing, as long as it has a clear explanation
and the problem isn't too complicated (AIs aren't magic). If it's doing poorly, let us know!
Use the UI to leave us some private feedback or save your project to our public set of problems
where AIs struggle.
Nut.new is already pretty good at fixing problems, and we're working to make it better. We want it to
reliably fix anything you're seeing, as long as it has a clear explanation and the problem isn't too
complicated (AIs aren't magic). If it's doing poorly, let us know! Use the UI to leave us some private
feedback or save your project to our public set of problems where AIs struggle.
</p>
<p>
@@ -71,15 +70,12 @@ function AboutPage() {
>
Replay.io
</a>{' '}
team.
We're offering unlimited free access to Nut.new for early adopters who can give us feedback
we'll use to improve Nut. Reach us at{' '}
<a
href="mailto:hi@replay.io"
className="text-bolt-elements-accent underline hover:no-underline"
>
team. We're offering unlimited free access to Nut.new for early adopters who can give us feedback we'll use
to improve Nut. Reach us at{' '}
<a href="mailto:hi@replay.io" className="text-bolt-elements-accent underline hover:no-underline">
hi@replay.io
</a>{' '} or fill out our{' '}
</a>{' '}
or fill out our{' '}
<a
href="https://replay.io/contact"
className="text-bolt-elements-accent underline hover:no-underline"

View File

@@ -15,4 +15,4 @@ export const loader = async ({ request: _request }: LoaderFunctionArgs) => {
},
},
);
};
};

View File

@@ -1,13 +1,13 @@
import { json, type ActionFunctionArgs } from '@remix-run/cloudflare';
async function pingTelemetry(event: string, data: any): Promise<boolean> {
console.log("PingTelemetry", event, data);
console.log('PingTelemetry', event, data);
try {
const response = await fetch("https://telemetry.replay.io/", {
method: "POST",
const response = await fetch('https://telemetry.replay.io/', {
method: 'POST',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
body: JSON.stringify({ event, ...data }),
});
@@ -16,9 +16,10 @@ async function pingTelemetry(event: string, data: any): Promise<boolean> {
console.error(`Telemetry request returned unexpected status: ${response.status}`);
return false;
}
return true;
} catch (error) {
console.error("Telemetry request failed:", error);
console.error('Telemetry request failed:', error);
return false;
}
}

View File

@@ -7,7 +7,13 @@ import { ToastContainerWrapper, Status, Keywords } from './problems';
import { toast } from 'react-toastify';
import { Suspense, useCallback, useEffect, useState } from 'react';
import { useParams } from '@remix-run/react';
import { getProblem, updateProblem as backendUpdateProblem, getProblemsUsername, BoltProblemStatus, getNutIsAdmin } from '~/lib/replay/Problems';
import {
getProblem,
updateProblem as backendUpdateProblem,
getProblemsUsername,
BoltProblemStatus,
getNutIsAdmin,
} from '~/lib/replay/Problems';
import type { BoltProblem, BoltProblemComment } from '~/lib/replay/Problems';
function Comments({ comments }: { comments: BoltProblemComment[] }) {
@@ -16,10 +22,8 @@ function Comments({ comments }: { comments: BoltProblemComment[] }) {
{comments.map((comment, index) => (
<div key={index} className="bg-bolt-elements-background-depth-2 rounded-lg p-4 shadow-sm">
<div className="flex items-center justify-between mb-2">
<span className="font-medium text-bolt-text">{comment.username ?? "Anonymous"}</span>
<span className="text-sm text-bolt-text-secondary">
{new Date(comment.timestamp).toLocaleString()}
</span>
<span className="font-medium text-bolt-text">{comment.username ?? 'Anonymous'}</span>
<span className="text-sm text-bolt-text-secondary">{new Date(comment.timestamp).toLocaleString()}</span>
</div>
<div className="text-bolt-text whitespace-pre-wrap">{comment.content}</div>
</div>
@@ -35,8 +39,8 @@ function ProblemViewer({ problem }: { problem: BoltProblem }) {
<div className="benchmark">
<h1 className="text-xl4 font-semibold mb-2">{title}</h1>
<p>{description}</p>
<a
href={`/load-problem/${problemId}`}
<a
href={`/load-problem/${problemId}`}
className="load-button inline-block px-4 py-2 mt-3 mb-3 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors duration-200 font-medium"
>
Load Problem
@@ -45,7 +49,7 @@ function ProblemViewer({ problem }: { problem: BoltProblem }) {
<Keywords keywords={keywords} />
<Comments comments={comments} />
</div>
)
);
}
interface UpdateProblemFormProps {
@@ -56,15 +60,16 @@ interface UpdateProblemFormProps {
function UpdateProblemForm(props: UpdateProblemFormProps) {
const { handleSubmit, updateText, placeholder } = props;
const [value, setValue] = useState("");
const [value, setValue] = useState('');
const onSubmitClicked = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
e.preventDefault();
if (value.trim()) {
handleSubmit(value)
setValue('')
handleSubmit(value);
setValue('');
}
}
};
return (
<form onSubmit={onSubmitClicked} className="mb-6 p-4 bg-bolt-elements-background-depth-2 rounded-lg">
@@ -76,81 +81,108 @@ function UpdateProblemForm(props: UpdateProblemFormProps) {
className="w-full p-3 mb-3 bg-bolt-elements-background-depth-3 rounded-md border border-bolt-elements-background-depth-4 text-black placeholder-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-y min-h-[100px]"
required
/>
<button
type="submit"
<button
type="submit"
disabled={!value.trim()}
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
>
{updateText}
</button>
</form>
)
);
}
type DoUpdateCallback = (problem: BoltProblem) => BoltProblem;
type UpdateProblemCallback = (doUpdate: DoUpdateCallback) => void;
type DeleteProblemCallback = () => void;
function UpdateProblemForms({ updateProblem, deleteProblem }: { updateProblem: UpdateProblemCallback, deleteProblem: DeleteProblemCallback }) {
function UpdateProblemForms({
updateProblem,
deleteProblem,
}: {
updateProblem: UpdateProblemCallback;
deleteProblem: DeleteProblemCallback;
}) {
const handleAddComment = (content: string) => {
const newComment: BoltProblemComment = {
timestamp: Date.now(),
username: getProblemsUsername(),
content,
}
updateProblem(problem => {
};
updateProblem((problem) => {
const comments = [...(problem.comments || []), newComment];
return {
...problem,
comments,
};
});
}
};
const handleSetTitle = (title: string) => {
updateProblem(problem => ({
updateProblem((problem) => ({
...problem,
title,
}));
}
};
const handleSetDescription = (description: string) => {
updateProblem(problem => ({
updateProblem((problem) => ({
...problem,
description,
}));
}
};
const handleSetStatus = (status: string) => {
const statusEnum = BoltProblemStatus[status as keyof typeof BoltProblemStatus];
if (!statusEnum) {
toast.error('Invalid status');
return;
}
updateProblem(problem => ({
updateProblem((problem) => ({
...problem,
status: statusEnum,
}));
}
};
const handleSetKeywords = (keywordString: string) => {
const keywords = keywordString.split(' ').map(keyword => keyword.trim()).filter(keyword => keyword.length > 0);
updateProblem(problem => ({
const keywords = keywordString
.split(' ')
.map((keyword) => keyword.trim())
.filter((keyword) => keyword.length > 0);
updateProblem((problem) => ({
...problem,
keywords,
}));
}
};
return (
<>
<UpdateProblemForm handleSubmit={handleAddComment} updateText="Add Comment" placeholder="Add a comment..." />
<UpdateProblemForm handleSubmit={handleSetTitle} updateText="Set Title" placeholder="Set the title of the problem..." />
<UpdateProblemForm handleSubmit={handleSetDescription} updateText="Set Description" placeholder="Set the description of the problem..." />
<UpdateProblemForm handleSubmit={handleSetStatus} updateText="Set Status" placeholder="Set the status of the problem..." />
<UpdateProblemForm handleSubmit={handleSetKeywords} updateText="Set Keywords" placeholder="Set the keywords of the problem..." />
<UpdateProblemForm
handleSubmit={handleSetTitle}
updateText="Set Title"
placeholder="Set the title of the problem..."
/>
<UpdateProblemForm
handleSubmit={handleSetDescription}
updateText="Set Description"
placeholder="Set the description of the problem..."
/>
<UpdateProblemForm
handleSubmit={handleSetStatus}
updateText="Set Status"
placeholder="Set the status of the problem..."
/>
<UpdateProblemForm
handleSubmit={handleSetKeywords}
updateText="Set Keywords"
placeholder="Set the keywords of the problem..."
/>
<div className="mb-6 p-4 bg-bolt-elements-background-depth-2 rounded-lg">
<button
<button
onClick={deleteProblem}
className="px-6 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 transition-colors duration-200 font-medium"
>
@@ -158,7 +190,7 @@ function UpdateProblemForms({ updateProblem, deleteProblem }: { updateProblem: U
</button>
</div>
</>
)
);
}
const Nothing = () => null;
@@ -166,27 +198,32 @@ const Nothing = () => null;
function ViewProblemPage() {
const params = useParams();
const problemId = params.id;
if (typeof problemId !== 'string') {
throw new Error('Problem ID is required');
}
const [problemData, setProblemData] = useState<BoltProblem | null>(null);
const updateProblem = useCallback(async (callback: DoUpdateCallback) => {
if (!problemData) {
toast.error('Problem data missing');
return;
}
const newProblem = callback(problemData);
setProblemData(newProblem);
console.log("BackendUpdateProblem", problemId, newProblem);
await backendUpdateProblem(problemId, newProblem);
}, [problemData]);
const updateProblem = useCallback(
async (callback: DoUpdateCallback) => {
if (!problemData) {
toast.error('Problem data missing');
return;
}
const newProblem = callback(problemData);
setProblemData(newProblem);
console.log('BackendUpdateProblem', problemId, newProblem);
await backendUpdateProblem(problemId, newProblem);
},
[problemData],
);
const deleteProblem = useCallback(async () => {
console.log("BackendDeleteProblem", problemId);
console.log('BackendDeleteProblem', problemId);
await backendUpdateProblem(problemId, undefined);
toast.success("Problem deleted");
toast.success('Problem deleted');
}, [problemData]);
useEffect(() => {
@@ -195,25 +232,27 @@ function ViewProblemPage() {
return (
<Suspense fallback={<Nothing />}>
<TooltipProvider>
<div className="flex flex-col h-full w-full bg-bolt-elements-background-depth-1 text-gray-900 dark:text-gray-200">
<BackgroundRays />
<Header />
<ClientOnly>{() => <Menu />}</ClientOnly>
<div className="p-6">
{problemData === null
? (<div className="flex items-center justify-center">
<TooltipProvider>
<div className="flex flex-col h-full w-full bg-bolt-elements-background-depth-1 text-gray-900 dark:text-gray-200">
<BackgroundRays />
<Header />
<ClientOnly>{() => <Menu />}</ClientOnly>
<div className="p-6">
{problemData === null ? (
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
</div>)
: <ProblemViewer problem={problemData} />}
</div>
) : (
<ProblemViewer problem={problemData} />
)}
</div>
{getNutIsAdmin() && problemData && (
<UpdateProblemForms updateProblem={updateProblem} deleteProblem={deleteProblem} />
)}
<ToastContainerWrapper />
</div>
{getNutIsAdmin() && problemData && (
<UpdateProblemForms updateProblem={updateProblem} deleteProblem={deleteProblem} />
)}
<ToastContainerWrapper />
</div>
</TooltipProvider>
</TooltipProvider>
</Suspense>
);
}

View File

@@ -14,33 +14,35 @@ const toastAnimation = cssTransition({
});
export function ToastContainerWrapper() {
return <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" />;
return (
<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" />;
}
}
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}
/>
return undefined;
}}
position="bottom-right"
pauseOnFocusLoss
transition={toastAnimation}
/>
);
}
export function Status({ status }: { status: BoltProblemStatus | undefined }) {
@@ -51,17 +53,17 @@ export function Status({ status }: { status: BoltProblemStatus | undefined }) {
const statusColors: Record<BoltProblemStatus, string> = {
[BoltProblemStatus.Pending]: 'bg-yellow-400 dark:text-yellow-400',
[BoltProblemStatus.Unsolved]: 'bg-orange-500 dark:text-orange-500',
[BoltProblemStatus.Solved]: 'bg-blue-500 dark:text-blue-500'
[BoltProblemStatus.Solved]: 'bg-blue-500 dark:text-blue-500',
};
return (
<div className="flex items-center gap-2 my-2">
<span className="font-semibold">Status:</span>
<div className={`inline-flex items-center px-3 py-1 rounded-full bg-opacity-10 dark:bg-opacity-20 ${statusColors[status]}`}>
<div
className={`inline-flex items-center px-3 py-1 rounded-full bg-opacity-10 dark:bg-opacity-20 ${statusColors[status]}`}
>
<span className={`w-2 h-2 rounded-full mr-2 ${statusColors[status]} bg-opacity-100`}></span>
<span className="font-medium">
{status.charAt(0).toUpperCase() + status.slice(1)}
</span>
<span className="font-medium">{status.charAt(0).toUpperCase() + status.slice(1)}</span>
</div>
</div>
);
@@ -100,72 +102,74 @@ function ProblemsPage() {
listAllProblems().then(setProblems);
}, []);
const filteredProblems = problems?.filter(problem => {
const filteredProblems = problems?.filter((problem) => {
return statusFilter === 'all' || getProblemStatus(problem) === statusFilter;
});
return (
<Suspense fallback={<Nothing />}>
<TooltipProvider>
<div className="flex flex-col min-h-fit w-full bg-bolt-elements-background-depth-1 dark:bg-black text-gray-900 dark:text-gray-200">
<BackgroundRays />
<Header />
<ClientOnly>{() => <Menu />}</ClientOnly>
<TooltipProvider>
<div className="flex flex-col min-h-fit w-full bg-bolt-elements-background-depth-1 dark:bg-black text-gray-900 dark:text-gray-200">
<BackgroundRays />
<Header />
<ClientOnly>{() => <Menu />}</ClientOnly>
<div className="p-6">
{problems && <div className="mb-4">
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as BoltProblemStatus | 'all')}
className="appearance-none w-48 px-4 py-2.5 rounded-lg bg-bolt-elements-background-depth-2 border border-bolt-elements-border text-bolt-content-primary hover:border-bolt-elements-border-hover focus:outline-none focus:ring-2 focus:ring-bolt-accent-primary/20 focus:border-bolt-accent-primary cursor-pointer relative pr-10"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'right 12px center',
backgroundSize: '16px'
}}
>
<option value="all">{`All Problems (${problems?.length ?? 0})`}</option>
{Object.values(BoltProblemStatus).map((status) => {
const count = problems?.filter(problem => getProblemStatus(problem) === status).length ?? 0;
return (
<option key={status} value={status}>
{status.charAt(0).toUpperCase() + status.slice(1) + ` (${count})`}
</option>
);
})}
</select>
</div>}
{problems === null ? (
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
</div>
) : problems.length === 0 ? (
<div className="text-center text-gray-600">No problems found</div>
) : (
<div className="grid gap-4">
{filteredProblems?.map((problem) => (
<a
href={`/problem/${problem.problemId}`}
key={problem.problemId}
className="p-4 rounded-lg bg-bolt-elements-background-depth-2 hover:bg-bolt-elements-background-depth-3 transition-colors cursor-pointer"
<div className="p-6">
{problems && (
<div className="mb-4">
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as BoltProblemStatus | 'all')}
className="appearance-none w-48 px-4 py-2.5 rounded-lg bg-bolt-elements-background-depth-2 border border-bolt-elements-border text-bolt-content-primary hover:border-bolt-elements-border-hover focus:outline-none focus:ring-2 focus:ring-bolt-accent-primary/20 focus:border-bolt-accent-primary cursor-pointer relative pr-10"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'right 12px center',
backgroundSize: '16px',
}}
>
<h2 className="text-xl font-semibold mb-2">{problem.title}</h2>
<p className="text-gray-700 dark:text-gray-200 mb-2">{problem.description}</p>
<Status status={problem.status} />
<Keywords keywords={problem.keywords} />
<p className="text-sm text-gray-600 dark:text-gray-200">
Time: {new Date(problem.timestamp).toLocaleString()}
</p>
</a>
))}
</div>
)}
<option value="all">{`All Problems (${problems?.length ?? 0})`}</option>
{Object.values(BoltProblemStatus).map((status) => {
const count = problems?.filter((problem) => getProblemStatus(problem) === status).length ?? 0;
return (
<option key={status} value={status}>
{status.charAt(0).toUpperCase() + status.slice(1) + ` (${count})`}
</option>
);
})}
</select>
</div>
)}
{problems === null ? (
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
</div>
) : problems.length === 0 ? (
<div className="text-center text-gray-600">No problems found</div>
) : (
<div className="grid gap-4">
{filteredProblems?.map((problem) => (
<a
href={`/problem/${problem.problemId}`}
key={problem.problemId}
className="p-4 rounded-lg bg-bolt-elements-background-depth-2 hover:bg-bolt-elements-background-depth-3 transition-colors cursor-pointer"
>
<h2 className="text-xl font-semibold mb-2">{problem.title}</h2>
<p className="text-gray-700 dark:text-gray-200 mb-2">{problem.description}</p>
<Status status={problem.status} />
<Keywords keywords={problem.keywords} />
<p className="text-sm text-gray-600 dark:text-gray-200">
Time: {new Date(problem.timestamp).toLocaleString()}
</p>
</a>
))}
</div>
)}
</div>
<ToastContainerWrapper />
</div>
<ToastContainerWrapper />
</div>
</TooltipProvider>
</TooltipProvider>
</Suspense>
);
}

View File

@@ -40,9 +40,11 @@ export const isBinaryFile = async (file: File): Promise<boolean> => {
};
export const shouldIncludeFile = (path: string): boolean => {
const projectDirectory = "/home/project/";
const projectDirectory = '/home/project/';
if (path.startsWith(projectDirectory)) {
path = path.substring(projectDirectory.length);
}
return !ig.ignores(path);
};

View File

@@ -43,6 +43,7 @@ export const createChatFromFolder = async (
let filesContent = `I've imported the contents of the "${folderName}" folder.${binaryFilesMessage}`;
filesContent += `<boltArtifact id="imported-files" title="Imported Files">`;
for (const file of fileArtifacts) {
if (shouldIncludeFile(file.path)) {
filesContent += `<boltAction type="file" filePath="${file.path}">${file.content}</boltAction>\n\n`;