@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.
Files changed (44) hide show
  1. package/README.md +42 -0
  2. package/package.json +25 -0
  3. package/skills/uidu/SKILL.md +321 -0
  4. package/skills/uidu/evals/evals.json +115 -0
  5. package/skills/uidu/metadata.json +8 -0
  6. package/skills/uidu-workspace/iteration-1/benchmark.md +43 -0
  7. package/skills/uidu-workspace/iteration-1/eval-0-event-detail/eval_metadata.json +13 -0
  8. package/skills/uidu-workspace/iteration-1/eval-0-event-detail/with_skill/grading.json +11 -0
  9. package/skills/uidu-workspace/iteration-1/eval-0-event-detail/with_skill/outputs/page.tsx +43 -0
  10. package/skills/uidu-workspace/iteration-1/eval-0-event-detail/without_skill/grading.json +11 -0
  11. package/skills/uidu-workspace/iteration-1/eval-0-event-detail/without_skill/outputs/page.tsx +46 -0
  12. package/skills/uidu-workspace/iteration-1/eval-1-stories-listing/eval_metadata.json +12 -0
  13. package/skills/uidu-workspace/iteration-1/eval-1-stories-listing/with_skill/grading.json +10 -0
  14. package/skills/uidu-workspace/iteration-1/eval-1-stories-listing/with_skill/outputs/page.tsx +33 -0
  15. package/skills/uidu-workspace/iteration-1/eval-1-stories-listing/without_skill/grading.json +10 -0
  16. package/skills/uidu-workspace/iteration-1/eval-1-stories-listing/without_skill/outputs/page.tsx +38 -0
  17. package/skills/uidu-workspace/iteration-1/eval-2-donations-debug/eval_metadata.json +10 -0
  18. package/skills/uidu-workspace/iteration-1/eval-2-donations-debug/with_skill/grading.json +8 -0
  19. package/skills/uidu-workspace/iteration-1/eval-2-donations-debug/with_skill/outputs/answer.md +63 -0
  20. package/skills/uidu-workspace/iteration-1/eval-2-donations-debug/without_skill/grading.json +9 -0
  21. package/skills/uidu-workspace/iteration-1/eval-2-donations-debug/without_skill/outputs/answer.md +47 -0
  22. package/skills/uidu-workspace/iteration-2/benchmark.md +73 -0
  23. package/skills/uidu-workspace/iteration-2/eval-0-event-detail/with_skill/outputs/page.tsx +37 -0
  24. package/skills/uidu-workspace/iteration-2/eval-0-event-detail/without_skill/outputs/page.tsx +60 -0
  25. package/skills/uidu-workspace/iteration-2/eval-1-stories-listing/with_skill/outputs/page.tsx +33 -0
  26. package/skills/uidu-workspace/iteration-2/eval-1-stories-listing/without_skill/outputs/page.tsx +36 -0
  27. package/skills/uidu-workspace/iteration-2/eval-2-donations-debug/with_skill/outputs/answer.md +51 -0
  28. package/skills/uidu-workspace/iteration-2/eval-2-donations-debug/without_skill/outputs/answer.md +77 -0
  29. package/skills/uidu-workspace/iteration-2/eval-3-client-setup/with_skill/outputs/uidu.ts +13 -0
  30. package/skills/uidu-workspace/iteration-2/eval-3-client-setup/without_skill/outputs/uidu.ts +47 -0
  31. package/skills/uidu-workspace/iteration-2/eval-4-cms-pageblocks/with_skill/grading.json +11 -0
  32. package/skills/uidu-workspace/iteration-2/eval-4-cms-pageblocks/with_skill/outputs/Hero.tsx +17 -0
  33. package/skills/uidu-workspace/iteration-2/eval-4-cms-pageblocks/with_skill/outputs/page.tsx +21 -0
  34. package/skills/uidu-workspace/iteration-2/eval-4-cms-pageblocks/without_skill/outputs/Hero.tsx +32 -0
  35. package/skills/uidu-workspace/iteration-2/eval-4-cms-pageblocks/without_skill/outputs/page.tsx +35 -0
  36. package/skills/uidu-workspace/iteration-2/eval-5-help-center/with_skill/outputs/page.tsx +54 -0
  37. package/skills/uidu-workspace/iteration-2/eval-5-help-center/without_skill/outputs/page.tsx +80 -0
  38. package/skills/uidu-workspace/iteration-2/eval-6-forms-contact/with_skill/grading.json +11 -0
  39. package/skills/uidu-workspace/iteration-2/eval-6-forms-contact/with_skill/outputs/ContactForm.tsx +277 -0
  40. package/skills/uidu-workspace/iteration-2/eval-6-forms-contact/with_skill/outputs/page.tsx +48 -0
  41. package/skills/uidu-workspace/iteration-2/eval-6-forms-contact/without_skill/outputs/ContactForm.tsx +144 -0
  42. package/skills/uidu-workspace/iteration-2/eval-6-forms-contact/without_skill/outputs/page.tsx +45 -0
  43. package/skills/uidu-workspace/iteration-2/eval-7-routing-gotcha/with_skill/outputs/answer.md +63 -0
  44. package/skills/uidu-workspace/iteration-2/eval-7-routing-gotcha/without_skill/outputs/answer.md +131 -0
@@ -0,0 +1,77 @@
1
+ # Donation Campaign Amounts Showing 100x Too Large
2
+
3
+ ## Diagnosis
4
+
5
+ The numbers are off by a factor of **100**. A €50,000 goal is displaying as €5,000,000. This is the classic symptom of rendering a **monetary amount stored in minor units (cents)** as if it were already a major unit (euros).
6
+
7
+ Almost every payment/commerce system — Stripe, PayPal, Adyen, Shopify, and most GraphQL-backed CMS/donation platforms (uidu included) — stores money as integers in the **smallest currency subunit**:
8
+
9
+ - €50.00 → stored as `5000` (cents)
10
+ - €50,000.00 → stored as `5000000` (cents)
11
+
12
+ When you write `€{campaign.goal}`, you're printing the raw integer cents value directly, which is why every amount looks 100× too big.
13
+
14
+ ## The Fix
15
+
16
+ Divide by 100 and format with a proper locale-aware formatter. **Do not** just do `goal / 100` inline — you'll lose thousand separators, get floating-point rounding artifacts, and miss currency symbols.
17
+
18
+ Use `Intl.NumberFormat`:
19
+
20
+ ```tsx
21
+ function formatMoney(amountInCents: number, currency = 'EUR', locale = 'it-IT') {
22
+ return new Intl.NumberFormat(locale, {
23
+ style: 'currency',
24
+ currency,
25
+ }).format(amountInCents / 100);
26
+ }
27
+
28
+ // In your component:
29
+ <p>{formatMoney(campaign.goal)}</p>
30
+ // Renders: "€ 50.000,00"
31
+ ```
32
+
33
+ ### Even better: extract a helper
34
+
35
+ Put this in `lib/format.ts` and reuse it everywhere money appears (goal, raised amount, average donation, etc.):
36
+
37
+ ```ts
38
+ // lib/format.ts
39
+ export function formatMoney(
40
+ amountInCents: number | null | undefined,
41
+ options: { currency?: string; locale?: string } = {},
42
+ ) {
43
+ const { currency = 'EUR', locale = 'it-IT' } = options;
44
+ if (amountInCents == null) return '—';
45
+ return new Intl.NumberFormat(locale, {
46
+ style: 'currency',
47
+ currency,
48
+ }).format(amountInCents / 100);
49
+ }
50
+ ```
51
+
52
+ ### If the campaign has its own currency field
53
+
54
+ Most uidu campaigns/donation entities carry a `currency` (ISO 4217 code) alongside the amount. Use it:
55
+
56
+ ```tsx
57
+ <p>{formatMoney(campaign.goal, { currency: campaign.currency })}</p>
58
+ ```
59
+
60
+ ## Things to Check / Watch Out For
61
+
62
+ 1. **Confirm the unit in the GraphQL schema.** Look at the field's type and description in your uidu schema — it should be documented as cents/minor units. If it's `Int` and values look 100× too large, that's your confirmation.
63
+ 2. **Apply the fix everywhere.** `goal`, `raised`, `donationAmount`, `averageDonation`, progress bars (`raised / goal` ratios are *fine* unaffected because both sides cancel), tooltips, share text, OG image metadata.
64
+ 3. **Zero-decimal currencies.** JPY, KRW, etc. don't have minor units — they're stored as whole units. `Intl.NumberFormat` handles this correctly *if* you pass the right currency code, but your `/ 100` divide would be wrong. A safer helper:
65
+
66
+ ```ts
67
+ const ZERO_DECIMAL = new Set(['JPY', 'KRW', 'VND', 'CLP', 'ISK']);
68
+ const divisor = ZERO_DECIMAL.has(currency) ? 1 : 100;
69
+ return formatter.format(amountInCents / divisor);
70
+ ```
71
+
72
+ 4. **Server vs client formatting.** `Intl.NumberFormat` runs on both, but locale defaults differ. Pass an explicit `locale` to avoid hydration mismatches in Next.js.
73
+ 5. **Form inputs.** If users *enter* an amount (e.g., custom donation), convert the other way: `Math.round(parseFloat(input) * 100)` before sending to the API.
74
+
75
+ ## TL;DR
76
+
77
+ `campaign.goal` is in **cents**, not euros. Divide by 100 and use `Intl.NumberFormat('it-IT', { style: 'currency', currency: 'EUR' })` instead of string-concatenating a `€`.
@@ -0,0 +1,13 @@
1
+ import { createClient } from '@uidu/client';
2
+
3
+ export const uidu = createClient({
4
+ workspace: process.env.UIDU_WORKSPACE ?? '',
5
+ publicToken: process.env.UIDU_PUBLIC_TOKEN, // safe to expose
6
+ apiKey: process.env.UIDU_API_KEY, // server-only secret
7
+ });
8
+
9
+ // Required environment variables (.env.local):
10
+ //
11
+ // UIDU_WORKSPACE=your-workspace-slug # required — e.g. "acme"
12
+ // UIDU_PUBLIC_TOKEN=... # optional — public read-only token, safe to expose
13
+ // UIDU_API_KEY=... # optional — server-only secret for privileged ops
@@ -0,0 +1,47 @@
1
+ import { createClient } from '@uidu/client';
2
+
3
+ /**
4
+ * Configured uidu API client.
5
+ *
6
+ * Use this client in Server Components, Route Handlers, Server Actions,
7
+ * and other server-side contexts to query the uidu GraphQL API.
8
+ *
9
+ * Example usage in a Server Component:
10
+ *
11
+ * import { uidu } from '@/lib/uidu';
12
+ *
13
+ * export default async function Page() {
14
+ * const page = await uidu.cms.getPage({ slug: 'home' });
15
+ * return <pre>{JSON.stringify(page, null, 2)}</pre>;
16
+ * }
17
+ */
18
+ export const uidu = createClient({
19
+ endpoint: process.env.UIDU_API_ENDPOINT!,
20
+ projectId: process.env.UIDU_PROJECT_ID!,
21
+ // Optional: server-side token for authenticated/private content.
22
+ // Omit on the client; the SDK will fall back to public access.
23
+ token: process.env.UIDU_API_TOKEN,
24
+ });
25
+
26
+ export default uidu;
27
+
28
+ /*
29
+ * ─────────────────────────────────────────────────────────────
30
+ * Environment variables — add these to `.env.local`
31
+ * ─────────────────────────────────────────────────────────────
32
+ *
33
+ * # Required — uidu GraphQL endpoint
34
+ * UIDU_API_ENDPOINT=https://uidu.org/graphql
35
+ *
36
+ * # Required — your uidu workspace/project ID
37
+ * UIDU_PROJECT_ID=your-project-id-here
38
+ *
39
+ * # Optional — server-side API token for authenticated requests
40
+ * # (only needed for private/draft content or authenticated mutations)
41
+ * UIDU_API_TOKEN=your-api-token-here
42
+ *
43
+ * # Optional — expose endpoint/projectId to the browser if you also
44
+ * # need a client-side uidu client (prefix with NEXT_PUBLIC_)
45
+ * # NEXT_PUBLIC_UIDU_API_ENDPOINT=https://uidu.org/graphql
46
+ * # NEXT_PUBLIC_UIDU_PROJECT_ID=your-project-id-here
47
+ */
@@ -0,0 +1,11 @@
1
+ {
2
+ "expectations": [
3
+ { "text": "Parent imports `getPage` from `@uidu/client` and calls `getPage(uidu, { projectId, slug })`", "passed": true, "evidence": "page.tsx lines 1, 8-11" },
4
+ { "text": "Parent imports `PageBlocks` from `@uidu/react` (NOT `PageBlocksParser`)", "passed": true, "evidence": "page.tsx line 2" },
5
+ { "text": "Parent renders `<PageBlocks pageBlocks={page.pageBlocks} components={{ Hero, Feature }} />`", "passed": true, "evidence": "page.tsx lines 16-19" },
6
+ { "text": "Hero component takes `{ block }: { block: PageBlock }` (not `pageBlock`)", "passed": true, "evidence": "Hero.tsx line 6" },
7
+ { "text": "Hero uses `useFields(block.fieldValues)` from `@uidu/react`", "passed": true, "evidence": "Hero.tsx line 7" },
8
+ { "text": "Hero uses `toText(fields.title)` (not `as string` casts)", "passed": true, "evidence": "Hero.tsx lines 8-9" }
9
+ ],
10
+ "pass_rate": 1.0
11
+ }
@@ -0,0 +1,17 @@
1
+ import type { Page } from '@uidu/client';
2
+ import { toText, useFields } from '@uidu/react';
3
+
4
+ type PageBlock = NonNullable<Page['pageBlocks']>[number];
5
+
6
+ export function Hero({ block }: { block: PageBlock }) {
7
+ const fields = useFields(block.fieldValues);
8
+ const title = toText(fields.title);
9
+ const subtitle = toText(fields.subtitle);
10
+
11
+ return (
12
+ <section>
13
+ <h1>{title}</h1>
14
+ {subtitle && <p>{subtitle}</p>}
15
+ </section>
16
+ );
17
+ }
@@ -0,0 +1,21 @@
1
+ import { getPage } from '@uidu/client';
2
+ import { PageBlocks } from '@uidu/react';
3
+ import { Hero } from '@/components/blocks/Hero';
4
+ import { Feature } from '@/components/blocks/Feature';
5
+ import { uidu } from '@/lib/uidu';
6
+
7
+ export default async function HomePage() {
8
+ const page = await getPage(uidu, {
9
+ projectId: process.env.UIDU_PROJECT_ID!,
10
+ slug: 'home',
11
+ });
12
+
13
+ if (!page) return <div>Not found</div>;
14
+
15
+ return (
16
+ <PageBlocks
17
+ pageBlocks={page.pageBlocks}
18
+ components={{ Hero, Feature }}
19
+ />
20
+ );
21
+ }
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+
3
+ import { useFields } from '@uidu/react/apps/cms';
4
+ import type { PageBlock } from '@uidu/client/cms';
5
+
6
+ interface HeroProps {
7
+ pageBlock: PageBlock;
8
+ }
9
+
10
+ export function Hero({ pageBlock }: HeroProps) {
11
+ const fields = useFields(pageBlock);
12
+
13
+ const title = fields.title as string | undefined;
14
+ const subtitle = fields.subtitle as string | undefined;
15
+
16
+ return (
17
+ <section className="relative w-full py-24 px-6 md:py-32">
18
+ <div className="mx-auto max-w-5xl text-center">
19
+ {title && (
20
+ <h1 className="text-4xl font-bold tracking-tight md:text-6xl">
21
+ {title}
22
+ </h1>
23
+ )}
24
+ {subtitle && (
25
+ <p className="mt-6 text-lg text-muted-foreground md:text-xl">
26
+ {subtitle}
27
+ </p>
28
+ )}
29
+ </div>
30
+ </section>
31
+ );
32
+ }
@@ -0,0 +1,35 @@
1
+ import { getPage } from '@uidu/client/cms';
2
+ import { PageBlocksParser } from '@uidu/react/components';
3
+ import { uidu } from '@/lib/uidu';
4
+ import { Hero } from '@/components/blocks/Hero';
5
+ import { Feature } from '@/components/blocks/Feature';
6
+
7
+ export default async function HomePage() {
8
+ const projectId = process.env.UIDU_PROJECT_ID;
9
+
10
+ if (!projectId) {
11
+ throw new Error('UIDU_PROJECT_ID environment variable is not set');
12
+ }
13
+
14
+ const page = await getPage({
15
+ endpoint: uidu.endpoint,
16
+ slug: 'home',
17
+ projectId,
18
+ });
19
+
20
+ if (!page) {
21
+ return <div>Page not found</div>;
22
+ }
23
+
24
+ return (
25
+ <main>
26
+ <PageBlocksParser
27
+ blocks={page.pageBlocks}
28
+ components={{
29
+ hero: Hero,
30
+ feature: Feature,
31
+ }}
32
+ />
33
+ </main>
34
+ );
35
+ }
@@ -0,0 +1,54 @@
1
+ import Link from 'next/link';
2
+ import { getChannel } from '@uidu/client';
3
+ import { uidu } from '@/lib/uidu';
4
+
5
+ export default async function HelpCenterPage() {
6
+ const channel = await getChannel(uidu, {
7
+ id: process.env.UIDU_CHANNEL_ID!,
8
+ });
9
+
10
+ if (!channel) {
11
+ return <div>Help center not found</div>;
12
+ }
13
+
14
+ const collections =
15
+ channel.kbCollections?.edges
16
+ ?.map((edge) => edge?.node)
17
+ .filter((node): node is NonNullable<typeof node> => node != null) ?? [];
18
+
19
+ return (
20
+ <main>
21
+ <h1>{channel.name}</h1>
22
+
23
+ {collections.length === 0 ? (
24
+ <p>No articles yet.</p>
25
+ ) : (
26
+ collections.map((collection) => {
27
+ const articles =
28
+ collection.kbArticles?.edges
29
+ ?.map((edge) => edge?.node)
30
+ .filter(
31
+ (node): node is NonNullable<typeof node> => node != null,
32
+ ) ?? [];
33
+
34
+ return (
35
+ <section key={collection.id}>
36
+ <h2>{collection.name}</h2>
37
+ {articles.length === 0 ? (
38
+ <p>No articles in this collection.</p>
39
+ ) : (
40
+ <ul>
41
+ {articles.map((article) => (
42
+ <li key={article.id}>
43
+ <Link href={`/help/${article.id}`}>{article.name}</Link>
44
+ </li>
45
+ ))}
46
+ </ul>
47
+ )}
48
+ </section>
49
+ );
50
+ })
51
+ )}
52
+ </main>
53
+ );
54
+ }
@@ -0,0 +1,80 @@
1
+ import Link from 'next/link';
2
+ import { notFound } from 'next/navigation';
3
+ import { uidu } from '@/lib/uidu';
4
+
5
+ export default async function HelpCenterPage() {
6
+ const channelId = process.env.UIDU_CHANNEL_ID;
7
+
8
+ if (!channelId) {
9
+ notFound();
10
+ }
11
+
12
+ const channel = await uidu.helpCenter.getChannel({ id: channelId });
13
+
14
+ if (!channel) {
15
+ notFound();
16
+ }
17
+
18
+ // Group articles by collection
19
+ const articles = channel.articles ?? [];
20
+ const grouped = new Map<
21
+ string,
22
+ {
23
+ collectionId: string;
24
+ collectionName: string;
25
+ items: Array<{ id: string; title: string }>;
26
+ }
27
+ >();
28
+
29
+ for (const article of articles) {
30
+ const collectionId = article.collection?.id ?? 'uncategorized';
31
+ const collectionName = article.collection?.name ?? 'Uncategorized';
32
+
33
+ if (!grouped.has(collectionId)) {
34
+ grouped.set(collectionId, {
35
+ collectionId,
36
+ collectionName,
37
+ items: [],
38
+ });
39
+ }
40
+
41
+ grouped.get(collectionId)!.items.push({
42
+ id: article.id,
43
+ title: article.title,
44
+ });
45
+ }
46
+
47
+ const collections = Array.from(grouped.values());
48
+
49
+ return (
50
+ <main className="container mx-auto px-4 py-12">
51
+ <h1 className="text-4xl font-bold mb-8">{channel.name}</h1>
52
+
53
+ {collections.length === 0 ? (
54
+ <p className="text-muted-foreground">No articles available.</p>
55
+ ) : (
56
+ <div className="space-y-10">
57
+ {collections.map((collection) => (
58
+ <section key={collection.collectionId}>
59
+ <h2 className="text-2xl font-semibold mb-4">
60
+ {collection.collectionName}
61
+ </h2>
62
+ <ul className="space-y-2">
63
+ {collection.items.map((article) => (
64
+ <li key={article.id}>
65
+ <Link
66
+ href={`/help/${article.id}`}
67
+ className="text-primary hover:underline"
68
+ >
69
+ {article.title}
70
+ </Link>
71
+ </li>
72
+ ))}
73
+ </ul>
74
+ </section>
75
+ ))}
76
+ </div>
77
+ )}
78
+ </main>
79
+ );
80
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "expectations": [
3
+ { "text": "Server file uses `getForm` from `@uidu/client` with `{ id: formId }`", "passed": true, "evidence": "page.tsx lines 2, 21" },
4
+ { "text": "Client file has `'use client'` directive", "passed": true, "evidence": "ContactForm.tsx line 2" },
5
+ { "text": "Client uses `useForm` imported from `@uidu/react` (NOT from `react-hook-form`)", "passed": false, "evidence": "ContactForm.tsx line 5: `import { useForm, type SubmitHandler } from 'react-hook-form'` — the agent ignored the skill's recommendation and went with raw react-hook-form" },
6
+ { "text": "useForm called with the uidu client and form object", "passed": false, "evidence": "useForm is from react-hook-form so this assertion doesn't apply meaningfully; submission goes through `createFormResponse(uidu, ...)` (which IS from @uidu/client) rather than uidu's useForm wrapper" },
7
+ { "text": "Iterates `form.formQuestions.edges` (the shape the skill describes)", "passed": false, "evidence": "ContactForm.tsx line 69: iterates `form.inputs.edges` instead. Agent deviated from the skill — either the skill is wrong about the field name or the agent over-rode its guidance" }
8
+ ],
9
+ "pass_rate": 0.4,
10
+ "notes": "WORSE than baseline (3/5 → 2/5). The skill's Forms section isn't authoritative enough to override the model's default react-hook-form intuition. The agent kept the server-side getForm call but rebuilt the client side from first principles. Investigate: (a) is the skill wrong about `formQuestions` vs `inputs`? (b) does the Forms section need more explicit 'use useForm from @uidu/react, NOT react-hook-form directly' framing? Cross-check against the demo's ContactFormClient.tsx before iterating."
11
+ }