Skip to content
Draft
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,6 @@ bun.lockb

# LangGraph API
.langgraph_api

# local shadcn reference checkout (not a workspace)
shadcn/
69 changes: 62 additions & 7 deletions apps/agent/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@
import warnings
from typing import Any, List, TypedDict

import uvicorn
from ag_ui_langgraph import add_langgraph_fastapi_endpoint
from copilotkit import CopilotKitMiddleware, CopilotKitState, LangGraphAGUIAgent
from dotenv import load_dotenv
from fastapi import FastAPI
import uvicorn
from langchain.agents import create_agent
from copilotkit import CopilotKitMiddleware, CopilotKitState, LangGraphAGUIAgent
from ag_ui_langgraph import add_langgraph_fastapi_endpoint

from src.bounded_memory_saver import BoundedMemorySaver
from src.middleware import apply_structured_output_schema, normalize_context
from src.patches import apply as apply_patches
from src.bounded_memory_saver import BoundedMemorySaver
from src.search import search_tools

_ = load_dotenv()
Expand All @@ -23,13 +24,20 @@

class AgentState(CopilotKitState):
proverbs: List[str]
design_params: dict[str, Any]


class AgentContext(TypedDict, total=False):
output_schema: dict[str, Any]


agent = create_agent(
model="openai:gpt-5.2",
middleware=[normalize_context, CopilotKitMiddleware(), apply_structured_output_schema],
model="openai:gpt-5.4",
middleware=[
normalize_context,
CopilotKitMiddleware(),
apply_structured_output_schema,
],
context_schema=AgentContext,
tools=[*search_tools],
state_schema=AgentState,
Expand All @@ -39,7 +47,53 @@ class AgentContext(TypedDict, total=False):
"Only wrap UI components into cards. For Markdown, don't wrap it in this. Use rows for "
"side-by-side layouts (2 columns max). Keep it clean and simple.\n"
"When generating large components, reports, dashboards, etc. Make sure the entire thing is in a card. "
"Only use components when necessary. Like for example just showing text you probably don't need to. Use your judgment."
"Only use components when necessary. Like for example just showing text you probably don't need to. Use your judgment.\n\n"
"When the user is on the /create route (the design-system customizer), you can help them tailor their theme. "
"The frontend exposes the current design parameters via readable context and a tool named 'updateDesignSystem'. "
"Valid parameters: base (radix|base), style (vega|nova|maia|lyra|mira|luma|sera), baseColor (neutral|gray|zinc|slate|stone|taupe), "
"theme (same palette as baseColor), chartColor (same palette), radius (default|none|small|medium|large), "
"font (inter|geist|figtree|jetbrains-mono|noto-sans|...), fontHeading (inherit or a font value), "
"iconLibrary (lucide|tabler|phosphor|hugeicons|remixicon), menuAccent (subtle|bold), "
"menuColor (default|inverted|default-translucent|inverted-translucent). "
"When the user asks you to change the look (e.g. 'make it pink with a serif body font and rounded corners'), "
"call updateDesignSystem with the subset of fields you want to change. Only pass fields you are changing.\n\n"
"CRITICAL: call 'randomize' EXACTLY ONCE per user request. After it returns, respond to the user with a short "
"summary of what changed (or what couldn't change because it was locked) — do NOT call randomize again in the "
"same turn, even to retry a locked param. If the user wants to unlock and reshuffle, explain the situation and "
"wait for them to confirm.\n\n"
"When the user asks you to build or create a custom component on /create (e.g. 'create a weather card', "
"'build a pricing card with three tiers', 'make a stat dashboard'), call the 'createCustomComponent' tool. "
"CRITICAL: call 'createCustomComponent' EXACTLY ONCE per user request. After the tool returns successfully, "
"respond with a short text reply — do NOT call the tool again. Panel 03 holds exactly ONE component at a time — "
"each call replaces the previous one. If the 'Current custom component' readable already shows the same title "
"the user is asking for, do not rebuild unless the user explicitly asks for a change. "
"It takes { title, prompt, spec } where spec is { root: Node }. Supported node types: "
"card {title?, description?, children: Node[], footer?: Node[]}, "
"column {gap?: sm|md|lg, children}, "
"row {gap?: sm|md|lg, align?: start|center|end|between, children}, "
"grid {columns: 2|3|4, gap?, children}, "
"heading {text, level?: 1|2|3|4, tone?}, "
"text {text, tone?, size?: xs|sm|base|lg, weight?: normal|medium|semibold|bold}, "
"badge {text, tone?}, "
"icon {name, size?, tone?}, "
"stat {label, value, trend?: up|down|flat|none, trendValue?}, "
"progress {value: 0-100, label?}, "
"separator {orientation?}, "
"image {src (url), alt?, aspect?: square|video|portrait}. "
"Tones: default|muted|primary|secondary|accent|destructive — never use hex colors, always use tones so the "
"component inherits the current theme. The root must be a 'card'. "
"Use sensible placeholder data (e.g. '72°F', 'San Francisco'). Keep the tree compact and readable. "
"Common icon names: sun, moon, cloud, cloud-rain, cloud-snow, wind, thermometer, droplets, map-pin, "
"trending-up, trending-down, star, heart, check, zap, sparkles, clock, calendar, user, users, dollar-sign, "
"shopping-cart, package, activity, bar-chart, line-chart, pie-chart. "
"Example (weather card): { title: 'Weather Card', root: { type: 'card', title: 'San Francisco', "
"description: 'Partly cloudy', children: [ { type: 'row', align: 'between', children: [ "
"{ type: 'stat', label: 'Current', value: '72°F', trend: 'up', trendValue: '+3°' }, "
"{ type: 'icon', name: 'cloud', size: 48, tone: 'primary' } ] }, "
"{ type: 'grid', columns: 3, gap: 'sm', children: [ "
"{ type: 'stat', label: 'Humidity', value: '54%' }, "
"{ type: 'stat', label: 'Wind', value: '8 mph' }, "
"{ type: 'stat', label: 'UV Index', value: '6' } ] } ] } }."
),
)

Expand All @@ -55,6 +109,7 @@ class AgentContext(TypedDict, total=False):
path="/",
)


def main():
"""Run the uvicorn server."""
port = int(os.getenv("PORT", "8123"))
Expand Down
2 changes: 1 addition & 1 deletion apps/runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"dev": "tsx watch server.ts"
},
"dependencies": {
"@copilotkit/runtime": "1.54.0",
"@copilotkit/runtime": "1.56.2",
"@hono/node-server": "^1.13.6",
"hono": "^4.11.4"
},
Expand Down
29 changes: 25 additions & 4 deletions apps/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,57 @@
"preview": "vite preview"
},
"dependencies": {
"@ag-ui/core": "^0.0.47",
"@ag-ui/core": "^0.0.52",
"@base-ui/react": "^1.2.0",
"@copilotkit/react-core": "1.54.0",
"@copilotkit/react-ui": "1.54.0",
"@copilotkit/react-core": "1.56.2",
"@copilotkit/react-ui": "1.56.2",
"@fontsource/figtree": "^5.2.10",
"@fontsource/geist": "^5.2.8",
"@fontsource/inter": "^5.2.8",
"@fontsource/jetbrains-mono": "^5.2.8",
"@fontsource/noto-sans": "^5.2.10",
"@fontsource/playfair-display": "^5.2.8",
"@frenchfryai/core": "0.1.1",
"@frenchfryai/react": "0.1.1",
"@frenchfryai/runtime": "0.1.1",
"@hashbrownai/core": "0.5.0-beta.4",
"@hashbrownai/react": "0.5.0-beta.4",
"@hookform/resolvers": "^5.2.2",
"@hugeicons/core-free-icons": "^4.1.1",
"@hugeicons/react": "^1.1.6",
"@phosphor-icons/react": "^2.1.10",
"@remixicon/react": "^4.9.0",
"@tabler/icons-react": "^3.41.1",
"@tanstack/react-form": "^1.29.0",
"change-case": "^5.4.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"dedent": "^1.7.2",
"embla-carousel-react": "^8.6.0",
"figma-squircle": "^1.1.0",
"input-otp": "^1.4.2",
"jotai": "^2.19.1",
"lucide-react": "^0.575.0",
"motion": "^12.38.0",
"next-themes": "^0.4.6",
"nuqs": "^2.8.9",
"prism-react-renderer": "^2.4.1",
"radix-ui": "^1.4.3",
"react": "^19.2.3",
"react-day-picker": "^9.13.2",
"react-dom": "^19.2.3",
"react-hook-form": "^7.71.2",
"react-qr-code": "^2.0.18",
"react-resizable-panels": "^4.6.5",
"react-router-dom": "^7.14.1",
"recharts": "2.15.4",
"sonner": "^2.0.7",
"streamdown": "^2.1.0",
"swr": "^2.4.1",
"tailwind-merge": "^3.5.0",
"ts-morph": "^28.0.0",
"vaul": "^1.1.2",
"zod": "^4.3.6"
},
Expand All @@ -46,7 +67,7 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^4",
"shadcn": "^3.8.5",
"shadcn": "^4.3.0",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
"typescript": "^5",
Expand Down
38 changes: 12 additions & 26 deletions apps/ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { CopilotKit } from "@copilotkit/react-core";
import { useAgentContext, CopilotChat } from "@copilotkit/react-core/v2";
import { NuqsAdapter } from "nuqs/adapters/react-router/v7";
import { BrowserRouter, Route, Routes } from "react-router-dom";

import { AppHeader } from "@/components/app-header";
import { useChatKit } from "@/components/chat/chat-kit";
import { s } from "@hashbrownai/core";
import { chatTheme } from "@/lib/chat-theme";
import { useInitialSuggestions } from "./lib/suggestions";
import { ChatRoute } from "@/routes/chat";
import { CreateRoute } from "@/routes/create/page";

export function App() {
return (
Expand All @@ -17,26 +15,14 @@ export function App() {
}
showDevConsole={false}
>
<Page />
<BrowserRouter>
<NuqsAdapter>
<Routes>
<Route path="/" element={<ChatRoute />} />
<Route path="/create" element={<CreateRoute />} />
</Routes>
</NuqsAdapter>
</BrowserRouter>
</CopilotKit>
);
}

export function Page() {
const chatKit = useChatKit();
useAgentContext({ description: "output_schema", value: s.toJsonSchema(chatKit.schema) });
useInitialSuggestions();

return <Chat />;
}

function Chat() {
return (
<main className="relative z-10 flex h-dvh w-full flex-col overflow-hidden text-[--foreground]">
<AppHeader title="Shadify" />
<div className="w-full h-[calc(100vh-60px)]">
<CopilotChat {...chatTheme} />
</div>
</main>
);
}
120 changes: 120 additions & 0 deletions apps/ui/src/components/copy-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@

import * as React from "react"
import { IconCheck, IconCopy } from "@tabler/icons-react"

import { trackEvent, type Event } from "@/lib/events"
import { cn } from "@/lib/utils"
import { Button } from "@/registry/new-york-v4/ui/button"

function legacyCopyToClipboard(value: string) {
const textArea = document.createElement("textarea")
textArea.value = value
textArea.setAttribute("readonly", "")
textArea.style.position = "fixed"
textArea.style.opacity = "0"
textArea.style.pointerEvents = "none"

document.body.appendChild(textArea)
textArea.focus()
textArea.select()
textArea.setSelectionRange(0, value.length)

let hasCopied = false
try {
hasCopied = document.execCommand("copy")
} catch {
hasCopied = false
}

document.body.removeChild(textArea)
return hasCopied
}

export async function copyToClipboardWithMeta(value: string, event?: Event) {
if (typeof window === "undefined") {
return false
}

if (!value) {
return false
}

let hasCopied = false

if (navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(value)
hasCopied = true
} catch {
hasCopied = legacyCopyToClipboard(value)
}
} else {
hasCopied = legacyCopyToClipboard(value)
}

if (!hasCopied) {
return false
}

if (event) {
trackEvent(event)
}

return true
}

export function CopyButton({
value,
className,
variant = "ghost",
event,
...props
}: React.ComponentProps<typeof Button> & {
value: string
src?: string
event?: Event["name"]
tooltip?: string
}) {
const [hasCopied, setHasCopied] = React.useState(false)

React.useEffect(() => {
if (hasCopied) {
const timer = setTimeout(() => setHasCopied(false), 2000)
return () => clearTimeout(timer)
}
}, [hasCopied])

return (
<Button
data-slot="copy-button"
data-copied={hasCopied}
size="icon"
variant={variant}
className={cn(
"absolute top-3 right-2 z-10 size-7 bg-code hover:opacity-100 focus-visible:opacity-100",
className
)}
onClick={async () => {
const hasCopied = await copyToClipboardWithMeta(
value,
event
? {
name: event,
properties: {
code: value,
},
}
: undefined
)

if (hasCopied) {
setHasCopied(true)
}
}}
{...props}
>
<span className="sr-only">Copy</span>
{hasCopied ? <IconCheck /> : <IconCopy />}
</Button>
)
}
Loading