@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
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { notFound } from 'next/navigation';
|
|
2
|
+
import { uidu } from '@/lib/uidu';
|
|
3
|
+
|
|
4
|
+
interface EventPageProps {
|
|
5
|
+
params: Promise<{ id: string }>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default async function EventPage({ params }: EventPageProps) {
|
|
9
|
+
const { id } = await params;
|
|
10
|
+
|
|
11
|
+
const event = await uidu.events.get({ id });
|
|
12
|
+
|
|
13
|
+
if (!event) {
|
|
14
|
+
notFound();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const startTime = new Date(event.startTime);
|
|
18
|
+
const formattedStartTime = new Intl.DateTimeFormat('en-US', {
|
|
19
|
+
weekday: 'long',
|
|
20
|
+
year: 'numeric',
|
|
21
|
+
month: 'long',
|
|
22
|
+
day: 'numeric',
|
|
23
|
+
hour: 'numeric',
|
|
24
|
+
minute: '2-digit',
|
|
25
|
+
timeZoneName: 'short',
|
|
26
|
+
}).format(startTime);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<article className="mx-auto max-w-3xl px-4 py-12">
|
|
30
|
+
<header className="mb-8 border-b pb-6">
|
|
31
|
+
<h1 className="text-4xl font-bold tracking-tight">{event.name}</h1>
|
|
32
|
+
<time
|
|
33
|
+
dateTime={event.startTime}
|
|
34
|
+
className="mt-3 block text-lg text-muted-foreground"
|
|
35
|
+
>
|
|
36
|
+
{formattedStartTime}
|
|
37
|
+
</time>
|
|
38
|
+
</header>
|
|
39
|
+
|
|
40
|
+
<div
|
|
41
|
+
className="prose prose-neutral max-w-none dark:prose-invert"
|
|
42
|
+
dangerouslySetInnerHTML={{ __html: event.body }}
|
|
43
|
+
/>
|
|
44
|
+
</article>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"eval_id": 1,
|
|
3
|
+
"eval_name": "stories-listing",
|
|
4
|
+
"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`.",
|
|
5
|
+
"assertions": [
|
|
6
|
+
{ "text": "Uses `listStories` imported from `@uidu/client`" },
|
|
7
|
+
{ "text": "Reads `story.name` (not `story.title`)" },
|
|
8
|
+
{ "text": "Sorts by `story.publishedAt` in descending order (newest first)" },
|
|
9
|
+
{ "text": "Links to `/story/${story.id}` (nested under /story/, not at root)" },
|
|
10
|
+
{ "text": "Uses `next/link` for navigation" }
|
|
11
|
+
]
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"expectations": [
|
|
3
|
+
{ "text": "Uses `listStories` imported from `@uidu/client`", "passed": true, "evidence": "Line 2" },
|
|
4
|
+
{ "text": "Reads `story.name`", "passed": true, "evidence": "Line 21" },
|
|
5
|
+
{ "text": "Sorts by `publishedAt` descending", "passed": true, "evidence": "Lines 8-12: localeCompare with b before a" },
|
|
6
|
+
{ "text": "Links to `/story/${story.id}`", "passed": true, "evidence": "Line 20" },
|
|
7
|
+
{ "text": "Uses `next/link`", "passed": true, "evidence": "Line 1" }
|
|
8
|
+
],
|
|
9
|
+
"pass_rate": 1.0
|
|
10
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { listStories } from '@uidu/client';
|
|
3
|
+
import { uidu } from '@/lib/uidu';
|
|
4
|
+
|
|
5
|
+
export default async function StoriesPage() {
|
|
6
|
+
const stories = await listStories(uidu, { first: 50 });
|
|
7
|
+
|
|
8
|
+
const sorted = [...stories].sort((a, b) =>
|
|
9
|
+
((b.publishedAt as string) ?? '').localeCompare(
|
|
10
|
+
(a.publishedAt as string) ?? '',
|
|
11
|
+
),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<main>
|
|
16
|
+
<h1>Stories</h1>
|
|
17
|
+
<ul>
|
|
18
|
+
{sorted.map((story) => (
|
|
19
|
+
<li key={story.id}>
|
|
20
|
+
<Link href={`/story/${story.id}`}>
|
|
21
|
+
<h2>{story.name}</h2>
|
|
22
|
+
{story.publishedAt && (
|
|
23
|
+
<time dateTime={story.publishedAt as string}>
|
|
24
|
+
{new Date(story.publishedAt as string).toLocaleDateString()}
|
|
25
|
+
</time>
|
|
26
|
+
)}
|
|
27
|
+
</Link>
|
|
28
|
+
</li>
|
|
29
|
+
))}
|
|
30
|
+
</ul>
|
|
31
|
+
</main>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"expectations": [
|
|
3
|
+
{ "text": "Uses `listStories` imported from `@uidu/client`", "passed": false, "evidence": "Hallucinated `uidu.stories.list({ orderBy: ... })` (line 5) — no such API" },
|
|
4
|
+
{ "text": "Reads `story.name`", "passed": true, "evidence": "Line 19" },
|
|
5
|
+
{ "text": "Sorts by `publishedAt` descending", "passed": false, "evidence": "Relied on a non-existent `orderBy` option in a hallucinated API call; no client-side sort exists, so sort order at runtime is whatever the SDK returns" },
|
|
6
|
+
{ "text": "Links to `/story/${story.id}`", "passed": true, "evidence": "Line 16" },
|
|
7
|
+
{ "text": "Uses `next/link`", "passed": true, "evidence": "Line 1" }
|
|
8
|
+
],
|
|
9
|
+
"pass_rate": 0.6
|
|
10
|
+
}
|
package/skills/uidu-workspace/iteration-1/eval-1-stories-listing/without_skill/outputs/page.tsx
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { uidu } from '@/lib/uidu';
|
|
3
|
+
|
|
4
|
+
export default async function StoriesPage() {
|
|
5
|
+
const stories = await uidu.stories.list({
|
|
6
|
+
orderBy: { publishedAt: 'desc' },
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<main className="mx-auto max-w-3xl px-6 py-12">
|
|
11
|
+
<h1 className="mb-8 text-3xl font-bold">Stories</h1>
|
|
12
|
+
<ul className="space-y-4">
|
|
13
|
+
{stories.map((story) => (
|
|
14
|
+
<li key={story.id}>
|
|
15
|
+
<Link
|
|
16
|
+
href={`/story/${story.id}`}
|
|
17
|
+
className="block rounded-lg border p-4 hover:bg-muted"
|
|
18
|
+
>
|
|
19
|
+
<h2 className="text-xl font-semibold">{story.name}</h2>
|
|
20
|
+
{story.publishedAt && (
|
|
21
|
+
<time
|
|
22
|
+
dateTime={story.publishedAt}
|
|
23
|
+
className="text-sm text-muted-foreground"
|
|
24
|
+
>
|
|
25
|
+
{new Date(story.publishedAt).toLocaleDateString(undefined, {
|
|
26
|
+
year: 'numeric',
|
|
27
|
+
month: 'long',
|
|
28
|
+
day: 'numeric',
|
|
29
|
+
})}
|
|
30
|
+
</time>
|
|
31
|
+
)}
|
|
32
|
+
</Link>
|
|
33
|
+
</li>
|
|
34
|
+
))}
|
|
35
|
+
</ul>
|
|
36
|
+
</main>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"eval_id": 2,
|
|
3
|
+
"eval_name": "donations-debug",
|
|
4
|
+
"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.",
|
|
5
|
+
"assertions": [
|
|
6
|
+
{ "text": "Identifies that `campaign.goal` (and `donationsAmount`) are returned in cents, not euros/dollars" },
|
|
7
|
+
{ "text": "Suggests dividing by 100 to display the value in major currency units" },
|
|
8
|
+
{ "text": "Shows a corrected code snippet using `campaign.goal / 100` or equivalent" }
|
|
9
|
+
]
|
|
10
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"expectations": [
|
|
3
|
+
{ "text": "Identifies cents convention", "passed": true, "evidence": "First line under 'What's wrong': 'returned in cents (integer minor units)'" },
|
|
4
|
+
{ "text": "Suggests dividing by 100", "passed": true, "evidence": "Code block on line 16-18 divides by 100" },
|
|
5
|
+
{ "text": "Shows corrected code with `campaign.goal / 100`", "passed": true, "evidence": "Lines 16-18, repeated in helper at 47-57" }
|
|
6
|
+
],
|
|
7
|
+
"pass_rate": 1.0
|
|
8
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Diagnosis: donation amounts are in cents, not euros
|
|
2
|
+
|
|
3
|
+
## What's wrong
|
|
4
|
+
|
|
5
|
+
Donation campaign monetary fields in the uidu API — `goal`, `donationsAmount` — are stored and returned **in cents** (integer minor units), not in the major currency unit.
|
|
6
|
+
|
|
7
|
+
So a campaign whose real goal is €50,000 comes back as `goal: 5000000` (5,000,000 cents). When you render `€{campaign.goal}` directly, you're displaying the raw cent value, which is exactly 100× too large — €50,000 shows up as €5,000,000.
|
|
8
|
+
|
|
9
|
+
The same applies to `donationsAmount` (the running total raised) and any progress-bar math derived from these two fields.
|
|
10
|
+
|
|
11
|
+
## How to fix it
|
|
12
|
+
|
|
13
|
+
Divide by 100 before display, and guard against `null`/`undefined`:
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
const raised = (campaign.donationsAmount ?? 0) / 100;
|
|
17
|
+
const goal = (campaign.goal ?? 0) / 100;
|
|
18
|
+
const progress = goal > 0 ? Math.min(100, (raised / goal) * 100) : 0;
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then render the divided values:
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
<p>€{goal.toLocaleString('it-IT')}</p>
|
|
25
|
+
<p>€{raised.toLocaleString('it-IT')} raised</p>
|
|
26
|
+
<div className="progress" style={{ width: `${progress}%` }} />
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Recommended: use `Intl.NumberFormat` for proper formatting
|
|
30
|
+
|
|
31
|
+
Rather than prefixing a hardcoded `€`, format with the locale's currency rules — this gives correct thousands separators, decimal places, and symbol placement:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
const fmt = new Intl.NumberFormat('it-IT', {
|
|
35
|
+
style: 'currency',
|
|
36
|
+
currency: 'EUR',
|
|
37
|
+
maximumFractionDigits: 0, // drop ",00" on whole-euro goals
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
<p>{fmt.format(goal)}</p> // "50.000 €"
|
|
41
|
+
<p>{fmt.format(raised)} raised</p>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
If you're showing many campaigns, extract a small helper:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
// src/lib/money.ts
|
|
48
|
+
export const centsToEuros = (cents: number | null | undefined) =>
|
|
49
|
+
(cents ?? 0) / 100;
|
|
50
|
+
|
|
51
|
+
export const formatEuros = (cents: number | null | undefined) =>
|
|
52
|
+
new Intl.NumberFormat('it-IT', {
|
|
53
|
+
style: 'currency',
|
|
54
|
+
currency: 'EUR',
|
|
55
|
+
maximumFractionDigits: 0,
|
|
56
|
+
}).format(centsToEuros(cents));
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Then `{formatEuros(campaign.goal)}` everywhere in your JSX.
|
|
60
|
+
|
|
61
|
+
## Why cents?
|
|
62
|
+
|
|
63
|
+
Storing money as integer cents avoids floating-point rounding errors (the classic `0.1 + 0.2 !== 0.3` problem). It's standard practice in payment APIs (Stripe, Braintree, etc.), and the uidu API follows the same convention — so the same fix applies to any other monetary field you encounter (e.g. paid-event ticket prices).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"expectations": [
|
|
3
|
+
{ "text": "Identifies cents convention", "passed": true, "evidence": "Line 5: 'stored and returned in minor currency units (cents)'" },
|
|
4
|
+
{ "text": "Suggests dividing by 100", "passed": true, "evidence": "Line 18: cents / 100 inside formatter" },
|
|
5
|
+
{ "text": "Shows corrected code with `campaign.goal / 100`", "passed": true, "evidence": "Lines 14-21" }
|
|
6
|
+
],
|
|
7
|
+
"pass_rate": 1.0,
|
|
8
|
+
"notes": "Baseline got this right from training data (cents is industry convention). This assertion is non-discriminating — skill earns no measurable credit here."
|
|
9
|
+
}
|
package/skills/uidu-workspace/iteration-1/eval-2-donations-debug/without_skill/outputs/answer.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Donation campaign goal displays as 100x the expected value
|
|
2
|
+
|
|
3
|
+
## Diagnosis
|
|
4
|
+
|
|
5
|
+
Your campaign goal is being stored and returned in **minor currency units (cents)**, not in major units (euros). This is the standard convention for monetary values in financial APIs — including most CMS/commerce backends — because integer arithmetic on cents avoids floating-point rounding bugs (`0.1 + 0.2 !== 0.3`).
|
|
6
|
+
|
|
7
|
+
So `campaign.goal` of `5000000` is **5,000,000 cents = €50,000.00**, exactly the value you expect. Rendering it directly with a `€` prefix prints the raw cent count, which is why the number looks 100x too large.
|
|
8
|
+
|
|
9
|
+
## The fix
|
|
10
|
+
|
|
11
|
+
Divide by 100 and format with `Intl.NumberFormat` so you get proper locale-aware currency output (correct decimal separator, thousands grouping, currency symbol placement):
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
const formatCurrency = (cents: number, currency = 'EUR', locale = 'it-IT') =>
|
|
15
|
+
new Intl.NumberFormat(locale, {
|
|
16
|
+
style: 'currency',
|
|
17
|
+
currency,
|
|
18
|
+
}).format(cents / 100);
|
|
19
|
+
|
|
20
|
+
// Usage
|
|
21
|
+
<p>Goal: {formatCurrency(campaign.goal)}</p>
|
|
22
|
+
// → "Goal: € 50.000,00"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Why `Intl.NumberFormat` instead of `€${value / 100}`?
|
|
26
|
+
|
|
27
|
+
- Handles locale-specific separators automatically (`50,000.00` in `en-US`, `50.000,00` in `it-IT`, `50 000,00` in `fr-FR`).
|
|
28
|
+
- Places the currency symbol on the correct side of the number.
|
|
29
|
+
- Pads decimals consistently (`€50,000.00`, not `€50000`).
|
|
30
|
+
- Works identically on server and client (no hydration mismatch) because it's a standard Web API available in Node and browsers.
|
|
31
|
+
|
|
32
|
+
### Things to double-check
|
|
33
|
+
|
|
34
|
+
1. **Confirm the unit in the schema.** Look at the GraphQL field for `goal` — if it's typed as `Int` and named something like `goalAmount` or `goalCents`, it's almost certainly cents. If there's a sibling field like `goalFormatted` or `goalDisplay`, prefer that (the backend handles formatting for you).
|
|
35
|
+
2. **Currency may not always be EUR.** If campaigns carry a `currency` field (e.g., `campaign.currency === 'USD'`), pass it into the formatter rather than hardcoding `'EUR'`.
|
|
36
|
+
3. **Same applies to `raised` / `pledged` / `donated` fields.** If you're computing a progress percentage like `raised / goal`, both values are in cents so the ratio is still correct — but any value you *display* needs the same `/ 100` + `Intl.NumberFormat` treatment.
|
|
37
|
+
4. **Avoid floating-point math on totals.** If you need to sum donations client-side, keep the values in cents (integers) and only divide by 100 at render time.
|
|
38
|
+
|
|
39
|
+
## Quick sanity check
|
|
40
|
+
|
|
41
|
+
Log the raw value once to confirm:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
console.log('raw goal:', campaign.goal, '→', campaign.goal / 100);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If `5000000 → 50000` matches what the campaign was set up for in the uidu dashboard, the cents-unit theory is confirmed and the formatter above is all you need.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Benchmark — uidu skill, iteration 2
|
|
2
|
+
|
|
3
|
+
## Aggregate (all 8 evals)
|
|
4
|
+
|
|
5
|
+
| Configuration | Pass rate | Assertions passed |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| with_skill | 89% | 34 / 38 |
|
|
8
|
+
| without_skill (baseline) | 47% | 18 / 38 |
|
|
9
|
+
| **Δ** | **+42 pp** | **+16** |
|
|
10
|
+
|
|
11
|
+
## Per-eval
|
|
12
|
+
|
|
13
|
+
| Eval | with_skill | baseline | Δ | Discriminating? |
|
|
14
|
+
| --- | --- | --- | --- | --- |
|
|
15
|
+
| 0 — event-detail | 6/6 (100%) | 3/6 (50%) | +50 pp | yes |
|
|
16
|
+
| 1 — stories-listing | 5/5 (100%) | 3/5 (60%) | +40 pp | yes |
|
|
17
|
+
| 2 — donations-debug | 3/3 (100%) | 3/3 (100%) | 0 | **no** — cents is industry convention |
|
|
18
|
+
| 3 — client-setup | 5/5 (100%) | 2/5 (40%) | +60 pp | yes (new) |
|
|
19
|
+
| 4 — cms-pageblocks | 6/6 (100%) | 0/6 (0%) | +100 pp | yes (new) — **biggest skill win** |
|
|
20
|
+
| 5 — help-center | 5/5 (100%) | 1/5 (20%) | +80 pp | yes (new) |
|
|
21
|
+
| 6 — forms-contact | 2/5 (40%) | 3/5 (60%) | **−20 pp** | yes (new) — **skill regressed** |
|
|
22
|
+
| 7 — routing-gotcha | 3/3 (100%) | 3/3 (100%) | 0 | **no** — Next.js knowledge baseline has |
|
|
23
|
+
|
|
24
|
+
## Comparison to iteration 1
|
|
25
|
+
|
|
26
|
+
| Metric | Iter-1 (3 evals) | Iter-2 (8 evals) |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| with_skill pass rate | 100% | 89% |
|
|
29
|
+
| Δ vs baseline | +43 pp | +42 pp |
|
|
30
|
+
|
|
31
|
+
Δ held steady despite eval set tripling and including the Forms regression.
|
|
32
|
+
|
|
33
|
+
## Analyst notes
|
|
34
|
+
|
|
35
|
+
1. **eval-4 (CMS PageBlocks) is the biggest win in the entire benchmark (+100 pp).**
|
|
36
|
+
Baseline scored 0/6 — every detail wrong (PageBlocksParser legacy name,
|
|
37
|
+
`pageBlock` prop, `useFields(pageBlock)`, `as string` casts). With-skill
|
|
38
|
+
scored 6/6 — the skill teaches this pattern definitively. This is the
|
|
39
|
+
strongest single argument for shipping the skill.
|
|
40
|
+
|
|
41
|
+
2. **eval-6 (Forms) is the only regression: with-skill scored *below* baseline.**
|
|
42
|
+
The agent followed the skill on the server side (correct `getForm(uidu, …)`)
|
|
43
|
+
but rebuilt the client form from first principles: imported `useForm` from
|
|
44
|
+
`react-hook-form` directly, used `createFormResponse(uidu, …)` for
|
|
45
|
+
submission, and iterated `form.inputs.edges` instead of the
|
|
46
|
+
`form.formQuestions.edges` the skill calls out. Two interpretations:
|
|
47
|
+
- **The skill is wrong** about `formQuestions` (real field might be `inputs`).
|
|
48
|
+
Cross-check against `demo/src/components/blocks/ContactFormClient.tsx`
|
|
49
|
+
before changing the skill.
|
|
50
|
+
- **The Forms section isn't authoritative enough.** It shows what to use
|
|
51
|
+
but doesn't explicitly say "DON'T import useForm from react-hook-form
|
|
52
|
+
directly." A negative framing line might be enough to override the
|
|
53
|
+
model's default intuition.
|
|
54
|
+
|
|
55
|
+
3. **eval-5 (help center) +80 pp** — Baseline did
|
|
56
|
+
`uidu.helpCenter.getChannel(...)` (namespaced + wrong arg style) and
|
|
57
|
+
`channel.articles` (flat array). The connection-style edges/node nesting
|
|
58
|
+
isn't guessable; the skill closes this gap completely.
|
|
59
|
+
|
|
60
|
+
4. **eval-3 (client setup) +60 pp** — Baseline invented an entire alternative
|
|
61
|
+
API surface: `endpoint`/`projectId`/`token` keys against
|
|
62
|
+
`UIDU_API_ENDPOINT`/`UIDU_PROJECT_ID`/`UIDU_API_TOKEN` env vars. The actual
|
|
63
|
+
shape (`workspace`/`publicToken`/`apiKey` against `UIDU_WORKSPACE`/
|
|
64
|
+
`UIDU_PUBLIC_TOKEN`/`UIDU_API_KEY`) is not derivable without docs.
|
|
65
|
+
|
|
66
|
+
5. **API Shape edit (iter-1 → iter-2) appears to have stuck.**
|
|
67
|
+
Zero iter-2 with-skill outputs hallucinated namespaced methods. Iter-2
|
|
68
|
+
baselines still did (`uidu.events.get`, `uidu.stories.list`,
|
|
69
|
+
`uidu.helpCenter.getChannel`). Strong signal the edit worked.
|
|
70
|
+
|
|
71
|
+
6. **Two non-discriminating evals: eval-2 and eval-7.**
|
|
72
|
+
Both groups score 100% on cents and root-route-favicon. General knowledge
|
|
73
|
+
the baseline has. Swap for sharper uidu-specific gotchas in iter-3.
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
export default async function EventDetailPage({
|
|
7
|
+
params,
|
|
8
|
+
}: {
|
|
9
|
+
params: Promise<{ id: string }>;
|
|
10
|
+
}) {
|
|
11
|
+
const { id } = await params;
|
|
12
|
+
const event = await getEvent(uidu, { id });
|
|
13
|
+
|
|
14
|
+
if (!event) {
|
|
15
|
+
notFound();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const beginsAt = event.instance?.beginsAt;
|
|
19
|
+
const formattedStart = beginsAt
|
|
20
|
+
? new Date(beginsAt as string).toLocaleString(undefined, {
|
|
21
|
+
dateStyle: 'full',
|
|
22
|
+
timeStyle: 'short',
|
|
23
|
+
})
|
|
24
|
+
: null;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<article>
|
|
28
|
+
<header>
|
|
29
|
+
<h1>{event.name}</h1>
|
|
30
|
+
{formattedStart && (
|
|
31
|
+
<time dateTime={beginsAt as string}>{formattedStart}</time>
|
|
32
|
+
)}
|
|
33
|
+
</header>
|
|
34
|
+
{event.body != null && <RichText doc={event.body} />}
|
|
35
|
+
</article>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { notFound } from 'next/navigation';
|
|
2
|
+
import { uidu } from '@/lib/uidu';
|
|
3
|
+
|
|
4
|
+
interface EventPageProps {
|
|
5
|
+
params: Promise<{ id: string }>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function formatStartTime(startTime: string | Date): string {
|
|
9
|
+
const date = typeof startTime === 'string' ? new Date(startTime) : startTime;
|
|
10
|
+
|
|
11
|
+
if (Number.isNaN(date.getTime())) {
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
16
|
+
weekday: 'long',
|
|
17
|
+
year: 'numeric',
|
|
18
|
+
month: 'long',
|
|
19
|
+
day: 'numeric',
|
|
20
|
+
hour: 'numeric',
|
|
21
|
+
minute: '2-digit',
|
|
22
|
+
timeZoneName: 'short',
|
|
23
|
+
}).format(date);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default async function EventPage({ params }: EventPageProps) {
|
|
27
|
+
const { id } = await params;
|
|
28
|
+
|
|
29
|
+
const event = await uidu.events.get({ id });
|
|
30
|
+
|
|
31
|
+
if (!event) {
|
|
32
|
+
notFound();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const formattedStart = event.startTime ? formatStartTime(event.startTime) : null;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<main className="mx-auto max-w-3xl px-6 py-12">
|
|
39
|
+
<article>
|
|
40
|
+
<header className="mb-8">
|
|
41
|
+
<h1 className="text-4xl font-bold tracking-tight">{event.name}</h1>
|
|
42
|
+
{formattedStart && (
|
|
43
|
+
<p className="mt-2 text-base text-muted-foreground">
|
|
44
|
+
<time dateTime={new Date(event.startTime).toISOString()}>
|
|
45
|
+
{formattedStart}
|
|
46
|
+
</time>
|
|
47
|
+
</p>
|
|
48
|
+
)}
|
|
49
|
+
</header>
|
|
50
|
+
|
|
51
|
+
{event.body && (
|
|
52
|
+
<div
|
|
53
|
+
className="prose prose-neutral max-w-none"
|
|
54
|
+
dangerouslySetInnerHTML={{ __html: event.body }}
|
|
55
|
+
/>
|
|
56
|
+
)}
|
|
57
|
+
</article>
|
|
58
|
+
</main>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { listStories } from '@uidu/client';
|
|
3
|
+
import { uidu } from '@/lib/uidu';
|
|
4
|
+
|
|
5
|
+
export default async function StoriesPage() {
|
|
6
|
+
const stories = await listStories(uidu, { first: 50 });
|
|
7
|
+
|
|
8
|
+
const sorted = [...stories].sort((a, b) =>
|
|
9
|
+
((b.publishedAt as string) ?? '').localeCompare(
|
|
10
|
+
(a.publishedAt as string) ?? '',
|
|
11
|
+
),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<main>
|
|
16
|
+
<h1>Stories</h1>
|
|
17
|
+
<ul>
|
|
18
|
+
{sorted.map((story) => (
|
|
19
|
+
<li key={story.id}>
|
|
20
|
+
<Link href={`/story/${story.id}`}>
|
|
21
|
+
<h2>{story.name}</h2>
|
|
22
|
+
{story.publishedAt && (
|
|
23
|
+
<time dateTime={story.publishedAt as string}>
|
|
24
|
+
{new Date(story.publishedAt as string).toLocaleDateString()}
|
|
25
|
+
</time>
|
|
26
|
+
)}
|
|
27
|
+
</Link>
|
|
28
|
+
</li>
|
|
29
|
+
))}
|
|
30
|
+
</ul>
|
|
31
|
+
</main>
|
|
32
|
+
);
|
|
33
|
+
}
|
package/skills/uidu-workspace/iteration-2/eval-1-stories-listing/without_skill/outputs/page.tsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { uidu } from '@/lib/uidu';
|
|
3
|
+
|
|
4
|
+
export default async function StoriesPage() {
|
|
5
|
+
const stories = await uidu.stories.list({
|
|
6
|
+
orderBy: { publishedAt: 'desc' },
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<main className="mx-auto max-w-3xl px-4 py-12">
|
|
11
|
+
<h1 className="mb-8 text-3xl font-bold">Stories</h1>
|
|
12
|
+
<ul className="space-y-4">
|
|
13
|
+
{stories.map((story) => (
|
|
14
|
+
<li key={story.id} className="border-b pb-4">
|
|
15
|
+
<Link
|
|
16
|
+
href={`/story/${story.id}`}
|
|
17
|
+
className="block hover:underline"
|
|
18
|
+
>
|
|
19
|
+
<h2 className="text-xl font-semibold">{story.name}</h2>
|
|
20
|
+
<time
|
|
21
|
+
dateTime={story.publishedAt}
|
|
22
|
+
className="text-sm text-muted-foreground"
|
|
23
|
+
>
|
|
24
|
+
{new Date(story.publishedAt).toLocaleDateString(undefined, {
|
|
25
|
+
year: 'numeric',
|
|
26
|
+
month: 'long',
|
|
27
|
+
day: 'numeric',
|
|
28
|
+
})}
|
|
29
|
+
</time>
|
|
30
|
+
</Link>
|
|
31
|
+
</li>
|
|
32
|
+
))}
|
|
33
|
+
</ul>
|
|
34
|
+
</main>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Why your donation amounts are 100x too large
|
|
2
|
+
|
|
3
|
+
## Diagnosis
|
|
4
|
+
|
|
5
|
+
Donation campaign amounts in uidu are stored and returned **in cents**, not in the major currency unit. A campaign with a €50,000 goal is returned by the API as `campaign.goal === 5000000`. When you render `€{campaign.goal}` directly, you display the raw cents value, which is why you see `€5000000`.
|
|
6
|
+
|
|
7
|
+
This convention applies to every monetary field on a donation campaign:
|
|
8
|
+
|
|
9
|
+
| Field | Returned as | €50,000 looks like |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `campaign.goal` | integer cents | `5000000` |
|
|
12
|
+
| `campaign.donationsAmount` | integer cents | `5000000` |
|
|
13
|
+
|
|
14
|
+
`campaign.donationsCount` is a plain integer (number of donations) and does **not** need conversion.
|
|
15
|
+
|
|
16
|
+
## The fix
|
|
17
|
+
|
|
18
|
+
Divide by 100 before rendering. The canonical pattern from the SDK guidance:
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
const raised = (campaign.donationsAmount ?? 0) / 100;
|
|
22
|
+
const goal = (campaign.goal ?? 0) / 100;
|
|
23
|
+
const progress = goal > 0 ? Math.min(100, (raised / goal) * 100) : 0;
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
For a properly formatted display, use `Intl.NumberFormat` instead of a hardcoded `€` so locale and grouping separators are correct:
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
const fmt = new Intl.NumberFormat('it-IT', {
|
|
30
|
+
style: 'currency',
|
|
31
|
+
currency: 'EUR',
|
|
32
|
+
maximumFractionDigits: 0,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
<p>{fmt.format(goal)} goal</p>
|
|
36
|
+
<p>{fmt.format(raised)} raised ({progress.toFixed(0)}%)</p>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Replace your current JSX:
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
// Before — shows €5000000
|
|
43
|
+
€{campaign.goal}
|
|
44
|
+
|
|
45
|
+
// After — shows €50,000
|
|
46
|
+
{fmt.format((campaign.goal ?? 0) / 100)}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Why cents?
|
|
50
|
+
|
|
51
|
+
Storing money as integer cents avoids floating-point rounding errors (e.g. `0.1 + 0.2 !== 0.3` in JavaScript). It is the standard convention for payment-adjacent APIs (Stripe, PayPal, etc.), and uidu follows it for consistency. Apply the `/100` conversion at the **display boundary only** — keep the integer values in your data layer and any arithmetic (progress calculations, comparisons, sorting).
|