@aastrika/ai-elements
v0.7.1
Published
AI Content Studio, Assessment and Reports as custom elements. Works in React, Vue, Angular or plain HTML.
Maintainers
Readme
@aastrika/ai-elements
Three AI features. Three HTML tags. Any framework.
<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-elements2 — 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 stepThe 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.
<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 themEvery 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.
<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.
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_if0dis 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.comfrom 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 worksThe same goes for creator: a function lets you switch user without reloading
the page.
creatoris 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-videois 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.
