Summary: A small full-stack app: Vite + React UI, Express with the OpenAI Responses API, seed Q&A pairs aimed at Cloudinary docs (extend later to real retrieval), plus optional DALL·E avatars. API keys stay on the server.
This tutorial builds a RAG-style seeded docs assistant: your Node server prepends example Cloudinary Q&As to the live thread, then returns plain text from the Responses API, with optional image avatars from /api/avatar. It is not fine-tuned—just hardcoded conversation seeds you can replace with vector search or Assistants later.
What you’ll build
- A chat UI that streams answers from your own examples/context
- A Node/Express API that calls OpenAI for text and image generation
- Two cute, auto-generated avatars (user & assistant)
- A path to swap seed Q&A for retrieval over your real Cloudinary docs (see Production notes)
Demo question shown here: “How do I use the Cloudinary React SDK?”
Repo (reference): Cloudinary-Chatbot-OpenAI-Demo
Prereqs
- Node 18+
- An OpenAI API key stored server-side (never in the browser). How to create/manage keys: see the official docs. (OpenAI Platform)
1) Create the React app (Vite)
# New project
npm create vite@latest cloudinary-chatbot -- --template react
cd cloudinary-chatbot
npm i
Vite proxy (avoid CORS while developing)
vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:6000',
changeOrigin: true,
secure: false,
},
},
},
})
Chatbot UI
Create a src/App.jsx file. You can find the full code of this file in the repo.
The chat functionality
const sendMessage = async () => {
if (!inputMessage.trim()) return;
const newMessages = [...messages, { role: "user", content: inputMessage.trim() }];
setMessages(newMessages);
setInputMessage("");
setStatus("loading");
try {
const res = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages: newMessages }),
});
const data = await res.json();
setMessages([...newMessages, { role: "assistant", content: data.content }]);
} catch (e) {
setMessages([...newMessages, { role: "assistant", content: "Server error. Try again." }]);
} finally {
setStatus("idle");
}
};
It POSTs the message list to /api/chat, appends the assistant reply, or an error string on failure, and always clears the loading state.
Generating avatars
useEffect(() => {
const makeAvatars = async () => {
try {
const res = await fetch("/api/avatar", { method: "POST" });
const data = await res.json(); // [{url}, {url}]
setUserImage(data[0].url);
setAssistantImage(data[1].url);
} catch {
// silently ignore; UI still works without avatars
}
};
makeAvatars();
}, []);
To mimic a two-party chat, generate avatars once on load. The backend exposes POST /api/avatar; the response maps to user and assistant images.
Add your own CSS or use our existing App.css.
2) Add the backend (Express + OpenAI)
Inside the project root, create a folder named backend and a file server.js. We’ll reuse the root package.json for simplicity.
Install deps
npm i express dotenv openai
# optional: nodemon for dev
npm i -D nodemon
Environment variables
Create .env in the project root:
OPENAI_API_KEY=sk-...
Server code (Responses API + Images API)
- Why Responses API? It’s the recommended, modern way to generate text and stream outputs going forward; if you’re coming from Chat Completions, see the official migration guide. (OpenAI Platform)
- Why SDK over raw fetch? Cleaner code, types, and built-ins for images. (OpenAI Platform). You can find the full code for the backend in the repo.
backend/server.js
Seed Q&A (demo “training data”)
Example seed pairs (replace with your own docs text or retrieval output):
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const demoModel = [
{
role: "user",
content: "Where is the Cloudinary React SDK documentation?"
},
{
role: "assistant",
content: "See: https://cloudinary.com/documentation/react_integration"
},
{
role: "user",
content: "Where can I read about Cloudinary image transformations in React?"
},
{
role: "assistant",
content: "See: https://cloudinary.com/documentation/react_image_transformations"
},
{
role: "user",
content: "How do I display an image using the Cloudinary React SDK?"
},
{
role: "assistant",
content:
`Use @cloudinary/react and @cloudinary/url-gen. Example:
import { AdvancedImage } from '@cloudinary/react';
import { Cloudinary } from '@cloudinary/url-gen';
import { sepia } from '@cloudinary/url-gen/actions/effect';
const cld = new Cloudinary({ cloud: { cloudName: 'demo' } });
const img = cld.image('front_face').effect(sepia());
<AdvancedImage cldImg={img} />`
}
];
demoModel is a list of user/assistant turns. The chat route prepends them to the request so the model follows the same Cloudinary doc-link style. Swap or augment this list later with RAG hits instead of hand-written pairs.
Creating the chat route with OpenAI
app.post("/api/chat", async (req, res) => {
try {
const { messages = [] } = req.body;
// System prompt keeps the bot scoped to your docs
const system = {
role: "system",
content:
"You are a helpful developer docs assistant. Prefer official Cloudinary docs. Include links when helpful."
};
const input = [system, ...demoModel, ...messages]
.map(m => ({ role: m.role, content: m.content }));
const r = await openai.responses.create({
model: "gpt-4o-mini",
input
});
// Convenience: return plain text
res.json({ content: r.output_text });
} catch (err) {
console.error(err);
res.status(500).json({ error: "Internal Server Error" });
}
});
This endpoint prepends the system + seeds + client messages, calls openai.responses.create, and returns output_text, or 500 on error.
Creating the avatars with DALL·E
app.post("/api/avatar", async (_req, res) => {
try {
const r = await openai.images.generate({
model: "gpt-image-1",
prompt:
"minimal, cute round animal avatar on flat background, high contrast, centered, no text",
n: 2,
size: "256x256"
});
// Return {url} objects for the UI
res.json(r.data.map(d => ({ url: d.url })));
} catch (err) {
console.error(err);
res.status(500).json({ error: "Internal Server Error" });
}
});
POST /api/avatar returns two gpt-image-1 URLs (see Images API for sizes and models).(OpenAI Platform)
- Responses API reference (Node): see Responses docs. (OpenAI Platform)
- Images API reference: see Images docs. (OpenAI Platform)
3) Dev scripts
Add these to your root package.json:
{
"scripts": {
"dev": "vite",
"server": "node backend/server.js",
"server:dev": "nodemon backend/server.js"
}
}
4) Run it
# terminal 1
npm run server:dev
# terminal 2
npm run dev
# open http://localhost:3000
Type:
“How do I use the Cloudinary React SDK?”
You should get a helpful, linked answer in the spirit of your seed examples.
Production notes (important)
- Never expose your API key in client code or public repos. Use env vars and a server. (OpenAI Platform)
- Prefer Responses API for new builds and streaming; see migration notes if you used Chat Completions before. (OpenAI Platform)
- If you want retrieval over your actual docs (beyond hardcoded examples), use the Assistants API with tools or Retrieval. (OpenAI Platform)
Wrap-up
You now have a lightweight, dev-friendly chatbot that answers in the style of your seeds and shows generated avatars. From here you can:
- Swap the seed examples for real retrieval.
- Add streaming UI for token-by-token responses. (OpenAI Platform)
- Validate outputs with structured JSON. (OpenAI Platform)
Further reading:
- OpenAI Quickstart (Node) (OpenAI Platform)
- Responses API (text generation) (OpenAI Platform)
- Images API (generation & sizes) (OpenAI Platform)
Repo: Cloudinary-Chatbot-OpenAI-Demo
| Cloudinary ❤️ developers |
|---|
| Ready to level up your media workflow? Start using Cloudinary for free and build better visual experiences today. |
| 👉 Create your free account |
Top comments (1)
I think I missed how it does this: "A chat UI that streams answers from your own examples/context". Would you please point that out to me? Thank You! I have a project with many .md files and would like to do this.