2026-03-18 23:52:06 +00:00
|
|
|
import { getAgentDefs } from './agents.js';
|
2026-03-19 01:59:41 +00:00
|
|
|
import { AGENT_DEFS, colorToCss } from './agent-defs.js';
|
2026-03-18 23:52:06 +00:00
|
|
|
|
|
|
|
|
const $agentCount = document.getElementById('agent-count');
|
|
|
|
|
const $activeJobs = document.getElementById('active-jobs');
|
|
|
|
|
const $fps = document.getElementById('fps');
|
|
|
|
|
const $agentList = document.getElementById('agent-list');
|
|
|
|
|
const $connStatus = document.getElementById('connection-status');
|
|
|
|
|
const $chatPanel = document.getElementById('chat-panel');
|
2026-03-19 01:59:41 +00:00
|
|
|
const $clearBtn = document.getElementById('chat-clear-btn');
|
2026-03-18 23:52:06 +00:00
|
|
|
|
|
|
|
|
const MAX_CHAT_ENTRIES = 12;
|
2026-03-19 01:59:41 +00:00
|
|
|
const MAX_STORED = 100;
|
|
|
|
|
const STORAGE_PREFIX = 'matrix:chat:';
|
|
|
|
|
|
2026-03-18 23:52:06 +00:00
|
|
|
const chatEntries = [];
|
2026-03-19 01:59:41 +00:00
|
|
|
const chatHistory = {};
|
2026-03-18 23:52:06 +00:00
|
|
|
|
fix: QA sprint v1 — 7 issues resolved
Fixes:
- #22 OrbitControls damping: call updateControls() in animate loop
- #23 Empty catch blocks: add console.warn + error surfacing to chat panel
- #24 escapeHtml: add quote escaping (" '), use in renderAgentList
- #25 WS reconnect: check close code (1000/1001) before reconnecting,
add exponential backoff + heartbeat zombie detection
- #26 IDLE state visibility: brighten from near-invisible to #005500
- #5 PWA: manifest.json, service worker (network-first), theme-color,
favicon, loading screen, safe-area-inset padding, apple-mobile-web-app
- #14 Adaptive render quality: new quality.js hardware detection (low/
medium/high tiers), tiered particle counts, grid density, antialias,
pixel ratio caps
New files:
- js/quality.js — hardware detection + quality tier logic
- manifest.json — PWA manifest
- public/sw.js — service worker (network-first with offline cache)
- public/favicon.svg — SVG favicon
- icons/icon-192.svg, icons/icon-512.svg — PWA icons
2026-03-19 00:14:27 +00:00
|
|
|
const IDLE_COLOR = '#005500';
|
|
|
|
|
const ACTIVE_COLOR = '#00ff41';
|
|
|
|
|
|
2026-03-19 01:59:41 +00:00
|
|
|
/* ── localStorage chat history ────────────────────────── */
|
|
|
|
|
|
|
|
|
|
function storageKey(agentId) {
|
|
|
|
|
return STORAGE_PREFIX + agentId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function loadChatHistory(agentId) {
|
|
|
|
|
try {
|
|
|
|
|
const raw = localStorage.getItem(storageKey(agentId));
|
|
|
|
|
if (!raw) return [];
|
|
|
|
|
const parsed = JSON.parse(raw);
|
|
|
|
|
if (!Array.isArray(parsed)) return [];
|
|
|
|
|
return parsed.filter(m =>
|
|
|
|
|
m && typeof m.agentLabel === 'string' && typeof m.text === 'string'
|
|
|
|
|
);
|
|
|
|
|
} catch {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function saveChatHistory(agentId, messages) {
|
|
|
|
|
try {
|
|
|
|
|
localStorage.setItem(storageKey(agentId), JSON.stringify(messages.slice(-MAX_STORED)));
|
|
|
|
|
} catch { /* quota exceeded or private mode */ }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatTimestamp(ts) {
|
|
|
|
|
const d = new Date(ts);
|
|
|
|
|
const hh = String(d.getHours()).padStart(2, '0');
|
|
|
|
|
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
|
|
|
return `${hh}:${mm}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function loadAllHistories() {
|
|
|
|
|
const all = [];
|
|
|
|
|
const agentIds = [...AGENT_DEFS.map(d => d.id), 'sys'];
|
|
|
|
|
for (const id of agentIds) {
|
|
|
|
|
const msgs = loadChatHistory(id);
|
|
|
|
|
chatHistory[id] = msgs;
|
|
|
|
|
all.push(...msgs);
|
|
|
|
|
}
|
|
|
|
|
all.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
|
|
|
|
for (const msg of all.slice(-MAX_CHAT_ENTRIES)) {
|
|
|
|
|
const entry = buildChatEntry(msg.agentLabel, msg.text, msg.cssColor, msg.timestamp);
|
|
|
|
|
chatEntries.push(entry);
|
|
|
|
|
$chatPanel.appendChild(entry);
|
|
|
|
|
}
|
|
|
|
|
$chatPanel.scrollTop = $chatPanel.scrollHeight;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearAllHistories() {
|
|
|
|
|
const agentIds = [...AGENT_DEFS.map(d => d.id), 'sys'];
|
|
|
|
|
for (const id of agentIds) {
|
|
|
|
|
localStorage.removeItem(storageKey(id));
|
|
|
|
|
chatHistory[id] = [];
|
|
|
|
|
}
|
|
|
|
|
while ($chatPanel.firstChild) $chatPanel.removeChild($chatPanel.firstChild);
|
|
|
|
|
chatEntries.length = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildChatEntry(agentLabel, message, cssColor, timestamp) {
|
|
|
|
|
const color = escapeAttr(cssColor || '#00ff41');
|
|
|
|
|
const entry = document.createElement('div');
|
|
|
|
|
entry.className = 'chat-entry';
|
|
|
|
|
const ts = timestamp ? `<span class="chat-ts">[${formatTimestamp(timestamp)}]</span> ` : '';
|
|
|
|
|
entry.innerHTML = `${ts}<span class="agent-name" style="color:${color}">${escapeHtml(agentLabel)}</span>: ${escapeHtml(message)}`;
|
|
|
|
|
return entry;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 23:52:06 +00:00
|
|
|
export function initUI() {
|
|
|
|
|
renderAgentList();
|
2026-03-19 01:59:41 +00:00
|
|
|
loadAllHistories();
|
|
|
|
|
if ($clearBtn) $clearBtn.addEventListener('click', clearAllHistories);
|
2026-03-18 23:52:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderAgentList() {
|
|
|
|
|
const defs = getAgentDefs();
|
|
|
|
|
$agentList.innerHTML = defs.map(a => {
|
fix: QA sprint v1 — 7 issues resolved
Fixes:
- #22 OrbitControls damping: call updateControls() in animate loop
- #23 Empty catch blocks: add console.warn + error surfacing to chat panel
- #24 escapeHtml: add quote escaping (" '), use in renderAgentList
- #25 WS reconnect: check close code (1000/1001) before reconnecting,
add exponential backoff + heartbeat zombie detection
- #26 IDLE state visibility: brighten from near-invisible to #005500
- #5 PWA: manifest.json, service worker (network-first), theme-color,
favicon, loading screen, safe-area-inset padding, apple-mobile-web-app
- #14 Adaptive render quality: new quality.js hardware detection (low/
medium/high tiers), tiered particle counts, grid density, antialias,
pixel ratio caps
New files:
- js/quality.js — hardware detection + quality tier logic
- manifest.json — PWA manifest
- public/sw.js — service worker (network-first with offline cache)
- public/favicon.svg — SVG favicon
- icons/icon-192.svg, icons/icon-512.svg — PWA icons
2026-03-19 00:14:27 +00:00
|
|
|
const css = escapeAttr(colorToCss(a.color));
|
|
|
|
|
const safeLabel = escapeHtml(a.label);
|
|
|
|
|
const safeId = escapeAttr(a.id);
|
2026-03-18 23:52:06 +00:00
|
|
|
return `<div class="agent-row">
|
|
|
|
|
<span class="label">[</span>
|
fix: QA sprint v1 — 7 issues resolved
Fixes:
- #22 OrbitControls damping: call updateControls() in animate loop
- #23 Empty catch blocks: add console.warn + error surfacing to chat panel
- #24 escapeHtml: add quote escaping (" '), use in renderAgentList
- #25 WS reconnect: check close code (1000/1001) before reconnecting,
add exponential backoff + heartbeat zombie detection
- #26 IDLE state visibility: brighten from near-invisible to #005500
- #5 PWA: manifest.json, service worker (network-first), theme-color,
favicon, loading screen, safe-area-inset padding, apple-mobile-web-app
- #14 Adaptive render quality: new quality.js hardware detection (low/
medium/high tiers), tiered particle counts, grid density, antialias,
pixel ratio caps
New files:
- js/quality.js — hardware detection + quality tier logic
- manifest.json — PWA manifest
- public/sw.js — service worker (network-first with offline cache)
- public/favicon.svg — SVG favicon
- icons/icon-192.svg, icons/icon-512.svg — PWA icons
2026-03-19 00:14:27 +00:00
|
|
|
<span style="color:${css}">${safeLabel}</span>
|
2026-03-18 23:52:06 +00:00
|
|
|
<span class="label">]</span>
|
fix: QA sprint v1 — 7 issues resolved
Fixes:
- #22 OrbitControls damping: call updateControls() in animate loop
- #23 Empty catch blocks: add console.warn + error surfacing to chat panel
- #24 escapeHtml: add quote escaping (" '), use in renderAgentList
- #25 WS reconnect: check close code (1000/1001) before reconnecting,
add exponential backoff + heartbeat zombie detection
- #26 IDLE state visibility: brighten from near-invisible to #005500
- #5 PWA: manifest.json, service worker (network-first), theme-color,
favicon, loading screen, safe-area-inset padding, apple-mobile-web-app
- #14 Adaptive render quality: new quality.js hardware detection (low/
medium/high tiers), tiered particle counts, grid density, antialias,
pixel ratio caps
New files:
- js/quality.js — hardware detection + quality tier logic
- manifest.json — PWA manifest
- public/sw.js — service worker (network-first with offline cache)
- public/favicon.svg — SVG favicon
- icons/icon-192.svg, icons/icon-512.svg — PWA icons
2026-03-19 00:14:27 +00:00
|
|
|
<span id="agent-state-${safeId}" style="color:${IDLE_COLOR}"> IDLE</span>
|
2026-03-18 23:52:06 +00:00
|
|
|
</div>`;
|
|
|
|
|
}).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function updateUI({ fps, agentCount, jobCount, connectionState }) {
|
|
|
|
|
$fps.textContent = `FPS: ${fps}`;
|
|
|
|
|
$agentCount.textContent = `AGENTS: ${agentCount}`;
|
|
|
|
|
$activeJobs.textContent = `JOBS: ${jobCount}`;
|
|
|
|
|
|
|
|
|
|
if (connectionState === 'connected') {
|
|
|
|
|
$connStatus.textContent = '● CONNECTED';
|
|
|
|
|
$connStatus.className = 'connected';
|
|
|
|
|
} else if (connectionState === 'connecting') {
|
|
|
|
|
$connStatus.textContent = '◌ CONNECTING...';
|
|
|
|
|
$connStatus.className = '';
|
|
|
|
|
} else {
|
|
|
|
|
$connStatus.textContent = '○ OFFLINE';
|
|
|
|
|
$connStatus.className = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const defs = getAgentDefs();
|
|
|
|
|
defs.forEach(a => {
|
|
|
|
|
const el = document.getElementById(`agent-state-${a.id}`);
|
|
|
|
|
if (el) {
|
|
|
|
|
el.textContent = ` ${a.state.toUpperCase()}`;
|
fix: QA sprint v1 — 7 issues resolved
Fixes:
- #22 OrbitControls damping: call updateControls() in animate loop
- #23 Empty catch blocks: add console.warn + error surfacing to chat panel
- #24 escapeHtml: add quote escaping (" '), use in renderAgentList
- #25 WS reconnect: check close code (1000/1001) before reconnecting,
add exponential backoff + heartbeat zombie detection
- #26 IDLE state visibility: brighten from near-invisible to #005500
- #5 PWA: manifest.json, service worker (network-first), theme-color,
favicon, loading screen, safe-area-inset padding, apple-mobile-web-app
- #14 Adaptive render quality: new quality.js hardware detection (low/
medium/high tiers), tiered particle counts, grid density, antialias,
pixel ratio caps
New files:
- js/quality.js — hardware detection + quality tier logic
- manifest.json — PWA manifest
- public/sw.js — service worker (network-first with offline cache)
- public/favicon.svg — SVG favicon
- icons/icon-192.svg, icons/icon-512.svg — PWA icons
2026-03-19 00:14:27 +00:00
|
|
|
el.style.color = a.state === 'active' ? ACTIVE_COLOR : IDLE_COLOR;
|
2026-03-18 23:52:06 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Append a line to the chat panel.
|
|
|
|
|
* @param {string} agentLabel — display name
|
|
|
|
|
* @param {string} message — message text (HTML-escaped before insertion)
|
|
|
|
|
* @param {string} cssColor — CSS color string, e.g. '#00ff88'
|
|
|
|
|
*/
|
feat: Workshop interaction layer — chat input, visitor presence, bark display (#40, #41, #42)
Implements the minimum viable conversation loop for Workshop #222:
visitor arrives → sends message → Timmy barks back.
- js/visitor.js: Visitor presence protocol (#41)
- visitor_entered on load (with device detection: ipad/desktop/mobile)
- visitor_left on unload or 30s hidden (iPad tab suspend)
- visitor_message dispatched from chat input
- visitor_interaction export for future tap-to-interact (#44)
- Session duration tracking
- js/bark.js: Bark display system (#42)
- showBark() renders prominent viewport toasts with typing animation
- Auto-dismiss after display time + typing duration
- Queue system (max 3 simultaneous, overflow queued)
- Demo barks in mock mode (Workshop-themed: 222, sovereignty, chain)
- Barks also logged permanently in chat panel
- index.html: Chat input bar (#40)
- Terminal-styled input + send button at viewport bottom
- Enter to send (desktop), button tap (iPad)
- Safe-area padding for notched devices
- Chat panel repositioned above input bar
- Bark container in upper viewport third
- js/websocket.js: New message handlers
- 'bark' message → showBark() dispatch
- 'ambient_state' message → placeholder for #43
- Demo barks start in mock mode
- js/ui.js: appendChatMessage() accepts optional CSS class
- Visitor messages styled differently from agent messages
Build: 18 modules, 0 errors
Tested: desktop (1280x800) + mobile (390x844) via Playwright
Closes #40, #41, #42
Ref: rockachopa/Timmy-time-dashboard#222, #243
2026-03-19 01:46:04 +00:00
|
|
|
export function appendChatMessage(agentLabel, message, cssColor, extraClass) {
|
2026-03-19 01:59:41 +00:00
|
|
|
const now = Date.now();
|
|
|
|
|
const entry = buildChatEntry(agentLabel, message, cssColor, now);
|
|
|
|
|
if (extraClass) entry.className += ' ' + extraClass;
|
fix: QA sprint v1 — 7 issues resolved
Fixes:
- #22 OrbitControls damping: call updateControls() in animate loop
- #23 Empty catch blocks: add console.warn + error surfacing to chat panel
- #24 escapeHtml: add quote escaping (" '), use in renderAgentList
- #25 WS reconnect: check close code (1000/1001) before reconnecting,
add exponential backoff + heartbeat zombie detection
- #26 IDLE state visibility: brighten from near-invisible to #005500
- #5 PWA: manifest.json, service worker (network-first), theme-color,
favicon, loading screen, safe-area-inset padding, apple-mobile-web-app
- #14 Adaptive render quality: new quality.js hardware detection (low/
medium/high tiers), tiered particle counts, grid density, antialias,
pixel ratio caps
New files:
- js/quality.js — hardware detection + quality tier logic
- manifest.json — PWA manifest
- public/sw.js — service worker (network-first with offline cache)
- public/favicon.svg — SVG favicon
- icons/icon-192.svg, icons/icon-512.svg — PWA icons
2026-03-19 00:14:27 +00:00
|
|
|
|
2026-03-18 23:52:06 +00:00
|
|
|
chatEntries.push(entry);
|
|
|
|
|
|
fix: QA sprint v1 — 7 issues resolved
Fixes:
- #22 OrbitControls damping: call updateControls() in animate loop
- #23 Empty catch blocks: add console.warn + error surfacing to chat panel
- #24 escapeHtml: add quote escaping (" '), use in renderAgentList
- #25 WS reconnect: check close code (1000/1001) before reconnecting,
add exponential backoff + heartbeat zombie detection
- #26 IDLE state visibility: brighten from near-invisible to #005500
- #5 PWA: manifest.json, service worker (network-first), theme-color,
favicon, loading screen, safe-area-inset padding, apple-mobile-web-app
- #14 Adaptive render quality: new quality.js hardware detection (low/
medium/high tiers), tiered particle counts, grid density, antialias,
pixel ratio caps
New files:
- js/quality.js — hardware detection + quality tier logic
- manifest.json — PWA manifest
- public/sw.js — service worker (network-first with offline cache)
- public/favicon.svg — SVG favicon
- icons/icon-192.svg, icons/icon-512.svg — PWA icons
2026-03-19 00:14:27 +00:00
|
|
|
while (chatEntries.length > MAX_CHAT_ENTRIES) {
|
2026-03-18 23:52:06 +00:00
|
|
|
const removed = chatEntries.shift();
|
fix: QA sprint v1 — 7 issues resolved
Fixes:
- #22 OrbitControls damping: call updateControls() in animate loop
- #23 Empty catch blocks: add console.warn + error surfacing to chat panel
- #24 escapeHtml: add quote escaping (" '), use in renderAgentList
- #25 WS reconnect: check close code (1000/1001) before reconnecting,
add exponential backoff + heartbeat zombie detection
- #26 IDLE state visibility: brighten from near-invisible to #005500
- #5 PWA: manifest.json, service worker (network-first), theme-color,
favicon, loading screen, safe-area-inset padding, apple-mobile-web-app
- #14 Adaptive render quality: new quality.js hardware detection (low/
medium/high tiers), tiered particle counts, grid density, antialias,
pixel ratio caps
New files:
- js/quality.js — hardware detection + quality tier logic
- manifest.json — PWA manifest
- public/sw.js — service worker (network-first with offline cache)
- public/favicon.svg — SVG favicon
- icons/icon-192.svg, icons/icon-512.svg — PWA icons
2026-03-19 00:14:27 +00:00
|
|
|
try { $chatPanel.removeChild(removed); } catch { /* already removed */ }
|
2026-03-18 23:52:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$chatPanel.appendChild(entry);
|
|
|
|
|
$chatPanel.scrollTop = $chatPanel.scrollHeight;
|
2026-03-19 01:59:41 +00:00
|
|
|
|
|
|
|
|
/* persist per-agent history */
|
|
|
|
|
const agentId = AGENT_DEFS.find(d => d.label === agentLabel)?.id || 'sys';
|
|
|
|
|
if (!chatHistory[agentId]) chatHistory[agentId] = [];
|
|
|
|
|
chatHistory[agentId].push({ agentLabel, text: message, cssColor, timestamp: now });
|
|
|
|
|
saveChatHistory(agentId, chatHistory[agentId]);
|
2026-03-18 23:52:06 +00:00
|
|
|
}
|
|
|
|
|
|
fix: QA sprint v1 — 7 issues resolved
Fixes:
- #22 OrbitControls damping: call updateControls() in animate loop
- #23 Empty catch blocks: add console.warn + error surfacing to chat panel
- #24 escapeHtml: add quote escaping (" '), use in renderAgentList
- #25 WS reconnect: check close code (1000/1001) before reconnecting,
add exponential backoff + heartbeat zombie detection
- #26 IDLE state visibility: brighten from near-invisible to #005500
- #5 PWA: manifest.json, service worker (network-first), theme-color,
favicon, loading screen, safe-area-inset padding, apple-mobile-web-app
- #14 Adaptive render quality: new quality.js hardware detection (low/
medium/high tiers), tiered particle counts, grid density, antialias,
pixel ratio caps
New files:
- js/quality.js — hardware detection + quality tier logic
- manifest.json — PWA manifest
- public/sw.js — service worker (network-first with offline cache)
- public/favicon.svg — SVG favicon
- icons/icon-192.svg, icons/icon-512.svg — PWA icons
2026-03-19 00:14:27 +00:00
|
|
|
/**
|
|
|
|
|
* Escape HTML text content — prevents tag injection.
|
|
|
|
|
*/
|
2026-03-18 23:52:06 +00:00
|
|
|
function escapeHtml(str) {
|
fix: QA sprint v1 — 7 issues resolved
Fixes:
- #22 OrbitControls damping: call updateControls() in animate loop
- #23 Empty catch blocks: add console.warn + error surfacing to chat panel
- #24 escapeHtml: add quote escaping (" '), use in renderAgentList
- #25 WS reconnect: check close code (1000/1001) before reconnecting,
add exponential backoff + heartbeat zombie detection
- #26 IDLE state visibility: brighten from near-invisible to #005500
- #5 PWA: manifest.json, service worker (network-first), theme-color,
favicon, loading screen, safe-area-inset padding, apple-mobile-web-app
- #14 Adaptive render quality: new quality.js hardware detection (low/
medium/high tiers), tiered particle counts, grid density, antialias,
pixel ratio caps
New files:
- js/quality.js — hardware detection + quality tier logic
- manifest.json — PWA manifest
- public/sw.js — service worker (network-first with offline cache)
- public/favicon.svg — SVG favicon
- icons/icon-192.svg, icons/icon-512.svg — PWA icons
2026-03-19 00:14:27 +00:00
|
|
|
return String(str)
|
|
|
|
|
.replace(/&/g, '&')
|
|
|
|
|
.replace(/</g, '<')
|
|
|
|
|
.replace(/>/g, '>')
|
|
|
|
|
.replace(/"/g, '"')
|
|
|
|
|
.replace(/'/g, ''');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Escape a value for use inside an HTML attribute (style="...", id="...").
|
|
|
|
|
*/
|
|
|
|
|
function escapeAttr(str) {
|
|
|
|
|
return String(str)
|
2026-03-18 23:52:06 +00:00
|
|
|
.replace(/&/g, '&')
|
fix: QA sprint v1 — 7 issues resolved
Fixes:
- #22 OrbitControls damping: call updateControls() in animate loop
- #23 Empty catch blocks: add console.warn + error surfacing to chat panel
- #24 escapeHtml: add quote escaping (" '), use in renderAgentList
- #25 WS reconnect: check close code (1000/1001) before reconnecting,
add exponential backoff + heartbeat zombie detection
- #26 IDLE state visibility: brighten from near-invisible to #005500
- #5 PWA: manifest.json, service worker (network-first), theme-color,
favicon, loading screen, safe-area-inset padding, apple-mobile-web-app
- #14 Adaptive render quality: new quality.js hardware detection (low/
medium/high tiers), tiered particle counts, grid density, antialias,
pixel ratio caps
New files:
- js/quality.js — hardware detection + quality tier logic
- manifest.json — PWA manifest
- public/sw.js — service worker (network-first with offline cache)
- public/favicon.svg — SVG favicon
- icons/icon-192.svg, icons/icon-512.svg — PWA icons
2026-03-19 00:14:27 +00:00
|
|
|
.replace(/"/g, '"')
|
|
|
|
|
.replace(/'/g, ''')
|
2026-03-18 23:52:06 +00:00
|
|
|
.replace(/</g, '<')
|
|
|
|
|
.replace(/>/g, '>');
|
|
|
|
|
}
|