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 |
+
};
|