feat: add card examples and speed up rtc connect
This commit is contained in:
parent
04afead5af
commit
23fd806e6d
41 changed files with 3327 additions and 3 deletions
257
examples/cards/templates/calendar-agenda-live/template.html
Normal file
257
examples/cards/templates/calendar-agenda-live/template.html
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
<div data-calendar-card style="font-family: var(--card-font, 'Iosevka', 'SF Mono', ui-monospace, Menlo, Consolas, monospace); background:#ffffff; color:#111827; padding:14px 16px;">
|
||||
<div style="display:flex; align-items:flex-start; justify-content:space-between; gap:12px; margin-bottom:8px;">
|
||||
<div data-calendar-subtitle style="font-size:0.86rem; line-height:1.35; color:#4b5563; font-weight:600;">Loading…</div>
|
||||
</div>
|
||||
|
||||
<div data-calendar-range style="font-size:0.88rem; line-height:1.35; color:#6b7280; margin-bottom:6px;">--</div>
|
||||
<div data-calendar-empty style="display:none; padding:10px 0 2px; color:#475569; font-size:0.96rem; line-height:1.4;">No events scheduled.</div>
|
||||
<ul data-calendar-list style="list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:0;"></ul>
|
||||
<div style="margin-top:8px; font-size:0.82rem; line-height:1.35; color:#6b7280;">Updated <span data-calendar-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 subtitleEl = root.querySelector('[data-calendar-subtitle]');
|
||||
const statusEl = root.querySelector('[data-calendar-status]');
|
||||
const rangeEl = root.querySelector('[data-calendar-range]');
|
||||
const emptyEl = root.querySelector('[data-calendar-empty]');
|
||||
const listEl = root.querySelector('[data-calendar-list]');
|
||||
const updatedEl = root.querySelector('[data-calendar-updated]');
|
||||
if (!(subtitleEl instanceof HTMLElement) || !(rangeEl instanceof HTMLElement) || !(emptyEl instanceof HTMLElement) || !(listEl instanceof HTMLElement) || !(updatedEl instanceof HTMLElement)) return;
|
||||
|
||||
const subtitle = typeof state.subtitle === 'string' ? state.subtitle : '';
|
||||
const configuredToolName = typeof state.tool_name === 'string' ? state.tool_name.trim() : '';
|
||||
const calendarNames = Array.isArray(state.calendar_names)
|
||||
? state.calendar_names.map((value) => String(value || '').trim()).filter(Boolean)
|
||||
: [];
|
||||
const rangeDaysRaw = Number(state.range_days);
|
||||
const rangeDays = Number.isFinite(rangeDaysRaw) && rangeDaysRaw >= 1 ? Math.min(rangeDaysRaw, 7) : 1;
|
||||
const maxEventsRaw = Number(state.max_events);
|
||||
const maxEvents = Number.isFinite(maxEventsRaw) && maxEventsRaw >= 1 ? Math.min(maxEventsRaw, 30) : 8;
|
||||
const refreshMsRaw = Number(state.refresh_ms);
|
||||
const refreshMs = Number.isFinite(refreshMsRaw) && refreshMsRaw >= 60000 ? refreshMsRaw : 15 * 60 * 1000;
|
||||
const emptyText = typeof state.empty_text === 'string' && state.empty_text.trim() ? state.empty_text.trim() : 'No events scheduled.';
|
||||
|
||||
subtitleEl.textContent = subtitle || (calendarNames.length > 0 ? calendarNames.join(', ') : 'Loading calendars');
|
||||
emptyEl.textContent = emptyText;
|
||||
const updateLiveContent = (snapshot) => {
|
||||
window.__nanobotSetCardLiveContent?.(script, snapshot);
|
||||
};
|
||||
|
||||
const setStatus = (label, color) => {
|
||||
if (!(statusEl instanceof HTMLElement)) return;
|
||||
statusEl.textContent = label;
|
||||
statusEl.style.color = color;
|
||||
};
|
||||
|
||||
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 isAllDay = (start, end) => /^\d{4}-\d{2}-\d{2}$/.test(normalizeDateValue(start)) || /^\d{4}-\d{2}-\d{2}$/.test(normalizeDateValue(end));
|
||||
const eventSortKey = (event) => {
|
||||
const raw = normalizeDateValue(event && event.start);
|
||||
const time = new Date(raw).getTime();
|
||||
return Number.isFinite(time) ? time : Number.MAX_SAFE_INTEGER;
|
||||
};
|
||||
const formatTime = (value) => {
|
||||
const raw = normalizeDateValue(value);
|
||||
if (!raw) return '--:--';
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) return 'All day';
|
||||
const date = new Date(raw);
|
||||
if (Number.isNaN(date.getTime())) return '--:--';
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
const formatDay = (value) => {
|
||||
const raw = normalizeDateValue(value);
|
||||
if (!raw) return '--';
|
||||
const date = /^\d{4}-\d{2}-\d{2}$/.test(raw) ? new Date(`${raw}T00:00:00`) : new Date(raw);
|
||||
if (Number.isNaN(date.getTime())) return '--';
|
||||
return date.toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric' });
|
||||
};
|
||||
const formatRange = (start, end) => `${start.toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric' })} to ${end.toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric' })}`;
|
||||
|
||||
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 eventTime = (value) => {
|
||||
const raw = normalizeDateValue(value);
|
||||
if (!raw) return Number.MAX_SAFE_INTEGER;
|
||||
const normalized = /^\d{4}-\d{2}-\d{2}$/.test(raw) ? `${raw}T00:00:00` : raw;
|
||||
const time = new Date(normalized).getTime();
|
||||
return Number.isFinite(time) ? time : Number.MAX_SAFE_INTEGER;
|
||||
};
|
||||
|
||||
const resolveToolConfig = async () => {
|
||||
const fallbackName = configuredToolName || 'mcp_home_assistant_calendar_get_events';
|
||||
if (!window.__nanobotListTools) {
|
||||
return { name: fallbackName, availableCalendars: calendarNames };
|
||||
}
|
||||
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,
|
||||
availableCalendars: enumValues,
|
||||
};
|
||||
} catch {
|
||||
return { name: fallbackName, availableCalendars: calendarNames };
|
||||
}
|
||||
};
|
||||
|
||||
const renderEvents = (events) => {
|
||||
listEl.innerHTML = '';
|
||||
if (!Array.isArray(events) || events.length === 0) {
|
||||
emptyEl.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
emptyEl.style.display = 'none';
|
||||
|
||||
for (const [index, event] of events.slice(0, maxEvents).entries()) {
|
||||
const item = document.createElement('li');
|
||||
item.style.padding = index === 0 ? '10px 0 8px' : '10px 0 8px';
|
||||
item.style.borderTop = index === 0 ? '1px solid #e5e7eb' : '1px solid #e5e7eb';
|
||||
|
||||
const summary = document.createElement('div');
|
||||
summary.style.fontSize = '0.98rem';
|
||||
summary.style.lineHeight = '1.3';
|
||||
summary.style.fontWeight = '700';
|
||||
summary.style.color = '#111827';
|
||||
summary.textContent = String(event.summary || '(No title)');
|
||||
item.appendChild(summary);
|
||||
|
||||
const timing = document.createElement('div');
|
||||
timing.style.marginTop = '4px';
|
||||
timing.style.fontSize = '0.9rem';
|
||||
timing.style.lineHeight = '1.35';
|
||||
timing.style.color = '#4b5563';
|
||||
const dayLabel = formatDay(event.start);
|
||||
const timeLabel = isAllDay(event.start, event.end) ? 'All day' : `${formatTime(event.start)} - ${formatTime(event.end)}`;
|
||||
timing.textContent = dayLabel === '--' ? timeLabel : `${dayLabel} · ${timeLabel}`;
|
||||
item.appendChild(timing);
|
||||
|
||||
listEl.appendChild(item);
|
||||
}
|
||||
};
|
||||
|
||||
const refresh = async () => {
|
||||
setStatus('Refreshing', '#6b7280');
|
||||
const start = new Date();
|
||||
start.setHours(0, 0, 0, 0);
|
||||
const end = new Date(start);
|
||||
end.setDate(end.getDate() + Math.max(rangeDays - 1, 0));
|
||||
end.setHours(23, 59, 59, 999);
|
||||
rangeEl.textContent = formatRange(start, end);
|
||||
|
||||
try {
|
||||
const toolConfig = await resolveToolConfig();
|
||||
const selectedCalendars = calendarNames.length > 0 ? calendarNames : toolConfig.availableCalendars;
|
||||
if (!toolConfig.name) throw new Error('Calendar tool unavailable');
|
||||
if (!Array.isArray(selectedCalendars) || selectedCalendars.length === 0) {
|
||||
throw new Error('No calendars configured');
|
||||
}
|
||||
|
||||
const resolvedSubtitle = subtitle || selectedCalendars.join(', ');
|
||||
subtitleEl.textContent = resolvedSubtitle;
|
||||
|
||||
const allEvents = [];
|
||||
const rangeMode = rangeDays > 1 ? 'week' : 'today';
|
||||
const endExclusiveTime = end.getTime() + 1;
|
||||
for (const calendarName of selectedCalendars) {
|
||||
const toolResult = await window.__nanobotCallTool?.(toolConfig.name, {
|
||||
calendar: calendarName,
|
||||
range: rangeMode,
|
||||
});
|
||||
const events = extractEvents(toolResult);
|
||||
for (const event of events) {
|
||||
const startTime = eventTime(event?.start);
|
||||
if (startTime < start.getTime() || startTime >= endExclusiveTime) continue;
|
||||
allEvents.push({ ...event, _calendarName: calendarName });
|
||||
}
|
||||
}
|
||||
|
||||
allEvents.sort((left, right) => eventSortKey(left) - eventSortKey(right));
|
||||
renderEvents(allEvents);
|
||||
const updatedText = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
updatedEl.textContent = updatedText;
|
||||
const statusLabel = rangeDays > 1 ? `${rangeDays}-day` : 'Today';
|
||||
setStatus(statusLabel, '#047857');
|
||||
const snapshotEvents = allEvents.slice(0, maxEvents).map((event) => {
|
||||
const dayLabel = formatDay(event.start);
|
||||
const timeLabel = isAllDay(event.start, event.end) ? 'All day' : `${formatTime(event.start)} - ${formatTime(event.end)}`;
|
||||
return {
|
||||
summary: String(event.summary || '(No title)'),
|
||||
start: normalizeDateValue(event.start) || null,
|
||||
end: normalizeDateValue(event.end) || null,
|
||||
day_label: dayLabel === '--' ? null : dayLabel,
|
||||
time_label: timeLabel,
|
||||
all_day: isAllDay(event.start, event.end),
|
||||
};
|
||||
});
|
||||
updateLiveContent({
|
||||
kind: 'calendar_agenda',
|
||||
subtitle: resolvedSubtitle || null,
|
||||
tool_name: toolConfig.name,
|
||||
calendar_names: selectedCalendars,
|
||||
range_label: rangeEl.textContent || null,
|
||||
status: statusLabel,
|
||||
updated_at: updatedText,
|
||||
event_count: snapshotEvents.length,
|
||||
events: snapshotEvents,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorText = String(error);
|
||||
renderEvents([]);
|
||||
updatedEl.textContent = errorText;
|
||||
setStatus('Unavailable', '#b91c1c');
|
||||
updateLiveContent({
|
||||
kind: 'calendar_agenda',
|
||||
subtitle: subtitleEl.textContent || null,
|
||||
tool_name: configuredToolName || 'mcp_home_assistant_calendar_get_events',
|
||||
calendar_names: calendarNames,
|
||||
range_label: rangeEl.textContent || null,
|
||||
status: 'Unavailable',
|
||||
updated_at: errorText,
|
||||
event_count: 0,
|
||||
events: [],
|
||||
error: errorText,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.__nanobotSetCardRefresh?.(script, () => {
|
||||
void refresh();
|
||||
});
|
||||
void refresh();
|
||||
window.setInterval(() => { void refresh(); }, refreshMs);
|
||||
})();
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue