feat: make user made changes persistent after reload (#1387)
Browse files* feat: save user made changes persistent
* fix: remove artifact from user message on the UI
* fix: message Id generation fix
app/components/chat/Chat.client.tsx
CHANGED
|
@@ -25,6 +25,7 @@ import { createSampler } from '~/utils/sampler';
|
|
| 25 |
import { getTemplates, selectStarterTemplate } from '~/utils/selectStarterTemplate';
|
| 26 |
import { logStore } from '~/lib/stores/logs';
|
| 27 |
import { streamingState } from '~/lib/stores/streaming';
|
|
|
|
| 28 |
|
| 29 |
const toastAnimation = cssTransition({
|
| 30 |
enter: 'animated fadeInRight',
|
|
@@ -320,17 +321,17 @@ export const ChatImpl = memo(
|
|
| 320 |
const { assistantMessage, userMessage } = temResp;
|
| 321 |
setMessages([
|
| 322 |
{
|
| 323 |
-
id:
|
| 324 |
role: 'user',
|
| 325 |
content: messageContent,
|
| 326 |
},
|
| 327 |
{
|
| 328 |
-
id:
|
| 329 |
role: 'assistant',
|
| 330 |
content: assistantMessage,
|
| 331 |
},
|
| 332 |
{
|
| 333 |
-
id:
|
| 334 |
role: 'user',
|
| 335 |
content: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${userMessage}`,
|
| 336 |
annotations: ['hidden'],
|
|
@@ -371,17 +372,18 @@ export const ChatImpl = memo(
|
|
| 371 |
setMessages(messages.slice(0, -1));
|
| 372 |
}
|
| 373 |
|
| 374 |
-
const
|
| 375 |
|
| 376 |
chatStore.setKey('aborted', false);
|
| 377 |
|
| 378 |
-
if (
|
|
|
|
| 379 |
append({
|
| 380 |
role: 'user',
|
| 381 |
content: [
|
| 382 |
{
|
| 383 |
type: 'text',
|
| 384 |
-
text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${messageContent}`,
|
| 385 |
},
|
| 386 |
...imageDataList.map((imageData) => ({
|
| 387 |
type: 'image',
|
|
|
|
| 25 |
import { getTemplates, selectStarterTemplate } from '~/utils/selectStarterTemplate';
|
| 26 |
import { logStore } from '~/lib/stores/logs';
|
| 27 |
import { streamingState } from '~/lib/stores/streaming';
|
| 28 |
+
import { filesToArtifacts } from '~/utils/fileUtils';
|
| 29 |
|
| 30 |
const toastAnimation = cssTransition({
|
| 31 |
enter: 'animated fadeInRight',
|
|
|
|
| 321 |
const { assistantMessage, userMessage } = temResp;
|
| 322 |
setMessages([
|
| 323 |
{
|
| 324 |
+
id: `1-${new Date().getTime()}`,
|
| 325 |
role: 'user',
|
| 326 |
content: messageContent,
|
| 327 |
},
|
| 328 |
{
|
| 329 |
+
id: `2-${new Date().getTime()}`,
|
| 330 |
role: 'assistant',
|
| 331 |
content: assistantMessage,
|
| 332 |
},
|
| 333 |
{
|
| 334 |
+
id: `3-${new Date().getTime()}`,
|
| 335 |
role: 'user',
|
| 336 |
content: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${userMessage}`,
|
| 337 |
annotations: ['hidden'],
|
|
|
|
| 372 |
setMessages(messages.slice(0, -1));
|
| 373 |
}
|
| 374 |
|
| 375 |
+
const modifiedFiles = workbenchStore.getModifiedFiles();
|
| 376 |
|
| 377 |
chatStore.setKey('aborted', false);
|
| 378 |
|
| 379 |
+
if (modifiedFiles !== undefined) {
|
| 380 |
+
const userUpdateArtifact = filesToArtifacts(modifiedFiles, `${Date.now()}`);
|
| 381 |
append({
|
| 382 |
role: 'user',
|
| 383 |
content: [
|
| 384 |
{
|
| 385 |
type: 'text',
|
| 386 |
+
text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${userUpdateArtifact}${messageContent}`,
|
| 387 |
},
|
| 388 |
...imageDataList.map((imageData) => ({
|
| 389 |
type: 'image',
|
app/components/chat/UserMessage.tsx
CHANGED
|
@@ -43,5 +43,6 @@ export function UserMessage({ content }: UserMessageProps) {
|
|
| 43 |
}
|
| 44 |
|
| 45 |
function stripMetadata(content: string) {
|
| 46 |
-
|
|
|
|
| 47 |
}
|
|
|
|
| 43 |
}
|
| 44 |
|
| 45 |
function stripMetadata(content: string) {
|
| 46 |
+
const artifactRegex = /<boltArtifact\s+[^>]*>[\s\S]*?<\/boltArtifact>/gm;
|
| 47 |
+
return content.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, '').replace(artifactRegex, '');
|
| 48 |
}
|
app/lib/hooks/useMessageParser.ts
CHANGED
|
@@ -42,6 +42,10 @@ const messageParser = new StreamingMessageParser({
|
|
| 42 |
},
|
| 43 |
},
|
| 44 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
export function useMessageParser() {
|
| 47 |
const [parsedMessages, setParsedMessages] = useState<{ [key: number]: string }>({});
|
|
@@ -55,9 +59,8 @@ export function useMessageParser() {
|
|
| 55 |
}
|
| 56 |
|
| 57 |
for (const [index, message] of messages.entries()) {
|
| 58 |
-
if (message.role === 'assistant') {
|
| 59 |
-
const newParsedContent = messageParser.parse(message.id, message
|
| 60 |
-
|
| 61 |
setParsedMessages((prevParsed) => ({
|
| 62 |
...prevParsed,
|
| 63 |
[index]: !reset ? (prevParsed[index] || '') + newParsedContent : newParsedContent,
|
|
|
|
| 42 |
},
|
| 43 |
},
|
| 44 |
});
|
| 45 |
+
const extractTextContent = (message: Message) =>
|
| 46 |
+
Array.isArray(message.content)
|
| 47 |
+
? (message.content.find((item) => item.type === 'text')?.text as string) || ''
|
| 48 |
+
: message.content;
|
| 49 |
|
| 50 |
export function useMessageParser() {
|
| 51 |
const [parsedMessages, setParsedMessages] = useState<{ [key: number]: string }>({});
|
|
|
|
| 59 |
}
|
| 60 |
|
| 61 |
for (const [index, message] of messages.entries()) {
|
| 62 |
+
if (message.role === 'assistant' || message.role === 'user') {
|
| 63 |
+
const newParsedContent = messageParser.parse(message.id, extractTextContent(message));
|
|
|
|
| 64 |
setParsedMessages((prevParsed) => ({
|
| 65 |
...prevParsed,
|
| 66 |
[index]: !reset ? (prevParsed[index] || '') + newParsedContent : newParsedContent,
|
app/lib/stores/files.ts
CHANGED
|
@@ -75,6 +75,29 @@ export class FilesStore {
|
|
| 75 |
getFileModifications() {
|
| 76 |
return computeFileModifications(this.files.get(), this.#modifiedFiles);
|
| 77 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
resetFileModifications() {
|
| 80 |
this.#modifiedFiles.clear();
|
|
|
|
| 75 |
getFileModifications() {
|
| 76 |
return computeFileModifications(this.files.get(), this.#modifiedFiles);
|
| 77 |
}
|
| 78 |
+
getModifiedFiles() {
|
| 79 |
+
let modifiedFiles: { [path: string]: File } | undefined = undefined;
|
| 80 |
+
|
| 81 |
+
for (const [filePath, originalContent] of this.#modifiedFiles) {
|
| 82 |
+
const file = this.files.get()[filePath];
|
| 83 |
+
|
| 84 |
+
if (file?.type !== 'file') {
|
| 85 |
+
continue;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
if (file.content === originalContent) {
|
| 89 |
+
continue;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
if (!modifiedFiles) {
|
| 93 |
+
modifiedFiles = {};
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
modifiedFiles[filePath] = file;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
return modifiedFiles;
|
| 100 |
+
}
|
| 101 |
|
| 102 |
resetFileModifications() {
|
| 103 |
this.#modifiedFiles.clear();
|
app/lib/stores/workbench.ts
CHANGED
|
@@ -238,6 +238,9 @@ export class WorkbenchStore {
|
|
| 238 |
getFileModifcations() {
|
| 239 |
return this.#filesStore.getFileModifications();
|
| 240 |
}
|
|
|
|
|
|
|
|
|
|
| 241 |
|
| 242 |
resetAllFileModifications() {
|
| 243 |
this.#filesStore.resetFileModifications();
|
|
|
|
| 238 |
getFileModifcations() {
|
| 239 |
return this.#filesStore.getFileModifications();
|
| 240 |
}
|
| 241 |
+
getModifiedFiles() {
|
| 242 |
+
return this.#filesStore.getModifiedFiles();
|
| 243 |
+
}
|
| 244 |
|
| 245 |
resetAllFileModifications() {
|
| 246 |
this.#filesStore.resetFileModifications();
|
app/utils/fileUtils.ts
CHANGED
|
@@ -103,3 +103,19 @@ export const detectProjectType = async (
|
|
| 103 |
|
| 104 |
return { type: '', setupCommand: '', followupMessage: '' };
|
| 105 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
return { type: '', setupCommand: '', followupMessage: '' };
|
| 105 |
};
|
| 106 |
+
|
| 107 |
+
export const filesToArtifacts = (files: { [path: string]: { content: string } }, id: string): string => {
|
| 108 |
+
return `
|
| 109 |
+
<boltArtifact id="${id}" title="User Updated Files">
|
| 110 |
+
${Object.keys(files)
|
| 111 |
+
.map(
|
| 112 |
+
(filePath) => `
|
| 113 |
+
<boltAction type="file" filePath="${filePath}">
|
| 114 |
+
${files[filePath].content}
|
| 115 |
+
</boltAction>
|
| 116 |
+
`,
|
| 117 |
+
)
|
| 118 |
+
.join('\n')}
|
| 119 |
+
</boltArtifact>
|
| 120 |
+
`;
|
| 121 |
+
};
|