tmp/webapp-docs/frontend/components/home/BlockPalette.js

/**
 * The Home left pane: the building-block palette. A search box, the Blocks/Lists groups with
 * their folder sub-tabs, and the chip cloud. Self-contained — it owns its own search query and
 * active-group/sub-tab state and rebuilds its catalog (it's the only consumer). It reports up
 * only the cross-pane actions: inserting a token into the composer and the hover tooltip.
 * @module gui/components/home/BlockPalette
 */
import { Fragment, useEffect, useMemo, useState } from "react";
import { useIntl, defineMessages } from "react-intl";
import { getBlocks, subscribeCatalog } from "../../lib/promptEngine.js";
import { foldersOf } from "../../lib/home/blockCategories.js";
import { jsxDEV as _jsxDEV, Fragment as _Fragment } from "react/jsx-dev-runtime";
const msgs = defineMessages({
  buildingBlocks: {
    id: "home.buildingBlocks",
    defaultMessage: "Building blocks"
  },
  searchBlocks: {
    id: "home.searchBlocks",
    defaultMessage: "Search blocks…"
  },
  noBlocks: {
    id: "home.noBlocks",
    defaultMessage: "No building blocks match “{query}”."
  },
  all: {
    id: "home.all",
    defaultMessage: "all"
  },
  moreFilter: {
    id: "home.moreFilter",
    defaultMessage: "+{count} more — keep typing to filter"
  },
  closePalette: {
    id: "home.closePalette",
    defaultMessage: "Close building blocks",
    description: "aria-label for the phone-only ✕ that closes the building-block drawer"
  }
});

/**
 * The building-block palette.
 * @param {object} props
 * @param {boolean} props.includeAdult Whether nsfw-flagged blocks are shown.
 * @param {(token: string) => void} props.onInsert Insert a block token into the composer.
 * @param {(item: object, e: object) => void} props.onShowTip Show the hover tooltip.
 * @param {(e: object) => void} props.onMoveTip Move the hover tooltip.
 * @param {() => void} props.onHideTip Hide the hover tooltip.
 * @returns {JSX.Element}
 */
export default function BlockPalette({
  includeAdult,
  onInsert,
  onShowTip,
  onMoveTip,
  onHideTip,
  onClose
}) {
  const intl = useIntl();
  const [version, setVersion] = useState(0); // bump to refresh custom blocks
  const [query, setQuery] = useState("");
  const [activeCat, setActiveCat] = useState(""); // top group: Blocks | Lists
  const [activeSub, setActiveSub] = useState("All"); // folder sub-tab within the active group

  // Rebuild the block palette when custom blocks change or the NSFW switch flips (nsfw-flagged
  // generators are hidden entirely when adult is off).
  const blocks = useMemo(() => getBlocks({
    includeAdult
  }), [version, includeAdult]);

  // Rebuild the palette when the catalog hot-applies (a Manage edit / refresh re-reads disk).
  useEffect(() => subscribeCatalog(() => setVersion(v => v + 1)), []);

  // Filter blocks by the single search box (matches token or label). Category pills
  // (the Lists folder headers) are kept only when a following entry survives.
  const q = query.trim().toLowerCase();
  const matchItem = i => (i.token || "").toLowerCase().includes(q) || (i.label || "").toLowerCase().includes(q);
  function filterItems(items) {
    if (!q) return items;
    const out = [];
    for (let k = 0; k < items.length; k++) {
      const i = items[k];
      if (i.category) {
        let any = false;
        for (let j = k + 1; j < items.length && !items[j].category; j++) if (matchItem(items[j])) {
          any = true;
          break;
        }
        if (any) out.push(i);
      } else if (matchItem(i)) {
        out.push(i);
      }
    }
    return out;
  }
  const effItems = b => b.items;
  const filtered = blocks.map(b => ({
    ...b,
    items: filterItems(effItems(b))
  })).filter(b => b.items.some(i => !i.category));

  // The active top group (Blocks / Lists), falling back to the first available.
  const active = filtered.find(b => b.title === activeCat) || filtered[0] || null;
  const searching = !!q;
  // Folder sub-categories of the active group, and the currently-selected one (default All).
  const subCats = active ? foldersOf(active) : [];
  const effSub = activeSub === "All" || subCats.some(c => c.label === activeSub) ? activeSub : "All";

  // The chips to render. Searching → the flat matched run. All → every chip across folders, plus
  // the insertable group pills (the plain folder headers are sub-tabs now, so they're dropped). A
  // folder sub-tab → that folder's chips, led by its whole-group insert pill when it has one.
  let activeItems;
  if (searching) {
    activeItems = active ? active.items.filter(i => !i.category) : [];
  } else if (effSub === "All") {
    // All = every category shown with its pill header + buttons (special/any included as pills).
    activeItems = active ? active.items : [];
  } else {
    const cat = subCats.find(c => c.label === effSub);
    activeItems = cat ? [...(cat.token ? [{
      category: true,
      token: cat.token,
      label: cat.label,
      description: cat.description
    }] : []), ...cat.items] : [];
  }
  return /*#__PURE__*/_jsxDEV("aside", {
    className: "sidebar",
    id: "block-palette",
    children: [/*#__PURE__*/_jsxDEV("div", {
      className: "panel-head",
      children: [/*#__PURE__*/_jsxDEV("div", {
        className: "panel-head-row",
        children: [/*#__PURE__*/_jsxDEV("h3", {
          className: "panel-title",
          children: intl.formatMessage(msgs.buildingBlocks)
        }, void 0, false), onClose && /*#__PURE__*/_jsxDEV("button", {
          type: "button",
          className: "palette-close",
          onClick: onClose,
          "aria-label": intl.formatMessage(msgs.closePalette),
          title: intl.formatMessage(msgs.closePalette),
          children: "✕"
        }, void 0, false)]
      }, void 0, true), /*#__PURE__*/_jsxDEV("input", {
        className: "picker-filter",
        placeholder: intl.formatMessage(msgs.searchBlocks),
        value: query,
        onChange: e => setQuery(e.target.value)
      }, void 0, false)]
    }, void 0, true), filtered.length === 0 ? /*#__PURE__*/_jsxDEV("p", {
      className: "empty",
      children: intl.formatMessage(msgs.noBlocks, {
        query
      })
    }, void 0, false) : /*#__PURE__*/_jsxDEV(_Fragment, {
      children: [/*#__PURE__*/_jsxDEV("nav", {
        className: "cat-tabs",
        children: filtered.map(b => {
          const isActiveGroup = !!active && active.title === b.title;
          const groupFolders = foldersOf(b);
          const selectGroup = () => {
            setActiveCat(b.title);
            setActiveSub("All");
          };
          return /*#__PURE__*/_jsxDEV(Fragment, {
            children: [/*#__PURE__*/_jsxDEV("button", {
              className: "cat-tab",
              onClick: selectGroup,
              children: [/*#__PURE__*/_jsxDEV("span", {
                className: "cat-name",
                children: b.title
              }, void 0, false), /*#__PURE__*/_jsxDEV("span", {
                className: "count-pill",
                children: b.items.filter(i => !i.category).length
              }, void 0, false)]
            }, void 0, true), !searching && /*#__PURE__*/_jsxDEV("button", {
              className: `cat-tab sub${isActiveGroup && effSub === "All" ? " on" : ""}`,
              onClick: selectGroup,
              children: [/*#__PURE__*/_jsxDEV("span", {
                className: "cat-name",
                children: intl.formatMessage(msgs.all)
              }, void 0, false), /*#__PURE__*/_jsxDEV("span", {
                className: "count-pill",
                children: b.items.filter(i => !i.category).length
              }, void 0, false)]
            }, void 0, true), !searching && groupFolders.map(c => /*#__PURE__*/_jsxDEV("button", {
              className: `cat-tab sub${isActiveGroup && effSub === c.label ? " on" : ""}`,
              onClick: () => {
                setActiveCat(b.title);
                setActiveSub(c.label);
              },
              title: c.description || c.label,
              children: [/*#__PURE__*/_jsxDEV("span", {
                className: "cat-name",
                children: c.label
              }, void 0, false), /*#__PURE__*/_jsxDEV("span", {
                className: "count-pill",
                children: c.items.length
              }, void 0, false)]
            }, c.label, true))]
          }, b.title, true);
        })
      }, void 0, false), /*#__PURE__*/_jsxDEV("div", {
        className: "chip-area",
        children: [!searching && (() => {
          const desc = effSub === "All" ? active?.hint : subCats.find(c => c.label === effSub)?.description;
          return desc ? /*#__PURE__*/_jsxDEV("p", {
            className: "cat-hint",
            children: [/*#__PURE__*/_jsxDEV("span", {
              className: "cat-hint-icon",
              "aria-hidden": "true",
              children: "ⓘ"
            }, void 0, false), /*#__PURE__*/_jsxDEV("span", {
              className: "cat-hint-text",
              children: desc
            }, void 0, false)]
          }, void 0, true) : null;
        })(), /*#__PURE__*/_jsxDEV("div", {
          className: "picker-list",
          children: [activeItems.slice(0, 400).map((i, idx) => i.category ? i.token ? /*#__PURE__*/_jsxDEV("button", {
            className: "cat-pill cat-pill-group",
            onMouseEnter: e => onShowTip(i, e),
            onMouseMove: onMoveTip,
            onMouseLeave: onHideTip,
            onClick: () => onInsert(i.token),
            children: i.label
          }, `cat-${i.label}-${idx}`, false) : /*#__PURE__*/_jsxDEV("span", {
            className: "cat-pill",
            title: i.description || i.label,
            children: i.label
          }, `cat-${i.label}-${idx}`, false) : /*#__PURE__*/_jsxDEV("button", {
            className: "chip",
            onMouseEnter: e => onShowTip(i, e),
            onMouseMove: onMoveTip,
            onMouseLeave: onHideTip,
            onClick: () => onInsert(i.token),
            children: i.label
          }, i.token, false)), activeItems.length > 400 && /*#__PURE__*/_jsxDEV("span", {
            className: "picker-more",
            children: intl.formatMessage(msgs.moreFilter, {
              count: activeItems.length - 400
            })
          }, void 0, false)]
        }, void 0, true)]
      }, void 0, true)]
    }, void 0, true)]
  }, void 0, true);
}