-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent-detection.js
More file actions
307 lines (258 loc) · 11.5 KB
/
content-detection.js
File metadata and controls
307 lines (258 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
// SaveChat Content Detection Module
// Handles detecting ChatGPT responses and action trays
class SaveChatDetection {
constructor() {
this.observer = null;
}
startObserver(callback) {
console.log('SaveChat: Starting mutation observer...');
this.observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
callback(node);
}
});
});
});
const chatContainer = this.findChatContainer();
console.log('SaveChat: Found chat container:', chatContainer);
if (chatContainer) {
this.observer.observe(chatContainer, {
childList: true,
subtree: true
});
console.log('SaveChat: Observer started successfully');
} else {
console.log('SaveChat: No chat container found!');
}
}
stopObserver() {
if (this.observer) {
this.observer.disconnect();
this.observer = null;
}
}
findChatContainer() {
const selectors = [
'main',
'[role="main"]',
'.flex.flex-col.items-center',
'.flex.flex-col',
'#__next',
'[data-testid="conversation-turn"]'
];
for (const selector of selectors) {
const container = document.querySelector(selector);
console.log(`SaveChat: Checking selector "${selector}":`, container);
if (container) {
console.log(`SaveChat: Using container:`, container);
return container;
}
}
console.log('SaveChat: No container found, using document.body');
return document.body;
}
isResponseNode(node) {
const responseSelectors = [
'[data-message-author-role="assistant"]'
];
return node.matches && responseSelectors.some(selector => node.matches(selector));
}
looksLikeAssistantResponse(element) {
const isAssistant = element.getAttribute('data-message-author-role') === 'assistant';
const isUser = element.getAttribute('data-message-author-role') === 'user';
if (isUser) return false;
if (!isAssistant) {
const parentUser = element.closest('[data-message-author-role="user"]');
if (parentUser) return false;
const hasUserInput = element.querySelector('textarea, input, [contenteditable="true"], [role="textbox"]');
if (hasUserInput) return false;
const userContainer = element.closest('.group[data-message-author-role="user"], .flex[data-message-author-role="user"]');
if (userContainer) return false;
}
const hasMarkdown = element.querySelector('.markdown, .prose, .whitespace-pre-wrap');
const hasTextContent = element.textContent && element.textContent.trim().length > 10;
const hasResponseClasses = element.className.includes('group') ||
element.className.includes('flex') ||
element.className.includes('text-gray');
const isNotInput = !element.querySelector('textarea, input, [contenteditable="true"]');
const isNotUI = !element.matches('button, .button, [role="button"]') &&
!element.closest('button, .button, [role="button"]');
const isNotSaveButton = !element.classList.contains('savechat-button') &&
!element.classList.contains('savechat-button-container');
return (isAssistant || (hasMarkdown && hasTextContent && hasResponseClasses && isNotInput && isNotUI && isNotSaveButton));
}
findInsertTarget(responseElement) {
console.log('SaveChat: Finding insert target for:', responseElement);
const actionTraySelectors = [
'[data-testid="message-actions"]',
'[data-testid="message-actions-toolbar"]',
'.flex.items-center.justify-end.gap-1',
'.flex.items-center.justify-end.gap-2',
'.flex.items-center.gap-1',
'.flex.items-center.gap-2',
'div[class*="flex"][class*="items-center"][class*="gap"]:has(button[data-testid*="copy"])',
'div[class*="flex"][class*="items-center"][class*="gap"]:has(button[aria-label*="copy"])',
'div[class*="flex"][class*="items-center"][class*="gap"]:has(button[aria-label*="Copy"])',
'div:has(button[data-testid*="copy"])',
'div:has(button[aria-label*="copy"])',
'div:has(button[aria-label*="Copy"])',
'[data-message-author-role="assistant"] > div:last-child',
'.group > div:last-child',
'.flex.items-center.justify-end',
'.flex.items-center.gap-1',
'.flex.items-center.gap-2'
];
// Prefer the bottom-most matching action tray inside this response
const candidateTrays = [];
actionTraySelectors.forEach(selector => {
const matches = Array.from(responseElement.querySelectorAll(selector));
matches.forEach(m => {
if (this.looksLikeActionTray(m)) {
candidateTrays.push(m);
}
});
});
if (candidateTrays.length > 0) {
return candidateTrays[candidateTrays.length - 1];
}
// Fallback to previous, broader search up the tree
const parentResponse = responseElement.closest('[data-message-author-role="assistant"], .group, .flex.flex-col');
if (parentResponse) {
for (const selector of actionTraySelectors) {
const actionTray = parentResponse.querySelector(selector);
if (actionTray && this.looksLikeActionTray(actionTray)) {
return actionTray;
}
}
}
const responseContainer = responseElement.closest('[data-message-author-role="assistant"]');
if (responseContainer) {
const container = responseContainer.parentElement;
if (container) {
for (const selector of actionTraySelectors) {
const actionTray = container.querySelector(selector);
if (actionTray && this.looksLikeActionTray(actionTray)) {
return actionTray;
}
}
}
}
const messageContainer = responseElement.closest('[data-message-author-role="assistant"]');
if (messageContainer) {
const existingButtonContainer = messageContainer.querySelector('.flex.items-center, .flex.gap-2');
if (existingButtonContainer) {
return existingButtonContainer;
}
}
const contentSelectors = [
'.markdown',
'.prose',
'.whitespace-pre-wrap',
'[data-message-author-role="assistant"] > div:last-child'
];
for (const selector of contentSelectors) {
const target = responseElement.querySelector(selector);
if (target) {
return target;
}
}
return responseElement;
}
looksLikeActionTray(element) {
console.log('SaveChat: Checking if element looks like action tray:', element);
const hasButtons = element.querySelectorAll('button, [role="button"], [data-testid*="button"]').length > 0;
console.log('SaveChat: Has buttons:', hasButtons);
const hasCopyButton = element.textContent.includes('Copy') ||
element.querySelector('[data-testid*="copy"]') ||
element.querySelector('[aria-label*="copy"]') ||
element.querySelector('[aria-label*="Copy"]') ||
element.querySelector('button[title*="copy"]') ||
element.querySelector('button[title*="Copy"]');
const hasEditButton = element.textContent.includes('Edit') ||
element.querySelector('[data-testid*="edit"]') ||
element.querySelector('[aria-label*="edit"]') ||
element.querySelector('[aria-label*="Edit"]') ||
element.querySelector('button[title*="edit"]') ||
element.querySelector('button[title*="Edit"]');
const hasCanvasButton = element.textContent.includes('Canvas') ||
element.querySelector('[data-testid*="canvas"]') ||
element.querySelector('[aria-label*="canvas"]') ||
element.querySelector('[aria-label*="Canvas"]');
const hasRegenerateButton = element.textContent.includes('Regenerate') ||
element.querySelector('[data-testid*="regenerate"]') ||
element.querySelector('[aria-label*="regenerate"]');
const hasContinueButton = element.textContent.includes('Continue') ||
element.querySelector('[data-testid*="continue"]') ||
element.querySelector('[aria-label*="continue"]');
const hasReadAloudButton = element.textContent.includes('Read aloud') ||
element.querySelector('[data-testid*="read"]') ||
element.querySelector('[aria-label*="read"]');
const hasActionTrayClasses = element.className.includes('flex') &&
element.className.includes('items-center') &&
(element.className.includes('gap') || element.className.includes('space'));
const hasChatGPTButtons = hasCopyButton || hasEditButton || hasCanvasButton || hasRegenerateButton || hasContinueButton || hasReadAloudButton;
const hasSVGIcons = element.querySelectorAll('svg').length > 0;
const isAtEnd = element === element.parentElement?.lastElementChild ||
element.nextElementSibling === null;
// More lenient check - if it has buttons and looks like an action area, consider it valid
const isActionTray = hasButtons && (hasChatGPTButtons || hasActionTrayClasses || hasSVGIcons);
console.log('SaveChat: Action tray check results:', {
hasButtons,
hasChatGPTButtons,
hasActionTrayClasses,
hasSVGIcons,
isAtEnd,
isActionTray,
className: element.className,
textContent: element.textContent.substring(0, 100)
});
return isActionTray;
}
isResponseReady(responseElement) {
const hasTextContent = responseElement.textContent && responseElement.textContent.trim().length > 10;
const hasMarkdown = responseElement.querySelector('.markdown, .prose, .whitespace-pre-wrap');
const hasStructure = responseElement.children.length > 0;
// More lenient check - if it has any content, consider it ready
const isReady = hasTextContent || hasMarkdown || hasStructure;
console.log('SaveChat: Response ready check:', { hasTextContent, hasMarkdown, hasStructure, isReady });
return isReady;
}
async waitForActionTray(responseElement) {
const maxWaitTime = 3000; // Reduced from 10000ms to 3000ms
const checkInterval = 100; // Reduced from 200ms to 100ms
let elapsed = 0;
while (elapsed < maxWaitTime) {
// Check if element is still in DOM
if (!responseElement.isConnected) {
return null;
}
const actionTray = this.findInsertTarget(responseElement);
if (actionTray && this.looksLikeActionTray(actionTray)) {
return actionTray;
}
await new Promise(resolve => setTimeout(resolve, checkInterval));
elapsed += checkInterval;
}
return null;
}
findExistingResponses() {
const selectors = [
'[data-message-author-role="assistant"]',
'div[data-message-author-role="assistant"]'
];
const responses = [];
selectors.forEach(selector => {
const found = document.querySelectorAll(selector);
found.forEach(response => {
if (this.looksLikeAssistantResponse(response)) {
responses.push(response);
}
});
});
return responses;
}
}
// Export for use in other modules
window.SaveChatDetection = SaveChatDetection;