mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
Linter fixes
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,6 @@
|
||||
logs
|
||||
.cursor
|
||||
supabase/.temp
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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`,
|
||||
}}
|
||||
>
|
||||
❌
|
||||
</div>
|
||||
❌
|
||||
</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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(() => {});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -15,4 +15,4 @@ export const loader = async ({ request: _request }: LoaderFunctionArgs) => {
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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`;
|
||||
|
||||
Reference in New Issue
Block a user