Framework guides
Where the snippet goes in HTML, React, Next.js, Vue, Nuxt, Angular, SvelteKit and Laravel.
The widget is one script, so every framework does the same thing: load it once, with three attributes. Only the place where the script goes is different.
Before you start
- Get your snippet from the Embed page in the console. It has your key, app id and workspace id.
- Add your site's domain, and your local dev address (like localhost:5173), to the allowed domains.
- Load the script once, in the part of your app that never unmounts. It has no unmount and ignores a second copy, so do not put it inside a page component. The examples below use
dk_live_…as a sample key.
Keeping the three values in environment variables lets you use one app for staging and another for production:
DANESHYAR_WIDGET_KEY=dk_live_…
DANESHYAR_APP_ID=…
DANESHYAR_WORKSPACE_ID=…Static HTML
Paste the tag just before </body> on every page that should show the chat. If your site has a shared footer or template, put it there once.
<!doctype html>
<html lang="fa" dir="rtl">
<head>
<meta charset="utf-8" />
<title>My clinic</title>
</head>
<body>
<h1>Welcome</h1>
<!-- Daneshyar widget: once per page, just before </body> -->
<script
src="https://app.daneshyar.info/widget.js"
data-api-key="dk_live_9f3aC2xQ7mB1vT8sE4nK6pR0jY5wZ2hL"
data-app-id="2d7c0e91-4a5b-4f8e-b3c6-9e1f0a7d5c42"
data-workspace-id="8f14e45f-ceea-467a-9f4c-1a2b3c4d5e6f"
async
></script>
</body>
</html>Nothing else is needed. The script loads with async, after your content.
React (Vite)
Make a small component and render it once in App.tsx. It adds the script when the app first mounts.
import { useEffect } from "react";
const WIDGET_SRC = "https://app.daneshyar.info/widget.js";
/** Render once, in your root component (App.tsx). */
export function DaneshyarWidget() {
useEffect(() => {
// widget.js runs once per page. Don't add it twice.
if (document.querySelector(`script[src="${WIDGET_SRC}"]`)) return;
const script = document.createElement("script");
script.src = WIDGET_SRC;
script.async = true;
// dataset.apiKey becomes data-api-key, and so on.
script.dataset.apiKey = import.meta.env.VITE_DANESHYAR_WIDGET_KEY;
script.dataset.appId = import.meta.env.VITE_DANESHYAR_APP_ID;
script.dataset.workspaceId = import.meta.env.VITE_DANESHYAR_WORKSPACE_ID;
script.dataset.locale = "fa";
document.body.appendChild(script);
}, []);
return null; // the widget draws its own button
}With Create React App, use process.env.REACT_APP_… instead of import.meta.env.VITE_…. In development, React's Strict Mode runs effects twice. The querySelector check stops a second copy.
Next.js (App Router)
Use next/script in the root layout, app/layout.tsx, so the script stays loaded while visitors move between pages.
import Script from "next/script";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="fa" dir="rtl">
<body>
{children}
<Script
src="https://app.daneshyar.info/widget.js"
strategy="afterInteractive"
data-api-key={process.env.NEXT_PUBLIC_DANESHYAR_WIDGET_KEY}
data-app-id={process.env.NEXT_PUBLIC_DANESHYAR_APP_ID}
data-workspace-id={process.env.NEXT_PUBLIC_DANESHYAR_WORKSPACE_ID}
/>
</body>
</html>
);
}Variables used in the browser must start with NEXT_PUBLIC_. With the Pages Router, put the same <Script> in pages/_app.tsx.
Vue 3
Add the script from App.vue, the root component, in onMounted.
<script setup lang="ts">
import { onMounted } from "vue";
const WIDGET_SRC = "https://app.daneshyar.info/widget.js";
onMounted(() => {
// widget.js runs once per page. Don't add it twice.
if (document.querySelector(`script[src="${WIDGET_SRC}"]`)) return;
const script = document.createElement("script");
script.src = WIDGET_SRC;
script.async = true;
script.dataset.apiKey = import.meta.env.VITE_DANESHYAR_WIDGET_KEY;
script.dataset.appId = import.meta.env.VITE_DANESHYAR_APP_ID;
script.dataset.workspaceId = import.meta.env.VITE_DANESHYAR_WORKSPACE_ID;
document.body.appendChild(script);
});
</script>
<template>
<RouterView />
</template>For a Vue app without a build step, paste the HTML snippet into index.html instead.
Nuxt 3
Add the script to app.head in nuxt.config.ts. Nuxt adds it to every page.
export default defineNuxtConfig({
app: {
head: {
script: [
{
src: "https://app.daneshyar.info/widget.js",
async: true,
tagPosition: "bodyClose",
"data-api-key": "dk_live_9f3aC2xQ7mB1vT8sE4nK6pR0jY5wZ2hL",
"data-app-id": "2d7c0e91-4a5b-4f8e-b3c6-9e1f0a7d5c42",
"data-workspace-id": "8f14e45f-ceea-467a-9f4c-1a2b3c4d5e6f",
},
],
},
},
});To read the values from the environment, use runtimeConfig.public and useHead() in app.vue instead.
Angular
Paste the snippet into src/index.html, after <app-root>.
<!doctype html>
<html lang="fa" dir="rtl">
<head>
<meta charset="utf-8" />
<base href="/" />
</head>
<body>
<app-root></app-root>
<script
src="https://app.daneshyar.info/widget.js"
data-api-key="dk_live_9f3aC2xQ7mB1vT8sE4nK6pR0jY5wZ2hL"
data-app-id="2d7c0e91-4a5b-4f8e-b3c6-9e1f0a7d5c42"
data-workspace-id="8f14e45f-ceea-467a-9f4c-1a2b3c4d5e6f"
async
></script>
</body>
</html>This is the simplest place, and it works for every route. To use different values per environment, use one index.html per build with fileReplacements in angular.json.
SvelteKit
Paste the snippet into src/app.html, after %sveltekit.body%.
<!doctype html>
<html lang="fa" dir="rtl">
<head>
<meta charset="utf-8" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
<script
src="https://app.daneshyar.info/widget.js"
data-api-key="dk_live_9f3aC2xQ7mB1vT8sE4nK6pR0jY5wZ2hL"
data-app-id="2d7c0e91-4a5b-4f8e-b3c6-9e1f0a7d5c42"
data-workspace-id="8f14e45f-ceea-467a-9f4c-1a2b3c4d5e6f"
async
></script>
</body>
</html>For plain Svelte with Vite, add it to index.html. To read the values from the environment, add the script in onMount in your root +layout.svelte, as in the React example.
Laravel (Blade)
Put the tag in your main Blade layout and read the values from config/services.php.
{{-- config/services.php:
'daneshyar' => [
'widget_key' => env('DANESHYAR_WIDGET_KEY'),
'app_id' => env('DANESHYAR_APP_ID'),
'workspace_id' => env('DANESHYAR_WORKSPACE_ID'),
], --}}
<body>
@yield('content')
<script
src="https://app.daneshyar.info/widget.js"
data-api-key="{{ config('services.daneshyar.widget_key') }}"
data-app-id="{{ config('services.daneshyar.app_id') }}"
data-workspace-id="{{ config('services.daneshyar.workspace_id') }}"
async
></script>
</body>After you change .env, run php artisan config:clear so Laravel reads the new values.
Node.js and Express
The widget runs in the browser, so you don't need a server for it. Use a server only if you build your own chat screen. Then your server calls the REST API with the key, and your page calls your server:
import express from "express";
const BASE = "https://api.daneshyar.info/api/v1";
const KEY = process.env.DANESHYAR_API_KEY; // server only, never in the browser
const WORKSPACE = process.env.DANESHYAR_WORKSPACE_ID;
const app = express();
app.use(express.json());
// Your frontend calls POST /api/ask. Only this server knows the key.
app.post("/api/ask", async (req, res) => {
const { question, sessionId } = req.body;
if (typeof question !== "string" || !question.trim()) {
return res.status(400).json({ error: "question is required" });
}
const upstream = await fetch(`${BASE}/workspaces/${WORKSPACE}/chat/`, {
method: "POST",
headers: { "X-API-Key": KEY, "Content-Type": "application/json" },
body: JSON.stringify({
message: question,
session_id: sessionId ?? undefined,
// One id per signed-in user keeps their chats apart.
external_user_id: String(req.user?.id ?? "anonymous"),
}),
});
const body = await upstream.json();
if (!upstream.ok) {
return res.status(upstream.status).json({ error: body.message ?? body.detail });
}
const reply = body.data.assistant_message;
res.json({
sessionId: body.data.session.id,
answer: reply.content,
sources: reply.citations,
});
});
app.listen(3000);The full request and response are on the Chat API page. This key must stay on the server. Never send it to the browser.
What you should see
- A round button in the corner of every page (or the chat inside your element, in inline mode).
- Clicking it opens the chat with your title and the list of documents.
- A question about your documents gets an answer with numbered sources.
Nothing appears, or you see an error message? See Troubleshooting.