Modules

Spark AI in je eigen app

Zet het chatvenster van Spark AI in je eigen website of app. Het werkt zoals de chat in de Suite: antwoorden die je ziet ontstaan, de stappen die de assistent zet, tabellen met export, grafieken, formules, vervolgvragen, bijlagen en feedback. Je hebt één sleutel nodig, en die blijft op je eigen server.

Zo werkt het

Het pakket @floraspark/spark-chat heeft twee delen. In de browser staat het chatvenster, <spark-chat>. Op je eigen server staat één route, standaard /api/spark. Het venster praat alleen met die route, en de route zet je sleutel erbij en praat met api.floraspark.nl.

browser      <spark-chat endpoint="/api/spark">    geen sleutel
   │
jouw server  /api/spark                            SPARK_API_KEY staat hier
   │
FloraSpark   https://api.floraspark.nl/v1/chat/...

Waarom zo? Een sleutel leest alle data van je organisatie en gebruikt jullie AI-tegoed. In browsercode kan iedereen hem uitlezen. Op je server blijft hij van jou.

In drie stappen

1. Maak een sleutel aan

Ga in de Suite naar Ontwikkelaars, tabblad Sleutels, en maak een sleutel aan (zie Ontwikkelaars). Geef hem een herkenbaar label, zoals "Klantportaal": dat label zie je straks terug in de Chatbot Monitor. Wil je dat het venster minder ziet dan alles, geef de sleutel dan een rolbeperking, bijvoorbeeld Alleen inkoop.

Zet de sleutel op je server als omgevingsvariabele:

SPARK_API_KEY=fsk_...

2. Voeg de route toe

npm install @floraspark/spark-chat

Next.js (App Router), in app/api/spark/route.js:

export { GET, POST } from '@floraspark/spark-chat/server';
export const dynamic = 'force-dynamic';

Express:

import { sparkExpress } from '@floraspark/spark-chat/server/express';

app.all('/api/spark', sparkExpress());

Node zonder framework:

import http from 'node:http';
import { sparkNode } from '@floraspark/spark-chat/server/express';

const spark = sparkNode();
http.createServer((req, res) => {
  if (req.url.startsWith('/api/spark')) return spark(req, res);
  // je eigen routes
}).listen(3000);

Remix, Hono, Bun, Deno, Cloudflare Workers en alles wat met Request en Response werkt:

import { createSparkHandler } from '@floraspark/spark-chat/server';

const spark = createSparkHandler({ apiKey: env.SPARK_API_KEY });
// Hono:  app.all('/api/spark', (c) => spark(c.req.raw));

Python (FastAPI): het pakket bevat een kant-en-klare router in examples/python/spark_proxy.py. Voeg hem toe met app.include_router(router). Een andere taal? De route is klein; zie De route zelf bouwen.

3. Zet het venster op je pagina

Het venster vult het element waar het in staat, dus geef het een hoogte.

<script type="module">import '@floraspark/spark-chat';</script>
<spark-chat endpoint="/api/spark" style="height: 600px"></spark-chat>

In React:

import { SparkChat } from '@floraspark/spark-chat/react';

<SparkChat endpoint="/api/spark" style={{ height: 600 }} />

Zonder bundler, met één bestand:

<script src="https://cdn.jsdelivr.net/npm/@floraspark/spark-chat@0.1.0/dist/spark-chat.js" defer></script>
<spark-chat endpoint="/api/spark" style="height: 100vh"></spark-chat>

Open je pagina en stel een vraag.

Eerst proberen zonder sleutel

Het pakket bevat een nagebootste FloraSpark met dezelfde routes en verzonnen antwoorden: een tabel, een grafiek, een formule, vervolgvragen en een paar fouten om je foutafhandeling te testen.

node node_modules/@floraspark/spark-chat/examples/mock-upstream.mjs
SPARK_API_BASE_URL=http://localhost:8787 SPARK_API_KEY=fsk_test npm run dev

Een vraag met "fout" geeft een foutmelding, met "tegoed" een leeg AI-tegoed en met "lang" een traag antwoord om de stopknop te proberen. Haal SPARK_API_BASE_URL weg zodra je met je echte sleutel werkt.

Instellingen van het venster

AttribuutStandaardWat het doet
endpoint/api/sparkDe route op je eigen server
languagetaal van de pagina, anders nlnl of en: de teksten van het venster en de taal van de antwoorden
themeautoauto volgt het systeem, of light of dark
persistlocallocal bewaart het gesprek in de browser, none niet
storage-keyde endpointEigen naam voor die opslag, bijvoorbeeld per ingelogde gebruiker
headingSpark AIDe naam in de kopbalk. Gebruik heading en niet title
headernone verbergt de kopbalk
placeholderStel een vraag…De tekst in het lege invoerveld
welcome-title, welcome-subtitleDe tekst op het welkomstscherm
suggestionsvier standaardvragenStartvragen als JSON: '["Omzet deze maand?"]'
mathaanoff laadt het formulepakket nooit. Anders alleen als een antwoord een formule bevat
fontsaannone laadt geen eigen lettertypen; dan geldt jouw lettertype

Vanuit je eigen code kun je ook een vraag stellen (chat.send('...')), een nieuw gesprek beginnen (chat.reset()) en luisteren naar spark-answer en spark-error.

In je eigen stijl

Het venster heeft standaard de FloraSpark-stijl. Zet je het in de stijl van je eigen site, dan blijven alle functies hetzelfde. Er zijn drie niveaus.

Kleuren, lettertype en afronding

Zet CSS-variabelen op het element. Jouw waarden winnen altijd, ook in de donkere modus.

spark-chat {
  --spark-primary: #0a58ca;
  --spark-on-primary: #ffffff;
  --spark-bg: #f8f9fa;
  --spark-surface: #ffffff;
  --spark-text: #212529;
  --spark-muted: #6c757d;
  --spark-border: #dee2e6;
  --spark-font: Inter, system-ui, sans-serif;
  --spark-font-display: Inter, system-ui, sans-serif;
  --spark-radius: 6px;
}

Heeft je site een donkere modus, zet de kleuren dan ook voor spark-chat[theme="dark"] (en voor prefers-color-scheme: dark als je theme="auto" gebruikt). Anders blijven je lichte kleuren in de donkere modus staan.

VariabeleWaarvoor
--spark-primary, --spark-primary-hover, --spark-on-primaryKnoppen, vet in antwoorden, accenten, en de tekst daarop
--spark-accent, --spark-accent-mid, --spark-tintLichte accenten en de achtergrond van een gekozen knop
--spark-bg, --spark-surfaceDe achtergrond van het venster; invoerveld, tabellen en grafieken
--spark-text, --spark-muted, --spark-faintTekst, secundaire tekst, de lichtste tekst
--spark-border, --spark-dividerRanden en scheidingslijnen
--spark-danger, --spark-danger-strong, --spark-danger-bgFoutmeldingen
--spark-code-bg, --spark-code-textCodeblokken
--spark-chart-1 tot en met --spark-chart-4De reeksen in een grafiek
--spark-font, --spark-font-mono, --spark-font-displayGewone tekst, getallen en code, koppen
--spark-radius, --spark-max-widthDe afronding van het invoerveld, de breedte van de tekstkolom

Delen van het venster

Met ::part() stijl je de grote vlakken zelf: root, header, body en composer. Met header="none" en de tekstattributen hierboven maak je het venster verder je eigen.

spark-chat::part(composer) { border-top: 1px solid var(--spark-border); }

Een volledig eigen interface

Wil je ook tabellen, grafieken en de opbouw van een antwoord zelf tekenen, gebruik dan de client zonder weergave uit @floraspark/spark-chat/client. Je krijgt dezelfde stream, bijlagen en feedback, en dezelfde logica waarmee de Suite een antwoord opbouwt.

import { SparkChatClient, buildPayload, createTurn, applyTurnEvent } from '@floraspark/spark-chat/client';

const client = new SparkChatClient({ endpoint: '/api/spark' });
const { payload } = buildPayload(history, { role: 'user', content: 'Omzet deze week?' }, { sessionId });
let turn = createTurn('t1');
await client.stream(payload, {
  onEvent: (name, data) => { turn = applyTurnEvent(turn, name, data); render(turn); },
});

Wat er in een antwoord kan staan

Hetzelfde als in de Suite:

  • tekst met koppen, vet, lijsten en code;
  • tabellen: sorteren per kolom en downloaden als CSV;
  • lijn- en staafgrafieken met tot vier reeksen, op te slaan als afbeelding;
  • formules;
  • vervolgvragen als knoppen;
  • de stappen van de assistent, live en daarna ingeklapt;
  • een duim omhoog of omlaag. Een duim omlaag gaat met je toelichting naar FloraSpark, net als in de Suite.

Wat niet kan: kaartjes die iets in de Suite openen (een taak, een rapport, een dashboardtegel, churn of een product), en acties in apps die onder Koppelingen gekoppeld zijn. Die lopen via de koppeling van één medewerker, en horen niet bij iedereen die je app gebruikt.

Veiligheid en privacy

  • De sleutel staat alleen op je server. De route stuurt nooit de cookies of de eigen inloggegevens van de browser door naar FloraSpark.
  • Iedereen die bij de route kan, gebruikt jouw sleutel. Staat het venster achter je eigen login, laat de route dat dan ook controleren:
createSparkHandler({ authorize: async (request) => Boolean(await getSession(request)) });
  • Alle gebruikers delen de rol en het AI-tegoed van de sleutel. Een sleutel met Alleen inkoop laat in het venster ook alleen inkoop zien.
  • Gesprekken staan in de Chatbot Monitor. Beheerders van je organisatie zien ze in de Suite onder Toegang en toezicht, met het label App-sleutel en de naam van de sleutel. Als gebruiker staat er het e-mailadres van de sleutel, niet dat van de persoon in je app. Ze verschijnen niet in iemands eigen gespreksgeschiedenis, en worden na 90 dagen verwijderd. Zet in je privacyverklaring dat vragen worden bewaard en door beheerders kunnen worden ingezien.
  • In de browser staat het gesprek in de lokale opslag van de gebruiker. Delen meerdere mensen één browser, geef dan per gebruiker een eigen storage-key, of zet persist="none".
  • Intrekken werkt direct. Trek de sleutel in onder Ontwikkelaars en het venster stopt.

Grenzen en kosten

WatGrens
Vragen20 per minuut per sleutel
Instellingen, bijlagen en feedback60 verzoeken per minuut per sleutel
Lengte van een vraag8.000 tekens; het venster stuurt de laatste 40 berichten als geschiedenis mee
Bijlagen4 per vraag en 8 per gesprek, elk tot 10 MB: afbeeldingen, pdf, Word, Excel, csv en tekst
Een verzoek256 KB, een upload 12 MB

Elke vraag gaat van het AI-tegoed van je organisatie af, net als in Spark AI. Een bijlage blijft ongeveer anderhalf uur bruikbaar; daarna vraagt het venster om hem opnieuw toe te voegen.

Foutmeldingen

Het venster toont bij elke fout een begrijpelijke melding met Opnieuw proberen. Bouw je zelf, dan zijn dit de codes:

StatusCodeBetekenis
500missing_api_keySPARK_API_KEY staat niet op je server
401De sleutel is onbekend of ingetrokken, of je authorize weigerde (unauthorized)
402ai_budget_exhaustedHet AI-tegoed van deze maand is op
404chat_disabledHet chatvenster staat voor jullie organisatie nog niet aan. Neem contact met ons op
413payload_too_largeHet verzoek of de bijlage is te groot
422attachment_expiredEen bijlage is verlopen; voeg hem opnieuw toe. Zonder code: de vraag is te lang of er zijn te veel bijlagen
429Te veel vragen per minuut; wacht even
502upstream_unreachableJe server kan FloraSpark niet bereiken, of een storing aan onze kant

De route zelf bouwen

Werk je niet met JavaScript of Python, dan bouw je de route zelf. Hij doet vier dingen:

Het venster vraagtJouw route vraagt aan https://api.floraspark.nl
GET /api/spark?op=configGET /v1/chat/config
POST /api/spark?op=stream (JSON)POST /v1/chat/stream. Het antwoord is text/event-stream: geef het ongebufferd door
POST /api/spark?op=upload (multipart, veld file)POST /v1/chat/upload, met dezelfde Content-Type
POST /api/spark?op=feedback (JSON)POST /v1/chat/feedback
  • Zet Authorization: Bearer <SPARK_API_KEY> erbij, en stuur geen cookies of Authorization van de browser door.
  • Geef de statuscode en de JSON-fout ongewijzigd terug. Weiger andere waarden van op.
  • Zet geen compressie of buffering op de stream, anders komt het antwoord pas binnen als het af is.

De stream

POST /v1/chat/stream neemt {"messages": [...], "session_id": "...", "level": "snel", "language": "nl"}. messages is het gesprek tot nu toe, met als laatste de nieuwe vraag; een bericht is {"role": "user" | "assistant", "content": "...", "attachments": ["<file_id>"]}. Stuur de session_id uit het vorige antwoord mee, dan komt de vervolgvraag in hetzelfde gesprek in de Chatbot Monitor.

curl -N https://api.floraspark.nl/v1/chat/stream \
  -H "Authorization: Bearer $SPARK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"messages": [{"role": "user", "content": "Wat was de omzet vorige week?"}]}'

Het antwoord is een reeks events, elk als event: <naam> en data: <json>:

EventWat erin staat
thinkingEen nieuwe denkronde begint: {"round": 1}
reasoningEen stukje van de gedachtegang, in het Engels: {"text": "..."}
step_startDe assistent begint een stap, bijvoorbeeld een databasevraag: {"tool": "execute_sql", "title": "..."}
step_endDe stap is klaar: uitkomst, aantal rijen en duur
deltaEen stukje van het antwoord: {"text": "..."}
answerHet volledige antwoord, met answer, status, steps, session_id en message_id. Dit is altijd het laatste event
errorEr ging onderweg iets mis: {"error": "...", "code": "..."}

Het antwoord bevat gewone tekst en blokken tussen drie backticks: csv voor een tabel, chart voor een grafiek (JSON met type, title, labels en datasets) en suggestions voor vervolgvragen, één per regel. Andere blokken kun je weglaten.

Instellingen, bijlagen en feedback

  • GET /v1/chat/config geeft de niveaus die je mag kiezen (levels, default_level), de grenzen voor bijlagen en max_message_chars.
  • POST /v1/chat/upload neemt één bestand in het veld file en geeft {"file_id": "...", "filename": "...", "kind": "image" | "file" | "text"} terug. Zet het file_id bij de vraag in attachments.
  • POST /v1/chat/feedback neemt {"rating": "up" | "down", "category": "begrip" | "berekening" | "anders", "comment": "...", "session_id": "...", "message_id": 123, "question": "...", "answer": "..."}. Bij een duim omlaag zijn category en comment verplicht.