@cloudgrid-io/mcp 0.11.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/corpus/capability-map.md +40 -6
- package/src/corpus/examples/ai-app/index.md +237 -0
- package/src/corpus/examples/api-service/index.md +190 -0
- package/src/corpus/examples/crm/index.md +273 -0
- package/src/corpus/examples/kanban/index.md +256 -0
- package/src/corpus/templates/admin-dashboard/README.md +75 -0
- package/src/corpus/templates/admin-dashboard/cloudgrid.yaml +15 -0
- package/src/corpus/templates/admin-dashboard/index.md +78 -0
- package/src/corpus/templates/admin-dashboard/services/web/app/api/orders/route.js +64 -0
- package/src/corpus/templates/admin-dashboard/services/web/app/api/users/route.js +63 -0
- package/src/corpus/templates/admin-dashboard/services/web/app/layout.js +39 -0
- package/src/corpus/templates/admin-dashboard/services/web/app/page.js +73 -0
- package/src/corpus/templates/admin-dashboard/services/web/app/resource-table.js +94 -0
- package/src/corpus/templates/admin-dashboard/services/web/lib/db.js +32 -0
- package/src/corpus/templates/admin-dashboard/services/web/package.json +16 -0
- package/src/corpus/templates/ai-app/README.md +95 -0
- package/src/corpus/templates/ai-app/cloudgrid.yaml +20 -0
- package/src/corpus/templates/ai-app/index.md +269 -0
- package/src/corpus/templates/ai-app/services/web/app/api/chat/route.js +51 -0
- package/src/corpus/templates/ai-app/services/web/app/chat.js +60 -0
- package/src/corpus/templates/ai-app/services/web/app/layout.js +36 -0
- package/src/corpus/templates/ai-app/services/web/app/page.js +25 -0
- package/src/corpus/templates/ai-app/services/web/lib/db.js +32 -0
- package/src/corpus/templates/ai-app/services/web/package.json +17 -0
- package/src/corpus/templates/api-docs/README.md +28 -0
- package/src/corpus/templates/api-docs/cloudgrid.yaml +10 -0
- package/src/corpus/templates/api-docs/index.html +136 -0
- package/src/corpus/templates/api-docs/index.md +30 -0
- package/src/corpus/templates/api-service/README.md +87 -0
- package/src/corpus/templates/api-service/cloudgrid.yaml +18 -0
- package/src/corpus/templates/api-service/index.md +225 -0
- package/src/corpus/templates/api-service/services/api/package.json +13 -0
- package/src/corpus/templates/api-service/services/api/src/index.js +132 -0
- package/src/corpus/templates/changelog/README.md +27 -0
- package/src/corpus/templates/changelog/cloudgrid.yaml +10 -0
- package/src/corpus/templates/changelog/index.html +80 -0
- package/src/corpus/templates/changelog/index.md +30 -0
- package/src/corpus/templates/crm/README.md +72 -0
- package/src/corpus/templates/crm/cloudgrid.yaml +15 -0
- package/src/corpus/templates/crm/index.md +68 -0
- package/src/corpus/templates/crm/services/web/app/api/contacts/route.js +80 -0
- package/src/corpus/templates/crm/services/web/app/contact-manager.js +102 -0
- package/src/corpus/templates/crm/services/web/app/layout.js +35 -0
- package/src/corpus/templates/crm/services/web/app/page.js +34 -0
- package/src/corpus/templates/crm/services/web/lib/db.js +32 -0
- package/src/corpus/templates/crm/services/web/package.json +16 -0
- package/src/corpus/templates/docs-site/README.md +28 -0
- package/src/corpus/templates/docs-site/cloudgrid.yaml +10 -0
- package/src/corpus/templates/docs-site/index.html +122 -0
- package/src/corpus/templates/docs-site/index.md +30 -0
- package/src/corpus/templates/kanban/README.md +72 -0
- package/src/corpus/templates/kanban/cloudgrid.yaml +15 -0
- package/src/corpus/templates/kanban/index.md +68 -0
- package/src/corpus/templates/kanban/services/web/app/api/cards/route.js +70 -0
- package/src/corpus/templates/kanban/services/web/app/board.js +93 -0
- package/src/corpus/templates/kanban/services/web/app/layout.js +37 -0
- package/src/corpus/templates/kanban/services/web/app/page.js +32 -0
- package/src/corpus/templates/kanban/services/web/lib/db.js +32 -0
- package/src/corpus/templates/kanban/services/web/package.json +16 -0
- package/src/corpus/templates/portfolio/README.md +29 -0
- package/src/corpus/templates/portfolio/cloudgrid.yaml +10 -0
- package/src/corpus/templates/portfolio/index.html +126 -0
- package/src/corpus/templates/portfolio/index.md +30 -0
- package/src/corpus/templates/saas-marketing/README.md +30 -0
- package/src/corpus/templates/saas-marketing/cloudgrid.yaml +10 -0
- package/src/corpus/templates/saas-marketing/index.html +212 -0
- package/src/corpus/templates/saas-marketing/index.md +30 -0
- package/src/corpus/templates/status-page/README.md +28 -0
- package/src/corpus/templates/status-page/cloudgrid.yaml +10 -0
- package/src/corpus/templates/status-page/index.html +117 -0
- package/src/corpus/templates/status-page/index.md +30 -0
- package/src/corpus/templates/task-manager/README.md +72 -0
- package/src/corpus/templates/task-manager/cloudgrid.yaml +15 -0
- package/src/corpus/templates/task-manager/index.md +68 -0
- package/src/corpus/templates/task-manager/services/web/app/api/tasks/route.js +77 -0
- package/src/corpus/templates/task-manager/services/web/app/layout.js +39 -0
- package/src/corpus/templates/task-manager/services/web/app/page.js +33 -0
- package/src/corpus/templates/task-manager/services/web/app/task-list.js +110 -0
- package/src/corpus/templates/task-manager/services/web/lib/db.js +32 -0
- package/src/corpus/templates/task-manager/services/web/package.json +16 -0
- package/src/corpus/templates/waitlist/README.md +38 -0
- package/src/corpus/templates/waitlist/cloudgrid.yaml +10 -0
- package/src/corpus/templates/waitlist/index.html +69 -0
- package/src/corpus/templates/waitlist/index.md +41 -0
- package/src/corpus/workflows/admin-dashboard.md +102 -0
- package/src/corpus/workflows/ai-app.md +136 -0
- package/src/corpus/workflows/api-docs.md +36 -0
- package/src/corpus/workflows/api-service.md +118 -0
- package/src/corpus/workflows/changelog.md +33 -0
- package/src/corpus/workflows/crm.md +101 -0
- package/src/corpus/workflows/docs-site.md +35 -0
- package/src/corpus/workflows/kanban.md +102 -0
- package/src/corpus/workflows/portfolio.md +35 -0
- package/src/corpus/workflows/saas-marketing.md +37 -0
- package/src/corpus/workflows/status-page.md +35 -0
- package/src/corpus/workflows/task-manager.md +101 -0
- package/src/corpus/workflows/waitlist.md +42 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Template: ai-app (Next.js AI chatbot + grid AI gateway + Mongo)
|
|
2
|
+
|
|
3
|
+
A minimal but real, deployable AI chatbot. A Next.js App Router app that calls
|
|
4
|
+
the grid AI gateway (via `@cloudgrid-io/ai`) for replies and persists the
|
|
5
|
+
conversation to the grid-shared MongoDB. History survives refresh and is shared
|
|
6
|
+
across sessions.
|
|
7
|
+
|
|
8
|
+
**Key rules (all proven by a real end-to-end deploy):**
|
|
9
|
+
|
|
10
|
+
1. **App code MUST live under `services/<name>/`, not the repo/template root.**
|
|
11
|
+
`path:` in `cloudgrid.yaml` is the URL mount, NOT the filesystem path. The
|
|
12
|
+
service named `web` → the CLI looks for `services/web/`. Files at the root
|
|
13
|
+
fail with `Error: Service directory not found: …/services/web`.
|
|
14
|
+
2. **The AI call is zero-config — no API key.** `import { createClient } from
|
|
15
|
+
"@cloudgrid-io/ai"; const client = createClient();` auto-detects the in-grid
|
|
16
|
+
identity, so it only works inside a deployed grid app (or under `grid dev`).
|
|
17
|
+
Do NOT pass or set an API key. Read the reply from `r.text` (fall back to
|
|
18
|
+
`r.content`).
|
|
19
|
+
3. **Read the DB connection string LAZILY (inside the getter), never at module
|
|
20
|
+
top level.** The grid injects it as `DATABASE_MONGODB_URL` (plus the legacy
|
|
21
|
+
`MONGODB_URL` alias) at dev + runtime; the app reads
|
|
22
|
+
`process.env.DATABASE_MONGODB_URL || process.env.MONGODB_URL` inside `getDb`.
|
|
23
|
+
A top-level read fails `next build`. Never hardcode a connection string; never
|
|
24
|
+
commit a secret.
|
|
25
|
+
4. **Declare `needs: { ai: true, database: true }`.** The deployer wires the AI
|
|
26
|
+
gateway and provisions Mongo (injecting `DATABASE_MONGODB_URL` + legacy
|
|
27
|
+
`MONGODB_URL`). `requires:` is the deprecated v1 alias; don't author new yaml
|
|
28
|
+
with it, and never set `needs:` and `requires:` together (the validator
|
|
29
|
+
rejects the combination).
|
|
30
|
+
|
|
31
|
+
Write these files into the scaffolded app folder — the app code goes under
|
|
32
|
+
`services/web/` — adapt the persona/UI, then `grid dev` (local) / `grid plug`
|
|
33
|
+
(deploy, async — poll to a live URL).
|
|
34
|
+
|
|
35
|
+
## File tree
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
cloudgrid.yaml # name + services.web (nextjs) + needs: { ai: true, database: true }
|
|
39
|
+
services/web/package.json # next, react, react-dom, @cloudgrid-io/ai, mongodb
|
|
40
|
+
services/web/lib/db.js # lazy Mongo client from DATABASE_MONGODB_URL (legacy MONGODB_URL fallback)
|
|
41
|
+
services/web/app/layout.js # root layout + inline CSS
|
|
42
|
+
services/web/app/page.js # server component: reads chat history from Mongo
|
|
43
|
+
services/web/app/chat.js # client component: sends messages, renders the log
|
|
44
|
+
services/web/app/api/chat/route.js # POST: createClient().chat(...) -> persist -> reply; GET: history
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## cloudgrid.yaml
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
# Rename this chatbot. The grid wires the AI gateway (in-grid identity — no API
|
|
51
|
+
# key) and injects the DB connection string as an environment variable at
|
|
52
|
+
# runtime. Do NOT set an AI key, do NOT set the DB var yourself, and never commit
|
|
53
|
+
# a connection string or secret.
|
|
54
|
+
#
|
|
55
|
+
# `needs: { ai: true, database: true }` is the canonical shape. The deployer
|
|
56
|
+
# wires the AI gateway and provisions Mongo, injecting DATABASE_MONGODB_URL (plus
|
|
57
|
+
# the legacy MONGODB_URL alias). `requires:` is the deprecated v1 alias — don't
|
|
58
|
+
# author new yaml with it, and never set `needs:` and `requires:` together.
|
|
59
|
+
name: my-chatbot
|
|
60
|
+
services:
|
|
61
|
+
web:
|
|
62
|
+
type: nextjs
|
|
63
|
+
path: /
|
|
64
|
+
needs:
|
|
65
|
+
ai: true
|
|
66
|
+
database: true
|
|
67
|
+
# For retrieval-augmented (RAG) over your own docs, add `needs: { vector:
|
|
68
|
+
# pgvector }` once platform issue #1545 ships — not yet available.
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
> **Capability:** this template's needs are `ai: true` (the grid AI gateway,
|
|
72
|
+
> in-grid identity — no key) and `database: true` (Mongo, injected as
|
|
73
|
+
> `DATABASE_MONGODB_URL` + legacy `MONGODB_URL`). `requires:` is the deprecated
|
|
74
|
+
> v1 alias — don't mix it with `needs:`. See the capability-map for the full
|
|
75
|
+
> injection table.
|
|
76
|
+
|
|
77
|
+
## services/web/package.json
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"name": "my-chatbot",
|
|
82
|
+
"version": "0.1.0",
|
|
83
|
+
"private": true,
|
|
84
|
+
"scripts": { "dev": "next dev", "build": "next build", "start": "next start" },
|
|
85
|
+
"dependencies": {
|
|
86
|
+
"next": "^15.1.0",
|
|
87
|
+
"react": "^19.0.0",
|
|
88
|
+
"react-dom": "^19.0.0",
|
|
89
|
+
"@cloudgrid-io/ai": "^0.2.0",
|
|
90
|
+
"mongodb": "^6.12.0"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## services/web/lib/db.js
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
// Cached Mongo client for the chat history. The grid injects DATABASE_MONGODB_URL
|
|
99
|
+
// (plus the legacy MONGODB_URL alias) at dev + runtime. Resolve it LAZILY inside
|
|
100
|
+
// the getter — a top-level read fails `next build`. Never hardcode a connection
|
|
101
|
+
// string; never commit a secret.
|
|
102
|
+
import { MongoClient } from "mongodb";
|
|
103
|
+
|
|
104
|
+
function clientPromise() {
|
|
105
|
+
const uri = process.env.DATABASE_MONGODB_URL || process.env.MONGODB_URL;
|
|
106
|
+
if (!uri) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
"DATABASE_MONGODB_URL is not set. The grid injects it — run `grid dev` " +
|
|
109
|
+
"locally or deploy with `grid plug`. Do not set it by hand.",
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
if (!globalThis.__mongoClientPromise) {
|
|
113
|
+
globalThis.__mongoClientPromise = new MongoClient(uri).connect();
|
|
114
|
+
}
|
|
115
|
+
return globalThis.__mongoClientPromise;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function getDb() {
|
|
119
|
+
return (await clientPromise()).db();
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## services/web/app/api/chat/route.js
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
// Chat API route: take the user's message, ask the grid AI gateway for a reply,
|
|
127
|
+
// persist the exchange to Mongo, return the reply.
|
|
128
|
+
//
|
|
129
|
+
// The AI call uses @cloudgrid-io/ai with ZERO config — no API key. createClient()
|
|
130
|
+
// auto-detects the in-grid identity, so it only works inside a deployed grid app
|
|
131
|
+
// (or under `grid dev`). Do NOT pass a key.
|
|
132
|
+
import { NextResponse } from "next/server";
|
|
133
|
+
import { createClient } from "@cloudgrid-io/ai";
|
|
134
|
+
import { getDb } from "../../../lib/db.js";
|
|
135
|
+
|
|
136
|
+
export const dynamic = "force-dynamic";
|
|
137
|
+
|
|
138
|
+
async function messages() {
|
|
139
|
+
return (await getDb()).collection("messages");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// POST /api/chat — body { message: string }. Returns { reply: string }.
|
|
143
|
+
export async function POST(request) {
|
|
144
|
+
const body = await request.json().catch(() => ({}));
|
|
145
|
+
const text = typeof body.message === "string" ? body.message.trim() : "";
|
|
146
|
+
if (!text) {
|
|
147
|
+
return NextResponse.json({ error: "message is required" }, { status: 400 });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 1. Ask the grid AI gateway (zero-config in-grid identity — no key).
|
|
151
|
+
const client = createClient();
|
|
152
|
+
const r = await client.chat({ messages: [{ role: "user", content: text }] });
|
|
153
|
+
const reply = r.text ?? r.content ?? "";
|
|
154
|
+
|
|
155
|
+
// 2. Persist the exchange so the conversation survives refresh.
|
|
156
|
+
const col = await messages();
|
|
157
|
+
const now = new Date();
|
|
158
|
+
await col.insertMany([
|
|
159
|
+
{ role: "user", content: text, createdAt: now },
|
|
160
|
+
{ role: "assistant", content: reply, createdAt: new Date(now.getTime() + 1) },
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
// 3. Return the reply.
|
|
164
|
+
return NextResponse.json({ reply });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// GET /api/chat — the stored conversation, oldest first.
|
|
168
|
+
export async function GET() {
|
|
169
|
+
const col = await messages();
|
|
170
|
+
const history = await col.find({}).sort({ createdAt: 1 }).toArray();
|
|
171
|
+
return NextResponse.json(
|
|
172
|
+
history.map((m) => ({ id: m._id.toString(), role: m.role, content: m.content })),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## services/web/app/page.js
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
import { getDb } from "../lib/db.js";
|
|
181
|
+
import Chat from "./chat.js";
|
|
182
|
+
|
|
183
|
+
export const dynamic = "force-dynamic";
|
|
184
|
+
|
|
185
|
+
async function listHistory() {
|
|
186
|
+
const items = await (await getDb())
|
|
187
|
+
.collection("messages")
|
|
188
|
+
.find({})
|
|
189
|
+
.sort({ createdAt: 1 })
|
|
190
|
+
.toArray();
|
|
191
|
+
return items.map((m) => ({ id: m._id.toString(), role: m.role, content: m.content }));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export default async function Page() {
|
|
195
|
+
const history = await listHistory();
|
|
196
|
+
return (
|
|
197
|
+
<main>
|
|
198
|
+
<h1>AI Assistant</h1>
|
|
199
|
+
<p className="hint">Powered by the grid AI gateway — conversation persisted in Mongo.</p>
|
|
200
|
+
<Chat initialHistory={history} />
|
|
201
|
+
</main>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## services/web/app/chat.js
|
|
207
|
+
|
|
208
|
+
```js
|
|
209
|
+
"use client";
|
|
210
|
+
import { useState } from "react";
|
|
211
|
+
|
|
212
|
+
export default function Chat({ initialHistory }) {
|
|
213
|
+
const [history, setHistory] = useState(initialHistory);
|
|
214
|
+
const [text, setText] = useState("");
|
|
215
|
+
const [busy, setBusy] = useState(false);
|
|
216
|
+
|
|
217
|
+
async function send(e) {
|
|
218
|
+
e.preventDefault();
|
|
219
|
+
const value = text.trim();
|
|
220
|
+
if (!value || busy) return;
|
|
221
|
+
setBusy(true);
|
|
222
|
+
setHistory((p) => [...p, { id: `local-${Date.now()}`, role: "user", content: value }]);
|
|
223
|
+
setText("");
|
|
224
|
+
const res = await fetch("/api/chat", {
|
|
225
|
+
method: "POST",
|
|
226
|
+
headers: { "content-type": "application/json" },
|
|
227
|
+
body: JSON.stringify({ message: value }),
|
|
228
|
+
});
|
|
229
|
+
if (res.ok) {
|
|
230
|
+
const { reply } = await res.json();
|
|
231
|
+
setHistory((p) => [...p, { id: `local-${Date.now()}-a`, role: "assistant", content: reply }]);
|
|
232
|
+
}
|
|
233
|
+
setBusy(false);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<div>
|
|
238
|
+
<ul className="log">
|
|
239
|
+
{history.length === 0 && <li className="empty">Say hello to start the conversation.</li>}
|
|
240
|
+
{history.map((m) => (
|
|
241
|
+
<li key={m.id} className={m.role}>
|
|
242
|
+
<span className="who">{m.role === "user" ? "You" : "Assistant"}</span>
|
|
243
|
+
<span className="msg">{m.content}</span>
|
|
244
|
+
</li>
|
|
245
|
+
))}
|
|
246
|
+
</ul>
|
|
247
|
+
<form onSubmit={send} className="row">
|
|
248
|
+
<input value={text} onChange={(e) => setText(e.target.value)} placeholder="Ask something…" disabled={busy} />
|
|
249
|
+
<button type="submit" disabled={busy}>{busy ? "…" : "Send"}</button>
|
|
250
|
+
</form>
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Adapt it
|
|
257
|
+
|
|
258
|
+
- Give the assistant a persona — pass a `{ role: "system", content: "…" }`
|
|
259
|
+
message ahead of the user message in `chat({ messages })`.
|
|
260
|
+
- Send prior turns as context by including the stored history in `messages`.
|
|
261
|
+
- Change the `messages` collection / fields; add users, sessions, titles.
|
|
262
|
+
- Run `grid dev` to test locally, `grid plug` to deploy (async — poll to live).
|
|
263
|
+
|
|
264
|
+
## RAG / retrieval (not yet)
|
|
265
|
+
|
|
266
|
+
To answer over **your own documents** (retrieval-augmented generation) you need a
|
|
267
|
+
vector store — `needs: { vector: pgvector }`. That path is **not yet available**
|
|
268
|
+
(platform issue #1545). Ship the plain chatbot now; add retrieval once the vector
|
|
269
|
+
need is unblocked.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Chat API route: takes the user's message, asks the grid AI gateway for a
|
|
2
|
+
// reply, persists the exchange to Mongo, and returns the reply.
|
|
3
|
+
//
|
|
4
|
+
// The AI call uses @cloudgrid-io/ai with ZERO config — no API key. createClient()
|
|
5
|
+
// auto-detects the in-grid identity, so it only works inside a deployed grid app
|
|
6
|
+
// (or under `grid dev`). Do NOT pass a key.
|
|
7
|
+
import { NextResponse } from "next/server";
|
|
8
|
+
import { createClient } from "@cloudgrid-io/ai";
|
|
9
|
+
import { getDb } from "../../../lib/db.js";
|
|
10
|
+
|
|
11
|
+
// Never cache — always generate/persist live.
|
|
12
|
+
export const dynamic = "force-dynamic";
|
|
13
|
+
|
|
14
|
+
async function messages() {
|
|
15
|
+
const db = await getDb();
|
|
16
|
+
return db.collection("messages");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// POST /api/chat — body { message: string }. Returns { reply: string }.
|
|
20
|
+
export async function POST(request) {
|
|
21
|
+
const body = await request.json().catch(() => ({}));
|
|
22
|
+
const text = typeof body.message === "string" ? body.message.trim() : "";
|
|
23
|
+
if (!text) {
|
|
24
|
+
return NextResponse.json({ error: "message is required" }, { status: 400 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 1. Ask the grid AI gateway (zero-config in-grid identity — no key).
|
|
28
|
+
const client = createClient();
|
|
29
|
+
const r = await client.chat({ messages: [{ role: "user", content: text }] });
|
|
30
|
+
const reply = r.text ?? r.content ?? "";
|
|
31
|
+
|
|
32
|
+
// 2. Persist the exchange so the conversation survives refresh.
|
|
33
|
+
const col = await messages();
|
|
34
|
+
const now = new Date();
|
|
35
|
+
await col.insertMany([
|
|
36
|
+
{ role: "user", content: text, createdAt: now },
|
|
37
|
+
{ role: "assistant", content: reply, createdAt: new Date(now.getTime() + 1) },
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
// 3. Return the reply.
|
|
41
|
+
return NextResponse.json({ reply });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// GET /api/chat — the stored conversation, oldest first.
|
|
45
|
+
export async function GET() {
|
|
46
|
+
const col = await messages();
|
|
47
|
+
const history = await col.find({}).sort({ createdAt: 1 }).toArray();
|
|
48
|
+
return NextResponse.json(
|
|
49
|
+
history.map((m) => ({ id: m._id.toString(), role: m.role, content: m.content })),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// Small client component: renders the conversation and posts new messages to
|
|
4
|
+
// /api/chat. The route calls the grid AI gateway, persists the exchange, and
|
|
5
|
+
// returns the reply.
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
|
|
8
|
+
export default function Chat({ initialHistory }) {
|
|
9
|
+
const [history, setHistory] = useState(initialHistory);
|
|
10
|
+
const [text, setText] = useState("");
|
|
11
|
+
const [busy, setBusy] = useState(false);
|
|
12
|
+
|
|
13
|
+
async function send(e) {
|
|
14
|
+
e.preventDefault();
|
|
15
|
+
const value = text.trim();
|
|
16
|
+
if (!value || busy) return;
|
|
17
|
+
setBusy(true);
|
|
18
|
+
setHistory((prev) => [...prev, { id: `local-${Date.now()}`, role: "user", content: value }]);
|
|
19
|
+
setText("");
|
|
20
|
+
const res = await fetch("/api/chat", {
|
|
21
|
+
method: "POST",
|
|
22
|
+
headers: { "content-type": "application/json" },
|
|
23
|
+
body: JSON.stringify({ message: value }),
|
|
24
|
+
});
|
|
25
|
+
if (res.ok) {
|
|
26
|
+
const { reply } = await res.json();
|
|
27
|
+
setHistory((prev) => [...prev, { id: `local-${Date.now()}-a`, role: "assistant", content: reply }]);
|
|
28
|
+
} else {
|
|
29
|
+
setHistory((prev) => [
|
|
30
|
+
...prev,
|
|
31
|
+
{ id: `local-${Date.now()}-e`, role: "assistant", content: "Something went wrong. Try again." },
|
|
32
|
+
]);
|
|
33
|
+
}
|
|
34
|
+
setBusy(false);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div>
|
|
39
|
+
<ul className="log">
|
|
40
|
+
{history.length === 0 && <li className="empty">Say hello to start the conversation.</li>}
|
|
41
|
+
{history.map((m) => (
|
|
42
|
+
<li key={m.id} className={m.role}>
|
|
43
|
+
<span className="who">{m.role === "user" ? "You" : "Assistant"}</span>
|
|
44
|
+
<span className="msg">{m.content}</span>
|
|
45
|
+
</li>
|
|
46
|
+
))}
|
|
47
|
+
</ul>
|
|
48
|
+
<form onSubmit={send} className="row">
|
|
49
|
+
<input
|
|
50
|
+
value={text}
|
|
51
|
+
onChange={(e) => setText(e.target.value)}
|
|
52
|
+
placeholder="Ask something…"
|
|
53
|
+
aria-label="Message"
|
|
54
|
+
disabled={busy}
|
|
55
|
+
/>
|
|
56
|
+
<button type="submit" disabled={busy}>{busy ? "…" : "Send"}</button>
|
|
57
|
+
</form>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Root layout + plain inline CSS (no external CDNs, fonts, or stylesheets).
|
|
2
|
+
export const metadata = {
|
|
3
|
+
title: "AI Assistant",
|
|
4
|
+
description: "An AI chatbot on CloudGrid — grid AI gateway + grid-shared Mongo.",
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const css = `
|
|
8
|
+
:root { color-scheme: light dark; }
|
|
9
|
+
* { box-sizing: border-box; }
|
|
10
|
+
body { margin: 0; font-family: system-ui, -apple-system, sans-serif; line-height: 1.5; }
|
|
11
|
+
main { max-width: 42rem; margin: 3rem auto; padding: 0 1.25rem; }
|
|
12
|
+
h1 { margin: 0 0 .25rem; }
|
|
13
|
+
.hint { margin: 0 0 1.5rem; opacity: .7; font-size: .9rem; }
|
|
14
|
+
.log { list-style: none; padding: 0; margin: 0 0 1rem; display: flex; flex-direction: column; gap: .75rem; }
|
|
15
|
+
.log li { display: flex; flex-direction: column; gap: .15rem; }
|
|
16
|
+
.log li.user { align-items: flex-end; text-align: right; }
|
|
17
|
+
.log li.empty { opacity: .6; align-items: flex-start; }
|
|
18
|
+
.who { font-size: .75rem; opacity: .6; }
|
|
19
|
+
.msg { display: inline-block; padding: .5rem .75rem; border-radius: .6rem; background: #8882; max-width: 85%; white-space: pre-wrap; }
|
|
20
|
+
.log li.user .msg { background: #4a90d922; }
|
|
21
|
+
.row { display: flex; gap: .5rem; }
|
|
22
|
+
input { flex: 1; padding: .5rem .75rem; border: 1px solid #8886; border-radius: .5rem; font: inherit; }
|
|
23
|
+
button { padding: .5rem .9rem; border: 1px solid #8886; border-radius: .5rem; background: #6663; font: inherit; cursor: pointer; }
|
|
24
|
+
button:disabled { opacity: .5; cursor: default; }
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
export default function RootLayout({ children }) {
|
|
28
|
+
return (
|
|
29
|
+
<html lang="en">
|
|
30
|
+
<head>
|
|
31
|
+
<style dangerouslySetInnerHTML={{ __html: css }} />
|
|
32
|
+
</head>
|
|
33
|
+
<body>{children}</body>
|
|
34
|
+
</html>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Home page: a server component reads the stored conversation straight from
|
|
2
|
+
// Mongo, and a small client component sends new messages via the /api/chat
|
|
3
|
+
// route (which calls the grid AI gateway and persists the exchange). History
|
|
4
|
+
// persists across refresh because it lives in the grid-shared Mongo.
|
|
5
|
+
import { getDb } from "../lib/db.js";
|
|
6
|
+
import Chat from "./chat.js";
|
|
7
|
+
|
|
8
|
+
export const dynamic = "force-dynamic";
|
|
9
|
+
|
|
10
|
+
async function listHistory() {
|
|
11
|
+
const db = await getDb();
|
|
12
|
+
const items = await db.collection("messages").find({}).sort({ createdAt: 1 }).toArray();
|
|
13
|
+
return items.map((m) => ({ id: m._id.toString(), role: m.role, content: m.content }));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default async function Page() {
|
|
17
|
+
const history = await listHistory();
|
|
18
|
+
return (
|
|
19
|
+
<main>
|
|
20
|
+
<h1>AI Assistant</h1>
|
|
21
|
+
<p className="hint">Powered by the grid AI gateway — conversation persisted in Mongo.</p>
|
|
22
|
+
<Chat initialHistory={history} />
|
|
23
|
+
</main>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Cached MongoDB client for the App Router — stores the chat history.
|
|
2
|
+
//
|
|
3
|
+
// The grid injects the connection string as the DATABASE_MONGODB_URL environment
|
|
4
|
+
// variable (plus the legacy MONGODB_URL alias) at dev-time (`grid dev`) and
|
|
5
|
+
// runtime (after `grid plug`). Read the canonical var first, fall back to the
|
|
6
|
+
// legacy alias. Never hardcode a connection string here and never commit a secret.
|
|
7
|
+
//
|
|
8
|
+
// The env var and client are resolved LAZILY (inside getDb), never at module top
|
|
9
|
+
// level — otherwise `next build` throws when it imports this module for route
|
|
10
|
+
// analysis, before the grid injects the var.
|
|
11
|
+
import { MongoClient } from "mongodb";
|
|
12
|
+
|
|
13
|
+
function clientPromise() {
|
|
14
|
+
const uri = process.env.DATABASE_MONGODB_URL || process.env.MONGODB_URL;
|
|
15
|
+
if (!uri) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
"DATABASE_MONGODB_URL is not set. The grid injects it automatically — run " +
|
|
18
|
+
"this app with `grid dev` locally, or deploy it with `grid plug` (the grid " +
|
|
19
|
+
"injects the DB connection string at runtime). Do not set it by hand.",
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
if (!globalThis.__mongoClientPromise) {
|
|
23
|
+
globalThis.__mongoClientPromise = new MongoClient(uri).connect();
|
|
24
|
+
}
|
|
25
|
+
return globalThis.__mongoClientPromise;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function getDb() {
|
|
29
|
+
const client = await clientPromise();
|
|
30
|
+
// The default DB comes from the connection-string path segment the grid injects.
|
|
31
|
+
return client.db();
|
|
32
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-chatbot",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"next": "^15.1.0",
|
|
12
|
+
"react": "^19.0.0",
|
|
13
|
+
"react-dom": "^19.0.0",
|
|
14
|
+
"@cloudgrid-io/ai": "^0.2.0",
|
|
15
|
+
"mongodb": "^6.12.0"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# api-docs
|
|
2
|
+
|
|
3
|
+
A single self-contained HTML API reference: a sidebar endpoint list, per-endpoint method badges (GET / POST / PUT / DELETE), a params table, and request / response examples in code blocks.
|
|
4
|
+
|
|
5
|
+
Static inspiration — a single self-contained `index.html` (inline CSS and JS,
|
|
6
|
+
no external CDNs). It deploys instantly via `gridctl_drop` and works on every
|
|
7
|
+
CloudGrid edition, including hosted.
|
|
8
|
+
|
|
9
|
+
## Fill it in
|
|
10
|
+
|
|
11
|
+
Fetch the template with `gridctl_fetch("template", "api-docs")`, then replace the
|
|
12
|
+
placeholders:
|
|
13
|
+
|
|
14
|
+
- The API name, base URL, and auth note.
|
|
15
|
+
- One section per endpoint: method badge, path, description, a params table, and request/response example blocks.
|
|
16
|
+
- The sidebar links jump to each endpoint anchor; the filter box narrows them client-side (inline JS).
|
|
17
|
+
- The code blocks are plain `<pre><code>` — no external highlighter or CDN.
|
|
18
|
+
|
|
19
|
+
Keep it a single self-contained HTML file. Do not add external scripts,
|
|
20
|
+
stylesheets, fonts, or large embedded media.
|
|
21
|
+
|
|
22
|
+
## Deploy
|
|
23
|
+
|
|
24
|
+
- Hosted MCP edition: call the drop tool with the HTML.
|
|
25
|
+
- Local MCP / CLI edition: write the HTML to a file and run `gridctl plug`.
|
|
26
|
+
|
|
27
|
+
It deploys as an inspiration and returns a live share URL right away. Re-deploy
|
|
28
|
+
with the same entity id to update the same URL.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# static — deploys as an inspiration (instant, works on hosted) via gridctl_drop;
|
|
2
|
+
# this yaml is only for plugging it as an OWNED static runtime (async, local
|
|
3
|
+
# edition only). To publish the page, prefer gridctl_drop, not grid plug.
|
|
4
|
+
#
|
|
5
|
+
# needs: {} # none — static, no infrastructure
|
|
6
|
+
name: api-docs
|
|
7
|
+
services:
|
|
8
|
+
web:
|
|
9
|
+
type: static
|
|
10
|
+
path: /
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>{{API_NAME}} API reference</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #0b0f17; --panel: #121826; --fg: #e8edf5; --muted: #9aa7bd;
|
|
10
|
+
--accent: #5b8cff; --border: #223049; --code-bg: #0f1523;
|
|
11
|
+
--get: #34d399; --post: #5b8cff; --put: #fbbf24; --delete: #f87171;
|
|
12
|
+
}
|
|
13
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
14
|
+
html { scroll-behavior: smooth; }
|
|
15
|
+
body {
|
|
16
|
+
background: var(--bg); color: var(--fg);
|
|
17
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
18
|
+
-webkit-font-smoothing: antialiased; line-height: 1.6;
|
|
19
|
+
}
|
|
20
|
+
.layout { display: grid; grid-template-columns: 250px 1fr; min-height: 100vh; }
|
|
21
|
+
aside { border-right: 1px solid var(--border); padding: 1.5rem 1.25rem; position: sticky; top: 0; align-self: start; height: 100vh; overflow-y: auto; }
|
|
22
|
+
aside .brand { font-weight: 600; letter-spacing: -0.01em; display: block; margin-bottom: 1rem; }
|
|
23
|
+
.filter { width: 100%; padding: 0.55rem 0.75rem; border-radius: 8px; border: 1px solid var(--border); background: var(--code-bg); color: var(--fg); font-size: 0.9rem; margin-bottom: 1.25rem; }
|
|
24
|
+
.filter::placeholder { color: var(--muted); }
|
|
25
|
+
nav.eps { display: grid; gap: 0.15rem; }
|
|
26
|
+
nav.eps a { display: flex; align-items: center; gap: 0.5rem; color: var(--muted); text-decoration: none; padding: 0.35rem 0.5rem; border-radius: 6px; font-size: 0.88rem; }
|
|
27
|
+
nav.eps a:hover { background: var(--panel); color: var(--fg); }
|
|
28
|
+
.method { flex: none; font-size: 0.65rem; font-weight: 700; padding: 0.1rem 0.4rem; border-radius: 4px; letter-spacing: 0.03em; }
|
|
29
|
+
.method.get { color: var(--get); background: rgba(52,211,153,0.14); }
|
|
30
|
+
.method.post { color: var(--post); background: rgba(91,140,255,0.14); }
|
|
31
|
+
.method.put { color: var(--put); background: rgba(251,191,36,0.14); }
|
|
32
|
+
.method.delete { color: var(--delete); background: rgba(248,113,113,0.14); }
|
|
33
|
+
|
|
34
|
+
main { padding: clamp(1.5rem, 4vw, 3rem); max-width: 860px; }
|
|
35
|
+
main h1 { font-size: clamp(2rem, 5vw, 2.6rem); letter-spacing: -0.02em; margin-bottom: 0.5rem; }
|
|
36
|
+
main .lede { color: var(--muted); margin-bottom: 1rem; }
|
|
37
|
+
.base { background: var(--code-bg); border: 1px solid var(--border); border-radius: 8px; padding: 0.6rem 0.9rem; font-family: "SF Mono", ui-monospace, Menlo, Consolas, monospace; font-size: 0.9rem; margin-bottom: 2.5rem; display: inline-block; }
|
|
38
|
+
|
|
39
|
+
.endpoint { padding-top: 2rem; margin-bottom: 2.5rem; border-top: 1px solid var(--border); }
|
|
40
|
+
.endpoint .sig { display: flex; align-items: center; gap: 0.65rem; margin-bottom: 0.75rem; flex-wrap: wrap; }
|
|
41
|
+
.endpoint .sig .path { font-family: "SF Mono", ui-monospace, Menlo, Consolas, monospace; font-size: 1.05rem; }
|
|
42
|
+
.endpoint p { color: var(--muted); margin-bottom: 1rem; }
|
|
43
|
+
h3 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin: 1.5rem 0 0.6rem; }
|
|
44
|
+
table { width: 100%; border-collapse: collapse; margin-bottom: 1rem; font-size: 0.9rem; }
|
|
45
|
+
th, td { text-align: left; padding: 0.55rem 0.6rem; border-bottom: 1px solid var(--border); }
|
|
46
|
+
th { color: var(--muted); font-weight: 600; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.03em; }
|
|
47
|
+
td code { background: var(--code-bg); border: 1px solid var(--border); border-radius: 5px; padding: 0.1rem 0.35rem; }
|
|
48
|
+
.table-scroll { overflow-x: auto; }
|
|
49
|
+
pre { background: var(--code-bg); border: 1px solid var(--border); border-radius: 10px; padding: 1rem 1.1rem; overflow-x: auto; margin-bottom: 1rem; }
|
|
50
|
+
code { font-family: "SF Mono", ui-monospace, Menlo, Consolas, monospace; font-size: 0.88rem; }
|
|
51
|
+
@media (max-width: 720px) {
|
|
52
|
+
.layout { grid-template-columns: 1fr; }
|
|
53
|
+
aside { position: static; height: auto; border-right: none; border-bottom: 1px solid var(--border); }
|
|
54
|
+
}
|
|
55
|
+
</style>
|
|
56
|
+
</head>
|
|
57
|
+
<body>
|
|
58
|
+
<div class="layout">
|
|
59
|
+
<aside>
|
|
60
|
+
<span class="brand">{{API_NAME}} API</span>
|
|
61
|
+
<input class="filter" id="filter" type="search" placeholder="Filter endpoints" aria-label="Filter endpoints" />
|
|
62
|
+
<nav class="eps" id="eps">
|
|
63
|
+
<!-- NAV_START -->
|
|
64
|
+
<a href="#list-items"><span class="method get">GET</span>List items</a>
|
|
65
|
+
<a href="#create-item"><span class="method post">POST</span>Create item</a>
|
|
66
|
+
<a href="#update-item"><span class="method put">PUT</span>Update item</a>
|
|
67
|
+
<a href="#delete-item"><span class="method delete">DEL</span>Delete item</a>
|
|
68
|
+
<!-- Add more <a> links; keep them matching endpoint anchor ids. -->
|
|
69
|
+
<!-- NAV_END -->
|
|
70
|
+
</nav>
|
|
71
|
+
</aside>
|
|
72
|
+
|
|
73
|
+
<main>
|
|
74
|
+
<h1>{{API_NAME}} API reference</h1>
|
|
75
|
+
<p class="lede">{{API_LEDE}}</p>
|
|
76
|
+
<div class="base">Base URL: {{BASE_URL}}</div>
|
|
77
|
+
<p class="lede" style="margin-top:-1.5rem">{{AUTH_NOTE}}</p>
|
|
78
|
+
|
|
79
|
+
<!-- ENDPOINTS_START -->
|
|
80
|
+
<section class="endpoint" id="list-items">
|
|
81
|
+
<div class="sig"><span class="method get">GET</span><span class="path">{{EP_1_PATH}}</span></div>
|
|
82
|
+
<p>{{EP_1_DESC}}</p>
|
|
83
|
+
<h3>Query parameters</h3>
|
|
84
|
+
<div class="table-scroll"><table>
|
|
85
|
+
<tr><th>Name</th><th>Type</th><th>Description</th></tr>
|
|
86
|
+
<tr><td><code>{{EP_1_PARAM_1}}</code></td><td>{{EP_1_PARAM_1_TYPE}}</td><td>{{EP_1_PARAM_1_DESC}}</td></tr>
|
|
87
|
+
<tr><td><code>{{EP_1_PARAM_2}}</code></td><td>{{EP_1_PARAM_2_TYPE}}</td><td>{{EP_1_PARAM_2_DESC}}</td></tr>
|
|
88
|
+
</table></div>
|
|
89
|
+
<h3>Response</h3>
|
|
90
|
+
<pre><code>{{EP_1_RESPONSE}}</code></pre>
|
|
91
|
+
</section>
|
|
92
|
+
|
|
93
|
+
<section class="endpoint" id="create-item">
|
|
94
|
+
<div class="sig"><span class="method post">POST</span><span class="path">{{EP_2_PATH}}</span></div>
|
|
95
|
+
<p>{{EP_2_DESC}}</p>
|
|
96
|
+
<h3>Request body</h3>
|
|
97
|
+
<pre><code>{{EP_2_REQUEST}}</code></pre>
|
|
98
|
+
<h3>Response</h3>
|
|
99
|
+
<pre><code>{{EP_2_RESPONSE}}</code></pre>
|
|
100
|
+
</section>
|
|
101
|
+
|
|
102
|
+
<section class="endpoint" id="update-item">
|
|
103
|
+
<div class="sig"><span class="method put">PUT</span><span class="path">{{EP_3_PATH}}</span></div>
|
|
104
|
+
<p>{{EP_3_DESC}}</p>
|
|
105
|
+
<h3>Request body</h3>
|
|
106
|
+
<pre><code>{{EP_3_REQUEST}}</code></pre>
|
|
107
|
+
<h3>Response</h3>
|
|
108
|
+
<pre><code>{{EP_3_RESPONSE}}</code></pre>
|
|
109
|
+
</section>
|
|
110
|
+
|
|
111
|
+
<section class="endpoint" id="delete-item">
|
|
112
|
+
<div class="sig"><span class="method delete">DELETE</span><span class="path">{{EP_4_PATH}}</span></div>
|
|
113
|
+
<p>{{EP_4_DESC}}</p>
|
|
114
|
+
<h3>Response</h3>
|
|
115
|
+
<pre><code>{{EP_4_RESPONSE}}</code></pre>
|
|
116
|
+
</section>
|
|
117
|
+
<!-- Add more .endpoint sections (with matching nav links) as needed. -->
|
|
118
|
+
<!-- ENDPOINTS_END -->
|
|
119
|
+
</main>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<script>
|
|
123
|
+
// Client-side endpoint filter. No network, no dependencies.
|
|
124
|
+
(function () {
|
|
125
|
+
var filter = document.getElementById("filter");
|
|
126
|
+
var links = Array.prototype.slice.call(document.querySelectorAll("#eps a"));
|
|
127
|
+
filter.addEventListener("input", function () {
|
|
128
|
+
var q = filter.value.trim().toLowerCase();
|
|
129
|
+
links.forEach(function (a) {
|
|
130
|
+
a.style.display = a.textContent.toLowerCase().indexOf(q) !== -1 ? "" : "none";
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
})();
|
|
134
|
+
</script>
|
|
135
|
+
</body>
|
|
136
|
+
</html>
|