Skip to content

Instantly share code, notes, and snippets.

@robinsonkwame
Created January 6, 2025 21:14
Show Gist options
  • Save robinsonkwame/a44630bb43f8c8822ce54413a6006d3a to your computer and use it in GitHub Desktop.
Save robinsonkwame/a44630bb43f8c8822ce54413a6006d3a to your computer and use it in GitHub Desktop.
Get CoralAI Tagger for documents
// ==UserScript==
// @name Chat Tag Manager for GetCoral AI
// @namespace Violentmonkey Scripts
// @match https://app.getcoralai.com/dashboard*
// @grant none
// @version 1.1
// @author -
// @description Allows you to check a set of documents and press Ctrl-T to tag them as Chat With Me; Then you can chat with those documents. Or press Ctrl-R to remove all Chat With Me tags. In combination with other site functionality, this facilitates rapid selection and chatting with document subsets
// @require https://cdn.jsdelivr.net/npm/@violentmonkey/shortcut@1
// ==/UserScript==
// Configuration
const CONFIG = {
chatTag: {
id: 'f7sA6snjzuJtBWLcJdP4y',
text: 'chat with me'
},
delays: {
elementWait: 2000,
betweenOperations: 1000,
afterClick: 500
}
};
class DocumentScanner {
static scan(debug = false) {
const docs = this._parseTableRows();
const result = {
checkedDocs: docs.filter(doc => doc.isChecked),
taggedDocs: docs.filter(doc => doc.hasChatTag),
untaggedDocs: docs.filter(doc => !doc.hasChatTag)
};
if (debug) this._logScanResults(result, docs);
return result;
}
static _parseTableRows() {
return Array.from(document.querySelectorAll('table tbody tr'))
.map(row => ({
uuid: this._getUUID(row),
name: this._getName(row),
isChecked: this._isChecked(row),
tags: this._getTags(row),
hasChatTag: this._hasChatTag(row)
}));
}
static _getUUID(row) {
return row.querySelector('a[href^="/chat/"]')?.href.split('/').pop();
}
static _getName(row) {
const nameElem = row.querySelector('a.font-medium');
const link = row.querySelector('a[href^="/chat/"]');
return nameElem?.textContent?.trim() || link?.textContent?.trim() || 'Unnamed';
}
static _isChecked(row) {
return row.querySelector('[role="checkbox"]')?.getAttribute('aria-checked') === 'true';
}
static _getTags(row) {
return Array.from(row.querySelectorAll('.rounded-full'))
.map(tag => ({
id: tag.dataset.tagId || '',
text: tag.textContent
}));
}
static _hasChatTag(row) {
return this._getTags(row).some(tag => tag.text === CONFIG.chatTag.text);
}
static _logScanResults({ checkedDocs, taggedDocs, untaggedDocs }, allDocs) {
console.group('Scan Results');
this._logDocGroup('Checked documents', checkedDocs);
this._logDocGroup('Tagged documents', taggedDocs);
this._logDocGroup('Untagged documents', untaggedDocs);
console.log(`Totals: ${allDocs.length} docs, ${checkedDocs.length} checked, ${taggedDocs.length} tagged, ${untaggedDocs.length} untagged`);
console.groupEnd();
}
static _logDocGroup(title, docs) {
console.group(title);
docs.forEach(doc => {
console.log(doc.name);
console.log(` Tags: ${doc.tags.map(t => t.text).join(', ') || 'none'}`);
});
console.groupEnd();
}
}
class TagManager {
static async waitForElement(selector) {
const start = Date.now();
while (Date.now() - start < CONFIG.delays.elementWait) {
const element = document.querySelector(selector);
if (element) return element;
await new Promise(r => setTimeout(r, 50));
}
return null;
}
static async setReactInput(element, value) {
const lastValue = element.value;
element.value = value;
const event = new Event('input', { bubbles: true });
event.simulated = true;
const tracker = element._valueTracker;
if (tracker) tracker.setValue(lastValue);
element.dispatchEvent(event);
}
static async removeTag(doc) {
const logger = new OperationLogger(`Removing tag from ${doc.name}`);
const row = this._findDocumentRow(doc, logger);
if (!row) return logger.getLog();
const tagButton = this._findTagButton(row, logger);
if (!tagButton) return logger.getLog();
logger.log('Clicking tag button');
tagButton.click();
await new Promise(r => setTimeout(r, CONFIG.delays.afterClick));
const removeButton = this._findRemoveButton(logger);
if (!removeButton) return logger.getLog();
logger.log('Clicking remove button');
removeButton.click();
await this._saveChanges(logger);
return logger.getLog();
}
static async addTag(doc) {
const logger = new OperationLogger(`Adding tag to ${doc.name}`);
const row = this._findDocumentRow(doc, logger);
if (!row) return logger.getLog();
const tagButton = this._findTagButton(row, logger);
if (!tagButton) return logger.getLog();
logger.log('Clicking tag button');
tagButton.click();
await new Promise(r => setTimeout(r, CONFIG.delays.afterClick));
const input = await this.waitForElement('[id^="react-select"][id$="-input"]');
if (!input) {
logger.log('Input not found');
return logger.getLog();
}
await this.setReactInput(input, CONFIG.chatTag.text);
logger.log('Input value set');
await new Promise(r => setTimeout(r, CONFIG.delays.afterClick));
const optionsContainer = document.querySelector('.react-select__menu-list');
if (optionsContainer) {
const options = Array.from(optionsContainer.querySelectorAll('.react-select__option'));
logger.log(`Found ${options.length} options`);
if (options.length > 0) {
logger.log('Clicking first option');
options[0].click();
}
}
await this._saveChanges(logger);
return logger.getLog();
}
static _findDocumentRow(doc, logger) {
const row = Array.from(document.querySelectorAll('tr'))
.find(r => r.innerHTML.includes(doc.uuid));
if (!row) logger.log('Row not found');
return row;
}
static _findTagButton(row, logger) {
const button = row.querySelector('button:has(.lucide-tags)');
if (!button) logger.log('Tag button not found');
return button;
}
static _findRemoveButton(logger) {
const button = Array.from(document.querySelectorAll('.react-select__multi-value'))
.find(div => div.textContent.includes(CONFIG.chatTag.text))
?.querySelector('.react-select__multi-value__remove');
if (!button) logger.log('Remove button not found');
return button;
}
static async _saveChanges(logger) {
await new Promise(r => setTimeout(r, CONFIG.delays.afterClick));
const saveButton = document.querySelector('button[type="submit"]');
if (saveButton) {
logger.log('Clicking save');
saveButton.click();
}
}
}
class OperationLogger {
constructor(operation) {
this.logs = [];
this.log(`Starting ${operation}`);
}
log(message) {
const entry = `${new Date().toISOString()}: ${message}`;
console.log(entry);
this.logs.push(entry);
}
getLog() {
return this.logs.join('\n');
}
}
class TagOperations {
static async removeAllTags() {
const { taggedDocs } = DocumentScanner.scan();
for (const doc of taggedDocs) {
await TagManager.removeTag(doc);
await new Promise(r => setTimeout(r, CONFIG.delays.betweenOperations));
}
}
static async tagSelected() {
const { taggedDocs, checkedDocs } = DocumentScanner.scan();
console.group('Tag Operation Plan');
console.log(`Found ${taggedDocs.length} tagged docs, ${checkedDocs.length} selected docs`);
const tagsToRemove = taggedDocs.filter(doc =>
!checkedDocs.some(d => d.uuid === doc.uuid));
const tagsToAdd = checkedDocs.filter(doc =>
!taggedDocs.some(d => d.uuid === doc.uuid));
this._logOperationPlan(tagsToRemove, tagsToAdd);
await this._executeOperations(tagsToRemove, tagsToAdd);
console.groupEnd();
}
static _logOperationPlan(tagsToRemove, tagsToAdd) {
console.group('Documents to untag');
tagsToRemove.forEach(doc => console.log(`- ${doc.name}`));
console.groupEnd();
console.group('Documents to tag');
tagsToAdd.forEach(doc => console.log(`- ${doc.name}`));
console.groupEnd();
}
static async _executeOperations(tagsToRemove, tagsToAdd) {
for (const doc of tagsToRemove) {
console.log(`\nRemoving tag from: ${doc.name}`);
const debug = await TagManager.removeTag(doc);
console.log(debug);
await new Promise(r => setTimeout(r, CONFIG.delays.betweenOperations));
}
for (const doc of tagsToAdd) {
console.log(`\nAdding tag to: ${doc.name}`);
const debug = await TagManager.addTag(doc);
console.log(debug);
await new Promise(r => setTimeout(r, CONFIG.delays.betweenOperations));
}
}
}
// Initialize keyboard shortcuts
const service = new VM.shortcut.KeyboardService();
service.enable();
service.register('control-t', () => TagOperations.tagSelected());
service.register('control-r', () => TagOperations.removeAllTags());
console.log('Chat tag manager loaded - Press Control-T to tag selected, Control-R to remove all chat tags');
@robinsonkwame
Copy link
Author

Note if you do not have a Chat with Me tag, make one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment