AkurAI Build
Menu

popagent

public

Latest change fadf21d1cd584745f6f92eaa60509e0bef19d242 - fix task orchestration and compact overview cards by AkurAI Build

import type { ReactNode } from "react";
import { CircleDot, GitBranch, GitCommitHorizontal, Sparkles, Tag } from "lucide-react";
import changelogMarkdown from "../../CHANGELOG.md" with { type: "text" };

export type ChangelogRelease = {
  title: string;
  date?: string;
  groups: Array<{ title: string; items: string[] }>;
};


function changelogLine(line: string): string {
  return line
    .replace(/<code>(.*?)<\/code>/g, "`$1`")
    .replace(/^<h2>(.*?)<\/h2>$/, "## $1")
    .replace(/^<h3>(.*?)<\/h3>$/, "### $1")
    .replace(/^<li>(.*?)<\/li>$/, "- $1")
    .replace(/<[^>]+>/g, "")
    .replaceAll("&amp;", "&")
    .replaceAll("&lt;", "<")
    .replaceAll("&gt;", ">")
    .replaceAll("&quot;", "\"")
    .replaceAll("&#39;", "'");
}

export function parseChangelog(markdown: string): ChangelogRelease[] {
  const releases: ChangelogRelease[] = [];
  let release: ChangelogRelease | undefined;
  let group: ChangelogRelease["groups"][number] | undefined;
  for (const sourceLine of markdown.split("\n")) {
    const line = changelogLine(sourceLine);
    const releaseMatch = /^## \[?([^\]]+?)\]?\s*(?:-\s*(\d{4}-\d{2}-\d{2}))?$/.exec(line.trim());
    if (releaseMatch) {
      release = { title: releaseMatch[1]!.trim(), date: releaseMatch[2], groups: [] };
      releases.push(release);
      group = undefined;
      continue;
    }
    const groupMatch = /^###\s+(.+)$/.exec(line.trim());
    if (groupMatch && release) {
      group = { title: groupMatch[1]!.trim(), items: [] };
      release.groups.push(group);
      continue;
    }
    const itemMatch = /^-\s+(.+)$/.exec(line.trim());
    if (itemMatch && group) group.items.push(itemMatch[1]!.trim());
  }
  return releases.filter((entry) => entry.groups.some((entryGroup) => entryGroup.items.length));
}

function inlineText(text: string): ReactNode[] {
  return text.split(/(`[^`]+`)/g).filter(Boolean).map((part, index) => part.startsWith("`") && part.endsWith("`")
    ? <code key={index} className="rounded border border-line bg-bg px-1.5 py-0.5 font-mono text-[0.82em] text-accent">{part.slice(1, -1)}</code>
    : part);
}

const groupTone: Record<string, string> = {
  added: "border-emerald-500/30 bg-emerald-500/10 text-emerald-400",
  changed: "border-sky-500/30 bg-sky-500/10 text-sky-400",
  fixed: "border-amber-500/30 bg-amber-500/10 text-amber-400",
  removed: "border-rose-500/30 bg-rose-500/10 text-rose-400",
  security: "border-violet-500/30 bg-violet-500/10 text-violet-400",
};

export function ChangelogTimeline({ markdown }: { markdown: string }) {
  const releases = parseChangelog(markdown);
  return (
    <main className="page-content min-h-0 flex-1 overflow-y-auto bg-bg">
      <div className="w-full">
        <header className="overflow-hidden rounded-xl border border-line bg-panel shadow-xl shadow-black/10">
          <div className="flex flex-wrap items-center gap-2 border-b border-line bg-panel-2/70 px-4 py-3 font-mono text-xs text-ink-muted">
            <span className="font-semibold text-ink">popagent</span><span>/</span><span>CHANGELOG.md</span>
            <span className="ml-auto inline-flex items-center gap-1.5 rounded-full border border-line bg-bg px-2.5 py-1"><GitBranch className="size-3" />main</span>
          </div>
          <div className="relative px-5 py-8 sm:px-8 sm:py-10">
            <div aria-hidden="true" className="absolute -right-10 -top-14 size-44 rounded-full bg-accent/10 blur-3xl" />
            <div className="relative flex items-start gap-4">
              <div className="brand-orb flex size-11 shrink-0 items-center justify-center rounded-lg"><Sparkles className="size-5" /></div>
              <div><p className="font-mono text-xs uppercase tracking-[0.16em] text-accent">Release feed</p><h1 className="mt-2 text-3xl font-semibold tracking-[-0.04em] sm:text-4xl">Commits with better manners.</h1><p className="mt-3 max-w-2xl text-sm leading-relaxed text-ink-muted">The mildly dramatic history of what shipped, what got fixed, and which tiny gremlins were politely escorted out.</p></div>
            </div>
          </div>
        </header>

        <section aria-label="Release feed" className="relative mt-7 space-y-6 pl-5 sm:pl-8">
          <div aria-hidden="true" className="absolute bottom-8 left-[1.7rem] top-6 w-px bg-gradient-to-b from-accent via-line to-transparent sm:left-[2.45rem]" />
          {releases.map((release, releaseIndex) => (
            <article key={`${release.title}-${release.date ?? "pending"}`} className="relative pl-8 sm:pl-11">
              <div aria-hidden="true" className={`absolute left-0 top-6 z-10 flex size-5 items-center justify-center rounded-full border-2 bg-bg ${releaseIndex === 0 ? "border-accent text-accent" : "border-line text-ink-dim"}`}><CircleDot className="size-2.5" /></div>
              <div className="overflow-hidden rounded-xl border border-line bg-panel transition-colors hover:border-accent/30">
                <header className="flex flex-wrap items-center gap-3 border-b border-line bg-panel-2/50 px-4 py-4 sm:px-5">
                  <span className="inline-flex items-center gap-2 font-mono text-sm font-semibold text-ink"><Tag className="size-4 text-accent" />{release.title}</span>
                  {release.date ? <time className="font-mono text-xs text-ink-dim" dateTime={release.date}>{release.date}</time> : <span className="rounded-full border border-accent/30 bg-accent-soft px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-accent">Next release</span>}
                  <span className="ml-auto inline-flex items-center gap-1.5 font-mono text-[10px] text-ink-dim"><GitCommitHorizontal className="size-3.5" />{release.groups.reduce((count, entry) => count + entry.items.length, 0)} changes</span>
                </header>
                <div className="divide-y divide-line">
                  {release.groups.map((group) => (
                    <section key={group.title} className="grid gap-3 px-4 py-5 sm:grid-cols-[7rem_minmax(0,1fr)] sm:px-5">
                      <div><span className={`inline-flex rounded-full border px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.12em] ${groupTone[group.title.toLowerCase()] ?? "border-line bg-bg text-ink-muted"}`}>{group.title}</span></div>
                      <ul className="space-y-3 text-sm leading-relaxed text-ink-muted">
                        {group.items.map((item) => <li key={item} className="relative pl-5 before:absolute before:left-0 before:top-[0.7em] before:size-1.5 before:rounded-full before:bg-accent/70">{inlineText(item)}</li>)}
                      </ul>
                    </section>
                  ))}
                </div>
              </div>
            </article>
          ))}
        </section>
      </div>
    </main>
  );
}

export function ChangelogPage({ onOpenNavigation }: { onOpenNavigation: () => void }) {
  return <div className="flex min-h-0 flex-1 flex-col"><header className="flex min-h-14 items-center border-b border-line bg-panel px-3 lg:hidden"><button type="button" aria-label="Open session navigation" onClick={onOpenNavigation} className="liquid-control flex size-10 items-center justify-center rounded"><GitBranch className="size-4" /></button><span className="ml-3 text-sm font-medium">Changelog</span></header><ChangelogTimeline markdown={changelogMarkdown} /></div>;
}