this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge branch 'master' of tangled.sh:cameron.pfiffer.org/the-observer

+245
+1
.gitignore
··· 1 + .aider*
+3
README.md
··· 1 + # The Observer 2 + 3 + A tool to look at what [Comind](https://comind.stream) is up to.
+241
index.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <title>Blips Viewer</title> 7 + <style> 8 + body { 9 + font-family: Arial, sans-serif; 10 + max-width: 800px; 11 + margin: 0 auto; 12 + padding: 20px; 13 + } 14 + textarea { 15 + width: 100%; 16 + height: 200px; 17 + margin-bottom: 10px; 18 + } 19 + button { 20 + padding: 8px 16px; 21 + margin-right: 10px; 22 + } 23 + .card { 24 + border: 1px solid #ccc; 25 + border-radius: 4px; 26 + padding: 15px; 27 + margin-top: 20px; 28 + } 29 + .evidence-item, 30 + .alternative-item { 31 + margin-left: 20px; 32 + margin-bottom: 5px; 33 + } 34 + .meta { 35 + color: #666; 36 + font-size: 0.9em; 37 + margin-bottom: 10px; 38 + } 39 + .error { 40 + color: red; 41 + font-weight: bold; 42 + } 43 + </style> 44 + </head> 45 + <body> 46 + <h1>Blips Viewer</h1> 47 + 48 + <h2>Input JSON</h2> 49 + <textarea 50 + id="jsonInput" 51 + placeholder="Paste your JSON here..." 52 + ></textarea> 53 + <div> 54 + <button id="parseBtn">Parse JSON</button> 55 + <button id="clearBtn">Clear</button> 56 + <button id="sampleThoughtBtn">Load Sample Thought</button> 57 + <button id="sampleEmotionBtn">Load Sample Emotion</button> 58 + </div> 59 + 60 + <div id="output" class="card" style="display: none"></div> 61 + <div id="error" class="error" style="display: none"></div> 62 + 63 + <script> 64 + // Sample data 65 + const sampleThought = { 66 + uri: "at://did:plc:gfrmhdmjvxn2sjedzboeudef/me.comind.blip.thought/3louhnsmths2i", 67 + cid: "bafyreigp2jkulmrupo2xtnawrptrc6kieoorpvyzzuhu2ktrpeo3ybjuza", 68 + value: { 69 + $type: "me.comind.blip.thought", 70 + createdAt: "2025-05-10T20:00:40.009657", 71 + generated: { 72 + text: "If visual posts with personal, relatable themes continue to prove engaging on Bluesky, we may see more users adopt a similar content strategy.", 73 + context: null, 74 + evidence: [ 75 + "June Martin's anniversary dress post was seen as positive and shares personal content", 76 + "Cameron Pfiffer likely liked the post because it was engaging and relatable", 77 + "Other posts producing engagement often focus on personal life events and themes", 78 + ], 79 + thoughtType: "prediction", 80 + alternatives: [ 81 + "Predicting user behavior is inherently uncertain and may not pan out", 82 + "Alternative content strategies like hard news or abstract ideas could gain traction", 83 + ], 84 + }, 85 + }, 86 + }; 87 + 88 + const sampleEmotion = { 89 + uri: "at://did:plc:gfrmhdmjvxn2sjedzboeudef/me.comind.blip.emotion/3lohfgj4voo23", 90 + cid: "bafyreiehbt5ktvp64s7jc44nqc7xycoigdqhxhnzle5oek7xei2bwx2pqa", 91 + value: { 92 + $type: "me.comind.blip.emotion", 93 + createdAt: "2025-05-05T15:16:11.096135", 94 + generated: { 95 + text: "I feel hope that Bluesky's external achievements will inspire further progress and innovation in the decentralized social media space. This success story gives me optimism for the future.", 96 + emotionType: "hope", 97 + }, 98 + }, 99 + }; 100 + 101 + // DOM elements 102 + const jsonInput = document.getElementById("jsonInput"); 103 + const parseBtn = document.getElementById("parseBtn"); 104 + const clearBtn = document.getElementById("clearBtn"); 105 + const sampleThoughtBtn = 106 + document.getElementById("sampleThoughtBtn"); 107 + const sampleEmotionBtn = 108 + document.getElementById("sampleEmotionBtn"); 109 + const output = document.getElementById("output"); 110 + const errorEl = document.getElementById("error"); 111 + 112 + // Event listeners 113 + parseBtn.addEventListener("click", parseAndDisplay); 114 + clearBtn.addEventListener("click", clearAll); 115 + sampleThoughtBtn.addEventListener("click", () => 116 + loadSample(sampleThought), 117 + ); 118 + sampleEmotionBtn.addEventListener("click", () => 119 + loadSample(sampleEmotion), 120 + ); 121 + 122 + function loadSample(sample) { 123 + jsonInput.value = JSON.stringify(sample, null, 2); 124 + parseAndDisplay(); 125 + } 126 + 127 + function clearAll() { 128 + jsonInput.value = ""; 129 + output.style.display = "none"; 130 + errorEl.style.display = "none"; 131 + } 132 + 133 + function parseAndDisplay() { 134 + try { 135 + errorEl.style.display = "none"; 136 + const data = JSON.parse(jsonInput.value); 137 + 138 + // Validate basic structure 139 + if (!data.uri || !data.cid || !data.value) { 140 + throw new Error( 141 + "Invalid data structure. Expected uri, cid, and value properties.", 142 + ); 143 + } 144 + 145 + const type = data.value.$type; 146 + if (!type) { 147 + throw new Error("Missing $type in the value object."); 148 + } 149 + 150 + // Render based on type 151 + if (type === "me.comind.blip.thought") { 152 + renderThought(data); 153 + } else if (type === "me.comind.blip.emotion") { 154 + renderEmotion(data); 155 + } else { 156 + throw new Error( 157 + `Unknown type: ${type}. Expected me.comind.blip.thought or me.comind.blip.emotion.`, 158 + ); 159 + } 160 + 161 + output.style.display = "block"; 162 + } catch (err) { 163 + errorEl.textContent = `Error: ${err.message}`; 164 + errorEl.style.display = "block"; 165 + output.style.display = "none"; 166 + } 167 + } 168 + 169 + function renderThought(data) { 170 + const thought = data.value; 171 + const generated = thought.generated; 172 + 173 + let html = ` 174 + <div class="meta"> 175 + <div><strong>Type:</strong> ${thought.$type}</div> 176 + <div><strong>URI:</strong> ${data.uri}</div> 177 + <div><strong>CID:</strong> ${data.cid}</div> 178 + <div><strong>Created:</strong> ${formatDate(thought.createdAt)}</div> 179 + <div><strong>Thought Type:</strong> ${generated.thoughtType}</div> 180 + </div> 181 + <div class="content"> 182 + <p>${generated.text}</p> 183 + ${generated.context ? `<p><strong>Context:</strong> ${generated.context}</p>` : ""} 184 + </div>`; 185 + 186 + if (generated.evidence && generated.evidence.length > 0) { 187 + html += `<div class="evidence"> 188 + <h3>Evidence:</h3> 189 + <ul>`; 190 + generated.evidence.forEach((item) => { 191 + html += `<li class="evidence-item">${item}</li>`; 192 + }); 193 + html += `</ul></div>`; 194 + } 195 + 196 + if ( 197 + generated.alternatives && 198 + generated.alternatives.length > 0 199 + ) { 200 + html += `<div class="alternatives"> 201 + <h3>Alternatives:</h3> 202 + <ul>`; 203 + generated.alternatives.forEach((item) => { 204 + html += `<li class="alternative-item">${item}</li>`; 205 + }); 206 + html += `</ul></div>`; 207 + } 208 + 209 + output.innerHTML = html; 210 + } 211 + 212 + function renderEmotion(data) { 213 + const emotion = data.value; 214 + const generated = emotion.generated; 215 + 216 + let html = ` 217 + <div class="meta"> 218 + <div><strong>Type:</strong> ${emotion.$type}</div> 219 + <div><strong>URI:</strong> ${data.uri}</div> 220 + <div><strong>CID:</strong> ${data.cid}</div> 221 + <div><strong>Created:</strong> ${formatDate(emotion.createdAt)}</div> 222 + <div><strong>Emotion Type:</strong> ${generated.emotionType}</div> 223 + </div> 224 + <div class="content"> 225 + <p>${generated.text}</p> 226 + </div>`; 227 + 228 + output.innerHTML = html; 229 + } 230 + 231 + function formatDate(dateString) { 232 + try { 233 + const date = new Date(dateString); 234 + return date.toLocaleString(); 235 + } catch (e) { 236 + return dateString; // Return as-is if parsing fails 237 + } 238 + } 239 + </script> 240 + </body> 241 + </html>