npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cnt-dev/chat-ui

v0.5.2

Published

Embeddable AI chat UI control for UI5 applications — sidebar-based conversations, streaming answers, file attachments, rich UI5-control bubbles.

Readme

@cnt-dev/chat-ui

Eine schlanke, einbettbare UI5-Chat-Library mit genau einer Kontrolle: chatui.ChatBox. Die Library ist bewusst transport-agnostisch — sie kennt kein Backend, keine Session, keine Auth. Sie feuert send und erwartet, dass die einbettende App die Antwort per API zurückschiebt.

  • Zero-Deps außer sap.ui.core (kein sap.m, kein sap.viz).
  • Streaming-fähig via Chunk-API.
  • Konversations-Sidebar, File-Uploads, Rich Content, Editing, Copy out of the box.
  • npm-Name: @cnt-dev/chat-ui — UI5-Namespace / Modulpfad: chatui / chatui/….

Feature-Übersicht

| Feature | API | |--------------------------------|----------------------------------------------------------| | Text-Nachricht senden | send-Event → {message, files, conversationId} | | Streaming-Antwort | addAssistantMessage("") + appendToLastAssistantMessage(chunk) | | Markdown-Rendering | automatisch bei Assistant-Text | | Copy-to-Clipboard | Hover-Action pro Assistant-Bubble | | Datei-/Bild-Uploads | Paperclip-Button + Drag&Drop → files: File[] im send | | Nachrichten-Bearbeitung | Edit-Action pro User-Bubble, truncated + refire send | | Rich Content (Chart, Card, …) | addAssistantControl(control) | | Konversations-Sidebar | conversations-Property + conversationSelect / conversationDelete-Events | | Restore aus Persistenz | setConversations() / getConversations() (und setMessages / getMessages pro Konversation) | | Neue Konversation | Header-Button „+ Neue Konversation" → newChat-Event | | i18n | DE (default) + EN (chatbox.*-Keys) | | Themes | sap_fiori_3, _dark, _hcw, _hcb |

Installation

npm i @cnt-dev/chat-ui

In der Consumer-ui5.yaml:

framework:
  libraries:
    - name: sap.ui.core
    - name: chatui

In manifest.json:

"sap.ui5": {
  "dependencies": {
    "libs": {
      "chatui": {}
    }
  }
}

Bei nicht-UI5-tooling-Bootstraps ggf. Ressourcen-Root explizit setzen:

<script id="sap-ui-bootstrap"
        src="…/sap-ui-core.js"
        data-sap-ui-libs="chatui"
        data-sap-ui-resourceroots='{"chatui": "node_modules/@cnt-dev/chat-ui/resources/chatui"}'>
</script>

Schnellstart

import ChatBox from "chatui/ChatBox";
import type {
    ChatBox$SendEvent,
    ChatBox$ConversationDeleteEvent,
    Conversation
} from "chatui/ChatBox";

let conversations: Conversation[] = loadFromStorage();

const chat = new ChatBox({
    width: "100%",
    height: "100%",
    send: async (e: ChatBox$SendEvent) => {
        const message = e.getParameter("message") ?? "";
        const files = (e.getParameter("files") as File[] | undefined) ?? [];
        let id = e.getParameter("conversationId") ?? "";
        if (!id) {                                      // neue Konversation
            id = crypto.randomUUID();
            chat.setActiveConversationId(id);
        }
        upsert(conversations, id, chat.getMessages());
        chat.setConversations(conversations);

        chat.setBusy(true);
        chat.addAssistantMessage("");                   // pending bubble
        for await (const chunk of streamFromBackend(message, files, chat.getMessages())) {
            chat.appendToLastAssistantMessage(chunk);
        }
        chat.setBusy(false);

        upsert(conversations, id, chat.getMessages());
        chat.setConversations(conversations);
    },
    newChat: () => backend.resetSession(),
    conversationDelete: (e: ChatBox$ConversationDeleteEvent) => {
        const id = e.getParameter("id") ?? "";
        conversations = conversations.filter(c => c.id !== id);
        chat.setConversations(conversations);
        if (chat.getActiveConversationId() === id) {
            chat.setActiveConversationId(conversations[0]?.id ?? "");
        }
    }
});
chat.setConversations(conversations);                    // Library wählt automatisch die erste
chat.placeAt("content");

Public API

Properties

| Name | Typ | Default | Zweck | |-------------------------|------------------------|---------|-------| | placeholder | string | "" | Platzhaltertext (leer → i18n-Default) | | busy | boolean | false | Deaktiviert Input, Buttons, Edit | | width | sap.ui.core.CSSSize | 100% | Passt sich Parent an | | height | sap.ui.core.CSSSize | 100% | Passt sich Parent an | | allowAttachments | boolean | true | Blendet Paperclip + Drop-Zone aus, wenn false | | attachmentAccept | string | "" | 1:1 an <input type="file" accept="…"> (z.B. "image/*") | | conversations | Conversation[] | [] | Sidebar-Einträge (vom Consumer gehalten) | | activeConversationId | string | "" | Aktive Konversation. "" = neuer Chat-Draft |

Events

  • send({message, files, conversationId}) User hat einen Prompt und/oder Anhänge abgeschickt. conversationId ist die aktive Id oder "" bei einem neuen Chat — dann vergibt der Consumer eine frische Id und pusht eine aktualisierte Liste zurück.
  • newChat User hat „+ Neue Konversation" oben rechts gedrückt. Transcript und activeConversationId sind bereits zurückgesetzt; Handler räumt serverseitige Session-States auf.
  • conversationSelect({id}) User hat eine Konversation in der Sidebar angeklickt. Die Library hat die Ansicht bereits umgeschaltet.
  • conversationDelete({id}) User hat auf das × einer Konversation geklickt. Die Library mutiert conversations nicht selbst — der Consumer entfernt den Eintrag aus seinem Store und pusht via setConversations zurück.

Methoden

  • addAssistantMessage(text: string): this Hängt eine Assistant-Text-Bubble an. Leerer Text = „pending"-Bubble mit Animation.
  • appendToLastAssistantMessage(chunk: string): this Streaming: hängt Token an die letzte Assistant-Text-Bubble.
  • addAssistantControl(control: sap.ui.core.Control): this Hängt ein beliebiges UI5-Control als eigene Assistant-Bubble an. Die Library übernimmt den Lifecycle — Control wird bei clear(), newChat, setMessages() oder ChatBox-Destroy mit destroy() aufgeräumt.
  • clear(): this Leert Transcript ohne newChat-Event zu feuern.
  • getMessages() / setMessages(msgs) Snapshot / Ersetzen des Transcripts der aktiven Konversation. Control- Refs werden von setMessages bewusst nicht übernommen.
  • getConversations() / setConversations(convs) Snapshot / Ersetzen der Sidebar-Liste. Beim ersten Setzen mit non-empty Array wird automatisch die erste Konversation aktiv und deren messages in den Transcript geladen.
  • getActiveConversationId() / setActiveConversationId(id) Programmatischer Wechsel. Unbekannte Id (inkl. "") → Transcript geleert (neuer-Chat-Draft-State).

Message- und Conversation-Struktur (exportiert)

interface Message {
    role: "user" | "assistant";
    content: string;
    files?: File[];
    control?: sap.ui.core.Control;
}

interface Conversation {
    id: string;
    title?: string;                       // fallback: erste User-Message
    messages?: ReadonlyArray<Message>;
    updatedAt?: number | string | Date;   // heute → HH:MM, gestern, Wochentag, Datum
}

Consumer-Muster

Persistenz per localStorage

interface Stored { id: string; title?: string; updatedAt?: number;
                    messages: { role: "user"|"assistant"; content: string }[]; }

function load(): Stored[] { try { return JSON.parse(localStorage.getItem("chats") ?? "[]"); } catch { return []; } }
function save(c: Stored[]) { localStorage.setItem("chats", JSON.stringify(c)); }

const stored = load();
chat.setConversations(stored);       // Library wählt automatisch die erste

Rich Content (Chart als Assistant-Bubble)

import VizFrame from "sap/viz/ui5/controls/VizFrame";

// Die Deps für VizFrame liegen in der Consumer-App, nicht in der Library.
const chart = new VizFrame({ vizType: "column", /* bindings … */ });
chat.addAssistantControl(chart);

App-Kontext ans Backend mitschicken

Die Library reicht Kontext bewusst nicht durch — jede App kennt ihren Kontext selbst und packt ihn ins Request-Payload:

send: async (e) => {
    const message = e.getParameter("message");
    const files = e.getParameter("files") as File[];
    const context = {
        route: window.location.hash,
        selection: model.getProperty("/selection"),
        clAudeMd: await fetch("./CLAUDE.md").then(r => r.text()),
    };
    const reply = await backend.chat({ message, files, context });
    // …
}

Design-Grenzen (bewusst außerhalb)

  • Kein Backend-Wissen. Auth, Session, Transport, Retry — alles Consumer.
  • Kein Streaming über fetch/SSE. Die Library bietet nur die Streaming-API (appendToLastAssistantMessage); die Netzwerk-Schicht bringt der Consumer mit.
  • Kein Upload-Transport. Files werden gesammelt und im send-Event durchgereicht — wie sie zum Backend gehen, entscheidet der Consumer (multipart, base64, S3-presigned, …).
  • Kein Storage. getMessages / setMessages bilden den Roundtrip, Storage bringt der Consumer.
  • Keine sap.m- / sap.viz-Dependencies. Rich Content wird per addAssistantControl reingereicht; die Library kennt weiterhin nur sap.ui.core.Control.

Entwicklung

npm run build-interface   # regeneriert ChatBox.gen.d.ts aus dem Control-Code
npm run ts-typecheck      # tsc --noEmit
npm run lint              # eslint
npm test                  # karma + qunit, headless Chrome
npm start                 # startet fiori run mit test/ChatBox.html
npm run build-app         # ui5 build → dist/

Nach Änderungen an Properties/Events/Aggregations in ChatBox.ts muss build-interface laufen, damit die .gen.d.ts-Metadata nicht driftet.

Publish auf npm

Die Root-package.json ist private: true und trägt nur die Dev- Metadaten. Publiziert wird ausschließlich der Build-Output unter dist/ mit einer generierten, schlanken package.json, so dass der Consumer node_modules/@cnt-dev/chat-ui/resources/chatui/… direkt vorfindet.

npm run pack-dist         # baut + prepped + erzeugt einen Tarball zum Prüfen
npm run publish-dist      # baut + prepped + npm publish ./dist (npm login vorausgesetzt)

scripts/prepare-publish.mjs schreibt dist/package.json, dist/ui5.yaml und kopiert README.md, LICENSE, AGENTS.md in dist/ hinein. Version anheben (npm version patch|minor|major) vor dem Publish.

Verzeichnisstruktur

chatui/
├── src/chatui/
│   ├── ChatBox.ts               # Hauptkontrolle (inline renderer)
│   ├── ChatBox.gen.d.ts         # generierte Interface-Deklaration
│   ├── markdown.ts              # kleiner Markdown→HTML-Konverter
│   ├── library.ts               # Lib.init() — registriert ChatBox
│   ├── .library                 # Library-Metadaten
│   ├── messagebundle*.properties # i18n (DE default, EN)
│   └── themes/…                 # sap_fiori_3(_dark|_hcw|_hcb)
├── test/chatui/
│   ├── ChatBox.html             # `npm start`-Einstiegspunkt
│   ├── ChatBox.ts               # dummy Send-Handler (Echo-Stream)
│   └── qunit/*.qunit.ts         # Unit-Tests
└── ui5.yaml, tsconfig.json, package.json, karma.conf.js

Ausführliche interne Hintergründe (Design-Entscheidungen, ESLint-Ausnahmen, Referenzmaterial) stehen in ../CLAUDE.md.