Created
January 6, 2025 21:14
-
-
Save robinsonkwame/a44630bb43f8c8822ce54413a6006d3a to your computer and use it in GitHub Desktop.
Get CoralAI Tagger for documents
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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'); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Note if you do not have a Chat with Me tag, make one.