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,23 @@
{
"key": "todo-item-live",
"title": "Todo Item",
"notes": "Source-generated card for a single Home Assistant todo item. Do not use a live fetch URL. A card source script writes one card instance per todo uid and fills template_state with the current item fields plus source_id for refresh. The card completes the task by calling the Home Assistant HassListCompleteItem MCP tool with list_name and the task summary.",
"example_state": {
"source_id": "ha-todo-kacpers-to-do",
"entity_id": "todo.kacpers_to_do",
"list_name": "Kacper's To-Do",
"uid": "55be123e-1ef3-11f1-b5e6-001e06480aef",
"summary": "Get sneakers",
"complete_tool_name": "mcp_home_assistant_HassListCompleteItem",
"complete_item": "Get sneakers",
"status": "needs_action",
"completed": false,
"due": null,
"due_datetime": null,
"description": null,
"generated_at": "2026-03-13T16:03:26+00:00",
"can_complete": true
},
"created_at": "2026-03-13T00:00:00+00:00",
"updated_at": "2026-03-13T00:00:00+00:00"
}

View file

@ -0,0 +1,176 @@
<div data-todo-item-card style="font-family: var(--card-font, 'Iosevka', 'SF Mono', ui-monospace, Menlo, Consolas, monospace); background:#f7ecdf; color:#65483a; padding:12px 14px;">
<div style="display:flex; align-items:flex-start; justify-content:space-between; gap:10px;">
<div style="min-width:0; flex:1 1 auto;">
<div data-todo-subtitle style="font-size:0.78rem; line-height:1.25; color:#9a7b68; font-weight:700; text-transform:uppercase; letter-spacing:0.08em;">Todo</div>
<div data-todo-summary style="margin-top:6px; font-size:1.08rem; line-height:1.15; font-weight:800; letter-spacing:-0.02em; color:#65483a; word-break:break-word;">Loading...</div>
</div>
<span data-todo-status style="display:none; font-size:0.78rem; line-height:1.2; font-weight:800; color:#9a6a2f; background:#f4e2b8; white-space:nowrap; border-radius:999px; padding:4px 8px;"></span>
</div>
<div data-todo-due style="display:none; margin-top:8px; font-size:0.82rem; line-height:1.35; color:#947662; font-weight:700;"></div>
<div data-todo-description style="display:none; margin-top:8px; font-size:0.82rem; line-height:1.45; color:#7d5f4e;"></div>
<div style="margin-top:10px; display:flex; align-items:center; gap:10px;">
<button data-todo-complete type="button" style="display:none; border:none; border-radius:999px; padding:6px 10px; background:#dfe9d8; color:#35562c; font:800 0.76rem/1 var(--card-font, 'Iosevka', 'SF Mono', ui-monospace, Menlo, Consolas, monospace); cursor:pointer;">Done</button>
</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-todo-subtitle]');
const statusEl = root.querySelector('[data-todo-status]');
const summaryEl = root.querySelector('[data-todo-summary]');
const dueEl = root.querySelector('[data-todo-due]');
const descriptionEl = root.querySelector('[data-todo-description]');
const completeEl = root.querySelector('[data-todo-complete]');
if (!(subtitleEl instanceof HTMLElement) || !(statusEl instanceof HTMLElement) || !(summaryEl instanceof HTMLElement) || !(dueEl instanceof HTMLElement) || !(descriptionEl instanceof HTMLElement) || !(completeEl instanceof HTMLButtonElement)) return;
const sourceId = typeof state.source_id === 'string' ? state.source_id.trim() : '';
const entityId = typeof state.entity_id === 'string' ? state.entity_id.trim() : '';
const uid = typeof state.uid === 'string' ? state.uid.trim() : '';
const summary = typeof state.summary === 'string' ? state.summary.trim() : '';
const listName = typeof state.list_name === 'string' ? state.list_name.trim() : '';
const rawStatus = typeof state.status === 'string' ? state.status.trim() : 'needs_action';
const completed = Boolean(state.completed) || rawStatus === 'completed';
const due = typeof state.due === 'string' ? state.due.trim() : '';
const dueDateTime = typeof state.due_datetime === 'string' ? state.due_datetime.trim() : '';
const description = typeof state.description === 'string' ? state.description.trim() : '';
const configuredCompleteTool = typeof state.complete_tool_name === 'string' ? state.complete_tool_name.trim() : '';
const completeItem = typeof state.complete_item === 'string' ? state.complete_item.trim() : summary;
const canComplete = Boolean(state.can_complete) && Boolean(listName) && Boolean(completeItem) && !completed;
const setStatus = (label, fg, bg) => {
statusEl.textContent = label;
statusEl.style.color = fg;
statusEl.style.background = bg;
statusEl.style.display = label ? 'inline-flex' : 'none';
};
const formatDue = () => {
if (dueDateTime) {
const parsed = new Date(dueDateTime);
if (!Number.isNaN(parsed.getTime())) {
return `Due ${parsed.toLocaleString([], { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })}`;
}
return `Due ${dueDateTime}`;
}
if (due) {
const parsed = new Date(`${due}T00:00:00`);
if (!Number.isNaN(parsed.getTime())) {
return `Due ${parsed.toLocaleDateString([], { month: 'short', day: 'numeric' })}`;
}
return `Due ${due}`;
}
return completed ? 'Completed' : '';
};
const publishLiveContent = (statusValue, exists = true, error = '') => {
window.__nanobotSetCardLiveContent?.(script, {
kind: 'ha_todo_item',
exists,
source_id: sourceId || null,
entity_id: entityId || null,
list_name: listName || null,
uid: uid || null,
status: statusValue,
summary: summary || null,
due: due || null,
due_datetime: dueDateTime || null,
description: description || null,
complete_tool_name: configuredCompleteTool || null,
can_complete: canComplete,
error: error || null,
});
};
const resolveCompleteToolName = async () => {
if (configuredCompleteTool) return configuredCompleteTool;
if (!window.__nanobotListTools) return 'mcp_home_assistant_HassListCompleteItem';
try {
const tools = await window.__nanobotListTools();
const completeTool = Array.isArray(tools)
? tools.find((tool) => /(^|_)HassListCompleteItem$/i.test(String(tool?.name || '')))
: null;
return completeTool?.name || 'mcp_home_assistant_HassListCompleteItem';
} catch {
return 'mcp_home_assistant_HassListCompleteItem';
}
};
const render = () => {
subtitleEl.textContent = listName || 'Todo';
summaryEl.textContent = summary || '(Untitled task)';
const dueText = formatDue();
dueEl.textContent = dueText;
dueEl.style.display = dueText ? 'block' : 'none';
descriptionEl.textContent = description;
descriptionEl.style.display = description ? 'block' : 'none';
if (completed) {
setStatus('Done', '#6c8b63', '#dfe9d8');
summaryEl.style.textDecoration = 'line-through';
summaryEl.style.color = '#7d5f4e';
completeEl.style.display = 'none';
completeEl.disabled = true;
} else {
setStatus('', '', 'transparent');
summaryEl.style.textDecoration = 'none';
summaryEl.style.color = '#65483a';
if (canComplete) {
completeEl.style.display = 'inline-flex';
completeEl.disabled = false;
} else {
completeEl.style.display = 'none';
completeEl.disabled = true;
}
}
publishLiveContent(completed ? 'completed' : rawStatus, true, '');
};
const requestCardResync = async () => {
if (!sourceId) return;
await fetch('/cards/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ source_id: sourceId }),
});
window.dispatchEvent(new Event('nanobot:cards-refresh'));
};
completeEl.addEventListener('click', async (event) => {
event.preventDefault();
event.stopPropagation();
if (!canComplete) return;
completeEl.disabled = true;
setStatus('Saving', '#9a7b68', 'rgba(231, 220, 209, 0.92)');
try {
const completeToolName = await resolveCompleteToolName();
await window.__nanobotCallTool?.(completeToolName, {
name: listName,
item: completeItem,
});
setStatus('Done', '#6c8b63', '#dfe9d8');
summaryEl.style.textDecoration = 'line-through';
summaryEl.style.color = '#7d5f4e';
dueEl.textContent = 'Completed';
dueEl.style.display = 'block';
completeEl.style.display = 'none';
publishLiveContent('completed', true, '');
await requestCardResync();
} catch (error) {
setStatus('Unavailable', '#a14d43', '#f3d8d2');
console.error('Todo completion failed', error);
completeEl.disabled = false;
publishLiveContent(rawStatus, true, String(error));
}
});
render();
})();
</script>