# 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 (
AI
{process.env.NEXT_PUBLIC_APP_TITLE || "AI Studio App Viewer"}
{open && (
Home About Google AI Studio Built with anycoder
)}
); }; export default Header; === components/Loader.jsx === const Loader = ({ label = "Loading..." }) => { return (
{label}
); }; export default Loader; === components/ErrorMessage.jsx === const ErrorMessage = ({ message }) => { if (!message) return null; return (
{message}
); }; 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 (
{ 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 />
{!valid && (

Please enter a valid http(s) URL.

)}
); }; export default URLInput; === pages/_app.js === import "@/styles/globals.css"; export default function App({ Component, pageProps }) { return ; } === 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 (

Embed Google AI Studio App

Paste a Google AI Studio App URL to preview it below. We construct a safe embeddable URL and show it in an iframe.

{loading && } {!loading && error && }
{embedUrl ? (