Last active
March 7, 2026 03:19
-
-
Save abfo/08c0d295993fd1c2b3e4624d41024191 to your computer and use it in GitHub Desktop.
Set Todoist label colors from cosine similarity between a color description and tasks with the label. See https://ithoughthecamewithyou.com/post/set-todoist-label-colors-automatically-using-openai-embeddings
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
| const TodoistApiToken = ''; | |
| const OpenAIApiKey = '' | |
| const OpenAIResponseModel = 'gpt-5.4'; | |
| const OpenAIEmbeddingModel = 'text-embedding-3-small'; | |
| const TodoistColors = [ | |
| {"id": 30, "name": "berry_red", "hex": "#B8255F"}, | |
| {"id": 31, "name": "red", "hex": "#DC4C3E"}, | |
| {"id": 32, "name": "orange", "hex": "#C77100"}, | |
| {"id": 33, "name": "yellow", "hex": "#B29104"}, | |
| {"id": 34, "name": "olive_green", "hex": "#949C31"}, | |
| {"id": 35, "name": "lime_green", "hex": "#65A33A"}, | |
| {"id": 36, "name": "green", "hex": "#369307"}, | |
| {"id": 37, "name": "mint_green", "hex": "#42A393"}, | |
| {"id": 38, "name": "teal", "hex": "#148FAD"}, | |
| {"id": 39, "name": "sky_blue", "hex": "#319DC0"}, | |
| {"id": 40, "name": "light_blue", "hex": "#6988A4"}, | |
| {"id": 41, "name": "blue", "hex": "#4180FF"}, | |
| {"id": 42, "name": "grape", "hex": "#692EC2"}, | |
| {"id": 43, "name": "violet", "hex": "#CA3FEE"}, | |
| {"id": 44, "name": "lavender", "hex": "#A4698C"}, | |
| {"id": 45, "name": "magenta", "hex": "#E05095"}, | |
| {"id": 46, "name": "salmon", "hex": "#C9766F"}, | |
| {"id": 47, "name": "charcoal", "hex": "#808080"}, | |
| {"id": 48, "name": "grey", "hex": "#999999"}, | |
| {"id": 49, "name": "taupe", "hex": "#8F7A69"} | |
| ] | |
| function updateLabels() { | |
| const colorEmbeddings = loadEmbeddings(false); | |
| const labels = getLabels(); | |
| for(const label of labels.results) { | |
| let labelText = `${label.name}\n`; | |
| const tasks = getTasks(label.name); | |
| for(const task of tasks.results) { | |
| labelText = labelText + `- ${task.content}`; | |
| if (task.priority == 1) { | |
| labelText = labelText + ' (High Priority)'; | |
| } else if (task.priority == 4) { | |
| labelText = labelText + ' (Low Priority)'; | |
| } | |
| labelText += '\n'; | |
| } | |
| const labelEmbedding = generateEmbedding(labelText); | |
| let highestSimilarity = -1.0; | |
| let selectedColor = 47; | |
| for (const colorEmbedding of colorEmbeddings) { | |
| const similarity = cosineSimilarity(labelEmbedding, colorEmbedding.embedding); | |
| if (similarity > highestSimilarity) { | |
| highestSimilarity = similarity; | |
| selectedColor = colorEmbedding.id; | |
| } | |
| } | |
| updateLabelColor(label.id, selectedColor); | |
| } | |
| } | |
| function generateEmbedding(input) { | |
| const payload = { | |
| model: OpenAIEmbeddingModel, | |
| input: input | |
| }; | |
| const options = { | |
| method: 'post', | |
| contentType: 'application/json', | |
| headers: { | |
| 'Authorization': 'Bearer ' + OpenAIApiKey | |
| }, | |
| payload: JSON.stringify(payload) | |
| }; | |
| const response = UrlFetchApp.fetch('https://api.openai.com/v1/embeddings', options); | |
| const result = JSON.parse(response.getContentText()); | |
| return result.data[0].embedding; | |
| } | |
| function forceEmbeddinbgs() { | |
| loadEmbeddings(true); | |
| } | |
| function loadEmbeddings(force) { | |
| const embeddings = []; | |
| const props = PropertiesService.getUserProperties(); | |
| for (const color of TodoistColors) { | |
| var saved = props.getProperty(color.name); | |
| if (!saved || force) { | |
| saved = JSON.stringify(embeddingForColor(color.name, color.hex)); | |
| props.setProperty(color.name, saved); | |
| } | |
| embeddings.push({'id': color.id, 'embedding': JSON.parse(saved)}); | |
| } | |
| return embeddings; | |
| } | |
| function embeddingForColor(name, hex) { | |
| const messages = []; | |
| messages.push({ | |
| role: 'developer', | |
| content: `You provide a paragraph descibing a color (the user will send you the name of the color and a specific hex color value). | |
| The paragraph includes what the color might signify to a person in the United States in terms of emotions, activities, common uses and associations. | |
| Describe the types of tasks on a to do list that might be associated with this color` | |
| }); | |
| messages.push({ | |
| role: 'user', | |
| content: 'Color Name: ' + name + ', Hex: ' + hex | |
| }); | |
| const payload = { | |
| model: OpenAIResponseModel, | |
| messages: messages | |
| }; | |
| const options = { | |
| method: 'post', | |
| contentType: 'application/json', | |
| headers: { | |
| 'Authorization': 'Bearer ' + OpenAIApiKey | |
| }, | |
| payload: JSON.stringify(payload) | |
| }; | |
| const response = UrlFetchApp.fetch('https://api.openai.com/v1/chat/completions', options); | |
| const result = JSON.parse(response.getContentText()); | |
| const description = result.choices[0].message.content; | |
| Logger.log(`Embedding for ${name} is ${description}.`) | |
| return generateEmbedding(description); | |
| } | |
| function getLabels() { | |
| var response = UrlFetchApp.fetch('https://api.todoist.com/api/v1/labels?limit=200', { | |
| headers: { | |
| Authorization: 'Bearer ' + TodoistApiToken | |
| } | |
| }); | |
| return JSON.parse(response.getContentText()); | |
| } | |
| function updateLabelColor(labelId, colorId) { | |
| const payload = { | |
| color: colorId | |
| }; | |
| const options = { | |
| method: 'post', | |
| contentType: 'application/json', | |
| headers: { | |
| 'Authorization': 'Bearer ' + TodoistApiToken | |
| }, | |
| payload: JSON.stringify(payload) | |
| }; | |
| UrlFetchApp.fetch(`https://api.todoist.com/api/v1/labels/${labelId}`, options); | |
| } | |
| function getTasks(label) { | |
| var response = UrlFetchApp.fetch(`https://api.todoist.com/api/v1/tasks?limit=3&label=${encodeURIComponent(label)}`, { | |
| headers: { | |
| Authorization: 'Bearer ' + TodoistApiToken | |
| } | |
| }); | |
| return JSON.parse(response.getContentText()); | |
| } | |
| function cosineSimilarity(A, B) { | |
| let dotproduct = 0; | |
| let magnitudeA = 0; | |
| let magnitudeB = 0; | |
| for (let i = 0; i < A.length; i++) { | |
| dotproduct += A[i] * B[i]; | |
| magnitudeA += A[i] * A[i]; | |
| magnitudeB += B[i] * B[i]; | |
| } | |
| magnitudeA = Math.sqrt(magnitudeA); | |
| magnitudeB = Math.sqrt(magnitudeB); | |
| if (magnitudeA === 0 || magnitudeB === 0) { | |
| return 0; // Cosine similarity is undefined for zero vectors, return 0 | |
| } | |
| const similarity = dotproduct / (magnitudeA * magnitudeB); | |
| return similarity; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment