mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
Updates for async chat format (#71)
This commit is contained in:
parent
bd9d13ca5e
commit
c503fd244e
@ -1,37 +0,0 @@
|
|||||||
import { memo } from 'react';
|
|
||||||
import { Markdown } from './Markdown';
|
|
||||||
import type { JSONValue } from 'ai';
|
|
||||||
|
|
||||||
interface AssistantMessageProps {
|
|
||||||
content: string;
|
|
||||||
annotations?: JSONValue[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAnnotationsTokensUsage(annotations: JSONValue[] | undefined) {
|
|
||||||
const filteredAnnotations = (annotations?.filter(
|
|
||||||
(annotation: JSONValue) => annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'),
|
|
||||||
) || []) as { type: string; value: any }[];
|
|
||||||
|
|
||||||
const usage: {
|
|
||||||
completionTokens: number;
|
|
||||||
promptTokens: number;
|
|
||||||
totalTokens: number;
|
|
||||||
} = filteredAnnotations.find((annotation) => annotation.type === 'usage')?.value;
|
|
||||||
|
|
||||||
return usage;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AssistantMessage = memo(({ content, annotations }: AssistantMessageProps) => {
|
|
||||||
const usage = getAnnotationsTokensUsage(annotations);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="overflow-hidden w-full">
|
|
||||||
{usage && (
|
|
||||||
<div className="text-sm text-bolt-elements-textSecondary mb-2">
|
|
||||||
Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens})
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Markdown html>{content}</Markdown>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
@ -9,7 +9,8 @@ import { IconButton } from '~/components/ui/IconButton';
|
|||||||
import { Workbench } from '~/components/workbench/Workbench.client';
|
import { Workbench } from '~/components/workbench/Workbench.client';
|
||||||
import { classNames } from '~/utils/classNames';
|
import { classNames } from '~/utils/classNames';
|
||||||
import { Messages } from './Messages.client';
|
import { Messages } from './Messages.client';
|
||||||
import { getPreviousRepositoryId, type Message } from '~/lib/persistence/useChatHistory';
|
import { getPreviousRepositoryId } from '~/lib/persistence/useChatHistory';
|
||||||
|
import type { Message } from '~/lib/persistence/message';
|
||||||
import { SendButton } from './SendButton.client';
|
import { SendButton } from './SendButton.client';
|
||||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||||
|
|
||||||
|
@ -18,12 +18,11 @@ import { debounce } from '~/utils/debounce';
|
|||||||
import { useSearchParams } from '@remix-run/react';
|
import { useSearchParams } from '@remix-run/react';
|
||||||
import { createSampler } from '~/utils/sampler';
|
import { createSampler } from '~/utils/sampler';
|
||||||
import {
|
import {
|
||||||
getSimulationRecording,
|
|
||||||
getSimulationEnhancedPrompt,
|
|
||||||
simulationAddData,
|
simulationAddData,
|
||||||
|
simulationFinishData,
|
||||||
simulationRepositoryUpdated,
|
simulationRepositoryUpdated,
|
||||||
shouldUseSimulation,
|
sendChatMessage,
|
||||||
sendDeveloperChatMessage,
|
type ChatReference,
|
||||||
} from '~/lib/replay/SimulationPrompt';
|
} from '~/lib/replay/SimulationPrompt';
|
||||||
import { getIFrameSimulationData } from '~/lib/replay/Recording';
|
import { getIFrameSimulationData } from '~/lib/replay/Recording';
|
||||||
import { getCurrentIFrame } from '~/components/workbench/Preview';
|
import { getCurrentIFrame } from '~/components/workbench/Preview';
|
||||||
@ -32,8 +31,9 @@ import { anthropicNumFreeUsesCookieName, anthropicApiKeyCookieName, maxFreeUses
|
|||||||
import { getNutLoginKey, submitFeedback } from '~/lib/replay/Problems';
|
import { getNutLoginKey, submitFeedback } from '~/lib/replay/Problems';
|
||||||
import { ChatMessageTelemetry, pingTelemetry } from '~/lib/hooks/pingTelemetry';
|
import { ChatMessageTelemetry, pingTelemetry } from '~/lib/hooks/pingTelemetry';
|
||||||
import type { RejectChangeData } from './ApproveChange';
|
import type { RejectChangeData } from './ApproveChange';
|
||||||
import { generateRandomId } from '~/lib/replay/ReplayProtocolClient';
|
import { assert, generateRandomId } from '~/lib/replay/ReplayProtocolClient';
|
||||||
import { getMessagesRepositoryId, getPreviousRepositoryId, type Message } from '~/lib/persistence/useChatHistory';
|
import { getMessagesRepositoryId, getPreviousRepositoryId } from '~/lib/persistence/useChatHistory';
|
||||||
|
import type { Message } from '~/lib/persistence/message';
|
||||||
|
|
||||||
const toastAnimation = cssTransition({
|
const toastAnimation = cssTransition({
|
||||||
enter: 'animated fadeInRight',
|
enter: 'animated fadeInRight',
|
||||||
@ -64,15 +64,11 @@ async function flushSimulationData() {
|
|||||||
//console.log("HaveSimulationData", simulationData.length);
|
//console.log("HaveSimulationData", simulationData.length);
|
||||||
|
|
||||||
// Add the simulation data to the chat.
|
// Add the simulation data to the chat.
|
||||||
await simulationAddData(simulationData);
|
simulationAddData(simulationData);
|
||||||
}
|
}
|
||||||
|
|
||||||
let gLockSimulationData = false;
|
|
||||||
|
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
if (!gLockSimulationData) {
|
flushSimulationData();
|
||||||
flushSimulationData();
|
|
||||||
}
|
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
export function Chat() {
|
export function Chat() {
|
||||||
@ -154,16 +150,6 @@ async function clearActiveChat() {
|
|||||||
gActiveChatMessageTelemetry = undefined;
|
gActiveChatMessageTelemetry = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMessageId(prefix: string, chatId: string) {
|
|
||||||
return `${prefix}-${chatId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EnhancedPromptPrefix = 'enhanced-prompt';
|
|
||||||
|
|
||||||
export function isEnhancedPromptMessage(message: Message): boolean {
|
|
||||||
return message.id.startsWith(EnhancedPromptPrefix);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ChatImpl = memo(
|
export const ChatImpl = memo(
|
||||||
({ description, initialMessages, storeMessageHistory, importChat, exportChat }: ChatProps) => {
|
({ description, initialMessages, storeMessageHistory, importChat, exportChat }: ChatProps) => {
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
@ -261,50 +247,6 @@ export const ChatImpl = memo(
|
|||||||
setChatStarted(true);
|
setChatStarted(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
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.';
|
|
||||||
}
|
|
||||||
|
|
||||||
const recordingMessage: Message = {
|
|
||||||
id: buildMessageId('create-recording', chatId),
|
|
||||||
role: 'assistant',
|
|
||||||
content: message,
|
|
||||||
};
|
|
||||||
|
|
||||||
return { recordingId, recordingMessage };
|
|
||||||
};
|
|
||||||
|
|
||||||
const getEnhancedPrompt = async (chatId: string, userMessage: string) => {
|
|
||||||
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.';
|
|
||||||
hadError = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const enhancedPromptMessage: Message = {
|
|
||||||
id: buildMessageId(EnhancedPromptPrefix, chatId),
|
|
||||||
role: 'assistant',
|
|
||||||
content: message,
|
|
||||||
};
|
|
||||||
|
|
||||||
return { enhancedPrompt, enhancedPromptMessage, hadError };
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendMessage = async (messageInput?: string) => {
|
const sendMessage = async (messageInput?: string) => {
|
||||||
const _input = messageInput || input;
|
const _input = messageInput || input;
|
||||||
const numAbortsAtStart = gNumAborts;
|
const numAbortsAtStart = gNumAborts;
|
||||||
@ -340,130 +282,81 @@ export const ChatImpl = memo(
|
|||||||
setActiveChatId(chatId);
|
setActiveChatId(chatId);
|
||||||
|
|
||||||
const userMessage: Message = {
|
const userMessage: Message = {
|
||||||
id: buildMessageId('user', chatId),
|
id: `user-${chatId}`,
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: [
|
type: 'text',
|
||||||
{
|
content: _input,
|
||||||
type: 'text',
|
|
||||||
text: _input,
|
|
||||||
},
|
|
||||||
...imageDataList.map((imageData) => ({
|
|
||||||
type: 'image',
|
|
||||||
image: imageData,
|
|
||||||
})),
|
|
||||||
] as any, // Type assertion to bypass compiler check
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let newMessages = [...messages, userMessage];
|
let newMessages = [...messages, userMessage];
|
||||||
|
|
||||||
|
imageDataList.forEach((imageData, index) => {
|
||||||
|
const imageMessage: Message = {
|
||||||
|
id: `image-${chatId}-${index}`,
|
||||||
|
role: 'user',
|
||||||
|
type: 'image',
|
||||||
|
dataURL: imageData,
|
||||||
|
};
|
||||||
|
newMessages.push(imageMessage);
|
||||||
|
});
|
||||||
|
|
||||||
setMessages(newMessages);
|
setMessages(newMessages);
|
||||||
|
|
||||||
// Add file cleanup here
|
// Add file cleanup here
|
||||||
setUploadedFiles([]);
|
setUploadedFiles([]);
|
||||||
setImageDataList([]);
|
setImageDataList([]);
|
||||||
|
|
||||||
let simulation = false;
|
await flushSimulationData();
|
||||||
|
simulationFinishData();
|
||||||
try {
|
|
||||||
simulation = chatStarted && (await shouldUseSimulation(_input));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error checking simulation', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (numAbortsAtStart != gNumAborts) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('UseSimulation', simulation);
|
|
||||||
|
|
||||||
let simulationStatus = 'NoSimulation';
|
|
||||||
|
|
||||||
if (simulation) {
|
|
||||||
gActiveChatMessageTelemetry.startSimulation();
|
|
||||||
|
|
||||||
gLockSimulationData = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await flushSimulationData();
|
|
||||||
|
|
||||||
const createRecordingPromise = createRecording(chatId);
|
|
||||||
const enhancedPromptPromise = getEnhancedPrompt(chatId, _input);
|
|
||||||
|
|
||||||
const { recordingId, recordingMessage } = await createRecordingPromise;
|
|
||||||
|
|
||||||
if (numAbortsAtStart != gNumAborts) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('RecordingMessage', recordingMessage);
|
|
||||||
newMessages = [...newMessages, recordingMessage];
|
|
||||||
setMessages(newMessages);
|
|
||||||
|
|
||||||
if (recordingId) {
|
|
||||||
const info = await enhancedPromptPromise;
|
|
||||||
|
|
||||||
if (numAbortsAtStart != gNumAborts) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('EnhancedPromptMessage', info.enhancedPromptMessage);
|
|
||||||
newMessages = [...newMessages, info.enhancedPromptMessage];
|
|
||||||
setMessages(newMessages);
|
|
||||||
|
|
||||||
simulationStatus = info.hadError ? 'PromptError' : 'Success';
|
|
||||||
} else {
|
|
||||||
simulationStatus = 'RecordingError';
|
|
||||||
}
|
|
||||||
|
|
||||||
gActiveChatMessageTelemetry.endSimulation(simulationStatus);
|
|
||||||
} finally {
|
|
||||||
gLockSimulationData = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chatStore.setKey('aborted', false);
|
chatStore.setKey('aborted', false);
|
||||||
|
|
||||||
runAnimation();
|
runAnimation();
|
||||||
|
|
||||||
gActiveChatMessageTelemetry.sendPrompt(simulationStatus);
|
const addResponseMessage = (msg: Message) => {
|
||||||
|
|
||||||
const responseMessageId = buildMessageId('response', chatId);
|
|
||||||
let responseMessageContent = '';
|
|
||||||
let responseRepositoryId: string | undefined;
|
|
||||||
let hasResponseMessage = false;
|
|
||||||
|
|
||||||
const updateResponseMessage = () => {
|
|
||||||
if (gNumAborts != numAbortsAtStart) {
|
if (gNumAborts != numAbortsAtStart) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
newMessages = [...newMessages];
|
newMessages = [...newMessages];
|
||||||
|
|
||||||
if (hasResponseMessage) {
|
const lastMessage = newMessages[newMessages.length - 1];
|
||||||
|
|
||||||
|
if (lastMessage.id == msg.id) {
|
||||||
newMessages.pop();
|
newMessages.pop();
|
||||||
|
assert(lastMessage.type == 'text', 'Last message must be a text message');
|
||||||
|
assert(msg.type == 'text', 'Message must be a text message');
|
||||||
|
newMessages.push({
|
||||||
|
...msg,
|
||||||
|
content: lastMessage.content + msg.content,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
newMessages.push(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
newMessages.push({
|
|
||||||
id: responseMessageId,
|
|
||||||
role: 'assistant',
|
|
||||||
content: responseMessageContent,
|
|
||||||
repositoryId: responseRepositoryId,
|
|
||||||
});
|
|
||||||
setMessages(newMessages);
|
setMessages(newMessages);
|
||||||
hasResponseMessage = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const addResponseContent = (content: string) => {
|
const references: ChatReference[] = [];
|
||||||
responseMessageContent += content;
|
|
||||||
updateResponseMessage();
|
const mouseData = getCurrentMouseData();
|
||||||
};
|
|
||||||
|
if (mouseData) {
|
||||||
|
references.push({
|
||||||
|
kind: 'element',
|
||||||
|
selector: mouseData.selector,
|
||||||
|
x: mouseData.x,
|
||||||
|
y: mouseData.y,
|
||||||
|
width: mouseData.width,
|
||||||
|
height: mouseData.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const repositoryId = getMessagesRepositoryId(newMessages);
|
await sendChatMessage(newMessages, references, addResponseMessage);
|
||||||
responseRepositoryId = await sendDeveloperChatMessage(newMessages, repositoryId, addResponseContent);
|
|
||||||
updateResponseMessage();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
toast.error('Error sending message');
|
||||||
console.error('Error sending message', e);
|
console.error('Error sending message', e);
|
||||||
addResponseContent('Error sending message.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gNumAborts != numAbortsAtStart) {
|
if (gNumAborts != numAbortsAtStart) {
|
||||||
@ -480,9 +373,14 @@ export const ChatImpl = memo(
|
|||||||
|
|
||||||
textareaRef.current?.blur();
|
textareaRef.current?.blur();
|
||||||
|
|
||||||
if (responseRepositoryId) {
|
const existingRepositoryId = getMessagesRepositoryId(messages);
|
||||||
|
const responseRepositoryId = getMessagesRepositoryId(newMessages);
|
||||||
|
|
||||||
|
if (responseRepositoryId && existingRepositoryId != responseRepositoryId) {
|
||||||
simulationRepositoryUpdated(responseRepositoryId);
|
simulationRepositoryUpdated(responseRepositoryId);
|
||||||
setApproveChangesMessageId(responseMessageId);
|
|
||||||
|
const lastMessage = newMessages[newMessages.length - 1];
|
||||||
|
setApproveChangesMessageId(lastMessage.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import type { Message } from 'ai';
|
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '~/utils/fileUtils';
|
import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '~/utils/fileUtils';
|
||||||
import { createChatFromFolder, getFileRepositoryContents } from '~/utils/folderImport';
|
import { createChatFromFolder, getFileRepositoryContents } from '~/utils/folderImport';
|
||||||
import { logStore } from '~/lib/stores/logs'; // Assuming logStore is imported from this location
|
import { logStore } from '~/lib/stores/logs'; // Assuming logStore is imported from this location
|
||||||
import { createRepositoryImported } from '~/lib/replay/Repository';
|
import { createRepositoryImported } from '~/lib/replay/Repository';
|
||||||
|
import type { Message } from '~/lib/persistence/message';
|
||||||
|
|
||||||
interface ImportFolderButtonProps {
|
interface ImportFolderButtonProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import type { Message } from 'ai';
|
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { createChatFromFolder } from '~/utils/folderImport';
|
import { createChatFromFolder } from '~/utils/folderImport';
|
||||||
import { logStore } from '~/lib/stores/logs';
|
import { logStore } from '~/lib/stores/logs';
|
||||||
@ -7,6 +6,7 @@ import { assert } from '~/lib/replay/ReplayProtocolClient';
|
|||||||
import type { BoltProblem } from '~/lib/replay/Problems';
|
import type { BoltProblem } from '~/lib/replay/Problems';
|
||||||
import { getProblem } from '~/lib/replay/Problems';
|
import { getProblem } from '~/lib/replay/Problems';
|
||||||
import { createRepositoryImported } from '~/lib/replay/Repository';
|
import { createRepositoryImported } from '~/lib/replay/Repository';
|
||||||
|
import type { Message } from '~/lib/persistence/message';
|
||||||
|
|
||||||
interface LoadProblemButtonProps {
|
interface LoadProblemButtonProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
38
app/components/chat/MessageContents.tsx
Normal file
38
app/components/chat/MessageContents.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* @ts-nocheck
|
||||||
|
* Preventing TS checks with files presented in the video for a better presentation.
|
||||||
|
*/
|
||||||
|
import { MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants';
|
||||||
|
import { Markdown } from './Markdown';
|
||||||
|
import type { Message } from '~/lib/persistence/message';
|
||||||
|
|
||||||
|
interface MessageContentsProps {
|
||||||
|
message: Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MessageContents({ message }: MessageContentsProps) {
|
||||||
|
switch (message.type) {
|
||||||
|
case 'text':
|
||||||
|
return (
|
||||||
|
<div className="overflow-hidden pt-[4px]">
|
||||||
|
<Markdown html>{stripMetadata(message.content)}</Markdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'image':
|
||||||
|
return (
|
||||||
|
<div className="overflow-hidden pt-[4px]">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<img
|
||||||
|
src={message.dataURL}
|
||||||
|
className="max-w-full h-auto rounded-lg"
|
||||||
|
style={{ maxHeight: '512px', objectFit: 'contain' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripMetadata(content: string) {
|
||||||
|
return content.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, '');
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
import React, { Suspense } from 'react';
|
import React, { Suspense } from 'react';
|
||||||
import { classNames } from '~/utils/classNames';
|
import { classNames } from '~/utils/classNames';
|
||||||
import { AssistantMessage } from './AssistantMessage';
|
|
||||||
import { UserMessage } from './UserMessage';
|
|
||||||
import WithTooltip from '~/components/ui/Tooltip';
|
import WithTooltip from '~/components/ui/Tooltip';
|
||||||
import { getPreviousRepositoryId, type Message } from '~/lib/persistence/useChatHistory';
|
import { getPreviousRepositoryId } from '~/lib/persistence/useChatHistory';
|
||||||
|
import type { Message } from '~/lib/persistence/message';
|
||||||
|
import { MessageContents } from './MessageContents';
|
||||||
|
|
||||||
interface MessagesProps {
|
interface MessagesProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
@ -20,7 +20,7 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
|
|||||||
<div id={id} ref={ref} className={props.className}>
|
<div id={id} ref={ref} className={props.className}>
|
||||||
{messages.length > 0
|
{messages.length > 0
|
||||||
? messages.map((message, index) => {
|
? messages.map((message, index) => {
|
||||||
const { role, content, id: messageId, repositoryId } = message;
|
const { role, id: messageId, repositoryId } = message;
|
||||||
const previousRepositoryId = getPreviousRepositoryId(messages, index);
|
const previousRepositoryId = getPreviousRepositoryId(messages, index);
|
||||||
const isUserMessage = role === 'user';
|
const isUserMessage = role === 'user';
|
||||||
const isFirst = index === 0;
|
const isFirst = index === 0;
|
||||||
@ -47,11 +47,7 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="grid grid-col-1 w-full">
|
<div className="grid grid-col-1 w-full">
|
||||||
{isUserMessage ? (
|
<MessageContents message={message} />
|
||||||
<UserMessage content={content} />
|
|
||||||
) : (
|
|
||||||
<AssistantMessage content={content} annotations={message.annotations} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{previousRepositoryId && repositoryId && onRewind && (
|
{previousRepositoryId && repositoryId && onRewind && (
|
||||||
<div className="flex gap-2 flex-col lg:flex-row">
|
<div className="flex gap-2 flex-col lg:flex-row">
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
/*
|
|
||||||
* @ts-nocheck
|
|
||||||
* Preventing TS checks with files presented in the video for a better presentation.
|
|
||||||
*/
|
|
||||||
import { MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants';
|
|
||||||
import { Markdown } from './Markdown';
|
|
||||||
|
|
||||||
interface UserMessageProps {
|
|
||||||
content: string | Array<{ type: string; text?: string; image?: string }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UserMessage({ content }: UserMessageProps) {
|
|
||||||
if (Array.isArray(content)) {
|
|
||||||
const textItem = content.find((item) => item.type === 'text');
|
|
||||||
const textContent = stripMetadata(textItem?.text || '');
|
|
||||||
const images = content.filter((item) => item.type === 'image' && item.image);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="overflow-hidden pt-[4px]">
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
{textContent && <Markdown html>{textContent}</Markdown>}
|
|
||||||
{images.map((item, index) => (
|
|
||||||
<img
|
|
||||||
key={index}
|
|
||||||
src={item.image}
|
|
||||||
alt={`Image ${index + 1}`}
|
|
||||||
className="max-w-full h-auto rounded-lg"
|
|
||||||
style={{ maxHeight: '512px', objectFit: 'contain' }}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const textContent = stripMetadata(content);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="overflow-hidden pt-[4px]">
|
|
||||||
<Markdown html>{textContent}</Markdown>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function stripMetadata(content: string) {
|
|
||||||
return content.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, '');
|
|
||||||
}
|
|
@ -1,6 +1,6 @@
|
|||||||
import type { Message } from 'ai';
|
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { ImportFolderButton } from '~/components/chat/ImportFolderButton';
|
import { ImportFolderButton } from '~/components/chat/ImportFolderButton';
|
||||||
|
import type { Message } from '~/lib/persistence/message';
|
||||||
|
|
||||||
type ChatData = {
|
type ChatData = {
|
||||||
messages?: Message[]; // Standard Bolt format
|
messages?: Message[]; // Standard Bolt format
|
||||||
|
@ -5,7 +5,7 @@ import { toast } from 'react-toastify';
|
|||||||
import { database, deleteById, getAll, setMessages } from '~/lib/persistence';
|
import { database, deleteById, getAll, setMessages } from '~/lib/persistence';
|
||||||
import { logStore } from '~/lib/stores/logs';
|
import { logStore } from '~/lib/stores/logs';
|
||||||
import { classNames } from '~/utils/classNames';
|
import { classNames } from '~/utils/classNames';
|
||||||
import type { Message } from 'ai';
|
import type { Message } from '~/lib/persistence/message';
|
||||||
|
|
||||||
// List of supported providers that can have API keys
|
// List of supported providers that can have API keys
|
||||||
const API_KEY_PROVIDERS = [
|
const API_KEY_PROVIDERS = [
|
||||||
|
@ -7,7 +7,6 @@ import { getOrFetchLastLoadedProblem } from '~/components/chat/LoadProblemButton
|
|||||||
import {
|
import {
|
||||||
getLastUserSimulationData,
|
getLastUserSimulationData,
|
||||||
getLastSimulationChatMessages,
|
getLastSimulationChatMessages,
|
||||||
getSimulationRecordingId,
|
|
||||||
isSimulatingOrHasFinished,
|
isSimulatingOrHasFinished,
|
||||||
} from '~/lib/replay/SimulationPrompt';
|
} from '~/lib/replay/SimulationPrompt';
|
||||||
|
|
||||||
@ -54,18 +53,6 @@ export function SaveReproductionModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const loadId = toast.loading('Waiting for recording...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
/*
|
|
||||||
* Wait for simulation to finish.
|
|
||||||
* const recordingId =
|
|
||||||
*/
|
|
||||||
await getSimulationRecordingId();
|
|
||||||
} finally {
|
|
||||||
toast.dismiss(loadId);
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.info('Submitting reproduction...');
|
toast.info('Submitting reproduction...');
|
||||||
console.log('SubmitReproduction');
|
console.log('SubmitReproduction');
|
||||||
|
|
||||||
|
@ -1,103 +0,0 @@
|
|||||||
import { stripIndents } from '~/utils/stripIndent';
|
|
||||||
|
|
||||||
export const developerSystemPrompt = `
|
|
||||||
You are Nut, an expert AI assistant and exceptional senior software developer with vast knowledge across multiple programming languages, frameworks, and best practices.
|
|
||||||
|
|
||||||
For all designs you produce, make them beautiful and modern.
|
|
||||||
|
|
||||||
<code_formatting_info>
|
|
||||||
Use 2 spaces for code indentation
|
|
||||||
</code_formatting_info>
|
|
||||||
|
|
||||||
<chain_of_thought_instructions>
|
|
||||||
Before providing a solution, BRIEFLY outline your implementation steps.
|
|
||||||
This helps ensure systematic thinking and clear communication. Your planning should:
|
|
||||||
- List concrete steps you'll take
|
|
||||||
- Identify key components needed
|
|
||||||
- Note potential challenges
|
|
||||||
- Be concise (2-4 lines maximum)
|
|
||||||
|
|
||||||
Example responses:
|
|
||||||
|
|
||||||
User: "Create a todo list app with local storage"
|
|
||||||
Assistant: "Sure. I'll start by:
|
|
||||||
1. Set up Vite + React
|
|
||||||
2. Create TodoList and TodoItem components
|
|
||||||
3. Implement localStorage for persistence
|
|
||||||
4. Add CRUD operations
|
|
||||||
|
|
||||||
Let's start now.
|
|
||||||
|
|
||||||
[Rest of response...]"
|
|
||||||
|
|
||||||
User: "Help debug why my API calls aren't working"
|
|
||||||
Assistant: "Great. My first steps will be:
|
|
||||||
1. Check network requests
|
|
||||||
2. Verify API endpoint format
|
|
||||||
3. Examine error handling
|
|
||||||
|
|
||||||
[Rest of response...]"
|
|
||||||
|
|
||||||
</chain_of_thought_instructions>
|
|
||||||
|
|
||||||
IMPORTANT: Use valid markdown only for all your responses and DO NOT use HTML tags!
|
|
||||||
|
|
||||||
ULTRA IMPORTANT: Think first and reply with all the files needed to set up the project and get it running.
|
|
||||||
It is SUPER IMPORTANT to respond with this first. Create every needed file.
|
|
||||||
|
|
||||||
<example>
|
|
||||||
<user_query>Make a bouncing ball with real gravity using React</user_query>
|
|
||||||
|
|
||||||
<assistant_response>
|
|
||||||
Certainly! I'll create a bouncing ball with real gravity using React. We'll use the react-spring library for physics-based animations.
|
|
||||||
|
|
||||||
<file path="package.json">
|
|
||||||
{
|
|
||||||
"name": "bouncing-ball",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"react-spring": "^9.7.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/react": "^18.0.28",
|
|
||||||
"@types/react-dom": "^18.0.11",
|
|
||||||
"@vitejs/plugin-react": "^3.1.0",
|
|
||||||
"vite": "^4.2.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</file>
|
|
||||||
|
|
||||||
<file path="index.html">
|
|
||||||
...
|
|
||||||
</file>
|
|
||||||
|
|
||||||
<file path="src/main.jsx">
|
|
||||||
...
|
|
||||||
</file>
|
|
||||||
|
|
||||||
<file path="src/index.css">
|
|
||||||
...
|
|
||||||
</file>
|
|
||||||
|
|
||||||
<file path="src/App.jsx">
|
|
||||||
...
|
|
||||||
</file>
|
|
||||||
|
|
||||||
You can now view the bouncing ball animation in the preview. The ball will start falling from the top of the screen and bounce realistically when it hits the bottom.
|
|
||||||
</assistant_response>
|
|
||||||
</example>
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const CONTINUE_PROMPT = stripIndents`
|
|
||||||
Continue your prior response. IMPORTANT: Immediately begin from where you left off without any interruptions.
|
|
||||||
Do not repeat any content, including artifact and action tags.
|
|
||||||
`;
|
|
@ -43,16 +43,4 @@ export class ChatMessageTelemetry {
|
|||||||
abort(reason: string) {
|
abort(reason: string) {
|
||||||
this._ping('AbortMessage', { reason });
|
this._ping('AbortMessage', { reason });
|
||||||
}
|
}
|
||||||
|
|
||||||
startSimulation() {
|
|
||||||
this._ping('StartSimulation');
|
|
||||||
}
|
|
||||||
|
|
||||||
endSimulation(status: string) {
|
|
||||||
this._ping('EndSimulation', { status });
|
|
||||||
}
|
|
||||||
|
|
||||||
sendPrompt(simulationStatus: string) {
|
|
||||||
this._ping('SendPrompt', { simulationStatus });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { Message } from 'ai';
|
|
||||||
import { createScopedLogger } from '~/utils/logger';
|
import { createScopedLogger } from '~/utils/logger';
|
||||||
import type { ChatHistoryItem } from './useChatHistory';
|
import type { ChatHistoryItem } from './useChatHistory';
|
||||||
|
import type { Message } from './message';
|
||||||
|
|
||||||
const logger = createScopedLogger('ChatHistory');
|
const logger = createScopedLogger('ChatHistory');
|
||||||
|
|
||||||
|
@ -1,21 +1,12 @@
|
|||||||
import { useLoaderData, useNavigate, useSearchParams } from '@remix-run/react';
|
import { useLoaderData, useNavigate, useSearchParams } from '@remix-run/react';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { atom } from 'nanostores';
|
import { atom } from 'nanostores';
|
||||||
import type { Message as BaseMessage } from 'ai';
|
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { logStore } from '~/lib/stores/logs'; // Import logStore
|
import { logStore } from '~/lib/stores/logs'; // Import logStore
|
||||||
import { getMessages, getNextId, openDatabase, setMessages, duplicateChat, createChatFromMessages } from './db';
|
import { getMessages, getNextId, openDatabase, setMessages, duplicateChat, createChatFromMessages } from './db';
|
||||||
import { loadProblem } from '~/components/chat/LoadProblemButton';
|
import { loadProblem } from '~/components/chat/LoadProblemButton';
|
||||||
import { createAsyncSuspenseValue } from '~/lib/asyncSuspenseValue';
|
import { createAsyncSuspenseValue } from '~/lib/asyncSuspenseValue';
|
||||||
|
import type { Message } from './message';
|
||||||
/*
|
|
||||||
* Messages in a chat's history. The repository may update in response to changes in the messages.
|
|
||||||
* Each message which changes the repository state must have a repositoryId.
|
|
||||||
*/
|
|
||||||
export interface Message extends BaseMessage {
|
|
||||||
// Describes the state of the project after changes in this message were applied.
|
|
||||||
repositoryId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChatState {
|
export interface ChatState {
|
||||||
description: string;
|
description: string;
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { sendCommandDedicatedClient } from './ReplayProtocolClient';
|
import { sendCommandDedicatedClient } from './ReplayProtocolClient';
|
||||||
import type { ProtocolMessage } from './SimulationPrompt';
|
import type { Message } from '~/lib/persistence/message';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import { shouldUseSupabase } from '~/lib/supabase/client';
|
import { shouldUseSupabase } from '~/lib/supabase/client';
|
||||||
import {
|
import {
|
||||||
@ -22,7 +22,7 @@ export interface BoltProblemComment {
|
|||||||
|
|
||||||
export interface BoltProblemSolution {
|
export interface BoltProblemSolution {
|
||||||
simulationData: any;
|
simulationData: any;
|
||||||
messages: ProtocolMessage[];
|
messages: Message[];
|
||||||
evaluator?: string;
|
evaluator?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,14 +3,11 @@
|
|||||||
* the AI developer prompt.
|
* the AI developer prompt.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Message } from 'ai';
|
|
||||||
import type { SimulationData, SimulationPacket } from './SimulationData';
|
import type { SimulationData, SimulationPacket } from './SimulationData';
|
||||||
import { simulationDataVersion } from './SimulationData';
|
import { simulationDataVersion } from './SimulationData';
|
||||||
import { assert, generateRandomId, ProtocolClient } from './ReplayProtocolClient';
|
import { assert, generateRandomId, ProtocolClient } from './ReplayProtocolClient';
|
||||||
import type { MouseData } from './Recording';
|
|
||||||
import { developerSystemPrompt } from '~/lib/common/prompts/prompts';
|
|
||||||
import { updateDevelopmentServer } from './DevelopmentServer';
|
import { updateDevelopmentServer } from './DevelopmentServer';
|
||||||
import { isEnhancedPromptMessage } from '~/components/chat/Chat.client';
|
import type { Message } from '~/lib/persistence/message';
|
||||||
|
|
||||||
function createRepositoryIdPacket(repositoryId: string): SimulationPacket {
|
function createRepositoryIdPacket(repositoryId: string): SimulationPacket {
|
||||||
return {
|
return {
|
||||||
@ -20,31 +17,19 @@ function createRepositoryIdPacket(repositoryId: string): SimulationPacket {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProtocolMessageRole = 'user' | 'assistant' | 'system';
|
interface ChatReferenceElement {
|
||||||
|
kind: 'element';
|
||||||
type ProtocolMessageText = {
|
selector: string;
|
||||||
type: 'text';
|
width: number;
|
||||||
role: ProtocolMessageRole;
|
height: number;
|
||||||
content: string;
|
x: number;
|
||||||
};
|
y: number;
|
||||||
|
|
||||||
type ProtocolMessageImage = {
|
|
||||||
type: 'image';
|
|
||||||
role: ProtocolMessageRole;
|
|
||||||
dataURL: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ProtocolMessage = ProtocolMessageText | ProtocolMessageImage;
|
|
||||||
|
|
||||||
type ChatResponsePartCallback = (response: string) => void;
|
|
||||||
|
|
||||||
type ChatMessageMode = 'recording' | 'static' | 'developer';
|
|
||||||
|
|
||||||
interface ChatMessageOptions {
|
|
||||||
baseRepositoryId?: string;
|
|
||||||
onResponsePart?: ChatResponsePartCallback;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ChatReference = ChatReferenceElement;
|
||||||
|
|
||||||
|
type ChatResponsePartCallback = (message: Message) => void;
|
||||||
|
|
||||||
class ChatManager {
|
class ChatManager {
|
||||||
// Empty if this chat has been destroyed.
|
// Empty if this chat has been destroyed.
|
||||||
client: ProtocolClient | undefined;
|
client: ProtocolClient | undefined;
|
||||||
@ -52,9 +37,6 @@ class ChatManager {
|
|||||||
// Resolves when the chat has started.
|
// Resolves when the chat has started.
|
||||||
chatIdPromise: Promise<string>;
|
chatIdPromise: Promise<string>;
|
||||||
|
|
||||||
// Resolves when the recording has been created.
|
|
||||||
recordingIdPromise: Promise<string> | undefined;
|
|
||||||
|
|
||||||
// Whether all simulation data has been sent.
|
// Whether all simulation data has been sent.
|
||||||
simulationFinished?: boolean;
|
simulationFinished?: boolean;
|
||||||
|
|
||||||
@ -133,63 +115,37 @@ class ChatManager {
|
|||||||
assert(!this.simulationFinished, 'Simulation has been finished');
|
assert(!this.simulationFinished, 'Simulation has been finished');
|
||||||
assert(this.repositoryId, 'Expected repository ID');
|
assert(this.repositoryId, 'Expected repository ID');
|
||||||
|
|
||||||
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(recordingId, 'Recording ID not set');
|
|
||||||
|
|
||||||
return recordingId;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const allData = [createRepositoryIdPacket(this.repositoryId), ...this.pageData];
|
const allData = [createRepositoryIdPacket(this.repositoryId), ...this.pageData];
|
||||||
this.simulationFinished = true;
|
this.simulationFinished = true;
|
||||||
|
|
||||||
return allData;
|
return allData;
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendChatMessage(mode: ChatMessageMode, messages: ProtocolMessage[], options?: ChatMessageOptions) {
|
async sendChatMessage(messages: Message[], references: ChatReference[], onResponsePart: ChatResponsePartCallback) {
|
||||||
assert(this.client, 'Chat has been destroyed');
|
assert(this.client, 'Chat has been destroyed');
|
||||||
|
|
||||||
const responseId = `response-${generateRandomId()}`;
|
const responseId = `response-${generateRandomId()}`;
|
||||||
|
|
||||||
let response: string = '';
|
|
||||||
const removeResponseListener = this.client.listenForMessage(
|
const removeResponseListener = this.client.listenForMessage(
|
||||||
'Nut.chatResponsePart',
|
'Nut.chatResponsePart',
|
||||||
({ responseId: eventResponseId, message }: { responseId: string; message: ProtocolMessage }) => {
|
({ responseId: eventResponseId, message }: { responseId: string; message: Message }) => {
|
||||||
if (responseId == eventResponseId) {
|
if (responseId == eventResponseId) {
|
||||||
if (message.type == 'text') {
|
console.log('ChatResponse', chatId, message);
|
||||||
response += message.content;
|
onResponsePart(message);
|
||||||
options?.onResponsePart?.(message.content);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const chatId = await this.chatIdPromise;
|
const chatId = await this.chatIdPromise;
|
||||||
|
|
||||||
console.log(
|
console.log('ChatSendMessage', new Date().toISOString(), chatId, JSON.stringify({ messages, references }));
|
||||||
'ChatSendMessage',
|
|
||||||
new Date().toISOString(),
|
|
||||||
chatId,
|
|
||||||
JSON.stringify({ mode, messages, baseRepositoryId: options?.baseRepositoryId }),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { repositoryId } = (await this.client.sendCommand({
|
await this.client.sendCommand({
|
||||||
method: 'Nut.sendChatMessage',
|
method: 'Nut.sendChatMessage',
|
||||||
params: { chatId, responseId, mode, messages, baseRepositoryId: options?.baseRepositoryId },
|
params: { chatId, responseId, messages, references },
|
||||||
})) as { repositoryId?: string };
|
});
|
||||||
|
|
||||||
console.log('ChatResponse', chatId, repositoryId, response);
|
|
||||||
|
|
||||||
removeResponseListener();
|
removeResponseListener();
|
||||||
|
|
||||||
return { response, repositoryId };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,264 +188,40 @@ export async function simulationReloaded() {
|
|||||||
startChat(repositoryId, []);
|
startChat(repositoryId, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function simulationAddData(data: SimulationData) {
|
export function simulationAddData(data: SimulationData) {
|
||||||
assert(gChatManager, 'Expected to have an active chat');
|
assert(gChatManager, 'Expected to have an active chat');
|
||||||
gChatManager.addPageData(data);
|
gChatManager.addPageData(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function simulationFinishData() {
|
||||||
|
assert(gChatManager, 'Expected to have an active chat');
|
||||||
|
gChatManager.finishSimulationData();
|
||||||
|
}
|
||||||
|
|
||||||
let gLastUserSimulationData: SimulationData | undefined;
|
let gLastUserSimulationData: SimulationData | undefined;
|
||||||
|
|
||||||
export function getLastUserSimulationData(): SimulationData | undefined {
|
export function getLastUserSimulationData(): SimulationData | undefined {
|
||||||
return gLastUserSimulationData;
|
return gLastUserSimulationData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSimulationRecording(): Promise<string> {
|
|
||||||
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 != 'repositoryId');
|
|
||||||
|
|
||||||
console.log('SimulationData', new Date().toISOString(), JSON.stringify(simulationData));
|
|
||||||
|
|
||||||
assert(gChatManager.recordingIdPromise, 'Expected recording promise');
|
|
||||||
|
|
||||||
return gChatManager.recordingIdPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isSimulatingOrHasFinished(): boolean {
|
export function isSimulatingOrHasFinished(): boolean {
|
||||||
return gChatManager?.isValid() ?? false;
|
return gChatManager?.isValid() ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSimulationRecordingId(): Promise<string> {
|
let gLastSimulationChatMessages: Message[] | undefined;
|
||||||
assert(gChatManager, 'Chat not started');
|
|
||||||
assert(gChatManager.recordingIdPromise, 'Expected recording promise');
|
|
||||||
|
|
||||||
return gChatManager.recordingIdPromise;
|
export function getLastSimulationChatMessages(): Message[] | undefined {
|
||||||
}
|
|
||||||
|
|
||||||
let gLastSimulationChatMessages: ProtocolMessage[] | undefined;
|
|
||||||
|
|
||||||
export function getLastSimulationChatMessages(): ProtocolMessage[] | undefined {
|
|
||||||
return gLastSimulationChatMessages;
|
return gLastSimulationChatMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
const simulationSystemPrompt = `
|
export async function sendChatMessage(
|
||||||
The following user message describes a bug or other problem on the page which needs to be fixed.
|
|
||||||
You must respond with a useful explanation that will help the user understand the source of the problem.
|
|
||||||
Do not describe the specific fix needed.
|
|
||||||
`;
|
|
||||||
|
|
||||||
export async function getSimulationEnhancedPrompt(
|
|
||||||
chatMessages: Message[],
|
|
||||||
userMessage: string,
|
|
||||||
mouseData: MouseData | undefined,
|
|
||||||
): Promise<string> {
|
|
||||||
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',
|
|
||||||
content: system,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
type: 'text',
|
|
||||||
content: userMessage,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
gLastSimulationChatMessages = messages;
|
|
||||||
|
|
||||||
const { response } = await gChatManager.sendChatMessage('recording', messages);
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function shouldUseSimulation(messageInput: string) {
|
|
||||||
if (!gChatManager) {
|
|
||||||
gChatManager = new ChatManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
const systemPrompt = `
|
|
||||||
You are a helpful assistant that determines whether a user's message that is asking an AI
|
|
||||||
to make a change to an application should first perform a detailed analysis of the application's
|
|
||||||
behavior to generate a better answer.
|
|
||||||
|
|
||||||
This is most helpful when the user is asking the AI to fix a problem with the application.
|
|
||||||
When making straightforward improvements to the application a detailed analysis is not necessary.
|
|
||||||
|
|
||||||
The text of the user's message will be wrapped in \`<user_message>\` tags. You must describe your
|
|
||||||
reasoning and then respond with either \`<analyze>true</analyze>\` or \`<analyze>false</analyze>\`.
|
|
||||||
`;
|
|
||||||
|
|
||||||
const userMessage = `
|
|
||||||
Here is the user message you need to evaluate: <user_message>${messageInput}</user_message>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const messages: ProtocolMessage[] = [
|
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
type: 'text',
|
|
||||||
content: systemPrompt,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
type: 'text',
|
|
||||||
content: userMessage,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const { response } = await gChatManager.sendChatMessage('static', messages);
|
|
||||||
|
|
||||||
console.log('UseSimulationResponse', response);
|
|
||||||
|
|
||||||
const match = /<analyze>(.*?)<\/analyze>/.exec(response);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
return match[1] === 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProtocolRole(message: Message): 'user' | 'assistant' | 'system' {
|
|
||||||
switch (message.role) {
|
|
||||||
case 'user':
|
|
||||||
return 'user';
|
|
||||||
case 'assistant':
|
|
||||||
case 'data':
|
|
||||||
return 'assistant';
|
|
||||||
case 'system':
|
|
||||||
return 'system';
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown message role: ${message.role}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeBoltArtifacts(text: string): string {
|
|
||||||
const openTag = '<boltArtifact';
|
|
||||||
const closeTag = '</boltArtifact>';
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const openTagIndex = text.indexOf(openTag);
|
|
||||||
|
|
||||||
if (openTagIndex === -1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prefix = text.substring(0, openTagIndex);
|
|
||||||
|
|
||||||
const closeTagIndex = text.indexOf(closeTag, openTagIndex + openTag.length);
|
|
||||||
|
|
||||||
if (closeTagIndex === -1) {
|
|
||||||
text = prefix;
|
|
||||||
} else {
|
|
||||||
text = prefix + text.substring(closeTagIndex + closeTag.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildProtocolMessages(messages: Message[]): ProtocolMessage[] {
|
|
||||||
const rv: ProtocolMessage[] = [];
|
|
||||||
|
|
||||||
for (const msg of messages) {
|
|
||||||
const role = getProtocolRole(msg);
|
|
||||||
|
|
||||||
if (Array.isArray(msg.content)) {
|
|
||||||
for (const content of msg.content) {
|
|
||||||
switch (content.type) {
|
|
||||||
case 'text':
|
|
||||||
rv.push({
|
|
||||||
role,
|
|
||||||
type: 'text',
|
|
||||||
content: removeBoltArtifacts(content.text),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'image':
|
|
||||||
rv.push({
|
|
||||||
role,
|
|
||||||
type: 'image',
|
|
||||||
dataURL: content.image,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.error('Unknown message content', content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (typeof msg.content == 'string') {
|
|
||||||
rv.push({
|
|
||||||
role,
|
|
||||||
type: 'text',
|
|
||||||
content: msg.content,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rv;
|
|
||||||
}
|
|
||||||
|
|
||||||
function messagesHaveEnhancedPrompt(messages: Message[]): boolean {
|
|
||||||
const lastEnhancedPromptMessage = messages.findLastIndex((msg) => isEnhancedPromptMessage(msg));
|
|
||||||
|
|
||||||
if (lastEnhancedPromptMessage == -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastUserMessage = messages.findLastIndex((msg) => msg.role == 'user');
|
|
||||||
|
|
||||||
if (lastUserMessage == -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return lastUserMessage < lastEnhancedPromptMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function sendDeveloperChatMessage(
|
|
||||||
messages: Message[],
|
messages: Message[],
|
||||||
baseRepositoryId: string | undefined,
|
references: ChatReference[],
|
||||||
onResponsePart: ChatResponsePartCallback,
|
onResponsePart: ChatResponsePartCallback,
|
||||||
) {
|
) {
|
||||||
if (!gChatManager) {
|
if (!gChatManager) {
|
||||||
gChatManager = new ChatManager();
|
gChatManager = new ChatManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
let systemPrompt = developerSystemPrompt;
|
await gChatManager.sendChatMessage(messages, references, onResponsePart);
|
||||||
|
|
||||||
if (messagesHaveEnhancedPrompt(messages)) {
|
|
||||||
// Add directions to the LLM when we have an enhanced prompt describing the bug to fix.
|
|
||||||
const systemEnhancedPrompt = `
|
|
||||||
ULTRA IMPORTANT: You have been given a detailed description of a bug you need to fix.
|
|
||||||
Focus specifically on fixing this bug. Do not guess about other problems.
|
|
||||||
`;
|
|
||||||
systemPrompt += systemEnhancedPrompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
const protocolMessages = buildProtocolMessages(messages);
|
|
||||||
protocolMessages.unshift({
|
|
||||||
role: 'system',
|
|
||||||
type: 'text',
|
|
||||||
content: systemPrompt,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { repositoryId } = await gChatManager.sendChatMessage('developer', protocolMessages, {
|
|
||||||
baseRepositoryId,
|
|
||||||
onResponsePart,
|
|
||||||
});
|
|
||||||
|
|
||||||
return repositoryId;
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { Message } from '~/lib/persistence/useChatHistory';
|
import type { Message } from '~/lib/persistence/message';
|
||||||
import { generateId } from './fileUtils';
|
import { generateId } from './fileUtils';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
@ -43,15 +43,15 @@ export function createChatFromFolder(folderName: string, repositoryId: string):
|
|||||||
role: 'user',
|
role: 'user',
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
content: `Import the "${folderName}" folder`,
|
content: `Import the "${folderName}" folder`,
|
||||||
createdAt: new Date(),
|
type: 'text',
|
||||||
};
|
};
|
||||||
|
|
||||||
const filesMessage: Message = {
|
const filesMessage: Message = {
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: filesContent,
|
content: filesContent,
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
createdAt: new Date(),
|
|
||||||
repositoryId,
|
repositoryId,
|
||||||
|
type: 'text',
|
||||||
};
|
};
|
||||||
|
|
||||||
const messages = [userMessage, filesMessage];
|
const messages = [userMessage, filesMessage];
|
||||||
|
Loading…
Reference in New Issue
Block a user