incognitolm commited on
Commit ·
35318fb
1
Parent(s): a16dc1f
msg versions
Browse files- server/chatStream.js +25 -6
- server/wsHandler.js +33 -6
server/chatStream.js
CHANGED
|
@@ -130,33 +130,52 @@ export async function streamChat(ws, {
|
|
| 130 |
// Otherwise extract text
|
| 131 |
const hasImages = userMessage.some(item => item.type === 'image_url');
|
| 132 |
if (hasImages) {
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
} else {
|
| 135 |
-
|
|
|
|
| 136 |
}
|
| 137 |
}
|
| 138 |
|
| 139 |
// Build messages array: only include user message if it's provided
|
|
|
|
|
|
|
| 140 |
const messages = [
|
| 141 |
{ role: "system", content: SYSTEM_PROMPT },
|
| 142 |
...history.map(normalizeMessage).filter(Boolean),
|
| 143 |
];
|
| 144 |
|
| 145 |
// Add user message only if content was provided (not for edit-only requests)
|
| 146 |
-
if (
|
| 147 |
messages.push({ role: "user", content: normalizedUserMessage });
|
| 148 |
}
|
| 149 |
|
| 150 |
// Validate messages before API call
|
|
|
|
| 151 |
for (let i = 0; i < messages.length; i++) {
|
| 152 |
const msg = messages[i];
|
| 153 |
if (msg.role !== "assistant" || !msg.tool_calls) {
|
| 154 |
if (typeof msg.content !== "string" && !Array.isArray(msg.content)) {
|
| 155 |
-
|
| 156 |
-
throw new Error(`Message ${i} missing or invalid content property`);
|
| 157 |
}
|
| 158 |
}
|
| 159 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
|
| 161 |
try {
|
| 162 |
const stream = await client.chat.completions.create({
|
|
@@ -179,7 +198,7 @@ export async function streamChat(ws, {
|
|
| 179 |
];
|
| 180 |
|
| 181 |
// Include user message in follow-up only if it was in the original request
|
| 182 |
-
if (
|
| 183 |
followUpMessages.push({ role: "user", content: normalizedUserMessage });
|
| 184 |
}
|
| 185 |
|
|
|
|
| 130 |
// Otherwise extract text
|
| 131 |
const hasImages = userMessage.some(item => item.type === 'image_url');
|
| 132 |
if (hasImages) {
|
| 133 |
+
// Keep the full array - may need to add placeholder text if no text items
|
| 134 |
+
const textItems = userMessage.filter(item => item.type === 'text' && item.text?.trim());
|
| 135 |
+
if (textItems.length === 0) {
|
| 136 |
+
// Add a text placeholder to accompany images
|
| 137 |
+
normalizedUserMessage = [
|
| 138 |
+
{ type: 'text', text: '[Image(s) attached]' },
|
| 139 |
+
...userMessage.filter(item => item.type === 'image_url'),
|
| 140 |
+
];
|
| 141 |
+
} else {
|
| 142 |
+
normalizedUserMessage = userMessage;
|
| 143 |
+
}
|
| 144 |
} else {
|
| 145 |
+
// Text only - extract
|
| 146 |
+
normalizedUserMessage = userMessage.filter(b => b.type === "text").map(b => b.text).join("\n").trim() || "";
|
| 147 |
}
|
| 148 |
}
|
| 149 |
|
| 150 |
// Build messages array: only include user message if it's provided
|
| 151 |
+
const hasUserMessage = userMessage !== undefined && userMessage !== null && userMessage !== '' &&
|
| 152 |
+
!(Array.isArray(userMessage) && userMessage.length === 0);
|
| 153 |
const messages = [
|
| 154 |
{ role: "system", content: SYSTEM_PROMPT },
|
| 155 |
...history.map(normalizeMessage).filter(Boolean),
|
| 156 |
];
|
| 157 |
|
| 158 |
// Add user message only if content was provided (not for edit-only requests)
|
| 159 |
+
if (hasUserMessage) {
|
| 160 |
messages.push({ role: "user", content: normalizedUserMessage });
|
| 161 |
}
|
| 162 |
|
| 163 |
// Validate messages before API call
|
| 164 |
+
const invalidMessages = [];
|
| 165 |
for (let i = 0; i < messages.length; i++) {
|
| 166 |
const msg = messages[i];
|
| 167 |
if (msg.role !== "assistant" || !msg.tool_calls) {
|
| 168 |
if (typeof msg.content !== "string" && !Array.isArray(msg.content)) {
|
| 169 |
+
invalidMessages.push({index: i, role: msg.role, contentType: typeof msg.content});
|
|
|
|
| 170 |
}
|
| 171 |
}
|
| 172 |
}
|
| 173 |
+
|
| 174 |
+
if (invalidMessages.length > 0) {
|
| 175 |
+
const details = invalidMessages.map(m => `${m.index}:${m.role}(${m.contentType})`).join(',');
|
| 176 |
+
console.error(`streamChat: Invalid message content types: ${details}`);
|
| 177 |
+
throw new Error(`Message validation failed: invalid content types [${details}]`);
|
| 178 |
+
}
|
| 179 |
|
| 180 |
try {
|
| 181 |
const stream = await client.chat.completions.create({
|
|
|
|
| 198 |
];
|
| 199 |
|
| 200 |
// Include user message in follow-up only if it was in the original request
|
| 201 |
+
if (hasUserMessage) {
|
| 202 |
followUpMessages.push({ role: "user", content: normalizedUserMessage });
|
| 203 |
}
|
| 204 |
|
server/wsHandler.js
CHANGED
|
@@ -216,7 +216,9 @@ const handlers = {
|
|
| 216 |
const finalText = text || fullText;
|
| 217 |
|
| 218 |
// Only create user entry if content was actually provided
|
| 219 |
-
const
|
|
|
|
|
|
|
| 220 |
? buildEntry('user', content)
|
| 221 |
: null;
|
| 222 |
|
|
@@ -263,7 +265,11 @@ const handlers = {
|
|
| 263 |
|
| 264 |
safeSend(ws, { type: aborted ? 'chat:aborted' : 'chat:done', sessionId, name: newName, history: extractFlatHistory(newRootMessage) });
|
| 265 |
},
|
| 266 |
-
onError(err) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
});
|
| 268 |
},
|
| 269 |
|
|
@@ -274,14 +280,18 @@ const handlers = {
|
|
| 274 |
const session = client.userId
|
| 275 |
? sessionStore.getUserSession(client.userId, sessionId)
|
| 276 |
: sessionStore.getTempSession(client.tempId, sessionId);
|
| 277 |
-
if (!session) return;
|
| 278 |
|
| 279 |
const rootMessage = session.history?.[0];
|
| 280 |
-
if (!rootMessage) return;
|
| 281 |
|
| 282 |
const flatHistory = extractFlatHistory(rootMessage);
|
| 283 |
const targetMsg = flatHistory[messageIndex];
|
| 284 |
-
if (!targetMsg)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
|
| 286 |
// Find the target message in the tree and add new version
|
| 287 |
const newRoot = validateAndRepairTree(JSON.parse(JSON.stringify(rootMessage)));
|
|
@@ -312,6 +322,13 @@ const handlers = {
|
|
| 312 |
// Send back the updated message with its ID and the full flat history
|
| 313 |
const updatedFlatHistory = extractFlatHistory(newRoot);
|
| 314 |
const updatedTargetMsg = updatedFlatHistory[messageIndex];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
safeSend(ws, { type: 'chat:messageEdited', sessionId, messageId: targetMsg.id, messageIndex, message: updatedTargetMsg, history: updatedFlatHistory });
|
| 316 |
},
|
| 317 |
|
|
@@ -441,7 +458,6 @@ function extractFlatHistory(rootMessage) {
|
|
| 441 |
// Helper to ensure message has valid content
|
| 442 |
const ensureValidContent = (msg) => {
|
| 443 |
if (msg.content === undefined || msg.content === null) {
|
| 444 |
-
console.warn(`Message ${msg.id} has missing content, defaulting to empty string`);
|
| 445 |
msg.content = '';
|
| 446 |
}
|
| 447 |
return msg;
|
|
@@ -449,6 +465,17 @@ function extractFlatHistory(rootMessage) {
|
|
| 449 |
|
| 450 |
const history = [ensureValidContent(rootMessage)];
|
| 451 |
const currentVerIdx = rootMessage.currentVersionIdx ?? 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
const currentTail = rootMessage.versions[currentVerIdx]?.tail;
|
| 453 |
|
| 454 |
if (currentTail && Array.isArray(currentTail)) {
|
|
|
|
| 216 |
const finalText = text || fullText;
|
| 217 |
|
| 218 |
// Only create user entry if content was actually provided
|
| 219 |
+
const hasContent = content !== undefined && content !== null && content !== '' &&
|
| 220 |
+
!(Array.isArray(content) && content.length === 0);
|
| 221 |
+
const userEntry = hasContent
|
| 222 |
? buildEntry('user', content)
|
| 223 |
: null;
|
| 224 |
|
|
|
|
| 265 |
|
| 266 |
safeSend(ws, { type: aborted ? 'chat:aborted' : 'chat:done', sessionId, name: newName, history: extractFlatHistory(newRootMessage) });
|
| 267 |
},
|
| 268 |
+
onError(err) {
|
| 269 |
+
activeStreams.delete(ws);
|
| 270 |
+
console.error('streamChat error:', err);
|
| 271 |
+
safeSend(ws, { type: 'chat:error', error: String(err), sessionId });
|
| 272 |
+
},
|
| 273 |
});
|
| 274 |
},
|
| 275 |
|
|
|
|
| 280 |
const session = client.userId
|
| 281 |
? sessionStore.getUserSession(client.userId, sessionId)
|
| 282 |
: sessionStore.getTempSession(client.tempId, sessionId);
|
| 283 |
+
if (!session) return safeSend(ws, { type: 'error', message: 'Session not found' });
|
| 284 |
|
| 285 |
const rootMessage = session.history?.[0];
|
| 286 |
+
if (!rootMessage) return safeSend(ws, { type: 'error', message: 'No history' });
|
| 287 |
|
| 288 |
const flatHistory = extractFlatHistory(rootMessage);
|
| 289 |
const targetMsg = flatHistory[messageIndex];
|
| 290 |
+
if (!targetMsg) {
|
| 291 |
+
console.error(`chat:editMessage: Message at index ${messageIndex} not found. History length: ${flatHistory.length}`);
|
| 292 |
+
return safeSend(ws, { type: 'error', message: 'Message not found' });
|
| 293 |
+
}
|
| 294 |
+
console.log(`chat:editMessage: Editing message ${targetMsg.id} at index ${messageIndex}`);
|
| 295 |
|
| 296 |
// Find the target message in the tree and add new version
|
| 297 |
const newRoot = validateAndRepairTree(JSON.parse(JSON.stringify(rootMessage)));
|
|
|
|
| 322 |
// Send back the updated message with its ID and the full flat history
|
| 323 |
const updatedFlatHistory = extractFlatHistory(newRoot);
|
| 324 |
const updatedTargetMsg = updatedFlatHistory[messageIndex];
|
| 325 |
+
|
| 326 |
+
if (!updatedTargetMsg) {
|
| 327 |
+
console.error(`chat:editMessage: Updated message not found at index ${messageIndex}. Updated history length: ${updatedFlatHistory.length}`);
|
| 328 |
+
return safeSend(ws, { type: 'error', message: 'Failed to apply edit - message lost' });
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
console.log(`chat:editMessage: Edit complete. Message ${updatedTargetMsg.id} now has ${updatedTargetMsg.versions?.length ?? 0} versions`);
|
| 332 |
safeSend(ws, { type: 'chat:messageEdited', sessionId, messageId: targetMsg.id, messageIndex, message: updatedTargetMsg, history: updatedFlatHistory });
|
| 333 |
},
|
| 334 |
|
|
|
|
| 458 |
// Helper to ensure message has valid content
|
| 459 |
const ensureValidContent = (msg) => {
|
| 460 |
if (msg.content === undefined || msg.content === null) {
|
|
|
|
| 461 |
msg.content = '';
|
| 462 |
}
|
| 463 |
return msg;
|
|
|
|
| 465 |
|
| 466 |
const history = [ensureValidContent(rootMessage)];
|
| 467 |
const currentVerIdx = rootMessage.currentVersionIdx ?? 0;
|
| 468 |
+
|
| 469 |
+
if (!Array.isArray(rootMessage.versions)) {
|
| 470 |
+
console.warn(`extractFlatHistory: Root message ${rootMessage.id} missing versions array`);
|
| 471 |
+
return history;
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
if (currentVerIdx >= rootMessage.versions.length) {
|
| 475 |
+
console.warn(`extractFlatHistory: Root message currentVersionIdx ${currentVerIdx} out of bounds (${rootMessage.versions.length} versions)`);
|
| 476 |
+
return history;
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
const currentTail = rootMessage.versions[currentVerIdx]?.tail;
|
| 480 |
|
| 481 |
if (currentTail && Array.isArray(currentTail)) {
|