Skip to content

Commit 3e9cca7

Browse files
committed
cm
1 parent 139a9a6 commit 3e9cca7

File tree

11 files changed

+457
-1353
lines changed

11 files changed

+457
-1353
lines changed

.changeset/ten-beers-lose.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ovr": patch
3+
---
4+
5+
types: add `command` attribute to `button`, add `closedby` attribute to `dialog`

CONTRIBUTING.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,15 @@ ovr is open source with the MIT license. Feel free to create an issue on GitHub
88
2. The project requires Node and npm for development
99
3. Install dependencies from the root directory `npm install`
1010
4. Start the TypeScript and Vite development servers together by running `npm run dev` from the root directory.
11+
12+
## Conventions
13+
14+
- Casing - try to match built-in methods/casing whenever possible
15+
- Variables including constants are camelCase
16+
- Classes and types are PascalCase
17+
- File names are kebab-case
18+
- Prefer arrow functions over the `function` keyword.
19+
- Web standard APIs are exposed publicly, not Node.
20+
- For example, the `Request` is exposed within `Context` as the web standard request not the Node version.
21+
- Node APIs can be used privately if needed they are supported across Deno, Bun, and Cloudflare.
22+
- For example, `setImmediate` is faster than `setTimeout` and is used internally.

apps/docs/package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,22 @@
1212
"dependencies": {
1313
"@fontsource-variable/geist": "^5.2.6",
1414
"@fontsource-variable/geist-mono": "^5.2.6",
15-
"@openai/agents": "^0.0.11",
1615
"@robino/md": "^4.0.3",
1716
"clsx": "^2.1.1",
18-
"dotenv": "^17.2.0",
17+
"dotenv": "^17.2.1",
1918
"drab": "^7.0.3",
19+
"openai": "^5.10.2",
2020
"ovr": "*",
2121
"uico": "^0.10.3",
22-
"zod": "^4.0.5"
22+
"zod": "^4.0.10"
2323
},
2424
"devDependencies": {
2525
"@domcojs/vercel": "^3.0.0",
26-
"@iconify-json/lucide": "^1.2.57",
26+
"@iconify-json/lucide": "^1.2.58",
2727
"@iconify/tailwind4": "^1.0.6",
2828
"@tailwindcss/vite": "^4.1.11",
2929
"domco": "^4.3.0",
3030
"tailwindcss": "^4.1.11",
31-
"vite": "^7.0.4"
31+
"vite": "^7.0.6"
3232
}
3333
}

apps/docs/src/server/demo/chat/index.md

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,23 @@ title: Chat
33
description: Learn how to build a basic chat interface with ovr.
44
---
55

6-
Here's a chat example with the [OpenAI Agents SDK](https://openai.github.io/openai-agents-js/). The response is streamed _without_ client-side JavaScript using the async generator `Poet` component.
6+
Here's a chat example with the [OpenAI Responses API](https://platform.openai.com/docs/api-reference/responses). The response is streamed _without_ client-side JavaScript using the async generator `Poet` component.
77

88
```tsx
9+
import { OpenAI } from "openai";
10+
11+
const client = new OpenAI();
12+
913
async function* Poet(props: { message: string }) {
10-
const agent = new Agent({
11-
name: "Poet",
14+
const response = await client.responses.create({
15+
input: props.message,
1216
instructions: "You turn messages into poems.",
1317
model: "gpt-4.1-nano",
18+
stream: true,
1419
});
1520

16-
const result = await run(agent, props.message, { stream: true });
17-
18-
for await (const event of result) {
19-
if (
20-
event.type === "raw_model_stream_event" &&
21-
event.data.type === "output_text_delta"
22-
) {
23-
yield event.data.delta;
24-
}
21+
for await (const event of response) {
22+
if (event.type === "response.output_text.delta") yield event.delta;
2523
}
2624
}
2725
```

apps/docs/src/server/demo/chat/index.tsx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,18 @@ import { Chunk, Get, Post } from "ovr";
55
import * as z from "zod";
66

77
async function* Poet(props: { message: string }) {
8-
const { Agent, run } = await import("@openai/agents");
8+
const { OpenAI } = await import("openai");
9+
const client = new OpenAI();
910

10-
const agent = new Agent({
11-
name: "Poet",
11+
const response = await client.responses.create({
12+
input: props.message,
1213
instructions: "You turn messages into poems.",
1314
model: "gpt-4.1-nano",
15+
stream: true,
1416
});
1517

16-
const result = await run(agent, props.message, { stream: true });
17-
18-
for await (const event of result) {
19-
if (
20-
event.type === "raw_model_stream_event" &&
21-
event.data.type === "output_text_delta"
22-
) {
23-
yield event.data.delta;
24-
}
18+
for await (const event of response) {
19+
if (event.type === "response.output_text.delta") yield event.delta;
2520
}
2621
}
2722

apps/docs/src/server/demo/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export * from "@/server/demo/parallel";
55
export * from "@/server/demo/form";
66
export * from "@/server/demo/layout";
77
export * from "@/server/demo/todo";
8+
export * from "@/server/demo/wordle";
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
title: Wordle
3+
description: Create a page and POST request handler with ovr.
4+
---
5+
6+
TODO
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import * as worldContent from "@/server/demo/wordle/index.md";
2+
import { Head } from "@/ui/head";
3+
import { clsx } from "clsx";
4+
import { Context, Get, Post } from "ovr";
5+
6+
const solution = "CRANE";
7+
const length = 5;
8+
const tries = 6;
9+
10+
export const wordle = new Get("/demo/wordle", (c) => {
11+
c.head(<Head {...worldContent.frontmatter} />);
12+
13+
const guesses = getGuesses();
14+
const solved = guesses.includes(solution);
15+
const ended = solved || guesses.length === tries;
16+
17+
return (
18+
<div class="flex flex-col items-center justify-center gap-6">
19+
<Board guesses={guesses} />
20+
21+
{ended && (
22+
<div>
23+
{solved ? (
24+
<span class="text-green-600">You got it in {guesses.length}!</span>
25+
) : (
26+
<span class="text-red-600">
27+
Out of tries! The word was <b>{solution}</b>.
28+
</span>
29+
)}
30+
</div>
31+
)}
32+
33+
{!ended && (
34+
<guess.Form search class="flex gap-2" enctype="multipart/form-data">
35+
<input type="hidden" name="id" value="id1" />
36+
<input type="hidden" name="id" value="id2" />
37+
<input type="file" name="file" />
38+
<input type="text" name="text=" />
39+
<input
40+
type="text"
41+
maxlength={length}
42+
name="guess"
43+
pattern={`[A-Za-z]{${length}}`}
44+
class={clsx("w-24 text-center font-mono tracking-widest uppercase")}
45+
placeholder="GUESS"
46+
aria-label="Enter guess"
47+
/>
48+
<button>Guess</button>
49+
</guess.Form>
50+
)}
51+
</div>
52+
);
53+
});
54+
55+
export const guess = new Post("/demo/guess", async (c) => {
56+
for await (const part of c.data()) {
57+
console.log(part);
58+
}
59+
60+
return <p>Test</p>;
61+
62+
// const guesses = getGuesses();
63+
// const data = await c.req.formData();
64+
// const guess = z.string().toUpperCase().length(5).parse(data.get("guess"));
65+
66+
// if (guesses.length < tries) guesses.push(guess);
67+
68+
// c.redirect(wordle.url({ search: [].map((guess) => ["guess", guess]) }));
69+
});
70+
71+
const getGuesses = () => Context.get().url.searchParams.getAll("guess");
72+
73+
const Board = ({ guesses }: { guesses: string[] }) => {
74+
return (
75+
<div class="grid gap-1">
76+
{Array.from({ length: tries }).map((_, i) => (
77+
<Row
78+
word={guesses[i] ?? ""}
79+
solution={solution}
80+
isCurrent={!guesses[i] && i === guesses.length}
81+
/>
82+
))}
83+
</div>
84+
);
85+
};
86+
87+
const feedback = (guess: string, solution: string) => {
88+
const res: ("g" | "y" | "b")[] = Array(length).fill("b");
89+
const sol = solution.split("");
90+
const flag = Array<boolean>(length).fill(false);
91+
92+
for (let i = 0; i < length; i++) {
93+
if (guess[i] === sol[i]) {
94+
res[i] = "g";
95+
flag[i] = true;
96+
}
97+
}
98+
99+
for (let i = 0; i < length; i++) {
100+
if (res[i] === "g") continue;
101+
102+
const foundLetter = sol.findIndex((ch, j) => !flag[j] && ch === guess[i]);
103+
if (foundLetter !== -1) {
104+
res[i] = "y";
105+
flag[foundLetter] = true;
106+
}
107+
}
108+
109+
return res;
110+
};
111+
112+
const Row = ({
113+
word,
114+
solution,
115+
isCurrent,
116+
}: {
117+
word: string;
118+
solution: string;
119+
isCurrent?: boolean;
120+
}) => {
121+
const cells = Array.from({ length }).map((_, i) => word[i] ?? "");
122+
const colors =
123+
word.length === length && !isCurrent ? feedback(word, solution) : [];
124+
return (
125+
<div class="flex gap-1">
126+
{cells.map((ch, i) => (
127+
<Cell ch={ch} color={colors[i]} isCurrent={isCurrent} />
128+
))}
129+
</div>
130+
);
131+
};
132+
133+
const Cell = ({
134+
ch,
135+
color,
136+
isCurrent,
137+
}: {
138+
ch: string;
139+
color?: "g" | "y" | "b";
140+
isCurrent?: boolean;
141+
}) => {
142+
return (
143+
<div
144+
class={clsx(
145+
"flex h-12 w-12 items-center justify-center rounded border font-mono text-2xl font-bold shadow-sm select-none",
146+
color === "g" && "text-base-50 border-green-600 bg-green-500",
147+
color === "y" && "text-base-950 border-yellow-500 bg-yellow-400",
148+
color === "b" && "border-base-400 bg-base-300 text-base-950",
149+
!color && !isCurrent && "border-base-200 bg-background",
150+
)}
151+
>
152+
{ch}
153+
</div>
154+
);
155+
};

0 commit comments

Comments
 (0)