barokastor's picture
Upload app.py with huggingface_hub
7dc335d verified
# Use official Node.js LTS image
FROM node:18-alpine AS builder
# Create app directory
WORKDIR /app
# Install dependencies
COPY package.json package-lock.json* ./
RUN npm ci --no-audit --no-fund
# Copy source
COPY . .
# Build Next.js app
RUN npm run build
# Production image
FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=7860
# Copy only necessary files
COPY --from=builder /app/package.json ./
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/next.config.js ./next.config.js
EXPOSE 7860
# Start Next.js
CMD ["npm", "start"]
=== package.json ===
{
"name": "nextjs-tailwind-aistudio-viewer",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev -p 7860",
"build": "next build",
"start": "next start -p 7860",
"lint": "eslint ."
},
"dependencies": {
"next": "14.2.10",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"autoprefixer": "10.4.20",
"eslint": "8.57.1",
"eslint-config-next": "14.2.10",
"postcss": "8.4.49",
"tailwindcss": "3.4.14"
}
}
=== next.config.js ===
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
output: 'standalone',
// Hugging Face Spaces often expects the app to listen on 7860 and basePath "/"
env: {
NEXT_PUBLIC_APP_TITLE: "AI Studio App Viewer"
},
images: {
// Allow external images if needed
remotePatterns: [
{ protocol: 'https', hostname: '**' }
]
}
};
module.exports = nextConfig;
=== postcss.config.js ===
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};
=== tailwind.config.js ===
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./pages/**/*.{js,jsx}",
"./components/**/*.{js,jsx}"
],
theme: {
extend: {
colors: {
brand: {
50: "#f2f6ff",
100: "#dae6ff",
200: "#b3c9ff",
300: "#86a8ff",
400: "#5c86ff",
500: "#3f6fff",
600: "#2f56e6",
700: "#2643b3",
800: "#1f368c",
900: "#1b2d73"
}
}
}
},
plugins: []
};
=== components/Header.jsx ===
import Link from "next/link";
import { useState } from "react";
const Header = () => {
const [open, setOpen] = useState(false);
return (
<header className="sticky top-0 z-40 w-full border-b border-gray-200 bg-white/80 backdrop-blur supports-[backdrop-filter]:bg-white/60">
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-3 sm:px-6 lg:px-8">
<div className="flex items-center gap-3">
<div className="h-9 w-9 rounded-md bg-brand-600 text-white grid place-items-center font-semibold">AI</div>
<Link href="/" className="text-lg font-semibold text-gray-900 hover:text-brand-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2 rounded">
{process.env.NEXT_PUBLIC_APP_TITLE || "AI Studio App Viewer"}
</Link>
</div>
<nav className="hidden md:flex items-center gap-6">
<Link href="/" className="text-sm text-gray-700 hover:text-brand-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2 rounded">Home</Link>
<Link href="/#about" className="text-sm text-gray-700 hover:text-brand-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2 rounded">About</Link>
<a
href="https://aistudio.google.com/"
target="_blank"
rel="noreferrer"
className="text-sm text-gray-700 hover:text-brand-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2 rounded"
>
Google AI Studio
</a>
<a
href="https://huggingface.co/spaces/akhaliq/anycoder"
target="_blank"
rel="noreferrer"
className="inline-flex items-center rounded-md bg-brand-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-brand-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2"
>
Built with anycoder
</a>
</nav>
<button
aria-label="Open menu"
aria-expanded={open}
onClick={() => setOpen(!open)}
className="md:hidden inline-flex items-center justify-center rounded-md p-2 text-gray-700 hover:bg-gray-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
>
<svg className="h-6 w-6" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M4 6h16M4 12h16M4 18h16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
</button>
</div>
{open && (
<div className="md:hidden border-t border-gray-200 bg-white">
<div className="mx-auto max-w-7xl px-4 py-3 sm:px-6 lg:px-8 space-y-2">
<Link href="/" className="block rounded px-3 py-2 text-sm text-gray-700 hover:bg-gray-100">Home</Link>
<Link href="/#about" className="block rounded px-3 py-2 text-sm text-gray-700 hover:bg-gray-100">About</Link>
<a
href="https://aistudio.google.com/"
target="_blank"
rel="noreferrer"
className="block rounded px-3 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
Google AI Studio
</a>
<a
href="https://huggingface.co/spaces/akhaliq/anycoder"
target="_blank"
rel="noreferrer"
className="block rounded px-3 py-2 text-sm text-brand-700 hover:bg-brand-50"
>
Built with anycoder
</a>
</div>
</div>
)}
</header>
);
};
export default Header;
=== components/Loader.jsx ===
const Loader = ({ label = "Loading..." }) => {
return (
<div role="status" aria-live="polite" className="flex items-center gap-3 text-gray-700">
<svg className="h-5 w-5 animate-spin text-brand-600" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4A4 4 0 008 12H4z"></path>
</svg>
<span className="text-sm">{label}</span>
</div>
);
};
export default Loader;
=== components/ErrorMessage.jsx ===
const ErrorMessage = ({ message }) => {
if (!message) return null;
return (
<div role="alert" className="rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-800">
{message}
</div>
);
};
export default ErrorMessage;
=== components/URLInput.jsx ===
import { useState } from "react";
const URLInput = ({ defaultUrl, onSubmit }) => {
const [url, setUrl] = useState(defaultUrl || "");
const [valid, setValid] = useState(true);
const validate = (value) => {
try {
const u = new URL(value);
return u.protocol === "http:" || u.protocol === "https:";
} catch {
return false;
}
};
const handleSubmit = (e) => {
e.preventDefault();
const isValid = validate(url);
setValid(isValid);
if (isValid) onSubmit(url);
};
return (
<form onSubmit={handleSubmit} className="w-full">
<label htmlFor="targetUrl" className="block text-sm font-medium text-gray-700">
Enter an AI Studio App URL
</label>
<div className="mt-1 flex gap-2">
<input
id="targetUrl"
name="targetUrl"
type="url"
placeholder="https://aistudio.google.com/apps/..."
value={url}
onChange={(e) => {
setUrl(e.target.value);
if (!valid) setValid(true);
}}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-400 focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500"
aria-invalid={!valid}
aria-describedby={!valid ? "url-error" : undefined}
required
/>
<button
type="submit"
className="inline-flex items-center rounded-md bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
>
Load
</button>
</div>
{!valid && (
<p id="url-error" className="mt-2 text-sm text-red-600">
Please enter a valid http(s) URL.
</p>
)}
</form>
);
};
export default URLInput;
=== pages/_app.js ===
import "@/styles/globals.css";
export default function App({ Component, pageProps }) {
return <Component {...pageProps} />;
}
=== pages/index.js ===
import { useEffect, useState } from "react";
import Header from "@/components/Header";
import URLInput from "@/components/URLInput";
import Loader from "@/components/Loader";
import ErrorMessage from "@/components/ErrorMessage";
const DEFAULT_URL = "https://aistudio.google.com/apps/drive/1-7mSoYm5fqiIMDUfTcSylunPzGO9R_gG?showPreview=true&showAssistant=true&fullscreenApplet=true";
export default function Home() {
const [targetUrl, setTargetUrl] = useState(DEFAULT_URL);
const [embedUrl, setEmbedUrl] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
// Build an embeddable URL via the API to avoid CORS issues and validate input
useEffect(() => {
const run = async () => {
if (!targetUrl) return;
setLoading(true);
setError("");
try {
const res = await fetch(`/api/resolve?url=${encodeURIComponent(targetUrl)}`);
if (!res.ok) {
const text = await res.text();
throw new Error(text || "Failed to resolve URL");
}
const data = await res.json();
setEmbedUrl(data.embedUrl);
} catch (e) {
setError(e.message || "Unexpected error");
setEmbedUrl("");
} finally {
setLoading(false);
}
};
run();
}, [targetUrl]);
return (
<div className="min-h-screen bg-gray-50 text-gray-900">
<Header />
<main className="mx-auto max-w-7xl px-4 pb-16 pt-8 sm:px-6 lg:px-8">
<section className="mx-auto max-w-3xl">
<h1 className="text-2xl font-semibold tracking-tight text-gray-900">Embed Google AI Studio App</h1>
<p className="mt-2 text-sm text-gray-600">
Paste a Google AI Studio App URL to preview it below. We construct a safe embeddable URL and show it in an iframe.
</p>
<div className="mt-6">
<URLInput defaultUrl={DEFAULT_URL} onSubmit={setTargetUrl} />
</div>
<div className="mt-6">
{loading && <Loader label="Resolving and preparing preview..." />}
{!loading && error && <ErrorMessage message={error} />}
</div>
</section>
<section className="mt-8">
<div className="aspect-video w-full overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm">
{embedUrl ? (
<iframe
title="AI Studio App Preview"
src={embedUrl}
className="h-full w-full"
allow="clipboard-read; clipboard-write; geolocation; microphone; camera; encrypted-media; fullscreen"
sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-downloads"
/>
) : (
<div className="grid h-full place-items-center">
<p className="text-sm text-gray-500">Enter a valid AI Studio app URL to preview.</p>
</div>
)}
</div>
<p id="about" className="mt-6 text-sm text-gray-600">
Note: Some AI Studio apps may restrict embedding via X-Frame-Options or Content-Security-Policy.
If embedding is blocked, open the link directly.
</p>
</section>
</main>
<footer className="border-t border-gray-200 bg-white">
<div className="mx-auto max-w-7xl px-4 py-6 text-sm text-gray-600 sm:px-6 lg:px-8">
<div className="flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center">
<p>© {new Date().getFullYear()} AI Studio App Viewer</p>
<div className="flex items-center gap-4">
<a
href="https://aistudio.google.com/"
target="_blank"
rel="noreferrer"
className="text-gray-700 hover:text-brand-600"
>
Google AI Studio
</a>
<a
href="https://huggingface.co/spaces/akhaliq/anycoder"
target="_blank"
rel="noreferrer"
className="text-brand-700 hover:text-brand-900"
>
Built with anycoder
</a>
</div>
</div>
</div>
</footer>
</div>
);
}
=== pages/api/resolve.js ===
export default function handler(req, res) {
try {
const { url } = req.query;
if (!url || typeof url !== "string") {
return res.status(400).json({ error: "Missing url parameter" });
}
let parsed;
try {
parsed = new URL(url);
} catch {
return res.status(400).json({ error: "Invalid URL" });
}
// Only allow google aistudio domain to avoid open redirect / embedding arbitrary sites
const allowedHosts = [
"aistudio.google.com",
"www.aistudio.google.com"
];
if (!allowedHosts.includes(parsed.hostname)) {
return res.status(400).json({ error: "Only aistudio.google.com URLs are allowed" });
}
// We pass through the URL as the iframe src directly. If needed, we could transform params.
// Preserve query parameters.
const embedUrl = parsed.toString();
return res.status(200).json({ embedUrl });
} catch (e) {
return res.status(500).json({ error: "Server error" });
}
}
=== styles/globals.css ===
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Focus styles for better accessibility */
:focus-visible {
outline: none;
}
/* Page base */
html, body, #__next {
height: 100%;
}
body {
@apply bg-gray-50 text-gray-900 antialiased;
}
/* Utility compositions */
.container-page {
@apply mx-auto max-w-7xl px-4 sm:px-6 lg:px-8;
}