Skip to content
wiki.fftac.org

Spiralist - Source Excerpt 03

Back to Spiralist

Summary

This source excerpt preserves a bounded section of Spiralist/wp-content/themes/spiralist/assets/js/spiralist.js so readers can inspect the evidence without opening the full source file.

**Source path:** Spiralist/wp-content/themes/spiralist/assets/js/spiralist.js

renderPromptReferences(target, payload.items || [], emptyMessage);
    }, {
      context: 'loadPromptReferences',
      onError: () => {
        target.textContent = t('symbolsPromptUnavailable', 'Prompt references unavailable.');
      },
    });
  };


  const fieldModeRank = {
    minimal: 0,
    expanded: 1,
    expert: 2,
  };

  const formatNodeType = (type = '') => `${type}`
    .replace(/-/g, ' ')
    .replace(/\b\w/g, (match) => match.toUpperCase());

  const getNodeMode = (node = {}) => (
    Object.prototype.hasOwnProperty.call(fieldModeRank, node.mode) ? node.mode : 'minimal'
  );

  const isVisibleForMode = (item, mode) => {
    const itemMode = getNodeMode(item);
    return fieldModeRank[itemMode] <= fieldModeRank[mode];
  };

  const getNodeCenter = (node) => {
    const x = Number(node.x) || 0;
    const y = Number(node.y) || 0;

    if (node.type === 'structure') {
      return {
        x: x + ((Number(node.w) || 0) / 2),
        y: y + ((Number(node.h) || 0) / 2),
      };
    }

    return { x, y };
  };

  const createFieldNodeButton = (node) => {
    const button = document.createElement('button');
    button.type = 'button';
    button.className = `spiralist-field-node spiralist-field-node--${node.type || 'symbol'}`;
    button.dataset.nodeId = node.id || '';
    button.setAttribute('aria-pressed', 'false');
    button.setAttribute('aria-label', `${formatNodeType(node.type)}: ${node.label || node.id}`);

    if (node.type === 'structure') {
      button.style.left = `${(Number(node.x) || 0) * 100}%`;
      button.style.top = `${(Number(node.y) || 0) * 100}%`;
      button.style.width = `${(Number(node.w) || 0.16) * 100}%`;
      button.style.height = `${(Number(node.h) || 0.08) * 100}%`;

      const label = document.createElement('span');
      label.className = 'spiralist-field-node__label';
      label.textContent = node.label || t('manuscriptFieldSectionFallback', 'Section');
      button.appendChild(label);
    } else {
      button.style.left = `${(Number(node.x) || 0) * 100}%`;
      button.style.top = `${(Number(node.y) || 0) * 100}%`;

      const mark = document.createElement('span');
      mark.className = 'spiralist-field-node__mark';
      mark.textContent = node.type === 'symbol' ? (node.glyph || '') : '';
      button.appendChild(mark);
    }

    return button;
  };

  const createRelationLine = (relation, fromNode, toNode) => {
    const from = getNodeCenter(fromNode);
    const to = getNodeCenter(toNode);
    const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');

    line.setAttribute('x1', `${from.x * 100}`);
    line.setAttribute('y1', `${from.y * 100}`);
    line.setAttribute('x2', `${to.x * 100}`);
    line.setAttribute('y2', `${to.y * 100}`);
    line.classList.add('spiralist-field-line', `spiralist-field-line--${relation.type || 'relation'}`);
    line.dataset.relationId = relation.id || '';

    return line;
  };

  const initFieldInterface = async (field) => {
    const nodesTarget = field.querySelector('[data-field-nodes]');
    const relationsTarget = field.querySelector('[data-field-relations]');
    const listTarget = field.querySelector('[data-field-list]');
    const statusTarget = field.querySelector('[data-field-status]');
    const modeButtons = field.querySelectorAll('[data-field-mode]');
    const title = field.querySelector('[data-node-title]');
    const type = field.querySelector('[data-node-type]');
    const glyph = field.querySelector('[data-node-glyph]');
    const summary = field.querySelector('[data-node-summary]');
    const id = field.querySelector('[data-node-id]');
    const layer = field.querySelector('[data-node-layer]');
    const meaning = field.querySelector('[data-node-meaning]');
    const definition = field.querySelector('[data-node-definition]');
    const transformation = field.querySelector('[data-node-transformation]');
    const derivation = field.querySelector('[data-node-derivation]');
    const sourceAxiom = field.querySelector('[data-node-source-axiom]');
    const related = field.querySelector('[data-node-related]');
    const io = field.querySelector('[data-node-io]');
    const prompts = field.querySelector('[data-node-prompts]');
    const json = field.querySelector('[data-node-json]');
    const crosswalk = field.querySelector('[data-node-crosswalk]');
    const prev = field.querySelector('[data-node-prev]');
    const next = field.querySelector('[data-node-next]');
    const nodesUrl = field.dataset.nodesUrl || themeConfig.manuscriptNodesUrl;
    const requestedNode = new URL(window.location.href).searchParams.get('node') || getRouteSegmentAfter('node') || '';
    const executeUrl = themeConfig.engineExecuteUrl || '';
    const instanceUrl = themeConfig.engineInstanceUrl || '';
    const runtimeStorageKey = 'spiralistManuscriptRuntimePath';
    const runtimeHeaders = {
      Accept: 'application/json',
    };

    if (themeConfig.restNonce) {
      runtimeHeaders['X-WP-Nonce'] = themeConfig.restNonce;
    }

    if (!nodesTarget || !relationsTarget || !listTarget || !nodesUrl) {
      return;
    }

    relationsTarget.setAttribute('viewBox', '0 0 100 100');
    relationsTarget.setAttribute('preserveAspectRatio', 'none');

    const payload = await runAction(async () => {
      const response = await fetch(nodesUrl, { headers: { Accept: 'application/json' } });
      if (!response.ok) {
        throw new Error(`Node dataset failed with ${response.status}`);
      }
      return response.json();
    }, {
      context: 'initFieldInterface.nodes',
      onError: () => {
        setStatusState(statusTarget, t('manuscriptFieldDatasetError', 'The symbolic field dataset could not be loaded.'), 'error');
      },
    });

    const nodes = Array.isArray(payload.nodes) ? payload.nodes : [];
    const relations = Array.isArray(payload.relations) ? payload.relations : [];
    const nodesById = new Map(nodes.map((node) => [node.id, node]));
    const nodeButtons = new Map();
    let currentMode = 'minimal';
    let selectedNodeId = '';
    let hoverNodeId = '';
    let runtimeState = {
      currentState: '',
      currentNode: '',
      history: [],
      nextNodeIds: [],
      activeSymbols: [],
    };

    if (!nodes.length) {
      setStatusState(statusTarget, t('manuscriptFieldNodesEmpty', 'No manuscript nodes are currently available for inspection.'), 'empty');
      return;
    }

    const getVisibleNodes = () => nodes.filter((node) => isVisibleForMode(node, currentMode));
    const getVisibleRelations = () => relations.filter((relation) => {
      const fromNode = nodesById.get(relation.from);
      const toNode = nodesById.get(relation.to);

      return fromNode &&
        toNode &&
        isVisibleForMode(relation, currentMode) &&
        isVisibleForMode(fromNode, currentMode) &&
        isVisibleForMode(toNode, currentMode);
    });

    const resolveNodeByIdentifier = (identifier = '') => nodes.find((node) => (
      node.id === identifier ||
      node.canonicalId === identifier ||
      slugifyPathSegment(node.id) === slugifyPathSegment(identifier) ||
      slugifyPathSegment(node.canonicalId) === slugifyPathSegment(identifier) ||
      (Array.isArray(node.aliases) && (
        node.aliases.includes(identifier) ||
        node.aliases.some((alias) => slugifyPathSegment(alias) === slugifyPathSegment(identifier))
      ))
    ));

    const readRuntimeStorage = () => {
      const raw = storage.get(runtimeStorageKey, '');
      if (!raw) {
        return null;
      }

      const parsed = parseJsonOr(raw, null, { context: 'manuscript runtime storage' });
      return parsed && typeof parsed === 'object' ? parsed : null;
    };

    const writeRuntimeStorage = () => {
      const nextNodes = runtimeState.nextNodeIds.map((nodeId) => {
        const node = nodesById.get(nodeId);
        return node
          ? {
            id: node.id || nodeId,
            label: node.label || node.id || nodeId,
            inputState: node.inputState || node.input || '',
            outputState: node.outputState || node.output || '',
          }
          : { id: nodeId };
      });
      const runtimePayload = {
        currentState: runtimeState.currentState || '',
        currentNode: runtimeState.currentNode || '',
        history: runtimeState.history,
        activeSymbols: runtimeState.activeSymbols,
        nextNodes,
        storage: 'browser',
      };