Skip to content

Commit 84162e1

Browse files
committed
feat: Add example of the parser
1 parent 8305fec commit 84162e1

6 files changed

Lines changed: 301 additions & 8 deletions

File tree

src/app/globals.css

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,32 @@ html {
3131
animation: flash-update 0.6s ease-out;
3232
}
3333

34+
.weather-card-enter {
35+
animation: weather-card-enter 320ms ease-out;
36+
}
37+
38+
.weather-card-fallback {
39+
animation: weather-card-fallback-pulse 1.4s ease-in-out infinite;
40+
}
41+
42+
.weather-card-fallback-block {
43+
animation: weather-card-fallback-block 1.2s ease-in-out infinite;
44+
}
45+
46+
.weather-card-fallback::after {
47+
content: "";
48+
position: absolute;
49+
inset: 0;
50+
transform: translateX(-100%);
51+
background: linear-gradient(
52+
90deg,
53+
rgba(255, 255, 255, 0) 0%,
54+
rgba(255, 255, 255, 0.3) 45%,
55+
rgba(255, 255, 255, 0) 100%
56+
);
57+
animation: weather-card-shimmer 1.5s ease-in-out infinite;
58+
}
59+
3460
@keyframes flash-create {
3561
0% {
3662
box-shadow: 0 0 0 0 rgba(45, 212, 191, 0.65);
@@ -59,3 +85,40 @@ html {
5985
text-shadow: none;
6086
}
6187
}
88+
89+
@keyframes weather-card-enter {
90+
from {
91+
opacity: 0;
92+
transform: translateY(2px);
93+
}
94+
to {
95+
opacity: 1;
96+
transform: translateY(0);
97+
}
98+
}
99+
100+
@keyframes weather-card-fallback-pulse {
101+
0%,
102+
100% {
103+
opacity: 0.8;
104+
}
105+
50% {
106+
opacity: 1;
107+
}
108+
}
109+
110+
@keyframes weather-card-fallback-block {
111+
0%,
112+
100% {
113+
opacity: 0.6;
114+
}
115+
50% {
116+
opacity: 0.95;
117+
}
118+
}
119+
120+
@keyframes weather-card-shimmer {
121+
100% {
122+
transform: translateX(100%);
123+
}
124+
}

src/app/page.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ export default function CopilotKitPage() {
1515
value: s.toJsonSchema(chatKit.schema),
1616
});
1717

18+
console.log(s.toJsonSchema(chatKit.schema));
19+
20+
// useAgentContext
21+
1822
return (
1923
<main
2024
className="relative min-h-screen w-screen bg-slate-950 text-slate-100"
@@ -38,6 +42,12 @@ export default function CopilotKitPage() {
3842
>
3943
Parser Demo
4044
</a>
45+
<a
46+
href="/ui-renderer"
47+
className="absolute right-6 top-16 z-10 rounded-full border border-emerald-500/40 bg-emerald-500/10 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.2em] text-emerald-200 transition hover:border-emerald-400 hover:text-emerald-100"
48+
>
49+
UI Renderer
50+
</a>
4151
<CopilotChat
4252
disableSystemMessage={true}
4353
RenderMessage={CustomMessageRenderer}

src/app/parser/page.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @next/next/no-html-link-for-pages */
12
"use client";
23

34
import { useEffect, useMemo, useRef, useState } from "react";
@@ -30,7 +31,7 @@ const schema = s.streaming.object("demo-json", {
3031
title: s.streaming.string("Title"),
3132
version: s.number("Version"),
3233
status: s.string("Status"),
33-
features: s.streaming.array("Features", s.streaming.string("Feature")),
34+
features: s.streaming.array("Features", s.string("Feature")),
3435
theme: s.object("Theme", {
3536
primary: s.string("Primary color"),
3637
secondary: s.string("Secondary color"),
@@ -83,9 +84,9 @@ function getNodeValueSignature(node: JsonAstNode) {
8384
node.resolvedValue ?? [],
8485
)}`;
8586
case "object":
86-
return `${node.keys.join(",")}|${node.children.join(",")}|${JSON.stringify(
87-
node.resolvedValue ?? {},
88-
)}`;
87+
return `${node.keys.join(",")}|${node.children.join(
88+
",",
89+
)}|${JSON.stringify(node.resolvedValue ?? {})}`;
8990
default:
9091
return "";
9192
}
@@ -223,6 +224,7 @@ function useNodeFlashes(parserState: ParserState) {
223224

224225
prevSignatures.current = nextSignatures;
225226

227+
// eslint-disable-next-line react-hooks/set-state-in-effect
226228
setFlashes((current) => {
227229
const next: Record<number, FlashKind> = {};
228230
Object.entries(current).forEach(([id, kind]) => {
@@ -379,6 +381,12 @@ export default function ParserVisualizerPage() {
379381
>
380382
Chat Demo
381383
</a>
384+
<a
385+
href="/ui-renderer"
386+
className="rounded-full border border-emerald-500/40 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.2em] text-emerald-200 transition hover:border-emerald-400 hover:text-emerald-100"
387+
>
388+
UI Renderer
389+
</a>
382390
<span>Characters: {cursor}</span>
383391
</div>
384392
</header>

src/app/ui-renderer/page.tsx

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/* eslint-disable @next/next/no-html-link-for-pages */
2+
"use client";
3+
4+
import { useMemo, useState } from "react";
5+
import { useJsonParser } from "@hashbrownai/react";
6+
import { useChatKit } from "@/components/chat/chat-kit";
7+
8+
const jsonDocument = `{
9+
"ui": [
10+
{
11+
"weather": {
12+
"props":{
13+
"themeColor": "blue",
14+
"temperature": 29.7,
15+
"humidity": 70,
16+
"windSpeed": 12.7,
17+
"feelsLike": 19.2,
18+
"location": "Huntsville, Alabama, United States"
19+
}
20+
}
21+
},
22+
{
23+
"p": {
24+
"props": {},
25+
"children": "Huntsville, AL is cold right now: 29.7°F, feeling closer to 19.2°F with a brisk 12.7 mph wind. Humidity is 70%, so the air may feel a bit damp on top of the chill."}},{"p":{"props":{},"children":"If you're heading out, plan on a warm coat, gloves, and something to block the wind."
26+
}
27+
}
28+
]
29+
}`;
30+
31+
export default function UiRendererPage() {
32+
const [cursor, setCursor] = useState(jsonDocument.length);
33+
const jsonSlice = jsonDocument.slice(0, cursor);
34+
const kit = useChatKit();
35+
const { parserState, value } = useJsonParser(jsonSlice, kit.schema);
36+
37+
const ticks = useMemo(() => Array.from({ length: jsonDocument.length }), []);
38+
39+
return (
40+
<main className="flex h-screen w-screen flex-col bg-slate-950 text-slate-100">
41+
<header className="flex items-center justify-between border-b border-slate-800 px-6 py-4">
42+
<div>
43+
<div className="text-xs uppercase tracking-[0.35em] text-slate-400">
44+
Streaming JSON Renderer
45+
</div>
46+
<h1 className="text-2xl font-semibold text-slate-100">
47+
UI Renderer Playground
48+
</h1>
49+
</div>
50+
<div className="flex items-center gap-4 text-xs text-slate-400">
51+
<a
52+
href="/"
53+
className="rounded-full border border-slate-800 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.2em] text-slate-300 transition hover:border-slate-600 hover:text-white"
54+
>
55+
Chat Demo
56+
</a>
57+
<a
58+
href="/parser"
59+
className="rounded-full border border-slate-800 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.2em] text-slate-300 transition hover:border-slate-600 hover:text-white"
60+
>
61+
Parser Demo
62+
</a>
63+
<span>Characters: {cursor}</span>
64+
</div>
65+
</header>
66+
67+
<div className="grid min-h-0 flex-1 grid-cols-1 md:grid-cols-[minmax(0,1fr)_720px]">
68+
<section className="flex min-h-0 flex-col border-b border-slate-800 bg-slate-900/40 md:border-b-0 md:border-r">
69+
<div className="flex min-h-0 flex-1 flex-col">
70+
<div className="border-b border-slate-800 px-6 py-3 text-xs uppercase tracking-[0.3em] text-slate-400">
71+
JSON Document
72+
</div>
73+
<div className="min-h-0 flex-1 overflow-auto px-6 py-4">
74+
<pre className="whitespace-pre-wrap text-sm leading-6">
75+
<span className="text-emerald-200">
76+
{jsonDocument.slice(0, cursor)}
77+
</span>
78+
<span className="text-slate-600">
79+
{jsonDocument.slice(cursor)}
80+
</span>
81+
</pre>
82+
</div>
83+
</div>
84+
<div className="flex min-h-0 flex-[0.6] flex-col border-t border-slate-800">
85+
<div className="border-b border-slate-800 px-6 py-3 text-xs uppercase tracking-[0.3em] text-slate-400">
86+
Resolved Value
87+
</div>
88+
<div className="min-h-0 flex-1 overflow-auto px-6 py-4">
89+
<pre className="whitespace-pre-wrap text-sm leading-6 text-slate-200">
90+
{value ? JSON.stringify(value, null, 2) : "—"}
91+
</pre>
92+
</div>
93+
</div>
94+
</section>
95+
96+
<section className="flex min-h-0 flex-col bg-slate-950">
97+
<div className="border-b border-slate-800 px-6 py-3 text-xs uppercase tracking-[0.3em] text-slate-400">
98+
UI Renderer
99+
</div>
100+
<div className="min-h-0 flex-1 overflow-auto px-6 py-6">
101+
{value ? (
102+
<div className="rounded-2xl border border-emerald-500/20 bg-emerald-500/10 px-5 py-4 text-emerald-50 shadow-lg shadow-emerald-500/10">
103+
<div className="text-[10px] font-semibold uppercase tracking-[0.25em] text-emerald-200/80">
104+
Rendered UI
105+
</div>
106+
<div className="mt-3 space-y-3 text-[15px] leading-6 text-white [&_*]:text-white">
107+
{kit.render(value)}
108+
</div>
109+
</div>
110+
) : (
111+
<div className="flex h-full items-center justify-center text-sm text-slate-400">
112+
Start scrubbing to build the UI.
113+
</div>
114+
)}
115+
{parserState.error ? (
116+
<div className="mt-4 rounded-xl border border-rose-500/40 bg-rose-500/10 px-4 py-3 text-xs text-rose-200">
117+
{parserState.error.message} (line {parserState.error.line}, col
118+
{parserState.error.column})
119+
</div>
120+
) : null}
121+
</div>
122+
</section>
123+
</div>
124+
125+
<div className="border-t border-slate-800 bg-slate-950 px-6 py-4">
126+
<div className="mb-3 text-xs uppercase tracking-[0.3em] text-slate-400">
127+
Slider
128+
</div>
129+
<input
130+
type="range"
131+
min={0}
132+
max={jsonDocument.length}
133+
value={cursor}
134+
onChange={(event) => setCursor(Number(event.target.value))}
135+
className="h-2 w-full cursor-pointer accent-emerald-400"
136+
/>
137+
<div
138+
className="mt-3 grid h-3 w-full items-end gap-[1px]"
139+
style={{
140+
gridTemplateColumns: `repeat(${ticks.length}, minmax(0, 1fr))`,
141+
}}
142+
>
143+
{ticks.map((_, index) => (
144+
<span
145+
key={index}
146+
className={`h-full rounded-sm ${
147+
index < cursor ? "bg-emerald-500/80" : "bg-slate-700/60"
148+
}`}
149+
/>
150+
))}
151+
</div>
152+
</div>
153+
</main>
154+
);
155+
}

src/components/chat/chat-kit.tsx

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,56 @@
1-
import { s } from "@hashbrownai/core";
1+
import { s, prompt } from "@hashbrownai/core";
22
import { exposeComponent, useUiKit } from "@hashbrownai/react";
33
import { Paragraph } from "./paragraph";
44
import { ListItem } from "./list-item";
55
import { OrderedList } from "./ordered-list";
66
import { UnorderedList } from "./unordered-list";
77
import { WeatherCard } from "../weather";
88

9+
function WeatherCardFallback() {
10+
return (
11+
<div className="mt-6 mb-4 w-full max-w-md rounded-xl bg-sky-400/30 shadow-xl">
12+
<div className="relative h-full w-full overflow-hidden rounded-xl bg-white/20 p-4 weather-card-fallback">
13+
<div className="flex items-center justify-between">
14+
<div className="space-y-2">
15+
<div className="h-7 w-40 rounded bg-white/40 weather-card-fallback-block" />
16+
<div className="h-5 w-28 rounded bg-white/30 weather-card-fallback-block" />
17+
</div>
18+
<div className="h-14 w-14 rounded-full bg-white/30 weather-card-fallback-block" />
19+
</div>
20+
21+
<div className="mt-4 flex items-end justify-between">
22+
<div className="h-10 w-16 rounded bg-white/40 weather-card-fallback-block" />
23+
<div className="h-5 w-20 rounded bg-white/30 weather-card-fallback-block" />
24+
</div>
25+
26+
<div className="mt-4 border-t border-white/40 pt-4">
27+
<div className="grid grid-cols-3 gap-2">
28+
<div className="space-y-2">
29+
<div className="mx-auto h-3 w-12 rounded bg-white/25 weather-card-fallback-block" />
30+
<div className="mx-auto h-5 w-10 rounded bg-white/40 weather-card-fallback-block" />
31+
</div>
32+
<div className="space-y-2">
33+
<div className="mx-auto h-3 w-10 rounded bg-white/25 weather-card-fallback-block" />
34+
<div className="mx-auto h-5 w-10 rounded bg-white/40 weather-card-fallback-block" />
35+
</div>
36+
<div className="space-y-2">
37+
<div className="mx-auto h-3 w-12 rounded bg-white/25 weather-card-fallback-block" />
38+
<div className="mx-auto h-5 w-10 rounded bg-white/40 weather-card-fallback-block" />
39+
</div>
40+
</div>
41+
</div>
42+
</div>
43+
</div>
44+
);
45+
}
46+
947
export function useChatKit() {
1048
return useUiKit({
49+
examples: prompt`
50+
<ui>
51+
<p>with some text</p>
52+
</ui>
53+
`,
1154
components: [
1255
exposeComponent(Paragraph, {
1356
name: "p",
@@ -32,6 +75,10 @@ export function useChatKit() {
3275
exposeComponent(WeatherCard, {
3376
name: "weather",
3477
description: "Shows the weather for a given location",
78+
fallback: () => <WeatherCardFallback />,
79+
// fallback: (...args: any[]) => (
80+
// <pre>{JSON.stringify(args, null, 2)}</pre>
81+
// ),
3582
props: {
3683
themeColor: s.string("The theme to use for the weather card"),
3784
location: s.streaming.string("The location to get the weather for"),

0 commit comments

Comments
 (0)