@uidu/skills 0.2.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/README.md +42 -0
- package/package.json +25 -0
- package/skills/uidu/SKILL.md +321 -0
- package/skills/uidu/evals/evals.json +115 -0
- package/skills/uidu/metadata.json +8 -0
- package/skills/uidu-workspace/iteration-1/benchmark.md +43 -0
- package/skills/uidu-workspace/iteration-1/eval-0-event-detail/eval_metadata.json +13 -0
- package/skills/uidu-workspace/iteration-1/eval-0-event-detail/with_skill/grading.json +11 -0
- package/skills/uidu-workspace/iteration-1/eval-0-event-detail/with_skill/outputs/page.tsx +43 -0
- package/skills/uidu-workspace/iteration-1/eval-0-event-detail/without_skill/grading.json +11 -0
- package/skills/uidu-workspace/iteration-1/eval-0-event-detail/without_skill/outputs/page.tsx +46 -0
- package/skills/uidu-workspace/iteration-1/eval-1-stories-listing/eval_metadata.json +12 -0
- package/skills/uidu-workspace/iteration-1/eval-1-stories-listing/with_skill/grading.json +10 -0
- package/skills/uidu-workspace/iteration-1/eval-1-stories-listing/with_skill/outputs/page.tsx +33 -0
- package/skills/uidu-workspace/iteration-1/eval-1-stories-listing/without_skill/grading.json +10 -0
- package/skills/uidu-workspace/iteration-1/eval-1-stories-listing/without_skill/outputs/page.tsx +38 -0
- package/skills/uidu-workspace/iteration-1/eval-2-donations-debug/eval_metadata.json +10 -0
- package/skills/uidu-workspace/iteration-1/eval-2-donations-debug/with_skill/grading.json +8 -0
- package/skills/uidu-workspace/iteration-1/eval-2-donations-debug/with_skill/outputs/answer.md +63 -0
- package/skills/uidu-workspace/iteration-1/eval-2-donations-debug/without_skill/grading.json +9 -0
- package/skills/uidu-workspace/iteration-1/eval-2-donations-debug/without_skill/outputs/answer.md +47 -0
- package/skills/uidu-workspace/iteration-2/benchmark.md +73 -0
- package/skills/uidu-workspace/iteration-2/eval-0-event-detail/with_skill/outputs/page.tsx +37 -0
- package/skills/uidu-workspace/iteration-2/eval-0-event-detail/without_skill/outputs/page.tsx +60 -0
- package/skills/uidu-workspace/iteration-2/eval-1-stories-listing/with_skill/outputs/page.tsx +33 -0
- package/skills/uidu-workspace/iteration-2/eval-1-stories-listing/without_skill/outputs/page.tsx +36 -0
- package/skills/uidu-workspace/iteration-2/eval-2-donations-debug/with_skill/outputs/answer.md +51 -0
- package/skills/uidu-workspace/iteration-2/eval-2-donations-debug/without_skill/outputs/answer.md +77 -0
- package/skills/uidu-workspace/iteration-2/eval-3-client-setup/with_skill/outputs/uidu.ts +13 -0
- package/skills/uidu-workspace/iteration-2/eval-3-client-setup/without_skill/outputs/uidu.ts +47 -0
- package/skills/uidu-workspace/iteration-2/eval-4-cms-pageblocks/with_skill/grading.json +11 -0
- package/skills/uidu-workspace/iteration-2/eval-4-cms-pageblocks/with_skill/outputs/Hero.tsx +17 -0
- package/skills/uidu-workspace/iteration-2/eval-4-cms-pageblocks/with_skill/outputs/page.tsx +21 -0
- package/skills/uidu-workspace/iteration-2/eval-4-cms-pageblocks/without_skill/outputs/Hero.tsx +32 -0
- package/skills/uidu-workspace/iteration-2/eval-4-cms-pageblocks/without_skill/outputs/page.tsx +35 -0
- package/skills/uidu-workspace/iteration-2/eval-5-help-center/with_skill/outputs/page.tsx +54 -0
- package/skills/uidu-workspace/iteration-2/eval-5-help-center/without_skill/outputs/page.tsx +80 -0
- package/skills/uidu-workspace/iteration-2/eval-6-forms-contact/with_skill/grading.json +11 -0
- package/skills/uidu-workspace/iteration-2/eval-6-forms-contact/with_skill/outputs/ContactForm.tsx +277 -0
- package/skills/uidu-workspace/iteration-2/eval-6-forms-contact/with_skill/outputs/page.tsx +48 -0
- package/skills/uidu-workspace/iteration-2/eval-6-forms-contact/without_skill/outputs/ContactForm.tsx +144 -0
- package/skills/uidu-workspace/iteration-2/eval-6-forms-contact/without_skill/outputs/page.tsx +45 -0
- package/skills/uidu-workspace/iteration-2/eval-7-routing-gotcha/with_skill/outputs/answer.md +63 -0
- package/skills/uidu-workspace/iteration-2/eval-7-routing-gotcha/without_skill/outputs/answer.md +131 -0
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @uidu/skills
|
|
2
|
+
|
|
3
|
+
Agent skills for the uidu SDK. Install them into any AI agent — [Claude Code](https://claude.com/claude-code), Cursor, Copilot, Windsurf, Gemini, etc. — so the assistant knows the canonical patterns for `@uidu/client` and `@uidu/react`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Via the [skills.sh](https://www.skills.sh) CLI:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx skills add uidu-org/api.js --skill uidu -a claude-code
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Replace `claude-code` with your agent of choice (`cursor`, `copilot`, `windsurf`, etc.).
|
|
14
|
+
|
|
15
|
+
## Available skills
|
|
16
|
+
|
|
17
|
+
| Skill | Description |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `uidu` | Comprehensive guide to the uidu SDK — package map, env setup, fetch patterns for CMS pages, events, stories, donations, help center, and forms, plus RichText rendering and scaffolding via `create-uidu-app`. |
|
|
20
|
+
|
|
21
|
+
More skills will be split out from `uidu` as individual areas grow.
|
|
22
|
+
|
|
23
|
+
## Layout
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
skills/
|
|
27
|
+
└── uidu/
|
|
28
|
+
├── SKILL.md # the skill itself
|
|
29
|
+
└── metadata.json # author, version
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Each skill is a directory containing `SKILL.md` with YAML frontmatter — the standard agent-skills shape used by [vercel-labs/agent-skills](https://github.com/vercel-labs/agent-skills) and others.
|
|
33
|
+
|
|
34
|
+
## Contributing
|
|
35
|
+
|
|
36
|
+
Skills live in `skills/<name>/SKILL.md`. PRs welcome at https://github.com/uidu-org/api.js.
|
|
37
|
+
|
|
38
|
+
## See also
|
|
39
|
+
|
|
40
|
+
- The skills ecosystem: https://www.skills.sh
|
|
41
|
+
- Skills CLI: https://github.com/vercel-labs/skills
|
|
42
|
+
- Vercel agent skills: https://github.com/vercel-labs/agent-skills
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@uidu/skills",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Agent skills for the uidu SDK — installable via the open agent skills ecosystem (skills.sh).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "uidu",
|
|
7
|
+
"repository": "https://github.com/uidu-org/api.js/tree/main/packages/skills",
|
|
8
|
+
"homepage": "https://docs.uidu.org",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"files": [
|
|
11
|
+
"skills",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"uidu",
|
|
16
|
+
"agent-skills",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"cursor",
|
|
19
|
+
"ai-agents",
|
|
20
|
+
"skills"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: uidu
|
|
3
|
+
description: Use when building uidu-powered applications — fetching CMS pages, events, stories, donation campaigns, help center articles, or forms; rendering WYSIWYG content; or scaffolding new projects with create-uidu-app. Triggers on any task involving the uidu platform, the @uidu/client SDK, or the @uidu/react bindings.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: uidu
|
|
7
|
+
version: "0.1.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# uidu SDK
|
|
11
|
+
|
|
12
|
+
The uidu SDK is a TypeScript GraphQL toolkit for building websites and apps powered by the uidu platform. It is split into two packages:
|
|
13
|
+
|
|
14
|
+
- **`@uidu/client`** — framework-agnostic GraphQL client. Safe for server-side use (RSC, route handlers, server actions, scripts).
|
|
15
|
+
- **`@uidu/react`** — React hooks (`useFields`, `useQuery`, `useForm`, `useChannel`) and components (`<PageBlocks>`, `<RichText>`).
|
|
16
|
+
|
|
17
|
+
`@uidu/api.js` is a legacy bridge package and is **deprecated** — use `@uidu/react` directly.
|
|
18
|
+
|
|
19
|
+
## When to Use This Skill
|
|
20
|
+
|
|
21
|
+
Use this skill when the user is:
|
|
22
|
+
|
|
23
|
+
- Building a Next.js / Remix / Astro / React app powered by the uidu GraphQL API
|
|
24
|
+
- Fetching CMS pages, events, blog stories, donation campaigns, or help center content
|
|
25
|
+
- Rendering uidu blocks with `<PageBlocks>` and a component map
|
|
26
|
+
- Rendering WYSIWYG body fields with `<RichText>`
|
|
27
|
+
- Bootstrapping a new project with `create-uidu-app`
|
|
28
|
+
- Asking about `@uidu/client`, `@uidu/react`, `uidu`, or the uidu GraphQL API
|
|
29
|
+
|
|
30
|
+
## Package Map
|
|
31
|
+
|
|
32
|
+
| Package | When to use | Notes |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `@uidu/client` | Server-side queries (RSC, server actions, scripts) | Framework-agnostic |
|
|
35
|
+
| `@uidu/react` | React hooks, providers, and components | Some hooks are client-only |
|
|
36
|
+
| `@uidu/api.js` | **Do not use for new code.** | Deprecated; re-exports `@uidu/react` |
|
|
37
|
+
|
|
38
|
+
Install:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install @uidu/client @uidu/react
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## API Shape
|
|
45
|
+
|
|
46
|
+
All SDK functions are **standalone functions** that take the client as the first argument. They are never namespaced as methods on the client object:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { getEvent, listStories, getPage, getChannel, getForm } from '@uidu/client';
|
|
50
|
+
|
|
51
|
+
// ✅ Correct — standalone function, client first
|
|
52
|
+
await getEvent(uidu, { id });
|
|
53
|
+
await listStories(uidu, { first: 50 });
|
|
54
|
+
await getPage(uidu, { projectId, slug });
|
|
55
|
+
|
|
56
|
+
// ❌ Wrong — uidu has no namespaced methods
|
|
57
|
+
uidu.events.get(...)
|
|
58
|
+
uidu.stories.list(...)
|
|
59
|
+
uidu.cms.getPage(...)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This shape is uniform across **every** scope (CMS, events, stories, donations, help center, forms, courses, careers, bookings, …). If you can't find an import for the function you want, look for `listX` / `getX` / `createX` in `@uidu/client`, not for a method on the client.
|
|
63
|
+
|
|
64
|
+
## Quick Start
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// src/lib/uidu.ts
|
|
68
|
+
import { createClient } from '@uidu/client';
|
|
69
|
+
|
|
70
|
+
export const uidu = createClient({
|
|
71
|
+
workspace: process.env.UIDU_WORKSPACE ?? '',
|
|
72
|
+
publicToken: process.env.UIDU_PUBLIC_TOKEN, // safe to expose
|
|
73
|
+
apiKey: process.env.UIDU_API_KEY, // server-only secret
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Required environment:
|
|
78
|
+
|
|
79
|
+
| Variable | Required | Notes |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `UIDU_WORKSPACE` | yes | Your workspace slug (e.g. `acme`) |
|
|
82
|
+
| `UIDU_PUBLIC_TOKEN` | no | Public read-only token, safe to expose |
|
|
83
|
+
| `UIDU_API_KEY` | no | Server-only secret for privileged ops |
|
|
84
|
+
|
|
85
|
+
## Patterns by Scope
|
|
86
|
+
|
|
87
|
+
### CMS Pages
|
|
88
|
+
|
|
89
|
+
CMS pages are made of `pageBlocks`. Fetch a page server-side, pass blocks to `<PageBlocks>` with a component map keyed by the block's `shortname`:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
// src/app/page.tsx — Server Component
|
|
93
|
+
import { getPage } from '@uidu/client';
|
|
94
|
+
import { PageBlocks } from '@uidu/react';
|
|
95
|
+
import { Hero } from '@/components/blocks/Hero';
|
|
96
|
+
import { Feature } from '@/components/blocks/Feature';
|
|
97
|
+
import { uidu } from '@/lib/uidu';
|
|
98
|
+
|
|
99
|
+
export default async function HomePage() {
|
|
100
|
+
const page = await getPage(uidu, {
|
|
101
|
+
projectId: process.env.UIDU_PROJECT_ID!,
|
|
102
|
+
slug: 'home',
|
|
103
|
+
});
|
|
104
|
+
if (!page) return <div>Not found</div>;
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<PageBlocks
|
|
108
|
+
pageBlocks={page.pageBlocks}
|
|
109
|
+
components={{ Hero, Feature }}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Each block component receives `{ block: PageBlock }` and reads its fields:
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
// src/components/blocks/Hero.tsx
|
|
119
|
+
import type { Page } from '@uidu/client';
|
|
120
|
+
import { toText, useFields } from '@uidu/react';
|
|
121
|
+
|
|
122
|
+
type PageBlock = NonNullable<Page['pageBlocks']>[number];
|
|
123
|
+
|
|
124
|
+
export function Hero({ block }: { block: PageBlock }) {
|
|
125
|
+
const fields = useFields(block.fieldValues);
|
|
126
|
+
const title = toText(fields.title);
|
|
127
|
+
const subtitle = toText(fields.subtitle);
|
|
128
|
+
return (
|
|
129
|
+
<section>
|
|
130
|
+
<h1>{title}</h1>
|
|
131
|
+
{subtitle && <p>{subtitle}</p>}
|
|
132
|
+
</section>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`useFields(block.fieldValues)` returns a keyed map of field shortname → value. `toText(value)` extracts a plain string from a possibly-structured value.
|
|
138
|
+
|
|
139
|
+
### Events
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
import { listEvents, getEvent } from '@uidu/client';
|
|
143
|
+
import { uidu } from '@/lib/uidu';
|
|
144
|
+
|
|
145
|
+
// Listing
|
|
146
|
+
const events = await listEvents(uidu, { first: 50 });
|
|
147
|
+
|
|
148
|
+
// Detail — returns Event | null
|
|
149
|
+
const event = await getEvent(uidu, { id });
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Event fields:
|
|
153
|
+
|
|
154
|
+
| Field | Notes |
|
|
155
|
+
|---|---|
|
|
156
|
+
| `event.id` | |
|
|
157
|
+
| `event.name` | Display name |
|
|
158
|
+
| `event.body` | WYSIWYG doc — render with `<RichText>` |
|
|
159
|
+
| `event.cover` | Image URL (string) or null |
|
|
160
|
+
| `event.description` | Plain string summary |
|
|
161
|
+
| `event.instance.beginsAt` | ISO datetime of start |
|
|
162
|
+
| `event.instance.finishesAt` | ISO datetime of end |
|
|
163
|
+
| `event.instance.id` | Instance identifier |
|
|
164
|
+
| `event.primaryAddress` | Location data |
|
|
165
|
+
| `event.currentCapacity` | Numeric capacity |
|
|
166
|
+
| `event.isPaidEvent` | Boolean |
|
|
167
|
+
|
|
168
|
+
### Stories (Blog)
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
import { listStories, getStory } from '@uidu/client';
|
|
172
|
+
|
|
173
|
+
const stories = await listStories(uidu, { first: 50 });
|
|
174
|
+
const story = await getStory(uidu, { id });
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Story fields: `story.id`, `story.name`, `story.body` (WYSIWYG), `story.cover` (URL), `story.publishedAt` (ISO datetime).
|
|
178
|
+
|
|
179
|
+
Sort by `publishedAt` for a chronological feed:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
const sorted = [...stories].sort((a, b) =>
|
|
183
|
+
((b.publishedAt as string) ?? '').localeCompare(
|
|
184
|
+
(a.publishedAt as string) ?? '',
|
|
185
|
+
),
|
|
186
|
+
);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Donation Campaigns
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
import { listDonationCampaigns, getDonationCampaign } from '@uidu/client';
|
|
193
|
+
|
|
194
|
+
const campaigns = await listDonationCampaigns(uidu, { first: 50 });
|
|
195
|
+
const campaign = await getDonationCampaign(uidu, { id });
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Campaign fields: `campaign.id`, `campaign.name`, `campaign.body`, `campaign.cover`, `campaign.donationsAmount`, `campaign.donationsCount`, `campaign.goal`.
|
|
199
|
+
|
|
200
|
+
**Amounts are in cents.** Divide by 100 for display:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const raised = (campaign.donationsAmount ?? 0) / 100;
|
|
204
|
+
const goal = (campaign.goal ?? 0) / 100;
|
|
205
|
+
const progress = goal > 0 ? Math.min(100, (raised / goal) * 100) : 0;
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Help Center
|
|
209
|
+
|
|
210
|
+
```tsx
|
|
211
|
+
import { getChannel } from '@uidu/client';
|
|
212
|
+
|
|
213
|
+
const channel = await getChannel(uidu, { id: channelId });
|
|
214
|
+
// channel.kbCollections.edges[].node.kbArticles.edges[].node
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Forms
|
|
218
|
+
|
|
219
|
+
Forms are fetched server-side, then rendered with the `useForm` hook on the client:
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
// Server: fetch form schema
|
|
223
|
+
import { getForm } from '@uidu/client';
|
|
224
|
+
const form = await getForm(uidu, { id: formId });
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
// Client: render with @uidu/react
|
|
229
|
+
'use client';
|
|
230
|
+
import { useForm } from '@uidu/react';
|
|
231
|
+
|
|
232
|
+
export function ContactForm({ form }) {
|
|
233
|
+
const { register, handleSubmit, onSubmit } = useForm(uidu, { form });
|
|
234
|
+
return (
|
|
235
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
236
|
+
{/* render fields by iterating form.formQuestions.edges */}
|
|
237
|
+
</form>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Rendering WYSIWYG Content
|
|
243
|
+
|
|
244
|
+
All `body` fields on Event, Story, DonationCampaign, etc. are structured documents (not strings). Render them with `<RichText>` from `@uidu/react`:
|
|
245
|
+
|
|
246
|
+
```tsx
|
|
247
|
+
import { RichText } from '@uidu/react';
|
|
248
|
+
|
|
249
|
+
{story.body != null && <RichText doc={story.body} />}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The prop is `doc`, not `children`. The component handles paragraphs, headings, lists, links, and inline marks.
|
|
253
|
+
|
|
254
|
+
## Provider Setup (Client-Side Hooks)
|
|
255
|
+
|
|
256
|
+
Server-side `await getX(...)` calls do not need a provider. For React client-side hooks (`useQuery`, `useFields`, `useForm`), wrap your app in `<UiduProvider>`:
|
|
257
|
+
|
|
258
|
+
```tsx
|
|
259
|
+
// app/layout.tsx
|
|
260
|
+
'use client';
|
|
261
|
+
import { UiduProvider } from '@uidu/react';
|
|
262
|
+
|
|
263
|
+
export default function RootLayout({ children }) {
|
|
264
|
+
return (
|
|
265
|
+
<html>
|
|
266
|
+
<body>
|
|
267
|
+
<UiduProvider endpoint={process.env.NEXT_PUBLIC_UIDU_ENDPOINT!}>
|
|
268
|
+
{children}
|
|
269
|
+
</UiduProvider>
|
|
270
|
+
</body>
|
|
271
|
+
</html>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Routing Pattern
|
|
277
|
+
|
|
278
|
+
Nest dynamic detail routes under a scope path (`/event/[id]`, `/story/[id]`, `/campaign/[id]`) rather than at the root (`/[id]`). Root-level dynamic routes will catch requests like `/favicon.ico` and pass them to your fetch function, producing spurious GraphQL errors.
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
src/app/
|
|
282
|
+
├── page.tsx # listing at /
|
|
283
|
+
├── event/[id]/page.tsx # detail at /event/<id>
|
|
284
|
+
└── layout.tsx
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Scaffolding a New Project
|
|
288
|
+
|
|
289
|
+
Use `create-uidu-app`:
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
npm create uidu-app@latest my-app
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Or pick a template directly:
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
npm create uidu-app@latest my-app -- -t events
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
| Template | What it builds |
|
|
302
|
+
|---|---|
|
|
303
|
+
| `minimal` | CMS landing page with `PageBlocks` + Hero/Feature blocks |
|
|
304
|
+
| `events` | Event listing + `/event/[id]` detail |
|
|
305
|
+
| `stories` | Blog listing + `/story/[id]` detail (magazine layout) |
|
|
306
|
+
| `donations` | Campaigns listing + `/campaign/[id]` detail with progress bars |
|
|
307
|
+
|
|
308
|
+
Every template ships with:
|
|
309
|
+
- Next.js 16 App Router
|
|
310
|
+
- Tailwind v4
|
|
311
|
+
- A configured `src/lib/uidu.ts`
|
|
312
|
+
- A server-rendered listing + dynamic detail page
|
|
313
|
+
- `.env.example` with required vars (and the CLI offers to fill them in for you)
|
|
314
|
+
|
|
315
|
+
## See Also
|
|
316
|
+
|
|
317
|
+
- uidu docs: https://docs.uidu.org
|
|
318
|
+
- Source: https://github.com/uidu-org/api.js
|
|
319
|
+
- `@uidu/client`: https://github.com/uidu-org/api.js/tree/main/packages/client
|
|
320
|
+
- `@uidu/react`: https://github.com/uidu-org/api.js/tree/main/packages/react
|
|
321
|
+
- `create-uidu-app`: https://github.com/uidu-org/api.js/tree/main/packages/create-uidu-app
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "uidu",
|
|
3
|
+
"evals": [
|
|
4
|
+
{
|
|
5
|
+
"id": 0,
|
|
6
|
+
"name": "event-detail",
|
|
7
|
+
"prompt": "I'm building a Next.js 16 App Router page that shows event details from the uidu platform. The URL is `/event/[id]`. Write me a Server Component at `src/app/event/[id]/page.tsx` that fetches an event from uidu by ID, shows its name, start time (formatted nicely), and body content. Use the @uidu/client SDK. Assume there's already a configured `uidu` client at `@/lib/uidu`. Use Next.js conventions including `notFound()` from `next/navigation` if the event doesn't exist.",
|
|
8
|
+
"files": [],
|
|
9
|
+
"expected_output": "A TSX file using getEvent() with correct field names (event.instance.beginsAt, event.name, event.body rendered via RichText), handling the null case with notFound().",
|
|
10
|
+
"assertions": [
|
|
11
|
+
{ "text": "Uses `getEvent` imported from `@uidu/client`" },
|
|
12
|
+
{ "text": "Reads `event.name` (not `event.title`)" },
|
|
13
|
+
{ "text": "Reads `event.instance.beginsAt` (not `event.startsAt` or similar guess)" },
|
|
14
|
+
{ "text": "Handles null event via `notFound()` from `next/navigation`" },
|
|
15
|
+
{ "text": "Renders `event.body` with `<RichText doc={event.body} />` from `@uidu/react` (not as a plain string or via dangerouslySetInnerHTML)" },
|
|
16
|
+
{ "text": "params is typed as `Promise<{ id: string }>` and awaited (Next 16 convention)" }
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": 1,
|
|
21
|
+
"name": "stories-listing",
|
|
22
|
+
"prompt": "I have a uidu blog with stories. Write a Next.js 16 App Router Server Component at `src/app/page.tsx` that lists all stories sorted by publication date, newest first. Each story should link to `/story/<id>`. Display the story name and publish date. Use the @uidu/client SDK and assume `uidu` is configured at `@/lib/uidu`.",
|
|
23
|
+
"files": [],
|
|
24
|
+
"expected_output": "A TSX file using listStories(), sorting by publishedAt DESC, mapping over stories, linking each to /story/<id> with story.name and story.publishedAt.",
|
|
25
|
+
"assertions": [
|
|
26
|
+
{ "text": "Uses `listStories` imported from `@uidu/client`" },
|
|
27
|
+
{ "text": "Reads `story.name` (not `story.title`)" },
|
|
28
|
+
{ "text": "Sorts by `story.publishedAt` in descending order (newest first)" },
|
|
29
|
+
{ "text": "Links to `/story/${story.id}` (nested under /story/, not at root)" },
|
|
30
|
+
{ "text": "Uses `next/link` for navigation (idiomatic Next.js)" }
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": 2,
|
|
35
|
+
"name": "donations-debug",
|
|
36
|
+
"prompt": "In my uidu-powered Next.js app, I'm displaying donation campaigns. I'm currently showing `€{campaign.goal}` directly in JSX but the value looks wrong — campaigns are showing huge numbers like €5000000 for what should be a €50,000 goal. What's wrong and how do I fix it? Please write a short diagnostic explanation in markdown.",
|
|
37
|
+
"files": [],
|
|
38
|
+
"expected_output": "A markdown explanation identifying that goal/donationsAmount are in cents, with a code fix dividing by 100.",
|
|
39
|
+
"assertions": [
|
|
40
|
+
{ "text": "Identifies that `campaign.goal` (and `donationsAmount`) are returned in cents, not euros/dollars" },
|
|
41
|
+
{ "text": "Suggests dividing by 100 to display the value in major currency units" },
|
|
42
|
+
{ "text": "Shows a corrected code snippet using `campaign.goal / 100` or equivalent" }
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": 3,
|
|
47
|
+
"name": "client-setup",
|
|
48
|
+
"prompt": "I'm starting a fresh Next.js project that needs to use the uidu SDK. Set me up: write `src/lib/uidu.ts` that creates and exports a configured client, and at the bottom of the file list (as a comment) the env variables I need to set in `.env.local`. Use the @uidu/client SDK.",
|
|
49
|
+
"files": [],
|
|
50
|
+
"expected_output": "A TS file using createClient() with workspace/publicToken/apiKey wired to env vars, exporting `uidu`, and listing the required env var names in a comment.",
|
|
51
|
+
"assertions": [
|
|
52
|
+
{ "text": "Imports `createClient` from `@uidu/client` (not a default import or `new UiduClient(...)`)" },
|
|
53
|
+
{ "text": "Calls `createClient({ workspace, publicToken, apiKey })` with those three keys" },
|
|
54
|
+
{ "text": "Uses env vars `UIDU_WORKSPACE`, `UIDU_PUBLIC_TOKEN`, `UIDU_API_KEY` (correct names, no `NEXT_PUBLIC_` prefix on the API key)" },
|
|
55
|
+
{ "text": "Exports `uidu` (or equivalent named export) so other modules can import it" },
|
|
56
|
+
{ "text": "Notes that `UIDU_API_KEY` is server-only (a comment or guard)" }
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": 4,
|
|
61
|
+
"name": "cms-pageblocks",
|
|
62
|
+
"prompt": "Build a uidu-powered home page. Fetch the CMS page with slug `home` for project ID from `process.env.UIDU_PROJECT_ID` and render its blocks. I have two block types: a Hero block (reads `title` and `subtitle` fields) and a Feature block (reads `title` and `body` fields). Write TWO files: (a) `src/app/page.tsx` — the Server Component that fetches and renders blocks; (b) `src/components/blocks/Hero.tsx` — the Hero block component. Use @uidu/client and @uidu/react. Assume `uidu` is configured at `@/lib/uidu`.",
|
|
63
|
+
"files": [],
|
|
64
|
+
"expected_output": "Two TSX files: the parent uses getPage() + <PageBlocks> with a components map; Hero uses useFields(block.fieldValues) + toText() to read fields, taking { block } as its prop.",
|
|
65
|
+
"assertions": [
|
|
66
|
+
{ "text": "Parent file imports `getPage` from `@uidu/client` and calls `getPage(uidu, { projectId, slug })`" },
|
|
67
|
+
{ "text": "Parent file imports `PageBlocks` from `@uidu/react` (NOT `PageBlocksParser` — that's the legacy name in older CLAUDE.md)" },
|
|
68
|
+
{ "text": "Parent renders `<PageBlocks pageBlocks={page.pageBlocks} components={{ Hero, Feature }} />`" },
|
|
69
|
+
{ "text": "Hero component takes `{ block }: { block: PageBlock }` as props (not `pageBlock` or destructured fields)" },
|
|
70
|
+
{ "text": "Hero uses `useFields(block.fieldValues)` from `@uidu/react` to read fields by shortname" },
|
|
71
|
+
{ "text": "Hero uses `toText(fields.title)` (and equivalent for subtitle) — does NOT cast to string directly" }
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": 5,
|
|
76
|
+
"name": "help-center",
|
|
77
|
+
"prompt": "Build a help center page in Next.js 16 App Router at `src/app/help/page.tsx` that lists all articles from a uidu channel. Fetch the channel by ID (`process.env.UIDU_CHANNEL_ID`) and render every article as a link to `/help/<articleId>` grouped by their collection. Show the channel name as the page title. Use the @uidu/client SDK and assume `uidu` is configured at `@/lib/uidu`.",
|
|
78
|
+
"files": [],
|
|
79
|
+
"expected_output": "A TSX file using getChannel() then walking the kbCollections.edges -> node -> kbArticles.edges -> node structure to render grouped article links.",
|
|
80
|
+
"assertions": [
|
|
81
|
+
{ "text": "Uses `getChannel` imported from `@uidu/client` (NOT `getHelpCenter`, `listArticles`, or a namespaced method like `uidu.helpCenter.get`)" },
|
|
82
|
+
{ "text": "Walks `channel.kbCollections.edges` — uses the GraphQL connection (edges/node) shape, not a flat array" },
|
|
83
|
+
{ "text": "Drills into each collection's `node.kbArticles.edges` to reach individual articles" },
|
|
84
|
+
{ "text": "Filters out null edges (e.g., `.filter(Boolean)` or `if (edge?.node)` guard) since edges can be nullable" },
|
|
85
|
+
{ "text": "Links to `/help/${article.id}` using `next/link`" }
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": 6,
|
|
90
|
+
"name": "forms-contact",
|
|
91
|
+
"prompt": "Build a contact form using a uidu form. The form schema should be fetched server-side and passed to a client component that renders the form. Write TWO files: (a) `src/app/contact/page.tsx` — server fetch; (b) `src/components/ContactForm.tsx` — client form using react-hook-form integration. Use the @uidu/client and @uidu/react SDKs. The form ID comes from `process.env.UIDU_CONTACT_FORM_ID`.",
|
|
92
|
+
"files": [],
|
|
93
|
+
"expected_output": "Two files: server fetches form via getForm() and passes to client; client has 'use client' and renders fields using useForm() from @uidu/react (which integrates with react-hook-form internally).",
|
|
94
|
+
"assertions": [
|
|
95
|
+
{ "text": "Server file uses `getForm` imported from `@uidu/client` with `{ id: formId }`" },
|
|
96
|
+
{ "text": "Client file has `'use client'` directive at the top" },
|
|
97
|
+
{ "text": "Client uses `useForm` imported from `@uidu/react` (NOT directly from `react-hook-form`, which is the wrong import for this stack)" },
|
|
98
|
+
{ "text": "useForm is called with the uidu client and the form object (e.g., `useForm(uidu, { form })`), not just `useForm()` with empty args" },
|
|
99
|
+
{ "text": "Iterates `form.formQuestions.edges` (GraphQL connection shape) to render fields, not a flat array" }
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"id": 7,
|
|
104
|
+
"name": "routing-gotcha",
|
|
105
|
+
"prompt": "My uidu story detail page lives at `src/app/[id]/page.tsx` (catches the root path with a single segment). The detail page works correctly for real story IDs like `/abc123`. But I'm seeing GraphQL errors in my logs with `id: 'favicon.ico'` and occasionally other static asset paths. What's actually going on, and how should I fix this? Write a short diagnostic explanation in markdown with the recommended file-structure change.",
|
|
106
|
+
"files": [],
|
|
107
|
+
"expected_output": "A markdown answer identifying that a root-level [id] catches browser requests for /favicon.ico and other root paths, recommending nesting under a scope dir like /story/[id].",
|
|
108
|
+
"assertions": [
|
|
109
|
+
{ "text": "Identifies that a root-level dynamic segment `/[id]` catches ALL single-segment root requests, including `/favicon.ico`, `/robots.txt`, etc." },
|
|
110
|
+
{ "text": "Recommends moving the route under a scope path — e.g., `src/app/story/[id]/page.tsx` so the URL becomes `/story/<id>`" },
|
|
111
|
+
{ "text": "Explains the root cause is Next.js routing behavior, not the uidu SDK itself (the SDK is just receiving the bogus id string passed in)" }
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Benchmark — uidu skill, iteration 1
|
|
2
|
+
|
|
3
|
+
## Aggregate
|
|
4
|
+
|
|
5
|
+
| Configuration | Pass rate | Assertions passed |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| with_skill | 100% | 14 / 14 |
|
|
8
|
+
| without_skill (baseline) | 57% | 8 / 14 |
|
|
9
|
+
| **Δ** | **+43 pp** | **+6** |
|
|
10
|
+
|
|
11
|
+
## Per-eval
|
|
12
|
+
|
|
13
|
+
| Eval | with_skill | baseline | Δ |
|
|
14
|
+
| --- | --- | --- | --- |
|
|
15
|
+
| 0 — event-detail | 6/6 (100%) | 3/6 (50%) | +50% |
|
|
16
|
+
| 1 — stories-listing | 5/5 (100%) | 2/5 (40%) | +60% |
|
|
17
|
+
| 2 — donations-debug | 3/3 (100%) | 3/3 (100%) | 0% |
|
|
18
|
+
|
|
19
|
+
## Analyst notes
|
|
20
|
+
|
|
21
|
+
1. **Both baseline failures share one root cause**: hallucinated namespaced API style
|
|
22
|
+
(`uidu.events.get(...)`, `uidu.stories.list(...)`). The real SDK uses
|
|
23
|
+
standalone functions: `getEvent(client, vars)`, `listStories(client, vars)`.
|
|
24
|
+
The skill's code examples demonstrate this, but it's never called out as a
|
|
25
|
+
*pattern*. Promoting it to a top-level section would harden every with-skill
|
|
26
|
+
run.
|
|
27
|
+
|
|
28
|
+
2. **Eval 2 assertions don't discriminate.** Cents-is-cents is an industry
|
|
29
|
+
convention; even a cold-baseline got it right. The skill earns no measurable
|
|
30
|
+
credit on this eval. Either replace the eval with a sharper one (e.g., asking
|
|
31
|
+
about a uidu-specific field name) or accept that this scope is a "skill
|
|
32
|
+
doesn't hurt" eval rather than a "skill helps" eval.
|
|
33
|
+
|
|
34
|
+
3. **Biggest skill win is the WYSIWYG body case**: baseline tried
|
|
35
|
+
`dangerouslySetInnerHTML={{ __html: event.body }}`, which would either crash
|
|
36
|
+
(body is an object, not a string) or render `[object Object]`. The skill
|
|
37
|
+
prevented this. This is the kind of failure mode that doesn't show up in
|
|
38
|
+
type-checking — production-breaking.
|
|
39
|
+
|
|
40
|
+
4. **With-skill outputs are leaner and closer to the canonical demo style.**
|
|
41
|
+
Baselines added more Tailwind decoration, longer `Intl.DateTimeFormat`
|
|
42
|
+
options. Not wrong, but verbose. Skill seems to be implicitly anchoring on
|
|
43
|
+
the simpler patterns shown in its examples.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"eval_id": 0,
|
|
3
|
+
"eval_name": "event-detail",
|
|
4
|
+
"prompt": "I'm building a Next.js 16 App Router page that shows event details from the uidu platform. The URL is `/event/[id]`. Write me a Server Component at `src/app/event/[id]/page.tsx` that fetches an event from uidu by ID, shows its name, start time (formatted nicely), and body content. Use the @uidu/client SDK. Assume there's already a configured `uidu` client at `@/lib/uidu`. Use Next.js conventions including `notFound()` from `next/navigation` if the event doesn't exist.",
|
|
5
|
+
"assertions": [
|
|
6
|
+
{ "text": "Uses `getEvent` imported from `@uidu/client`" },
|
|
7
|
+
{ "text": "Reads `event.name` (not `event.title`)" },
|
|
8
|
+
{ "text": "Reads `event.instance.beginsAt` (not `event.startsAt` or similar guess)" },
|
|
9
|
+
{ "text": "Handles null event via `notFound()` from `next/navigation`" },
|
|
10
|
+
{ "text": "Renders `event.body` with `<RichText doc={event.body} />` from `@uidu/react`" },
|
|
11
|
+
{ "text": "params is typed as `Promise<{ id: string }>` and awaited (Next 16 convention)" }
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"expectations": [
|
|
3
|
+
{ "text": "Uses `getEvent` imported from `@uidu/client`", "passed": true, "evidence": "Line 1: import { getEvent } from '@uidu/client'" },
|
|
4
|
+
{ "text": "Reads `event.name` (not `event.title`)", "passed": true, "evidence": "Line 34: <h1>{event.name}</h1>" },
|
|
5
|
+
{ "text": "Reads `event.instance.beginsAt`", "passed": true, "evidence": "Line 19: const beginsAt = event.instance?.beginsAt" },
|
|
6
|
+
{ "text": "Handles null event via `notFound()`", "passed": true, "evidence": "Lines 15-17" },
|
|
7
|
+
{ "text": "Renders body with `<RichText doc={event.body} />`", "passed": true, "evidence": "Line 40: <RichText doc={event.body} />" },
|
|
8
|
+
{ "text": "params is typed as `Promise<{ id: string }>` and awaited", "passed": true, "evidence": "Line 7-11" }
|
|
9
|
+
],
|
|
10
|
+
"pass_rate": 1.0
|
|
11
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { getEvent } from '@uidu/client';
|
|
2
|
+
import { RichText } from '@uidu/react';
|
|
3
|
+
import { notFound } from 'next/navigation';
|
|
4
|
+
import { uidu } from '@/lib/uidu';
|
|
5
|
+
|
|
6
|
+
type EventPageProps = {
|
|
7
|
+
params: Promise<{ id: string }>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default async function EventPage({ params }: EventPageProps) {
|
|
11
|
+
const { id } = await params;
|
|
12
|
+
|
|
13
|
+
const event = await getEvent(uidu, { id });
|
|
14
|
+
|
|
15
|
+
if (!event) {
|
|
16
|
+
notFound();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const beginsAt = event.instance?.beginsAt;
|
|
20
|
+
const formattedStart = beginsAt
|
|
21
|
+
? new Date(beginsAt as string).toLocaleString(undefined, {
|
|
22
|
+
weekday: 'long',
|
|
23
|
+
year: 'numeric',
|
|
24
|
+
month: 'long',
|
|
25
|
+
day: 'numeric',
|
|
26
|
+
hour: '2-digit',
|
|
27
|
+
minute: '2-digit',
|
|
28
|
+
})
|
|
29
|
+
: null;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<article>
|
|
33
|
+
<header>
|
|
34
|
+
<h1>{event.name}</h1>
|
|
35
|
+
{formattedStart && (
|
|
36
|
+
<time dateTime={beginsAt as string}>{formattedStart}</time>
|
|
37
|
+
)}
|
|
38
|
+
</header>
|
|
39
|
+
|
|
40
|
+
{event.body != null && <RichText doc={event.body} />}
|
|
41
|
+
</article>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"expectations": [
|
|
3
|
+
{ "text": "Uses `getEvent` imported from `@uidu/client`", "passed": false, "evidence": "Hallucinated namespaced method: `uidu.events.get({ id })` — no such API on the client" },
|
|
4
|
+
{ "text": "Reads `event.name` (not `event.title`)", "passed": true, "evidence": "Line 31: <h1>{event.name}</h1>" },
|
|
5
|
+
{ "text": "Reads `event.instance.beginsAt`", "passed": false, "evidence": "Used `event.startTime` (lines 17, 33) — field does not exist; correct path is `event.instance.beginsAt`" },
|
|
6
|
+
{ "text": "Handles null event via `notFound()`", "passed": true, "evidence": "Lines 13-15" },
|
|
7
|
+
{ "text": "Renders body with `<RichText doc={event.body} />`", "passed": false, "evidence": "Used `dangerouslySetInnerHTML={{ __html: event.body }}` (line 43) — body is a structured RichText doc, not an HTML string" },
|
|
8
|
+
{ "text": "params is typed as `Promise<{ id: string }>` and awaited", "passed": true, "evidence": "Lines 5, 9" }
|
|
9
|
+
],
|
|
10
|
+
"pass_rate": 0.5
|
|
11
|
+
}
|