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

@devrobotlabs/visionapi-vue

v1.0.0

Published

Official Vue 3 composables for the Vision API — OCR and visual intelligence. Upload a file, get structured JSON with per-field confidence.

Readme

Vision API — Vue composables

Official Vue 3 composables for Vision API — let a user pick an image or a PDF and get structured JSON back with a confidence level on every value.

npm license


Read this first: where the key lives

There is no publishable key and no test mode. A Vision API key is a live spending credential — anyone who reads it out of your bundle can spend your credits. So these composables do not talk to api.visionapi.io. They talk to your endpoint, which holds the key and calls the API server-side with @devrobotlabs/visionapi.

In Nuxt that endpoint is a file in server/api/, and it is also where your own authorization, quota and audit trail belong:

// server/api/vision/analyze.post.ts — Nuxt
import { VisionAPI, VisionAPIError } from '@devrobotlabs/visionapi';

const vision = new VisionAPI(); // reads VISION_API_KEY, server-side only

export default defineEventHandler(async (event) => {
  const user = await requireUser(event);           // your auth
  const form = await readMultipartFormData(event);
  const file = form?.find((part) => part.name === 'file');
  if (!file) throw createError({ statusCode: 400 });

  try {
    return await vision.analyze({
      file: { data: file.data, filename: file.filename ?? 'upload' },
      preset: String(form?.find((p) => p.name === 'preset')?.data ?? 'auto'),
    });
  } catch (error) {
    if (error instanceof VisionAPIError) {
      // Forwarding the envelope is what lets the composables branch on error.code.
      throw createError({
        statusCode: error.status,
        data: { error: { code: error.code, message: error.message, details: error.details } },
      });
    }
    throw error;
  }
});

The composables expect POST {baseUrl}/analyze, /ask, /detect, and GET {baseUrl}/tasks/:id and /presets. Full proxies for Nuxt and Express are in examples/.


Install

npm install @devrobotlabs/visionapi-vue

Vue 3.4+. No dependencies of its own.

// main.ts
import { createVisionAPI } from '@devrobotlabs/visionapi-vue';

app.use(createVisionAPI({ baseUrl: '/api/vision' }));

The plugin is optional — every composable takes the same options directly.

Quick start

<script setup lang="ts">
import { useAnalyze, unwrap } from '@devrobotlabs/visionapi-vue';

const { analyze, data, isLoading, error, progress } = useAnalyze();

function onPick(event: Event) {
  const file = (event.target as HTMLInputElement).files?.[0];
  // The promise rejects on failure too; `error` is set either way.
  if (file) analyze(file, { preset: 'receipt' }).catch(() => {});
}
</script>

<template>
  <input type="file" accept="image/*,application/pdf" @change="onPick" />

  <progress v-if="isLoading" :value="progress" max="1" />
  <p v-if="error" role="alert">{{ error.userMessage }}</p>

  <dl v-if="data">
    <template v-for="(value, field) in unwrap(data.result, { dropNull: true })" :key="field">
      <dt>{{ field }}</dt>
      <dd>{{ value }}</dd>
    </template>
  </dl>
</template>

Composables

useAnalyze()

Extract structured data from one file.

const { analyze, data, isLoading, error, progress, cancel, reset } = useAnalyze();

await analyze(file, {
  preset: 'invoice',            // a catalog name, or 'auto' to classify first (free)
  pages: '1-3',                 // PDF page selection; you pay for selected pages only
  min_confidence: 'mid',        // weaker values come back null, confidence preserved
  schema: { machine_serial: 'Serial number of the machine being invoiced' },
});

Everything returned is a ref, so it works in a template without .value and in script with it. progress runs 0→1 during the upload, which is worth showing on a phone sending a 15 MB scan. cancel() aborts the request in flight; reset() clears the last result. Unmounting cancels automatically — the composable hooks onScopeDispose, so navigating away mid-upload leaves nothing behind.

useAsk()

Up to 5 questions about one file, priced exactly like an extraction. The questions themselves are free.

<script setup lang="ts">
const { ask, data } = useAsk();
await ask(file, ['Is the signature present?', 'Is the date after 2026-01-01?']);
</script>

<template>
  <li v-for="answer in data?.answers" :key="answer.question">
    {{ answer.question }} → <strong>{{ answer.verdict }}</strong>: {{ answer.answer }}
  </li>
</template>

verdict is 'yes', 'no', 'uncertain' (the image does not settle it — a real answer, not a failure) or 'n/a' (it wasn't a yes/no question). Branch on it instead of parsing the prose.

useDetect()

Ask what a file is before committing to an extraction. It is metered in batches whatever the page count, and far cheaper than an extraction (see pricing) — cheap enough to run on every upload:

const { detect } = useDetect();

const guess = await detect(file);
guess.recommended;   // exactly what preset: 'auto' would have run
guess.fallback;      // true = "shape unknown", not a match
guess.detections;    // the ranking, best first, with reasons

"This looks like a receipt — extract it?" is a much better experience than paying to extract a 40-page PDF nobody looked at.

useTask()

Poll an async task until it settles. Long PDFs and detail: 'high' need this: a synchronous request is killed at 60 seconds.

const taskId = ref<string | null>(null);

const { task, isPolling, isDone, stop } = useTask(taskId, {
  pollInterval: 2000,
  onSettled: (task) => toast(task.status === 'completed' ? 'Done' : 'Failed'),
});

async function submit(file: File) {
  const form = new FormData();
  form.set('file', file);
  const { task_id } = await $fetch('/api/vision/analyze-async', { method: 'POST', body: form });
  taskId.value = task_id;   // polling starts here
}

taskId is reactive — a ref, a getter or a plain string. Polling stops on its own when the task finishes, when maxWait elapses, and on unmount. A transient failure mid-poll does not throw the result away; only a terminal one (404, 410, 401, 403) stops it and lands on error.

usePresets()

The catalog, for a picker. Fetching beats hardcoding: presets are versioned, new ones appear, and the catalog carries the description you want next to each option.

<script setup lang="ts">
const { presets, isLoading } = usePresets();
const preset = ref('auto');
</script>

<template>
  <select v-model="preset" :disabled="isLoading">
    <option value="auto">Detect automatically</option>
    <option v-for="p in presets" :key="p.name" :value="p.name" :title="p.description">
      {{ p.title }} ({{ p.field_count }} fields)
    </option>
  </select>
</template>

Reading a result

Two rules explain almost every surprise:

1. Every scalar is wrapped. { value, confidence }, where confidence is 'low', 'mid' or 'high'. Read data.result.total.value, not data.result.total.

2. A preset response contains every field of that preset — including the ones the document does not carry, which come back as { value: null, confidence: 'low' }. A key being present does not mean a value was found.

Line-item arrays are the one shape worth looking at twice: the array itself is not wrapped, each cell inside each row is.

The helpers cover the common readings:

import { unwrap, value, rows, belowConfidence, atLeast } from '@devrobotlabs/visionapi-vue';

unwrap(data.value.result, { dropNull: true })   // { invoice_id: 'A-10422', total: 1284.5, … }
value(data.value.result, 'total', 0)            // 1284.5, or 0 when absent
rows(data.value.result, 'line_item')            // Row[] — [] when the invoice has no lines
belowConfidence(data.value.result, 'high')      // fields to highlight for review

unwrap() is also the right shape to seed a form — extract, prefill with v-model, let the user correct the weak fields, save:

<script setup lang="ts">
const form = ref<Record<string, unknown>>({});
const weak = computed(() => new Set(belowConfidence(data.value?.result, 'high')));

watch(data, (res) => {
  if (res) form.value = unwrap(res.result, { dropNull: true });
});
</script>

<template>
  <input v-model="form.total" :aria-invalid="weak.has('total')" />
</template>

Errors

Every failure is a VisionError with the HTTP status, the stable code, and whatever details your endpoint forwarded. Branch on code — never on the message text.

<template>
  <UpgradePrompt v-if="error?.isInsufficientCredits" />
  <p v-else-if="error?.isTooLarge">Try a smaller file — the limit is 20 MB.</p>
  <p v-else-if="error" role="alert">{{ error.userMessage }}</p>
</template>

error.userMessage is a plain sentence for each common code, so you get a decent UI without writing a switch. The convenience flags are isInsufficientCredits, isUnsupportedType, isTooLarge and isTimeout; anything else is error.code.

For this to work your proxy must forward the API's error envelope, as the example above does. If it swallows it, the composables still produce a structured http_<status> error.


Configuration

app.use(createVisionAPI({
  baseUrl: '/api/vision',                                    // your endpoint, not the API
  headers: () => ({ authorization: `Bearer ${session.token}` }), // read at request time
  credentials: 'same-origin',
}));

Pass headers as a function when the value changes — a rotating session token read at install time would go stale. Every composable also takes the same options directly:

const { analyze } = useAnalyze({ baseUrl: '/api/vision-admin' });

fetch can be swapped for a mock, which is how this package's own tests run.


Nuxt

Add the plugin client-side and put the proxy in server/api/:

// plugins/vision.client.ts
import { createVisionAPI } from '@devrobotlabs/visionapi-vue';

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(createVisionAPI({ baseUrl: '/api/vision' }));
});

The composables are client-side by nature — they start on a user picking a file — and nothing runs during SSR: useAnalyze fires only when you call analyze, and useTask(null) polls nothing. usePresets does fetch on setup, so wrap it in <ClientOnly> or call reload() from onMounted if you would rather it never ran on the server.


Limits worth designing around

| Limit | Default | What it means for your UI | | ---------------------------- | ------- | ------------------------------------------------------- | | Max file size | 20 MB | Validate before upload; a 413 wastes the round trip. | | Max PDF pages per request | 50 | Offer a page range for long documents. | | Sync request timeout | 60 s | Anything longer needs the async + useTask path. | | Max questions per ask | 5–10 | Per plan; the composable rejects one over the limit before sending. | | Requests per minute, per key | 10–600 | Per plan. Shared across your whole app — the key is per account. | | Concurrent async tasks | 1–32 | Per plan, per account. Over it: 429 too_many_tasks. |

Extractions are metered per image and per selected PDF page — a document page costs twice an image on analyze, the same as an image on ask — and detect far more cheaply. See pricing for current rates and the full per-plan matrix. Failures cost nothing, so a rejected upload never charges the user.


Examples

In examples/:

| File | What it shows | | ------------------------------------------------------------ | ---------------------------------------------------------- | | ReceiptScanner.vue | Upload → extract → editable form, with confidence highlights | | DetectThenAnalyze.vue | Confirm the document type before spending credits | | AsyncUpload.vue | A long PDF through the queue with useTask | | nuxt-server-route.ts | The Nuxt proxy, with auth and error forwarding |


Development

npm install
npm run build
npm test        # offline: fetch is stubbed, composables run in a real effect scope
npm run typecheck

Contributing

Issues and pull requests are welcome at https://github.com/devrobotlabs/visionapi-vue. For anything about the API itself — a preset, a limit, an error code — https://support.visionapi.io reaches the team faster.

License

MIT © Vision API