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

@aastrika/ai-elements

v0.7.1

Published

AI Content Studio, Assessment and Reports as custom elements. Works in React, Vue, Angular or plain HTML.

Readme

@aastrika/ai-elements

Three AI features. Three HTML tags. Any framework.

npm size deps licence

<aastrika-content-studio></aastrika-content-studio>   <!-- document → narrated video -->
<aastrika-assessment></aastrika-assessment>           <!-- material → MCQ set, translated -->
<aastrika-reports></aastrika-reports>                 <!-- who used what, and what it cost -->

Works in React, Vue, Angular, Svelte or plain HTML. Angular is compiled inside the package, so you never install it and never upgrade because we did.


Ship it in three steps

1 — Install. No peer dependencies, nothing to match.

npm i @aastrika/ai-elements

2 — Configure once, before anything renders.

import { configure } from '@aastrika/ai-elements';

configure({
  apiBase: 'https://api.aastrika.org/ai-studio',
  getAuthHeaders: async () => ({ Authorization: `Bearer ${await auth.token()}` }),
  creator: () => currentUser.username,
});

3 — Write the tag. Importing the package registers it, so there is no step 4.

<aastrika-assessment default-language="hi" default-translate-into="ta,or"></aastrika-assessment>

That is the whole integration. A trainer uploads a PDF and gets a validated quiz in Hindi, Tamil and Odia — same questions, same answer key, ready to download.

You also need gateway access. This package authenticates nobody — your gateway decides. See Access for the groups each feature needs.


Where to look

| | | |---|---| | The three features | What each element actually does | | Configuration | The three values, and why two are functions | | Access | Which gateway groups each feature needs | | Inputs and events | Open the form set up, act on the result | | Error reporting | Route failures to your monitoring | | | | | Integration guide ↗ | Step by step, both auth paths | | Runnable Angular app ↗ | Clone it, change one line, run it | | Changelog ↗ | What changed in each version |

Framework notes, theming, troubleshooting and server rendering are collapsed below — open the one you need.


The three features

<aastrika-content-studio>

A PDF goes in. A narrated, illustrated training video comes out.

 upload ──▶ pick style ──▶ review the plan ──▶ generate ──▶ play / download
            language        free, revisable      the only
            voice           cost shown first     paid step

The user uploads source material, picks a style, language and voice, and sees a scene-by-scene plan with an estimated cost. Planning is free and repeatable — they can rewrite it in plain words as often as they like. Only Generate video spends money, and the estimate is on screen before they commit.

While it renders they see five-phase progress and a live log. At the end they can play it, download it, or delete it.

Full walkthrough →

<aastrika-assessment>

Training material goes in. A validated MCQ set comes out — in as many languages as you need.

 files & links ──▶ settings ──▶ questions ──▶ review each ──▶ export
                   language      checked       language on     xlsx / csv
                   count         before you    its own tab
                   difficulty    see them

Every question is checked before it reaches the user: exactly one correct option, no duplicates, nothing the source material does not support.

Ask for other languages and you get the same questions translated — same order, same options, same answer key — so one answer key marks every paper and scores compare across them. The reviewer reads each language on its own tab and fixes wording in place; the correct option is shared, so it is set once.

Full walkthrough →

<aastrika-reports>

Who made what, and what it cost.

Totals for videos, assessments, people and estimated spend, split by tool, with a searchable table of creators.

Needs contentAdmin, and most consumers should not have it. Spend figures are not for a partner's ordinary users. The service applies no role check of its own — the gateway's ACL is the only thing keeping them apart.

Full walkthrough →


React

import '@aastrika/ai-elements';

<aastrika-content-studio />

React 19 passes properties and custom events natively — no wrapper, no ref.

Angular

Two steps: call configure() once at bootstrap, then add CUSTOM_ELEMENTS_SCHEMA to whichever component writes the tag.

1. Configure at bootstrap — main.ts, or an APP_INITIALIZER if the values come from something you have to load first:

import { configure } from '@aastrika/ai-elements';
import { environment } from './environments/environment';

configure({
  apiBase: environment.aastrikaApiBase,
  getAuthHeaders: async () => ({ Authorization: `Bearer ${await authService.token()}` }),
  creator: () => authService.username(),
});

bootstrapApplication(AppComponent, appConfig);

Import order matters: the import registers the three tags as a side effect, so it has to run before Angular renders a template containing one.

2. Host the tag

import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';

@Component({
  selector: 'app-training',
  standalone: true,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `<aastrika-content-studio></aastrika-content-studio>`,
})
export class TrainingComponent {}

CUSTOM_ELEMENTS_SCHEMA switches off template checking for every unknown tag in that component, not just ours — which is why the package ships tag declarations. They give it back for these three, so a typo in aastrika-assessment is still an error rather than a silently empty box.

Little else to wire. apiBase, the auth headers and the creator all come from configure(), and each element manages its own state, so there is no cleanup to write. Beyond that there are optional inputs and events — and edit-mode, which is worth knowing about up front:

<aastrika-content-studio edit-mode="on"></aastrika-content-studio>

edit-mode turns on the segment editor for an uploaded video, and defaults to off. Leave it off unless the host is same-origin with the service: the editor loads per-segment thumbnails the service streams off local disk, and unlike the finished video those have no presigned URL, so they cannot load from another origin. The other two elements take no attributes.

Routing works normally — put the tag in a lazily loaded route and the element mounts on navigation, unmounts on leaving.

Styling goes through CSS custom properties. Angular's view encapsulation rewrites a component-scoped rule into something that will not match the element, so set the properties from a global stylesheet:

/* styles.css */
aastrika-content-studio { --aastrika-gold: #0F766E; }

A blank box almost always means configure() ran after the template, or the import was tree-shaken because nothing referenced it. Importing configure and calling it, as above, prevents both.

Plain HTML

<script type="module" src="https://unpkg.com/@aastrika/ai-elements"></script>
<aastrika-content-studio></aastrika-content-studio>


How it fits together

  Your application                 @aastrika/ai-elements              Aastrika
  ────────────────                 ─────────────────────              ────────
  configure() ─────── apiBase ───▶ interceptor ─── Authorization ───▶ gateway
                      auth              ▲          creator in body       │
                      creator           │                                ▼
  <aastrika-… >  ────────────────▶ element ◀────────── JSON ────────  AI service
                      ▲                 │
                      └──── onError ────┘

Your application supplies three values. The package builds every request from apiBase, calls your two functions on each one, and renders into your page.


Configuration

Three values. That is the whole contract.

| | | | |---|---|---| | apiBase | required | Where the service is, no trailing slash. '' means same-origin | | getAuthHeaders | required | Headers proving the caller may use the service — a bearer token in practice | | creator | required | Who is signed into your app. A name or email, shown as-is in the usage report | | onError | optional | Called whenever a request fails, so your monitoring hears about it |

Pick the creator value with some care. It is stamped on the content when it is made and never changes afterwards, it is what the usage report groups by, and it is shown to whoever reads that report. So it wants to be a person's name — "Asha Kumari" reads well, and a login handle or an id does not.

Two traps worth knowing, both met in the wild:

  • A login handle is not a name. creatoruser_if0d is unreadable, and if you switch to it later, the report treats it as a different person from the name the same user's earlier content was filed under.
  • Check your email field is not masked. Some portals return cr********@yopmail.com from their profile API. Every user then files content under a near-identical string, and the report cannot tell them apart.

Returning null is a fair answer when you have no good name: the service applies its own default rather than recording something meaningless.

Why two of them are functions

Tokens expire. A string handed over at startup stops working mid-session, and the package has no way to ask for a fresh one.

getAuthHeaders: async () => ({ Authorization: `Bearer ${await auth.token()}` }),
//              ^^^^^ called on every request, so a refreshed token just works

The same goes for creator: a function lets you switch user without reloading the page.

creator is attribution, not identity. It is recorded against whatever the call creates and the usage report groups by it. Nothing verifies it — the gateway decides whether the call is allowed at all.

Use asha.kumari, not a UUID. It appears as-is in the report.


Access

Ask the Aastrika platform team for a Kong consumer with the groups each feature needs:

| Feature | Groups | |---|---| | aastrika-content-studio | contentCreate, contentAccess, contentUpdate | | aastrika-assessment | contentCreate, contentAccess, contentUpdate | | aastrika-reports | contentAdmin |

Deleting anything needs contentAdmin as well. Without contentUpdate the features still work, but the revise step returns 403.

generate-video is the only call that spends real money, and it sits on a deliberately low rate limit. Users see a cost estimate before triggering it.


CSS custom properties are the styling surface:

aastrika-content-studio {
  --aastrika-gold: #0F766E;
  --aastrika-surface: #ffffff;
  --aastrika-font: "Noto Sans", sans-serif;
}

These render into your page, not a shadow root. Angular's emulated view encapsulation scopes the package's own CSS, so it will not leak out and break your layout. It does not stop your CSS reaching in: a global rule on a common class name — .card, .btn, .field — will hit these elements too. If your stylesheet is broad, scope it away from the three tags.



Knowing when something failed

The elements show their own error to the user. onError is the second copy, for your monitoring:

configure({
  apiBase: '...',
  onError: ({ feature, message, status, code }) => {
    Sentry.captureMessage(`aastrika/${feature}: ${message}`, { extra: { status, code } });
  },
});

| | | |---|---| | feature | content-studio, assessment or reports | | message | The sentence shown to the user | | status | HTTP status. 0 means the request never left the browser — usually CORS | | code | The service's code, when it sent one, e.g. RENDER_TIMEOUT |

Without it, an element failing every call looks exactly like one nobody has used yet. Throwing from inside it is safe — the exception is swallowed, so a broken reporter cannot break the feature reporting to it.


Inputs and events

Every element works with nothing bound. These are for hosts that want to open the form already set up, or act on what the user produced.

Inputs

Attributes in HTML, properties in JS — Angular Elements maps one to the other. All are starting values the user can still change.

| Element | Input | | |---|---|---| | content-studio | default-language | hi, en, kn, te | | | default-aspect-ratio | 16:9 or 9:16 | | | default-depth | How much detail the video goes into | | | default-voice | A voice id from /v1/studio/list-voices | | | edit-mode | on shows the segment editor. Same-origin hosts only | | assessment | default-language | | | | default-question-count | | | | default-difficulty | mixed, easy, medium, hard | | | default-translate-into | Also produce the same questions in these languages, comma-separated: "ta,or,bn" | | reports | creator-filter | Opens filtered to one creator | | all three | heading | off hides the element's own eyebrow and title. Defaults to on |

heading="off" is for a host whose own chrome already names the screen — a breadcrumb or a tab bar — where the element's title would say it a second time. The description under the title is kept either way: it says what the feature does, which a breadcrumb does not. Use it instead of hiding the title with CSS; our class names are ours to rename, and a stylesheet reaching in breaks silently when they change.

<!-- your page already shows "AI Studio › Assessment Creation" -->
<aastrika-assessment heading="off"></aastrika-assessment>
<aastrika-content-studio default-language="kn" default-aspect-ratio="9:16">
</aastrika-content-studio>

Translations, in the event

el.addEventListener('assessmentReady', (e) => {
  e.detail.language;   // 'hi'         — the one the questions were written in
  e.detail.languages;  // ['en','mr']  — the translations produced
});

languages can be shorter than what was asked for. A translation is checked for question count, ids, answer key, clinical numbers and script; one that fails is dropped rather than stored wrong. What is not checked is whether it reads well — that still needs someone who speaks the language.

Events

| Element | Event | Fires when | detail | |---|---|---|---| | content-studio | planReady | A plan comes back. Repeatable — free, so this can fire several times | jobId, title, sceneCount, estimatedUsd | | | videoStarted | The user commits to a render. This is where money is spent | jobId | | | videoReady | The video is finished | jobId, videoUrl | | assessment | assessmentReady | A validated question set comes back | jobId, questionCount, language, languages | | reports | reportLoaded | The figures are on screen | videos, assessments, users, spendUsd |

el.addEventListener('videoReady', (e) => {
  saveToOurRecords(e.detail.jobId, e.detail.videoUrl);
});

videoUrl plays without an Authorization header, so it can go straight into a <video src> or be stored as-is. It is null on the local storage driver, where there is nothing to presign.

React passes properties and listeners natively. Angular binds them like any DOM event: (videoReady)="onReady($event)".


| Symptom | Cause | |---|---| | Throws naming configure() | it was never called. Supply at least apiBase | | 401 on everything | the token expired — check getAuthHeaders is a function, not a captured string | | 403 on revise only | missing contentUpdate | | 403 on delete only | missing contentAdmin | | 429 on Generate video | the low rate limit, and it is intentional | | Everything recorded as admin | creator was not set, or it returned null | | Blank box, no errors | the module did not load, so the tag is an unknown element |



Browser support

Any browser with Custom Elements v1 — everything since 2018.

Import this package only in browser code. It is not server-renderable, and the failure is a crash rather than an empty box: @angular/elements declares a class extending HTMLElement at module scope, so merely importing the package where there is no DOM throws ReferenceError: HTMLElement is not defined.

Next.js:

import dynamic from 'next/dynamic';

const Studio = dynamic(
  () => import('@aastrika/ai-elements').then(() => () => <aastrika-content-studio />),
  { ssr: false },
);

Angular Universal: keep the import inside a browser-only guard, or load it in ngAfterViewInit rather than at module scope.

Nothing is lost by this. The elements have no server-rendered output to hydrate — they fetch everything at runtime — so a client-only import renders exactly the same page.