crimata-os/ui/components/contacts/card.html
Andrew Gundersen 31b5e321a1 Add crimata-agent and redesign agentic OS UI
- crimata-agent (TypeScript, port 7702): WebSocket server + HTTP /events
  endpoint, Claude tool-use loop (render/remove/call_api/show_cursor_response),
  session.start fires on first browser connection to seed the canvas
- OS UI redesigned: light grey canvas (#e8e8e8), no dock bar, cursor dot,
  Cmd+Space input bubble above cursor, elements placed by agent
- Canvas elements: app_icon (floating icons with running dot), window (no-chrome
  content cards), cursor_response (text + action pills)
- Component system: HTML fragments fetched from /components/{app}/{name}.html,
  inline scripts re-executed after injection
- contacts/crimata.json: added icon, defaultComponent, components[], api[]
- dock: apps.h/c/main.c extended to parse and serve components + api from
  crimata.json; BUF_SIZE increased for richer JSON output
- ui/components/contacts/list.html + card.html: first component pair
- provision.sh: installs crimata-agent, adds WebSocket + /events + /components
  nginx locations, fixes /auth/ prefix stripping

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 15:52:05 -05:00

41 lines
1.7 KiB
HTML

<div class="contact-card">
<div class="cc-avatar" id="cc-avatar">?</div>
<h2 id="cc-name">—</h2>
<div class="cc-fields" id="cc-fields"></div>
</div>
<style>
.contact-card { padding: 28px 24px; font-family: -apple-system, sans-serif; text-align: center; width: 280px; }
.cc-avatar { width: 64px; height: 64px; background: #e8e8e8; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 26px; font-weight: 600; color: #666; margin: 0 auto 14px; }
.contact-card h2 { font-size: 18px; font-weight: 600; color: #1a1a1a; margin-bottom: 14px; }
.cc-fields { text-align: left; display: flex; flex-direction: column; gap: 8px; }
.cc-field { font-size: 13px; color: #888; }
.cc-field span { font-weight: 500; color: #333; }
.cc-empty { color: #aaa; font-size: 13px; }
</style>
<script>
(function () {
const props = window.__componentProps || {}
const id = props.id || props.contactId
if (!id) {
document.getElementById('cc-name').textContent = 'No contact selected'
return
}
fetch(`/apps/contacts/${id}`)
.then(r => r.ok ? r.json() : Promise.reject())
.then(c => {
document.getElementById('cc-avatar').textContent = (c.name || '?')[0].toUpperCase()
document.getElementById('cc-name').textContent = c.name || '—'
const fields = document.getElementById('cc-fields')
fields.innerHTML = [
c.domain ? `<div class="cc-field">Domain: <span>${c.domain}</span></div>` : '',
c.notes ? `<div class="cc-field">Notes: <span>${c.notes}</span></div>` : '',
].join('')
if (!fields.innerHTML) fields.innerHTML = '<div class="cc-empty">No details.</div>'
})
.catch(() => {
document.getElementById('cc-name').textContent = 'Contact not found'
})
})()
</script>