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
23
examples/cards/templates/todo-item-live/manifest.json
Normal file
23
examples/cards/templates/todo-item-live/manifest.json
Normal 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"
|
||||
}
|
||||
176
examples/cards/templates/todo-item-live/template.html
Normal file
176
examples/cards/templates/todo-item-live/template.html
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue