Skip to content

Commit 7dc27b1

Browse files
authored
feat: Edit AI Messages, Edit Messages in Place (danny-avila#825)
* refactor: replace lodash import with specific function import fix(api): esm imports to cjs * refactor(Messages.tsx): convert to TS, out-source scrollToDiv logic to a custom hook fix(ScreenshotContext.tsx): change Ref to RefObject in ScreenshotContextType feat(useScrollToRef.ts): add useScrollToRef hook for scrolling to a ref with throttle fix(Chat.tsx): update import path for Messages component fix(Search.tsx): update import path for Messages component * chore(types.ts): add TAskProps and TOptions types refactor(useMessageHandler.ts): use TAskFunction type for ask function signature * refactor(Message/Content): convert to TS, move Plugin component to Content dir * feat(MessageContent.tsx): add MessageContent component for displaying and editing message content feat(index.ts): export MessageContent component from Messages/Content directory * wip(Message.jsx): conversion and use of new component in progress * refactor: convert Message.jsx to TS and fix typing/imports based on changes * refactor: add typed props and refactor MultiMessage to TS, fix typing issues resulting from the conversion * edit message in progress * feat: complete edit AI message logic, refactor continue logic * feat(middleware): add validateMessageReq middleware feat(routes): add validation for message requests using validateMessageReq middleware feat(routes): add create, read, update, and delete routes for messages * feat: complete frontend logic for editing messages in place feat(messages.js): update route for updating a specific message - Change the route for updating a message to include the messageId in the URL - Update the request handler to use the messageId from the request parameters and the text from the request body - Call the updateMessage function with the updated parameters feat(MessageContent.tsx): add functionality to update a message - Import the useUpdateMessageMutation hook from the data provider - Destructure the conversationId, parentMessageId, and messageId from the message object - Create a mutation function using the useUpdateMessageMutation hook - Implement the updateMessage function to call the mutation function with the updated message parameters - Update the messages state to reflect the updated message text feat(api-endpoints.ts): update messages endpoint to include messageId - Update the messages endpoint to include the messageId as an optional parameter feat(data-service.ts): add updateMessage function - Implement the updateMessage function to make a PUT request to * fix(messages.js): make updateMessage function asynchronous and await its execution * style(EditIcon): make icon active for AI message * feat(gptPlugins/anthropic): add edit support * fix(validateMessageReq.js): handle case when conversationId is 'new' and return empty array feat(Message.tsx): pass message prop to SiblingSwitch component refactor(SiblingSwitch.tsx): convert to TS * fix(useMessageHandler.ts): remove message from currentMessages if isContinued is true feat(useMessageHandler.ts): add support for submission messages in setMessages fix(useServerStream.ts): remove unnecessary conditional in setMessages fix(useServerStream.ts): remove isContinued variable from submission * fix(continue): switch to continued message generation when continuing an earlier branch in conversation * fix(abortMiddleware.js): fix condition to check partialText length chore(abortMiddleware.js): add error logging when abortMessage fails * refactor(MessageHeader.tsx): convert to TS fix(Plugin.tsx): add default value for className prop in Plugin component * refactor(MultiMessage.tsx): remove commented out code docs(MultiMessage.tsx): update comment to clarify when siblingIdx is reset * fix(GenerationButtons): optimistic state for continue button * fix(MessageContent.tsx): add data-testid attribute to message text editor fix(messages.spec.ts): update waitForServerStream function to include edit endpoint check feat(messages.spec.ts): add test case for editing messages * fix(HoverButtons & Message & useGenerations): Refactor edit functionality and related conditions - Update enterEdit function signature and prop - Create and utilize hideEditButton variable - Enhance conditions for edit button visibility and active state - Update button event handlers - Introduce isEditableEndpoint in useGenerations and refine continueSupported condition. * fix(useGenerations.ts): fix condition for hideEditButton to include error and searchResult chore(data-provider): bump version to 0.1.6 fix(types.ts): add status property to TError type * chore: bump @dqbd/tiktoken to 1.0.7 * fix(abortMiddleware.js): add required isCreatedByUser property to the error response object * refactor(Message.tsx): remove unnecessary props from SiblingSwitch component, as setLatestMessage is firing on every switch already refactor(SiblingSwitch.tsx): remove unused imports and code * chore(BaseClient.js): move console.debug statements back inside if block
1 parent db77163 commit 7dc27b1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+908
-459
lines changed

api/app/clients/BaseClient.js

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,23 @@ class BaseClient {
5252
if (opts && typeof opts === 'object') {
5353
this.setOptions(opts);
5454
}
55+
56+
const { isEdited, isContinued } = opts;
5557
const user = opts.user ?? null;
58+
const saveOptions = this.getSaveOptions();
59+
this.abortController = opts.abortController ?? new AbortController();
5660
const conversationId = opts.conversationId ?? crypto.randomUUID();
5761
const parentMessageId = opts.parentMessageId ?? '00000000-0000-0000-0000-000000000000';
5862
const userMessageId = opts.overrideParentMessageId ?? crypto.randomUUID();
59-
const responseMessageId = opts.responseMessageId ?? crypto.randomUUID();
60-
const saveOptions = this.getSaveOptions();
61-
const head = opts.isEdited ? responseMessageId : parentMessageId;
63+
let responseMessageId = opts.responseMessageId ?? crypto.randomUUID();
64+
let head = isEdited ? responseMessageId : parentMessageId;
6265
this.currentMessages = (await this.loadHistory(conversationId, head)) ?? [];
63-
this.abortController = opts.abortController ?? new AbortController();
66+
67+
if (isEdited && !isContinued) {
68+
responseMessageId = crypto.randomUUID();
69+
head = responseMessageId;
70+
this.currentMessages[this.currentMessages.length - 1].messageId = head;
71+
}
6472

6573
return {
6674
...opts,
@@ -397,11 +405,16 @@ class BaseClient {
397405
const { user, head, isEdited, conversationId, responseMessageId, saveOptions, userMessage } =
398406
await this.handleStartMethods(message, opts);
399407

408+
const { generation = '' } = opts;
409+
400410
this.user = user;
401411
// It's not necessary to push to currentMessages
402412
// depending on subclass implementation of handling messages
403413
// When this is an edit, all messages are already in currentMessages, both user and response
404-
if (!isEdited) {
414+
if (isEdited) {
415+
/* TODO: edge case where latest message doesn't exist */
416+
this.currentMessages[this.currentMessages.length - 1].text = generation;
417+
} else {
405418
this.currentMessages.push(userMessage);
406419
}
407420

@@ -419,7 +432,7 @@ class BaseClient {
419432

420433
if (this.options.debug) {
421434
console.debug('payload');
422-
// console.debug(payload);
435+
console.debug(payload);
423436
}
424437

425438
if (tokenCountMap) {
@@ -442,7 +455,6 @@ class BaseClient {
442455
await this.saveMessageToDatabase(userMessage, saveOptions, user);
443456
}
444457

445-
const generation = isEdited ? this.currentMessages[this.currentMessages.length - 1].text : '';
446458
const responseMessage = {
447459
messageId: responseMessageId,
448460
conversationId,

api/app/titleConvo.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const _ = require('lodash');
1+
const throttle = require('lodash/throttle');
22
const { genAzureChatCompletion, getAzureCredentials } = require('../utils/');
33

44
const titleConvo = async ({ text, response, openAIApiKey, azure = false }) => {
@@ -52,6 +52,6 @@ const titleConvo = async ({ text, response, openAIApiKey, azure = false }) => {
5252
return title;
5353
};
5454

55-
const throttledTitleConvo = _.throttle(titleConvo, 1000);
55+
const throttledTitleConvo = throttle(titleConvo, 1000);
5656

5757
module.exports = throttledTitleConvo;

api/app/titleConvoBing.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const _ = require('lodash');
1+
const throttle = require('lodash/throttle');
22

33
const titleConvo = async ({ text, response }) => {
44
let title = 'New Chat';
@@ -32,6 +32,6 @@ const titleConvo = async ({ text, response }) => {
3232
return title;
3333
};
3434

35-
const throttledTitleConvo = _.throttle(titleConvo, 3000);
35+
const throttledTitleConvo = throttle(titleConvo, 3000);
3636

3737
module.exports = throttledTitleConvo;

api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"dependencies": {
2323
"@anthropic-ai/sdk": "^0.5.4",
2424
"@azure/search-documents": "^11.3.2",
25-
"@dqbd/tiktoken": "^1.0.2",
25+
"@dqbd/tiktoken": "^1.0.7",
2626
"@fortaine/fetch-event-source": "^3.0.6",
2727
"@keyv/mongo": "^2.1.8",
2828
"@waylaidwanderer/chatgpt-api": "^1.37.2",

api/server/middleware/abortMiddleware.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ const handleAbortError = async (res, req, error, data) => {
7979
cancelled: false,
8080
error: true,
8181
text: error.message,
82+
isCreatedByUser: false,
8283
};
8384
if (abortControllers.has(conversationId)) {
8485
const { abortController } = abortControllers.get(conversationId);
@@ -89,10 +90,11 @@ const handleAbortError = async (res, req, error, data) => {
8990
handleError(res, errorMessage);
9091
};
9192

92-
if (partialText?.length > 2) {
93+
if (partialText && partialText.length > 5) {
9394
try {
9495
return await abortMessage(req, res);
9596
} catch (err) {
97+
console.error(err);
9698
return respondWithError();
9799
}
98100
} else {

api/server/middleware/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const setHeaders = require('./setHeaders');
33
const requireJwtAuth = require('./requireJwtAuth');
44
const requireLocalAuth = require('./requireLocalAuth');
55
const validateEndpoint = require('./validateEndpoint');
6+
const validateMessageReq = require('./validateMessageReq');
67
const buildEndpointOption = require('./buildEndpointOption');
78
const validateRegistration = require('./validateRegistration');
89

@@ -12,6 +13,7 @@ module.exports = {
1213
requireJwtAuth,
1314
requireLocalAuth,
1415
validateEndpoint,
16+
validateMessageReq,
1517
buildEndpointOption,
1618
validateRegistration,
1719
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const { getConvo } = require('../../models');
2+
3+
// Middleware to validate conversationId and user relationship
4+
const validateMessageReq = async (req, res, next) => {
5+
let conversationId = req.params.conversationId || req.body.conversationId;
6+
7+
if (conversationId === 'new') {
8+
return res.status(200).send([]);
9+
}
10+
11+
if (!conversationId && req.body.message) {
12+
conversationId = req.body.message.conversationId;
13+
}
14+
15+
const conversation = await getConvo(req.user.id, conversationId);
16+
17+
if (!conversation) {
18+
return res.status(404).json({ error: 'Conversation not found' });
19+
}
20+
21+
if (conversation.user !== req.user.id) {
22+
return res.status(403).json({ error: 'User not authorized for this conversation' });
23+
}
24+
25+
next();
26+
};
27+
28+
module.exports = validateMessageReq;

api/server/routes/edit/anthropic.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,23 @@ router.post(
2929
endpointOption,
3030
conversationId,
3131
responseMessageId,
32+
isContinued = false,
3233
parentMessageId = null,
3334
overrideParentMessageId = null,
3435
} = req.body;
3536
console.log('edit log');
36-
console.dir({ text, conversationId, endpointOption }, { depth: null });
37+
console.dir({ text, generation, isContinued, conversationId, endpointOption }, { depth: null });
3738
let metadata;
3839
let userMessage;
3940
let lastSavedTimestamp = 0;
4041
let saveDelay = 100;
4142
const userMessageId = parentMessageId;
4243

4344
const addMetadata = (data) => (metadata = data);
44-
const getIds = (data) => (userMessage = data.userMessage);
45+
const getIds = (data) => {
46+
userMessage = data.userMessage;
47+
responseMessageId = data.responseMessageId;
48+
};
4549

4650
const { onProgress: progressCallback, getPartialText } = createOnProgress({
4751
generation,
@@ -87,6 +91,8 @@ router.post(
8791

8892
let response = await client.sendMessage(text, {
8993
user: req.user.id,
94+
generation,
95+
isContinued,
9096
isEdited: true,
9197
conversationId,
9298
parentMessageId,

api/server/routes/edit/gptPlugins.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@ router.post(
3030
endpointOption,
3131
conversationId,
3232
responseMessageId,
33+
isContinued = false,
3334
parentMessageId = null,
3435
overrideParentMessageId = null,
3536
} = req.body;
3637
console.log('edit log');
37-
console.dir({ text, conversationId, endpointOption }, { depth: null });
38+
console.dir({ text, generation, isContinued, conversationId, endpointOption }, { depth: null });
3839
let metadata;
3940
let userMessage;
4041
let lastSavedTimestamp = 0;
@@ -50,7 +51,10 @@ router.post(
5051
};
5152

5253
const addMetadata = (data) => (metadata = data);
53-
const getIds = (data) => (userMessage = data.userMessage);
54+
const getIds = (data) => {
55+
userMessage = data.userMessage;
56+
responseMessageId = data.responseMessageId;
57+
};
5458

5559
const {
5660
onProgress: progressCallback,
@@ -128,6 +132,8 @@ router.post(
128132

129133
let response = await client.sendMessage(text, {
130134
user,
135+
generation,
136+
isContinued,
131137
isEdited: true,
132138
conversationId,
133139
parentMessageId,

api/server/routes/edit/openAI.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,23 @@ router.post(
2929
endpointOption,
3030
conversationId,
3131
responseMessageId,
32+
isContinued = false,
3233
parentMessageId = null,
3334
overrideParentMessageId = null,
3435
} = req.body;
3536
console.log('edit log');
36-
console.dir({ text, conversationId, endpointOption }, { depth: null });
37+
console.dir({ text, generation, isContinued, conversationId, endpointOption }, { depth: null });
3738
let metadata;
3839
let userMessage;
3940
let lastSavedTimestamp = 0;
4041
let saveDelay = 100;
4142
const userMessageId = parentMessageId;
4243

4344
const addMetadata = (data) => (metadata = data);
44-
const getIds = (data) => (userMessage = data.userMessage);
45+
const getIds = (data) => {
46+
userMessage = data.userMessage;
47+
responseMessageId = data.responseMessageId;
48+
};
4549

4650
const { onProgress: progressCallback, getPartialText } = createOnProgress({
4751
generation,
@@ -90,6 +94,8 @@ router.post(
9094

9195
let response = await client.sendMessage(text, {
9296
user: req.user.id,
97+
generation,
98+
isContinued,
9399
isEdited: true,
94100
conversationId,
95101
parentMessageId,

0 commit comments

Comments
 (0)