feat: add card examples and speed up rtc connect
Some checks failed
CI / Backend Checks (push) Failing after 29s
CI / Frontend Checks (push) Failing after 36s

This commit is contained in:
kacper 2026-03-15 21:44:47 -04:00
parent 04afead5af
commit 23fd806e6d
41 changed files with 3327 additions and 3 deletions

View file

@ -0,0 +1,8 @@
{
"key": "live-calendar-today",
"title": "Live Calendar Today",
"notes": "Use for today's calendar summary. Pull events through the Home Assistant calendar_get_events MCP tool, optionally pinning calendar_names in template_state.",
"created_at": "2026-03-09T00:00:00+00:00",
"updated_at": "2026-03-11T04:12:48.601255+00:00",
"deprecated": true
}

View file

@ -0,0 +1,273 @@
<div data-calendar-card="today" style="font-family: var(--card-font, 'Iosevka', 'SF Mono', ui-monospace, Menlo, Consolas, monospace); background: #ffffff; color: #111827; border: 1px solid #e5e7eb; border-radius: 16px; box-shadow: 0 8px 24px rgba(17, 24, 39, 0.12); padding: 24px; max-width: 640px;">
<div style="display:flex; align-items:flex-start; justify-content:space-between; gap:12px; margin-bottom: 12px;">
<div>
<h2 style="margin:0; font-size:1.15rem; font-weight:700; color:#111827;">Today's Calendar</h2>
<div data-cal-subtitle style="margin-top:4px; font-size:0.84rem; color:#6b7280;">Loading calendars...</div>
</div>
<span data-cal-status style="font-size:0.82rem; color:#6b7280;">Loading...</span>
</div>
<div style="font-size:0.88rem; color:#6b7280; margin-bottom:10px;">
Date: <strong data-cal-date style="color:#374151;">--</strong>
</div>
<div data-cal-empty style="display:none; padding:12px; border-radius:10px; background:#f8fafc; color:#475569; font-size:0.94rem;">
No events for today.
</div>
<ul data-cal-list style="list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:10px;"></ul>
<div style="margin-top:12px; font-size:0.84rem; color:#6b7280;">
Updated: <span data-cal-updated>--</span>
</div>
</div>
<script>
(() => {
const script = document.currentScript;
const root = script?.closest('[data-nanobot-card-root]');
const state = window.__nanobotGetCardState?.(script) || {};
if (!(root instanceof HTMLElement)) return;
const statusEl = root.querySelector("[data-cal-status]");
const subtitleEl = root.querySelector("[data-cal-subtitle]");
const dateEl = root.querySelector("[data-cal-date]");
const listEl = root.querySelector("[data-cal-list]");
const emptyEl = root.querySelector("[data-cal-empty]");
const updatedEl = root.querySelector("[data-cal-updated]");
if (!(statusEl instanceof HTMLElement) || !(subtitleEl instanceof HTMLElement) || !(dateEl instanceof HTMLElement) ||
!(listEl instanceof HTMLElement) || !(emptyEl instanceof HTMLElement) || !(updatedEl instanceof HTMLElement)) {
return;
}
const configuredToolName = typeof state.tool_name === 'string' ? state.tool_name.trim() : '';
const configuredCalendarNames = Array.isArray(state.calendar_names)
? state.calendar_names.map((value) => String(value || '').trim()).filter(Boolean)
: [];
const refreshMsRaw = Number(state.refresh_ms);
const REFRESH_MS = Number.isFinite(refreshMsRaw) && refreshMsRaw >= 60000 ? refreshMsRaw : 15 * 60 * 1000;
const setStatus = (label, color) => {
statusEl.textContent = label;
statusEl.style.color = color;
};
const updateLiveContent = (snapshot) => {
window.__nanobotSetCardLiveContent?.(script, snapshot);
};
const dayBounds = () => {
const now = new Date();
const start = new Date(now);
start.setHours(0, 0, 0, 0);
const end = new Date(now);
end.setHours(23, 59, 59, 999);
return { start, end };
};
const formatDateHeader = () => {
const now = new Date();
return now.toLocaleDateString([], { weekday: "long", month: "short", day: "numeric", year: "numeric" });
};
const normalizeDateValue = (value) => {
if (typeof value === "string") return value;
if (value && typeof value === "object") {
if (typeof value.dateTime === "string") return value.dateTime;
if (typeof value.date === "string") return value.date;
}
return "";
};
const formatTime = (value) => {
const raw = normalizeDateValue(value);
if (!raw) return "--:--";
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) return "All day";
const d = new Date(raw);
if (Number.isNaN(d.getTime())) return "--:--";
return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
};
const isAllDay = (start, end) => {
const s = normalizeDateValue(start);
const e = normalizeDateValue(end);
return /^\d{4}-\d{2}-\d{2}$/.test(s) || /^\d{4}-\d{2}-\d{2}$/.test(e);
};
const eventSortKey = (evt) => {
const raw = normalizeDateValue(evt && evt.start);
const t = new Date(raw).getTime();
return Number.isFinite(t) ? t : Number.MAX_SAFE_INTEGER;
};
const extractEvents = (toolResult) => {
if (toolResult && toolResult.parsed && typeof toolResult.parsed === 'object') {
const parsed = toolResult.parsed;
if (Array.isArray(parsed.result)) return parsed.result;
}
if (typeof toolResult?.content === 'string') {
try {
const parsed = JSON.parse(toolResult.content);
if (parsed && typeof parsed === 'object' && Array.isArray(parsed.result)) {
return parsed.result;
}
} catch {
return [];
}
}
return [];
};
const resolveToolConfig = async () => {
const fallbackName = configuredToolName || 'mcp_home_assistant_calendar_get_events';
if (!window.__nanobotListTools) {
return { name: fallbackName, calendars: configuredCalendarNames };
}
try {
const tools = await window.__nanobotListTools();
const tool = Array.isArray(tools)
? tools.find((item) => /(^|_)calendar_get_events$/i.test(String(item?.name || '')))
: null;
const enumValues = Array.isArray(tool?.parameters?.properties?.calendar?.enum)
? tool.parameters.properties.calendar.enum.map((value) => String(value || '').trim()).filter(Boolean)
: [];
return {
name: tool?.name || fallbackName,
calendars: configuredCalendarNames.length > 0 ? configuredCalendarNames : enumValues,
};
} catch {
return { name: fallbackName, calendars: configuredCalendarNames };
}
};
const renderEvents = (events) => {
listEl.innerHTML = "";
if (!Array.isArray(events) || events.length === 0) {
emptyEl.style.display = "block";
return;
}
emptyEl.style.display = "none";
for (const evt of events) {
const li = document.createElement("li");
li.style.border = "1px solid #e5e7eb";
li.style.borderRadius = "10px";
li.style.padding = "10px 12px";
li.style.background = "#ffffff";
const summary = document.createElement("div");
summary.style.fontSize = "0.96rem";
summary.style.fontWeight = "600";
summary.style.color = "#111827";
summary.textContent = String(evt.summary || "(No title)");
const timing = document.createElement("div");
timing.style.marginTop = "4px";
timing.style.fontSize = "0.86rem";
timing.style.color = "#475569";
if (isAllDay(evt.start, evt.end)) {
timing.textContent = "All day";
} else {
timing.textContent = `${formatTime(evt.start)} - ${formatTime(evt.end)}`;
}
li.appendChild(summary);
li.appendChild(timing);
if (evt.location) {
const location = document.createElement("div");
location.style.marginTop = "4px";
location.style.fontSize = "0.84rem";
location.style.color = "#6b7280";
location.textContent = `Location: ${String(evt.location)}`;
li.appendChild(location);
}
listEl.appendChild(li);
}
};
const refresh = async () => {
dateEl.textContent = formatDateHeader();
setStatus("Refreshing", "#6b7280");
try {
const toolConfig = await resolveToolConfig();
if (!toolConfig.name) throw new Error('Calendar tool unavailable');
if (!Array.isArray(toolConfig.calendars) || toolConfig.calendars.length === 0) {
subtitleEl.textContent = "No Home Assistant calendars available";
renderEvents([]);
updatedEl.textContent = new Date().toLocaleTimeString();
setStatus("OK", "#047857");
updateLiveContent({
kind: 'calendar_today',
tool_name: toolConfig.name,
calendar_names: [],
updated_at: updatedEl.textContent || null,
event_count: 0,
events: [],
});
return;
}
subtitleEl.textContent = `${toolConfig.calendars.length} calendar${toolConfig.calendars.length === 1 ? "" : "s"}`;
const allEvents = [];
for (const calendarName of toolConfig.calendars) {
const toolResult = await window.__nanobotCallTool?.(toolConfig.name, {
calendar: calendarName,
range: 'today',
});
const events = extractEvents(toolResult);
for (const evt of events) {
allEvents.push({ ...evt, _calendarName: calendarName });
}
}
allEvents.sort((a, b) => eventSortKey(a) - eventSortKey(b));
renderEvents(allEvents);
updatedEl.textContent = new Date().toLocaleTimeString();
setStatus("Daily", "#047857");
updateLiveContent({
kind: 'calendar_today',
tool_name: toolConfig.name,
calendar_names: toolConfig.calendars,
updated_at: updatedEl.textContent || null,
event_count: allEvents.length,
events: allEvents.map((evt) => ({
summary: String(evt.summary || '(No title)'),
start: normalizeDateValue(evt.start) || null,
end: normalizeDateValue(evt.end) || null,
location: typeof evt.location === 'string' ? evt.location : null,
calendar_name: evt._calendarName || null,
})),
});
} catch (err) {
subtitleEl.textContent = "Could not load Home Assistant calendar";
renderEvents([]);
updatedEl.textContent = String(err);
setStatus("Unavailable", "#b91c1c");
updateLiveContent({
kind: 'calendar_today',
tool_name: configuredToolName || 'mcp_home_assistant_calendar_get_events',
calendar_names: configuredCalendarNames,
updated_at: String(err),
event_count: 0,
events: [],
error: String(err),
});
}
};
window.__nanobotSetCardRefresh?.(script, () => {
void refresh();
});
void refresh();
window.setInterval(() => {
void refresh();
}, REFRESH_MS);
})();
</script>