akif/arclet-boards

Arclet Boards — private Kanban workspaces with AppID, DataK3, shared web and MCP actions.

A remix template · 1 remixes · Remix akif/arclet-boards

app/web/static/extras.js

'use strict';

function decorateExtras() {
  const toolbar = $('.board-toolbar .view-tabs');
  if (state.view === 'board' && toolbar && !$('#daily-summary-button')) {
    const button = document.createElement('button');
    button.id = 'daily-summary-button'; button.className = 'view-tab';
    button.dataset.extraAction = 'summary'; button.innerHTML = `${icon('spark')}Daily brief`;
    toolbar.append(button);
  }
  const form = $('#card-edit-form');
  if (!state.card || !form || $('#card-extras')) return;
  const card = state.card, editable = card.role !== 'viewer';
  const dependencies = card.dependencies || [];
  const available = state.board.cards.filter(row => row.id !== card.id && !dependencies.some(dep => dep.card_id === row.id));
  const html = `<section id="card-extras" class="detail-section"><h3>${icon('spark')}A little help, when you need it</h3><div class="extras-actions"><button class="button compact" data-extra-action="draft-checklist">Draft a checklist</button><button class="button compact" data-extra-action="similar">Find similar cards</button></div><p class="field-hint">Checklist drafts use the saved description. AI suggestions stay drafts until you review them.</p><div id="extras-output" aria-live="polite"></div><div class="detail-section"><h3>${icon('boards')}Prerequisites</h3>${dependencies.map(dep => `<div class="dependency-row"><span class="dependency-status ${dep.completed ? 'is-complete' : ''}">${dep.completed ? '✓' : '○'}</span><button class="text-button" data-extra-action="open-related" data-card-id="${E(dep.card_id)}">${E(dep.title)}</button>${editable ? `<button class="icon-button" data-extra-action="remove-dependency" data-card-id="${E(dep.card_id)}" aria-label="Remove prerequisite ${E(dep.title)}">×</button>` : ''}</div>`).join('') || '<p class="field-hint">No prerequisites linked. A clear path ahead.</p>'}${card.blocking_chain?.blocked ? `<div class="permission-note blocking-note">${E(card.blocking_chain.explanation)}${card.blocking_chain.truncated ? ' Some longer paths are omitted.' : ''}</div>` : ''}${editable && available.length ? `<form data-extra-form="dependency" class="compact-form"><select name="depends_on_id" aria-label="Prerequisite card">${available.map(row => `<option value="${E(row.id)}">${E(row.title)}</option>`).join('')}</select><button class="button compact" type="submit">Link prerequisite</button></form><p class="field-hint">Finish prerequisites before moving a card to a completed list. Cycles are prevented automatically.</p>` : ''}</div></section>`;
  form.insertAdjacentHTML('afterend',html);
}

async function reopenPreservingEdits(cardId) {
  await reloadBoard(); await openCard(cardId);
}

function showSimilar(result) {
  const output = $('#extras-output'); if (!output) return;
  if (result.needs_index) {
    output.innerHTML = `<div class="permission-note">This board’s search needs a refresh. ${state.card.role !== 'viewer' ? '<button class="text-button" data-extra-action="index">Build semantic search</button><p class="field-hint">Uses Dodil Models for up to 100 cards; model usage is billed to the workspace’s Dodil organization. Only changed card text is embedded.</p>' : 'Ask an editor to refresh semantic search.'}</div>`;
  } else {
    output.innerHTML = `<div class="extras-result"><h3>Related ideas on this board</h3>${result.cards.map(card => `<button class="related-card" data-extra-action="open-related" data-card-id="${E(card.id)}"><strong>${E(card.title)}</strong><span>${E(card.description.slice(0,140))}</span></button>`).join('') || '<p class="field-hint">No other up-to-date cards are indexed yet.</p>'}<p class="field-hint">${E(result.source)}</p></div>`;
  }
}

document.addEventListener('click',async event => {
  const button = event.target.closest('[data-extra-action]'); if (!button || button.disabled) return;
  const type = button.dataset.extraAction;
  button.disabled = true;
  try {
    if (type === 'summary') {
      const result = await action('daily_summary',{board_id:state.board.id});
      showModal('Today, a little clearer.', `${state.board.title} · ${result.day} (UTC)`,
        `<p class="summary-text">${E(result.summary)}</p><div class="extras-result">${result.focus_cards.map(card => `<button class="related-card" data-extra-action="open-related" data-card-id="${E(card.id)}"><strong>${E(card.title)}</strong><span>${card.due_date ? E(dateLabel(card.due_date)) : 'No due date'}</span></button>`).join('')}</div><p class="field-hint">Counts come directly from this board. Dodil Models selects focus from existing cards; it cannot add invented tasks to this brief.</p>`);
    } else if (type === 'draft-checklist') {
      const result = await action('suggest_checklist',{card_id:state.card.id});
      $('#extras-output').innerHTML = `<div class="extras-result"><form data-extra-form="apply-draft" data-version="${result.version}"><label for="ai-checklist-draft">Review your checklist draft</label><textarea id="ai-checklist-draft" name="items" rows="6">${E(result.suggested_checklist.join('\n'))}</textarea><p class="field-hint">Edit freely, one step per line. ${E(result.source)}.</p>${state.card.role !== 'viewer' ? '<button class="button primary compact" type="submit">Use these steps</button>' : '<p class="field-hint">An editor can add these steps to the card.</p>'}</form></div>`;
    } else if (type === 'similar') showSimilar(await action('find_similar_cards',{card_id:state.card.id}));
    else if (type === 'index') {
      state.board = await api(`/api/boards/${encodeURIComponent(state.board.id)}`);
      await action('refresh_semantic_index',{board_id:state.board.id,version:state.board.version});
      showSimilar(await action('find_similar_cards',{card_id:state.card.id}));
      toast('Semantic search is ready for this board.');
    } else if (type === 'open-related') await openCard(button.dataset.cardId);
    else if (type === 'remove-dependency') {
      requireSavedCardDetails();
      const id = state.card.id;
      await action('remove_dependency',{card_id:id,depends_on_id:button.dataset.cardId,version:state.card.version});
      await reopenPreservingEdits(id); toast('Prerequisite removed.');
    }
  } catch (error) { failure(error); }
  finally { if (button.isConnected) button.disabled = false; }
});

document.addEventListener('submit',async event => {
  const form = event.target.closest('form[data-extra-form]'); if (!form) return;
  event.preventDefault();
  if (form.dataset.busy === 'true') return;
  form.dataset.busy = 'true';
  const submit = $('button[type="submit"]',form); submit.disabled = true;
  const values = Object.fromEntries(new FormData(form)), id = state.card.id;
  try {
    requireSavedCardDetails();
    if (form.dataset.extraForm === 'dependency') {
      await action('add_dependency',{card_id:id,depends_on_id:values.depends_on_id,version:state.card.version});
      await reopenPreservingEdits(id); toast('Prerequisite linked.');
    } else {
      const items = values.items.split('\n').map(item => item.trim()).filter(Boolean);
      await action('apply_checklist',{card_id:id,items,version:Number(form.dataset.version)});
      await reopenPreservingEdits(id); toast('Your reviewed steps are saved.');
    }
  } catch (error) { failure(error); }
  finally { form.dataset.busy = 'false'; if (submit.isConnected) submit.disabled = false; }
});

new MutationObserver(decorateExtras).observe($('#main'),{childList:true});
new MutationObserver(decorateExtras).observe($('#modal-content'),{childList:true});
decorateExtras();