RayMelius Claude Opus 4.6 commited on
Commit
0871b6d
·
1 Parent(s): 316428d

Add notification toasts and location hover tooltips to web UI

Browse files

- Floating toast notifications for notable events: romance milestones,
world events, gossip, and new conversations
- Toasts auto-fade after 5 seconds, max 5 stacked from bottom-left
- Color-coded borders: pink for romance, red for events, purple for gossip,
yellow for conversations
- Location hover tooltips showing name, zone, description, occupant list,
and active conversation indicator
- Toasts triggered from both WebSocket events and polling updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. web/index.html +103 -14
web/index.html CHANGED
@@ -111,8 +111,26 @@
111
  #tooltip {
112
  position: absolute; background: #16213eee; border: 1px solid #4ecca3;
113
  border-radius: 6px; padding: 8px 12px; font-size: 12px;
114
- pointer-events: none; display: none; z-index: 100; max-width: 250px;
115
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  /* Section headers */
117
  .section-header {
118
  font-size: 12px; color: #4ecca3; margin: 10px 0 4px 0; font-weight: 600;
@@ -172,6 +190,7 @@
172
  <div id="canvas-container">
173
  <canvas id="cityCanvas"></canvas>
174
  <div id="tooltip"></div>
 
175
  </div>
176
  <div id="sidebar">
177
  <div class="sidebar-tabs">
@@ -846,26 +865,59 @@ function onCanvasClick(e) {
846
  function onCanvasMouseMove(e) {
847
  const rect=canvas.getBoundingClientRect();
848
  const mx=e.clientX-rect.left, my=e.clientY-rect.top;
849
- let found=null;
 
 
 
 
850
  for (const [id,pos] of Object.entries(agentPositions)) {
851
- if(Math.hypot(mx-pos.x,my-pos.y)<22){found=id;break;}
 
 
 
 
 
 
 
 
 
852
  }
853
- if(found!==hoveredAgent){
854
- hoveredAgent=found;
855
- canvas.style.cursor=found?'pointer':'default';
856
- const tt=document.getElementById('tooltip');
857
- if(found&&agents[found]){
858
- const a=agents[found];
859
  let extra='';
860
  if(a.partner_id&&agents[a.partner_id]) extra=`<br><span style="color:#e91e90">Partner: ${agents[a.partner_id].name}</span>`;
861
  tt.innerHTML=`<b>${a.name}</b><br><span style="color:#a0a0c0">${a.action||'idle'}</span>${extra}`;
862
  tt.style.display='block';
863
- } else tt.style.display='none';
 
 
864
  }
865
- if(found){
866
- const tt=document.getElementById('tooltip');
867
- tt.style.left=(e.clientX-canvas.getBoundingClientRect().left+15)+'px';
868
- tt.style.top=(e.clientY-canvas.getBoundingClientRect().top+15)+'px';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
869
  }
870
  }
871
 
@@ -1107,6 +1159,7 @@ async function fetchSecondaryData() {
1107
  if (er.ok) {
1108
  const d2 = await er.json();
1109
  eventLog = (d2.events||[]).map(e => e.message||'').filter(m => m.trim());
 
1110
  if (activeTab === 'events') renderEventLog();
1111
  }
1112
  } catch(e) {}
@@ -1142,6 +1195,7 @@ function connectWebSocket() {
1142
  } else if (msg.type === 'event') {
1143
  eventLog.push(msg.message);
1144
  if (eventLog.length > 200) eventLog = eventLog.slice(-200);
 
1145
  if (activeTab === 'events') renderEventLog();
1146
  }
1147
  } catch(e) {}
@@ -1232,6 +1286,41 @@ async function fetchControls() {
1232
  } catch(e) {}
1233
  }
1234
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1235
  // ============================================================
1236
  // INIT
1237
  // ============================================================
 
111
  #tooltip {
112
  position: absolute; background: #16213eee; border: 1px solid #4ecca3;
113
  border-radius: 6px; padding: 8px 12px; font-size: 12px;
114
+ pointer-events: none; display: none; z-index: 100; max-width: 280px;
115
  }
116
+ /* NOTIFICATION TOASTS */
117
+ #toast-container {
118
+ position: absolute; bottom: 12px; left: 12px; z-index: 200;
119
+ display: flex; flex-direction: column-reverse; gap: 6px;
120
+ pointer-events: none; max-width: 360px;
121
+ }
122
+ .toast {
123
+ background: #16213eee; border-radius: 6px; padding: 8px 14px;
124
+ font-size: 12px; color: #e0e0e0; border-left: 3px solid #4ecca3;
125
+ animation: toastIn 0.3s ease, toastOut 0.5s ease 4.5s forwards;
126
+ backdrop-filter: blur(4px); line-height: 1.4;
127
+ }
128
+ .toast.romance { border-left-color: #e91e90; }
129
+ .toast.event { border-left-color: #e94560; }
130
+ .toast.conv { border-left-color: #f0c040; }
131
+ .toast.gossip { border-left-color: #9b59b6; }
132
+ @keyframes toastIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
133
+ @keyframes toastOut { from { opacity: 1; } to { opacity: 0; } }
134
  /* Section headers */
135
  .section-header {
136
  font-size: 12px; color: #4ecca3; margin: 10px 0 4px 0; font-weight: 600;
 
190
  <div id="canvas-container">
191
  <canvas id="cityCanvas"></canvas>
192
  <div id="tooltip"></div>
193
+ <div id="toast-container"></div>
194
  </div>
195
  <div id="sidebar">
196
  <div class="sidebar-tabs">
 
865
  function onCanvasMouseMove(e) {
866
  const rect=canvas.getBoundingClientRect();
867
  const mx=e.clientX-rect.left, my=e.clientY-rect.top;
868
+ const W=canvas.width, H=canvas.height;
869
+ const tt=document.getElementById('tooltip');
870
+
871
+ // Check agents first
872
+ let foundAgent=null;
873
  for (const [id,pos] of Object.entries(agentPositions)) {
874
+ if(Math.hypot(mx-pos.x,my-pos.y)<22){foundAgent=id;break;}
875
+ }
876
+
877
+ // Check locations if no agent
878
+ let foundLoc=null;
879
+ if (!foundAgent) {
880
+ for (const [id, pos] of Object.entries(LOCATION_POSITIONS)) {
881
+ const lx=pos.x*W, ly=pos.y*H;
882
+ if(Math.hypot(mx-lx,my-ly)<35){foundLoc=id;break;}
883
+ }
884
  }
885
+
886
+ if(foundAgent!==hoveredAgent){
887
+ hoveredAgent=foundAgent;
888
+ canvas.style.cursor=(foundAgent||foundLoc)?'pointer':'default';
889
+ if(foundAgent&&agents[foundAgent]){
890
+ const a=agents[foundAgent];
891
  let extra='';
892
  if(a.partner_id&&agents[a.partner_id]) extra=`<br><span style="color:#e91e90">Partner: ${agents[a.partner_id].name}</span>`;
893
  tt.innerHTML=`<b>${a.name}</b><br><span style="color:#a0a0c0">${a.action||'idle'}</span>${extra}`;
894
  tt.style.display='block';
895
+ } else if (!foundLoc) {
896
+ tt.style.display='none';
897
+ }
898
  }
899
+
900
+ // Location tooltip
901
+ if (!foundAgent && foundLoc && locations[foundLoc]) {
902
+ const loc = locations[foundLoc];
903
+ const occ = (loc.occupants||[]);
904
+ const occNames = occ.map(o => (typeof o === 'object' ? o.name : (agents[o]?.name || o))).slice(0, 8);
905
+ const hasConv = conversationData.active?.some(c => c.location === foundLoc);
906
+ tt.innerHTML = `<b>${loc.name||foundLoc}</b> <span style="color:#666">(${loc.zone||''})</span>
907
+ ${loc.description ? `<br><span style="color:#888;font-size:10px">${esc(loc.description)}</span>` : ''}
908
+ <br><span style="color:#a0a0c0">${occ.length} occupant${occ.length!==1?'s':''}</span>
909
+ ${occNames.length > 0 ? `<br><span style="font-size:10px;color:#b0b0c0">${occNames.join(', ')}${occ.length>8?'...':''}</span>` : ''}
910
+ ${hasConv ? '<br><span style="color:#f0c040;font-size:10px">Active conversation here</span>' : ''}`;
911
+ tt.style.display = 'block';
912
+ canvas.style.cursor = 'pointer';
913
+ } else if (!foundAgent && !foundLoc) {
914
+ canvas.style.cursor = 'default';
915
+ if (!hoveredAgent) tt.style.display = 'none';
916
+ }
917
+
918
+ if(foundAgent||foundLoc){
919
+ tt.style.left=(e.clientX-rect.left+15)+'px';
920
+ tt.style.top=(e.clientY-rect.top+15)+'px';
921
  }
922
  }
923
 
 
1159
  if (er.ok) {
1160
  const d2 = await er.json();
1161
  eventLog = (d2.events||[]).map(e => e.message||'').filter(m => m.trim());
1162
+ checkForNotableEvents();
1163
  if (activeTab === 'events') renderEventLog();
1164
  }
1165
  } catch(e) {}
 
1195
  } else if (msg.type === 'event') {
1196
  eventLog.push(msg.message);
1197
  if (eventLog.length > 200) eventLog = eventLog.slice(-200);
1198
+ checkForNotableEvents();
1199
  if (activeTab === 'events') renderEventLog();
1200
  }
1201
  } catch(e) {}
 
1286
  } catch(e) {}
1287
  }
1288
 
1289
+ // ============================================================
1290
+ // TOAST NOTIFICATIONS
1291
+ // ============================================================
1292
+ let lastEventCount = 0;
1293
+
1294
+ function showToast(message, type='info') {
1295
+ const container = document.getElementById('toast-container');
1296
+ const toast = document.createElement('div');
1297
+ toast.className = 'toast ' + type;
1298
+ toast.textContent = message;
1299
+ container.appendChild(toast);
1300
+ // Remove after animation
1301
+ setTimeout(() => toast.remove(), 5000);
1302
+ // Max 5 toasts at once
1303
+ while (container.children.length > 5) container.firstChild.remove();
1304
+ }
1305
+
1306
+ function checkForNotableEvents() {
1307
+ if (eventLog.length <= lastEventCount) return;
1308
+ const newEvents = eventLog.slice(lastEventCount);
1309
+ lastEventCount = eventLog.length;
1310
+
1311
+ for (const msg of newEvents) {
1312
+ if (msg.includes('[ROMANCE]')) {
1313
+ showToast(msg.replace(/\s*\[ROMANCE\]\s*/, ''), 'romance');
1314
+ } else if (msg.includes('[EVENT]') && !msg.includes('Weather')) {
1315
+ showToast(msg.replace(/\s*\[EVENT\]\s*/, ''), 'event');
1316
+ } else if (msg.includes('[GOSSIP]')) {
1317
+ showToast(msg.replace(/\s*\[GOSSIP\]\s*/, ''), 'gossip');
1318
+ } else if (msg.includes('[CONV]') && msg.includes('starts talking')) {
1319
+ showToast(msg.replace(/\s*\[CONV\]\s*/, ''), 'conv');
1320
+ }
1321
+ }
1322
+ }
1323
+
1324
  // ============================================================
1325
  // INIT
1326
  // ============================================================