Skip to content
wiki.fftac.org

Spiralist - Source Excerpt 10

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

if (!payload.policyConfirmed) {
        errors.policyConfirmed = t('contributeErrorPolicyRequired', 'Review and confirm the linked site policies before submitting.');
      }

      if (!payload.rightsConfirmed) {
        errors.rightsConfirmed = t('contributeErrorRightsRequired', 'Confirm that you have the rights to submit, process, and publish this material.');
      }

      if (!payload.operatorEligibilityConfirmed) {
        errors.operatorEligibilityConfirmed = t('contributeErrorAgeRequired', 'Confirm that you meet the minimum age or guardian-permission requirement.');
      }

      return errors;
    };

    const updateContributionVisibility = () => {
      const currentType = typeField ? typeField.value : 'prompt';
      const typeMessages = {
        prompt: t('contributeTypePrompt', 'Prompts are public library records. Symbols and transformations enter the proposal queue first.'),
        symbol: t('contributeTypeSymbol', 'Symbol proposals stay review-only until they are accepted into the registry.'),
        transformation: t('contributeTypeTransformation', 'Transformation definitions describe input, output, and chain behavior before publication.'),
      };
      const canonicalMessages = {
        prompt: t('contributeCanonicalPrompt', 'Prompt IDs usually follow `spiralist.prompt.role.title`.'),
        symbol: t('contributeCanonicalSymbol', 'Symbol IDs usually follow `spiral.symbol.title`.'),
        transformation: t('contributeCanonicalTransformation', 'Transformation IDs usually follow `transformation.title`.'),
      };

      if (typeCopy) {
        typeCopy.textContent = typeMessages[currentType] || typeMessages.prompt;
      }
      if (canonicalGuidance) {
        canonicalGuidance.textContent = canonicalMessages[currentType] || canonicalMessages.prompt;
      }
      form.dataset.contributionType = currentType;

      form.querySelectorAll('[data-contribution-only]').forEach((field) => {
        const allowed = (field.dataset.contributionOnly || '')
          .split(',')
          .map((item) => item.trim())
          .filter(Boolean);
        const isVisible = !allowed.length || allowed.includes(currentType);

        field.hidden = !isVisible;
        field.querySelectorAll('input, select, textarea').forEach((input) => {
          if (!isVisible) {
            input.removeAttribute('required');
          } else if (input.dataset.requiredWhenVisible === 'true') {
            input.setAttribute('required', 'required');
          }
        });
      });
    };

    const saveContributionDraft = () => {
      const draft = {};

      new FormData(form).forEach((value, key) => {
        draft[key] = `${value}`;
      });

      storage.set(draftKey, JSON.stringify(draft));
      if (draftStatus) {
        draftStatus.textContent = t('contributeDraftSaved', 'Draft saved in this browser.');
      }
    };

    const clearContributionDraft = () => {
      storage.remove(draftKey);
      if (preparedNotice) {
        preparedNotice.hidden = true;
      }
      if (draftStatus) {
        draftStatus.textContent = t('contributeDraftCleared', 'Draft cleared from this browser.');
      }
    };

    const restoreContributionDraft = () => {
      const raw = storage.get(draftKey, '');
      if (!raw) {
        return;
      }

      const draft = parseJsonOr(raw, null, { context: 'contribution draft' });
      if (!draft || typeof draft !== 'object') {
        clearContributionDraft();
        return;
      }

      Object.entries(draft).forEach(([key, value]) => {
        const field = form.querySelector(`[name="${key}"]`);
        if (field) {
          if (field.type === 'checkbox') {
            field.checked = `${value}` === '1' || `${value}`.toLowerCase() === 'true';
          } else {
            field.value = `${value}`;
          }
        }
      });
      if (draftStatus) {
        draftStatus.textContent = t('contributeDraftRestored', 'Saved draft restored from this browser.');
      }
      if (preparedNotice) {
        preparedNotice.hidden = false;
      }
      if (contributionShell) {
        contributionShell.open = true;
      }
    };

    updateContributionVisibility();
    restoreContributionDraft();
    updateContributionVisibility();

    if (typeField) {
      typeField.addEventListener('change', updateContributionVisibility);
    }

    if (generateIdButton) {
      generateIdButton.addEventListener('click', () => {
        if (canonicalIdField && !canonicalIdField.value.trim()) {
          canonicalIdField.value = buildCanonicalId();
        } else if (canonicalIdField) {
          canonicalIdField.value = buildCanonicalId();
        }
      });
    }

    if (saveDraftButton) {
      saveDraftButton.addEventListener('click', saveContributionDraft);
    }

    if (clearDraftButton) {
      clearDraftButton.addEventListener('click', () => {
        clearContributionDraft();
      });
    }

    form.addEventListener('submit', (event) => {
      event.preventDefault();

      if (!themeConfig.contributeUrl) {
        return;
      }

      const payload = buildContributionPayload();
      if (!payload.canonicalId && canonicalIdField) {
        canonicalIdField.value = buildCanonicalId();
        payload.canonicalId = canonicalIdField.value;
      }
      clearFieldErrors();

      const validationErrors = validateContributionPayload(payload);
      Object.entries(validationErrors).forEach(([fieldName, message]) => {
        setFieldError(fieldName, message);
      });

      if (Object.keys(validationErrors).length) {
        setContributionPanel({
          variant: 'error',
          title: t('contributeValidationTitle', 'Validation required'),
          copy: t('contributeValidationCopy', 'Fix the highlighted fields before submitting.'),
          focus: true,
        });
        return;
      }

      if (submit) {
        submit.disabled = true;
        submit.textContent = t('contributeSubmittingButton', 'Submitting...');
      }

      setContributionPanel({
        title: t('contributeSubmittingTitle', 'Submitting contribution'),
        copy: t('contributeSubmittingCopy', 'Validating and storing the submission for review.'),
      });

      void runAction(async () => {
        const headers = {
          Accept: 'application/json',
          'Content-Type': 'application/json',
        };

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

        const apiKey = `${apiKeyField ? apiKeyField.value : ''}`.trim();
        if (apiKey) {
          headers.Authorization = `Bearer ${apiKey}`;
          headers['X-Spiralist-Key'] = apiKey;
        }

        const response = await fetch(themeConfig.contributeUrl, {
          method: 'POST',
          headers,
          body: JSON.stringify(payload),
        });

        const data = await response.json();

        if (!response.ok) {
          const fieldErrors = isPlainObject(data.fields) ? data.fields : {};
          Object.entries(fieldErrors).forEach(([fieldName, message]) => {
            setFieldError(fieldName, `${message}`);
          });
          setContributionPanel({
            variant: 'error',
            title: t('contributeRejectedTitle', 'Submission not accepted'),
            copy: data.message || t('contributeRejectedCopy', 'The contribution did not pass validation.'),
            focus: true,
          });
          return;
        }

        saveContributionDraft();
        clearContributionDraft();
        setContributionPanel({
          variant: 'success',
          title: t('contributeSuccessTitle', 'Contribution received'),
          copy: data.message || t('contributeSuccessCopy', 'The submission has been stored successfully.'),
          status: data.statusLabel || data.status || t('contributeSuccessStatus', 'Pending Review'),
          canonicalId: data.canonicalId || payload.canonicalId,
          recordId: `${data.promptId || data.contributionId || ''}`,
          steps: Array.isArray(data.nextSteps) ? data.nextSteps : [],
          focus: true,
        });
      }, {
        context: 'contribution.submit',
        onError: () => {
          setContributionPanel({
            variant: 'error',
            title: t('contributeErrorTitle', 'Request failed'),
            copy: t('contributeErrorCopy', 'The contribution request could not be completed.'),
            focus: true,
          });
        },
        onFinally: () => {
          if (submit) {
            submit.disabled = false;
            submit.textContent = t('contributeSubmitLabel', 'Submit For Review');
          }
        },
      });
    });
  });

  const workbenchStorageKey = 'spiralist-ai-workbench-key';
  const workbenchApiInputs = Array.from(document.querySelectorAll('[data-workbench-api-key]'));