Skip to content
Closed
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
399 changes: 394 additions & 5 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@
"remark-gfm": "^4.0.1",
"shadcn": "^4.1.0",
"tailwind-merge": "^3.5.0",
"tw-animate-css": "^1.4.0"
"tw-animate-css": "^1.4.0",
"wagmi": "^3.1.0",
"viem": "^2.43.2",
"@tanstack/react-query": "^5.90.12"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
Expand Down
78 changes: 78 additions & 0 deletions src/app/api/blockrun/[...path]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { NextRequest } from "next/server";

// Transparent proxy: franklin.run/api/blockrun/<path> -> https://blockrun.ai/api/<path>
//
// The /try playground is a browser-wallet client for BlockRun's existing
// x402-paid API (chat / images / videos). Calling blockrun.ai cross-origin
// from the browser would hit CORS and would not expose the 402 "payment-required"
// header, so we forward through this same-origin proxy instead. It passes the
// X-Payment header upstream and relays the payment-required header back to the
// client verbatim — settlement still happens on BlockRun's side, paid to
// BlockRun's configured wallet.

const UPSTREAM = process.env.BLOCKRUN_API_BASE || "https://blockrun.ai/api";

// Headers we forward from the client to BlockRun.
const FORWARD_REQ_HEADERS = ["content-type", "accept", "x-payment", "authorization"];
// Headers we relay from BlockRun back to the client (x402 lives in these).
const FORWARD_RES_HEADERS = [
"content-type",
"payment-required",
"x-payment-required",
"x-payment-response",
"cache-control",
];

async function proxy(req: NextRequest, path: string[]) {
const search = req.nextUrl.search || "";
const target = `${UPSTREAM}/${path.join("/")}${search}`;

const headers = new Headers();
for (const h of FORWARD_REQ_HEADERS) {
const v = req.headers.get(h);
if (v) headers.set(h, v);
}

const init: RequestInit = { method: req.method, headers };
if (req.method !== "GET" && req.method !== "HEAD") {
init.body = await req.text();
}

let upstream: Response;
try {
upstream = await fetch(target, init);
} catch {
return new Response(
JSON.stringify({ error: "Could not reach the Franklin backend. Try again." }),
{ status: 502, headers: { "content-type": "application/json" } },
);
}

const resHeaders = new Headers();
for (const h of FORWARD_RES_HEADERS) {
const v = upstream.headers.get(h);
if (v) resHeaders.set(h, v);
}

return new Response(upstream.body, {
status: upstream.status,
statusText: upstream.statusText,
headers: resHeaders,
});
}

export async function POST(
req: NextRequest,
{ params }: { params: Promise<{ path: string[] }> },
) {
const { path } = await params;
return proxy(req, path);
}

export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ path: string[] }> },
) {
const { path } = await params;
return proxy(req, path);
}
260 changes: 260 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -3186,3 +3186,263 @@ h2.dark-h,
/* Replace ornate-rule gap with a clean section gap */
.light + .light,
.dark-section + .dark-section { border-top: 1px solid var(--border); }

/* =====================================================
/try — in-browser Franklin playground
Warm-minimal, matching the rest of the site.
===================================================== */
.try-main {
min-height: 100vh;
padding: 68px 0 0;
background: var(--bg);
display: flex;
flex-direction: column;
}

.try-chat {
flex: 1;
width: 100%;
max-width: 820px;
margin: 0 auto;
display: flex;
flex-direction: column;
min-height: calc(100vh - 68px);
}

/* Top bar — model picker + wallet */
.try-chat-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 18px 24px;
border-bottom: 1px solid var(--border);
position: sticky;
top: 68px;
background: rgba(255, 255, 255, 0.86);
backdrop-filter: saturate(140%) blur(10px);
-webkit-backdrop-filter: saturate(140%) blur(10px);
z-index: 5;
}
.try-model {
display: flex;
align-items: center;
gap: 10px;
}
.try-model-label {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--fg-subtle);
}
.try-model-select {
font-family: var(--font-sans);
font-size: 14px;
color: var(--fg);
background: var(--bg);
border: 1px solid var(--border-strong);
border-radius: var(--radius);
padding: 7px 10px;
cursor: pointer;
}
.try-model-select:focus-visible { outline: 2px solid var(--gold); }
.try-bar-right { display: flex; align-items: center; gap: 12px; }
.try-reset {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
color: var(--fg-muted);
background: none;
border: none;
cursor: pointer;
}
.try-reset:hover { color: var(--fg); }

/* Wallet chip */
.try-wallet { display: flex; align-items: center; gap: 10px; }
.try-wallet-bal {
font-family: var(--font-mono);
font-size: 13px;
color: var(--gold-dim);
}
.try-wallet-addr {
font-family: var(--font-mono);
font-size: 13px;
color: var(--fg-muted);
}
.try-wallet-disconnect {
display: inline-flex;
padding: 6px;
color: var(--fg-subtle);
background: none;
border: none;
cursor: pointer;
border-radius: var(--radius);
}
.try-wallet-disconnect:hover { color: var(--fg); background: var(--bg-alt); }

/* Message stream */
.try-messages {
flex: 1;
overflow-y: auto;
padding: 32px 24px 24px;
display: flex;
flex-direction: column;
gap: 24px;
}

.try-empty {
margin: auto 0;
max-width: 620px;
padding: 24px 0;
}
.try-empty-eyebrow {
font-family: var(--font-mono);
font-size: 12px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--fg-subtle);
margin-bottom: 16px;
}
.try-empty-title {
font-family: var(--font-serif);
font-size: 40px;
line-height: 1.05;
letter-spacing: -0.015em;
color: var(--fg);
margin-bottom: 16px;
}
.try-empty-title em { font-style: italic; color: var(--gold); }
.try-empty-sub {
font-size: 16px;
line-height: 1.65;
color: var(--fg-muted);
margin-bottom: 28px;
}
.try-suggestions { display: flex; flex-direction: column; gap: 10px; }
.try-suggestion {
text-align: left;
font-size: 14px;
color: var(--fg);
background: var(--bg-alt);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 12px 14px;
cursor: pointer;
transition: border-color 0.15s, background 0.15s;
}
.try-suggestion:hover:not(:disabled) { border-color: var(--gold-line); }
.try-suggestion:disabled { opacity: 0.5; cursor: not-allowed; }

/* Bubbles */
.try-msg { display: flex; flex-direction: column; gap: 6px; }
.try-msg-role {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--fg-subtle);
}
.try-msg-assistant .try-msg-role { color: var(--gold-dim); }
.try-msg-body {
font-size: 15.5px;
line-height: 1.7;
color: var(--fg);
white-space: pre-wrap;
word-break: break-word;
}
.try-msg-user .try-msg-body {
background: var(--bg-alt);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 12px 14px;
}

.try-status {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: var(--fg-muted);
}
.try-error {
font-size: 14px;
color: #9a3412;
background: #fff4ec;
border: 1px solid #f3d4be;
border-radius: var(--radius);
padding: 12px 14px;
}

/* Composer */
.try-input-wrap {
border-top: 1px solid var(--border);
padding: 16px 24px 20px;
background: var(--bg);
position: sticky;
bottom: 0;
}
.try-input-hint {
font-size: 13px;
color: var(--fg-muted);
margin-bottom: 10px;
}
.try-input-row {
display: flex;
align-items: flex-end;
gap: 10px;
border: 1px solid var(--border-strong);
border-radius: 12px;
padding: 8px 8px 8px 14px;
background: var(--bg);
transition: border-color 0.15s;
}
.try-input-row:focus-within { border-color: var(--gold-line); }
.try-input {
flex: 1;
resize: none;
border: none;
outline: none;
background: none;
font-family: var(--font-sans);
font-size: 15.5px;
line-height: 1.5;
color: var(--fg);
max-height: 200px;
padding: 6px 0;
}
.try-send {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 8px;
background: var(--accent);
color: #fff;
border: none;
cursor: pointer;
flex-shrink: 0;
transition: background 0.15s;
}
.try-send:hover:not(:disabled) { background: var(--accent-hover); }
.try-send:disabled { opacity: 0.4; cursor: not-allowed; }
.try-disclaimer {
margin-top: 10px;
font-size: 12px;
line-height: 1.5;
color: var(--fg-subtle);
text-align: center;
}

/* Nav highlight for Try Franklin */
.nav-link-try { color: var(--gold-dim); }

@media (max-width: 640px) {
.try-empty-title { font-size: 30px; }
.try-chat-bar { padding: 14px 16px; }
.try-messages { padding: 24px 16px; }
.try-input-wrap { padding: 12px 16px 16px; }
}
26 changes: 26 additions & 0 deletions src/app/try/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Metadata } from "next";
import { Header } from "@/components/Header";
import { Footer } from "@/components/Footer";
import { WalletProvider } from "@/components/try/WalletProvider";
import { FranklinChat } from "@/components/try/FranklinChat";

export const metadata: Metadata = {
title: "Try Franklin — the AI agent with a wallet, in your browser",
description:
"A web preview of Franklin. Connect a browser wallet and chat with frontier AI models, paying per message in USDC via x402. No account, no subscription.",
alternates: { canonical: "https://franklin.run/try" },
};

export default function TryPage() {
return (
<>
<Header variant="paper" />
<main className="try-main">
<WalletProvider>
<FranklinChat />
</WalletProvider>
</main>
<Footer />
</>
);
}
Loading
Loading