Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions examples/invoices-structured-output/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
40 changes: 40 additions & 0 deletions examples/invoices-structured-output/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Invoice Structured Output Example

A Next.js app demonstrating `@openuidev/structured-output` — the standalone OpenUI Lang library for schema-driven structured output from LLMs.

## What it does

1. **Select an invoice format** — Standard, Freelance, or International
2. **Describe the invoice** — or use the provided sample prompt
3. **Watch the LLM stream** — the raw OpenUI Lang appears on the left as tokens arrive
4. **See parsed JSON** — the streaming parser progressively builds the validated JSON on the right

The backend generates a system prompt from the Zod schema using `schema.prompt()`, sends it to the LLM, and streams back the raw OpenUI Lang. The frontend uses `schema.streamingParser()` to parse and validate the output in real time.

## Setup

```bash
# From the monorepo root
pnpm install

# Add your OpenRouter API key
cp examples/invoices-structured-output/.env.example examples/invoices-structured-output/.env.local
# Edit .env.local with your key

# Run the dev server
pnpm --filter invoices-structured-output dev
```

## Invoice Formats

| Format | Description |
| --- | --- |
| **Standard** | Simple business invoice with line items, tax rate, and totals |
| **Freelance** | Consulting invoice with hourly time entries and project details |
| **International** | Multi-currency invoice with addresses, tax breakdown, and shipping |

## Key files

- `src/app/schemas.ts` — Invoice type definitions using `defineType` and `createSchema`
- `src/app/api/generate/route.ts` — API route that generates the prompt and streams LLM output
- `src/app/page.tsx` — Frontend with format picker, streaming display, and parsed JSON view
18 changes: 18 additions & 0 deletions examples/invoices-structured-output/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";

const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);

export default eslintConfig;
8 changes: 8 additions & 0 deletions examples/invoices-structured-output/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
transpilePackages: ["@openuidev/structured-output"],
serverExternalPackages: ["openai"],
};

export default nextConfig;
29 changes: 29 additions & 0 deletions examples/invoices-structured-output/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "invoices-structured-output",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@openuidev/structured-output": "workspace:^",
"next": "16.1.6",
"openai": "^6.27.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"zod": "^4.3.6"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.6",
"tailwindcss": "^4",
"typescript": "^5"
}
}
7 changes: 7 additions & 0 deletions examples/invoices-structured-output/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};

export default config;
82 changes: 82 additions & 0 deletions examples/invoices-structured-output/src/app/api/generate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { NextRequest } from "next/server";
import OpenAI from "openai";
import { invoiceSchemaMap } from "../../schemas";

let _client: OpenAI | null = null;
function getClient() {
if (!_client) {
_client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
}
return _client;
}

const BUSINESS_PREAMBLE = `You are an expert accounting assistant that generates structured invoice data.
Given a description of an invoice, extract or generate all relevant fields with realistic data.
Use plausible business names, dates, amounts, and tax calculations.
All monetary amounts should be numbers (not strings). Dates should be in YYYY-MM-DD format.
Ensure line item amounts equal quantity × unitPrice. Ensure totals are arithmetically correct.`;

export async function POST(req: NextRequest) {
const { userPrompt } = (await req.json()) as {
userPrompt: string;
};

const systemPrompt = invoiceSchemaMap.prompt({
preamble: BUSINESS_PREAMBLE,
additionalRules: [
"All dates must be YYYY-MM-DD format",
"Monetary amounts must be numbers with up to 2 decimal places",
"NEVER use arithmetic expressions like 8*125 or 10+20 — always write the pre-computed numeric result (e.g. 1000, 30)",
"Ensure mathematical consistency: subtotal = sum of item amounts, total = subtotal + tax + shipping",
],
});

console.log(systemPrompt);

const stream = await getClient().chat.completions.create({
model: "gpt-5",
stream: true,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },
],
reasoning_effort: "minimal",
});

const encoder = new TextEncoder();

const readable = new ReadableStream({
async start(controller) {
const send = (type: string, content: string) =>
controller.enqueue(encoder.encode(JSON.stringify({ type, content }) + "\n"));

try {
for await (const chunk of stream) {
const delta = chunk.choices?.[0]?.delta as Record<string, unknown> | undefined;
if (!delta) continue;

const thinking =
(delta.reasoning as string | undefined) ??
(delta.reasoning_content as string | undefined);
if (thinking) send("thinking", thinking);

const content = delta.content as string | undefined;
if (content) send("content", content);
}
} catch (err) {
const message = err instanceof Error ? err.message : "Stream error";
send("error", message);
} finally {
controller.close();
}
},
});

return new Response(readable, {
headers: {
"Content-Type": "application/x-ndjson; charset=utf-8",
"Transfer-Encoding": "chunked",
"Cache-Control": "no-cache",
},
});
}
Binary file not shown.
5 changes: 5 additions & 0 deletions examples/invoices-structured-output/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import "tailwindcss";

html {
font-size: 150%;
}
19 changes: 19 additions & 0 deletions examples/invoices-structured-output/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
title: "Invoice Structured Output",
description: "Generate structured invoice data using OpenUI Lang and streaming LLM output",
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className="dark">
<body className="antialiased">{children}</body>
</html>
);
}
Loading
Loading