moadim 1.6.0

Loop engine for AI agents — routines over REST, MCP, and a built-in web UI
import { useEffect, useRef, useState } from "react";
import { filterLines, highlightSegments, matchCount } from "./logSearch";

export interface LogViewerProps {
  /** Raw log text; `undefined` while loading. */
  content: string | undefined;
  loading: boolean;
  err: string | undefined;
}

function HighlightedLine({ text, query }: { text: string; query: string }) {
  return (
    <>
      {highlightSegments(text, query).map(([isMatch, slice], i) =>
        isMatch ? (
          <mark key={i} className="log-hl">
            {slice}
          </mark>
        ) : (
          <span key={i}>{slice}</span>
        ),
      )}
    </>
  );
}

/** Scrollable log body with a live search box (filter + count + highlight) and auto-tail. */
export function LogViewer({ content, loading, err }: LogViewerProps) {
  const [query, setQuery] = useState("");
  const wrapRef = useRef<HTMLDivElement>(null);

  // Auto-tail: scroll to the bottom whenever the content changes, so newest lines stay visible.
  useEffect(() => {
    const el = wrapRef.current;
    if (el) el.scrollTop = el.scrollHeight;
  }, [content?.length]);

  let body: React.ReactNode = null;
  if (loading) {
    body = (
      <div className="empty">
        <div className="spinner" />
      </div>
    );
  } else if (err !== undefined) {
    body = <div className="logs-error">Error: {err}</div>;
  } else if (content !== undefined) {
    if (content === "") {
      body = <div className="logs-empty">— no logs yet —</div>;
    } else {
      const lines = filterLines(content, query);
      const [hits, total] = matchCount(content, query);
      const countLabel = query.trim() === "" ? `${total} lines` : `${hits} / ${total} matches`;
      body = (
        <>
          <div className="log-search">
            <input
              type="search"
              className="log-search-input"
              placeholder="Search logs…"
              aria-label="Search log lines"
              value={query}
              onChange={(e) => setQuery(e.target.value)}
            />
            {query !== "" && (
              <button
                type="button"
                className="btn btn-ghost btn-sm"
                title="Clear search"
                aria-label="Clear search"
                onClick={() => setQuery("")}
              >
                ✕
              </button>
            )}
            <span className="log-match-count">{countLabel}</span>
          </div>
          <div className="log-lines">
            {lines.map(([ln, text]) => (
              <div className="log-line" key={ln}>
                <span className="log-ln">{ln}</span>
                <span className="log-lc">
                  <HighlightedLine text={text} query={query} />
                </span>
              </div>
            ))}
          </div>
        </>
      );
    }
  }

  return (
    <div className="logs-wrap" ref={wrapRef}>
      {body}
    </div>
  );
}