barokastor commited on
Commit
7dc335d
·
verified ·
1 Parent(s): c9e6598

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +457 -0
app.py ADDED
@@ -0,0 +1,457 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use official Node.js LTS image
2
+ FROM node:18-alpine AS builder
3
+
4
+ # Create app directory
5
+ WORKDIR /app
6
+
7
+ # Install dependencies
8
+ COPY package.json package-lock.json* ./
9
+ RUN npm ci --no-audit --no-fund
10
+
11
+ # Copy source
12
+ COPY . .
13
+
14
+ # Build Next.js app
15
+ RUN npm run build
16
+
17
+ # Production image
18
+ FROM node:18-alpine AS runner
19
+ WORKDIR /app
20
+
21
+ ENV NODE_ENV=production
22
+ ENV NEXT_TELEMETRY_DISABLED=1
23
+ ENV PORT=7860
24
+
25
+ # Copy only necessary files
26
+ COPY --from=builder /app/package.json ./
27
+ COPY --from=builder /app/.next ./.next
28
+ COPY --from=builder /app/public ./public
29
+ COPY --from=builder /app/node_modules ./node_modules
30
+ COPY --from=builder /app/next.config.js ./next.config.js
31
+
32
+ EXPOSE 7860
33
+
34
+ # Start Next.js
35
+ CMD ["npm", "start"]
36
+
37
+ === package.json ===
38
+ {
39
+ "name": "nextjs-tailwind-aistudio-viewer",
40
+ "version": "1.0.0",
41
+ "private": true,
42
+ "scripts": {
43
+ "dev": "next dev -p 7860",
44
+ "build": "next build",
45
+ "start": "next start -p 7860",
46
+ "lint": "eslint ."
47
+ },
48
+ "dependencies": {
49
+ "next": "14.2.10",
50
+ "react": "18.3.1",
51
+ "react-dom": "18.3.1"
52
+ },
53
+ "devDependencies": {
54
+ "autoprefixer": "10.4.20",
55
+ "eslint": "8.57.1",
56
+ "eslint-config-next": "14.2.10",
57
+ "postcss": "8.4.49",
58
+ "tailwindcss": "3.4.14"
59
+ }
60
+ }
61
+
62
+ === next.config.js ===
63
+ /** @type {import('next').NextConfig} */
64
+ const nextConfig = {
65
+ reactStrictMode: true,
66
+ output: 'standalone',
67
+ // Hugging Face Spaces often expects the app to listen on 7860 and basePath "/"
68
+ env: {
69
+ NEXT_PUBLIC_APP_TITLE: "AI Studio App Viewer"
70
+ },
71
+ images: {
72
+ // Allow external images if needed
73
+ remotePatterns: [
74
+ { protocol: 'https', hostname: '**' }
75
+ ]
76
+ }
77
+ };
78
+
79
+ module.exports = nextConfig;
80
+
81
+ === postcss.config.js ===
82
+ module.exports = {
83
+ plugins: {
84
+ tailwindcss: {},
85
+ autoprefixer: {}
86
+ }
87
+ };
88
+
89
+ === tailwind.config.js ===
90
+ /** @type {import('tailwindcss').Config} */
91
+ module.exports = {
92
+ content: [
93
+ "./pages/**/*.{js,jsx}",
94
+ "./components/**/*.{js,jsx}"
95
+ ],
96
+ theme: {
97
+ extend: {
98
+ colors: {
99
+ brand: {
100
+ 50: "#f2f6ff",
101
+ 100: "#dae6ff",
102
+ 200: "#b3c9ff",
103
+ 300: "#86a8ff",
104
+ 400: "#5c86ff",
105
+ 500: "#3f6fff",
106
+ 600: "#2f56e6",
107
+ 700: "#2643b3",
108
+ 800: "#1f368c",
109
+ 900: "#1b2d73"
110
+ }
111
+ }
112
+ }
113
+ },
114
+ plugins: []
115
+ };
116
+
117
+ === components/Header.jsx ===
118
+ import Link from "next/link";
119
+ import { useState } from "react";
120
+
121
+ const Header = () => {
122
+ const [open, setOpen] = useState(false);
123
+
124
+ return (
125
+ <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">
126
+ <div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-3 sm:px-6 lg:px-8">
127
+ <div className="flex items-center gap-3">
128
+ <div className="h-9 w-9 rounded-md bg-brand-600 text-white grid place-items-center font-semibold">AI</div>
129
+ <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">
130
+ {process.env.NEXT_PUBLIC_APP_TITLE || "AI Studio App Viewer"}
131
+ </Link>
132
+ </div>
133
+
134
+ <nav className="hidden md:flex items-center gap-6">
135
+ <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>
136
+ <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>
137
+ <a
138
+ href="https://aistudio.google.com/"
139
+ target="_blank"
140
+ rel="noreferrer"
141
+ 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"
142
+ >
143
+ Google AI Studio
144
+ </a>
145
+ <a
146
+ href="https://huggingface.co/spaces/akhaliq/anycoder"
147
+ target="_blank"
148
+ rel="noreferrer"
149
+ 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"
150
+ >
151
+ Built with anycoder
152
+ </a>
153
+ </nav>
154
+
155
+ <button
156
+ aria-label="Open menu"
157
+ aria-expanded={open}
158
+ onClick={() => setOpen(!open)}
159
+ 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"
160
+ >
161
+ <svg className="h-6 w-6" viewBox="0 0 24 24" fill="none" aria-hidden="true">
162
+ <path d="M4 6h16M4 12h16M4 18h16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
163
+ </svg>
164
+ </button>
165
+ </div>
166
+
167
+ {open && (
168
+ <div className="md:hidden border-t border-gray-200 bg-white">
169
+ <div className="mx-auto max-w-7xl px-4 py-3 sm:px-6 lg:px-8 space-y-2">
170
+ <Link href="/" className="block rounded px-3 py-2 text-sm text-gray-700 hover:bg-gray-100">Home</Link>
171
+ <Link href="/#about" className="block rounded px-3 py-2 text-sm text-gray-700 hover:bg-gray-100">About</Link>
172
+ <a
173
+ href="https://aistudio.google.com/"
174
+ target="_blank"
175
+ rel="noreferrer"
176
+ className="block rounded px-3 py-2 text-sm text-gray-700 hover:bg-gray-100"
177
+ >
178
+ Google AI Studio
179
+ </a>
180
+ <a
181
+ href="https://huggingface.co/spaces/akhaliq/anycoder"
182
+ target="_blank"
183
+ rel="noreferrer"
184
+ className="block rounded px-3 py-2 text-sm text-brand-700 hover:bg-brand-50"
185
+ >
186
+ Built with anycoder
187
+ </a>
188
+ </div>
189
+ </div>
190
+ )}
191
+ </header>
192
+ );
193
+ };
194
+
195
+ export default Header;
196
+
197
+ === components/Loader.jsx ===
198
+ const Loader = ({ label = "Loading..." }) => {
199
+ return (
200
+ <div role="status" aria-live="polite" className="flex items-center gap-3 text-gray-700">
201
+ <svg className="h-5 w-5 animate-spin text-brand-600" viewBox="0 0 24 24">
202
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none"></circle>
203
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4A4 4 0 008 12H4z"></path>
204
+ </svg>
205
+ <span className="text-sm">{label}</span>
206
+ </div>
207
+ );
208
+ };
209
+
210
+ export default Loader;
211
+
212
+ === components/ErrorMessage.jsx ===
213
+ const ErrorMessage = ({ message }) => {
214
+ if (!message) return null;
215
+ return (
216
+ <div role="alert" className="rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-800">
217
+ {message}
218
+ </div>
219
+ );
220
+ };
221
+
222
+ export default ErrorMessage;
223
+
224
+ === components/URLInput.jsx ===
225
+ import { useState } from "react";
226
+
227
+ const URLInput = ({ defaultUrl, onSubmit }) => {
228
+ const [url, setUrl] = useState(defaultUrl || "");
229
+ const [valid, setValid] = useState(true);
230
+
231
+ const validate = (value) => {
232
+ try {
233
+ const u = new URL(value);
234
+ return u.protocol === "http:" || u.protocol === "https:";
235
+ } catch {
236
+ return false;
237
+ }
238
+ };
239
+
240
+ const handleSubmit = (e) => {
241
+ e.preventDefault();
242
+ const isValid = validate(url);
243
+ setValid(isValid);
244
+ if (isValid) onSubmit(url);
245
+ };
246
+
247
+ return (
248
+ <form onSubmit={handleSubmit} className="w-full">
249
+ <label htmlFor="targetUrl" className="block text-sm font-medium text-gray-700">
250
+ Enter an AI Studio App URL
251
+ </label>
252
+ <div className="mt-1 flex gap-2">
253
+ <input
254
+ id="targetUrl"
255
+ name="targetUrl"
256
+ type="url"
257
+ placeholder="https://aistudio.google.com/apps/..."
258
+ value={url}
259
+ onChange={(e) => {
260
+ setUrl(e.target.value);
261
+ if (!valid) setValid(true);
262
+ }}
263
+ 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"
264
+ aria-invalid={!valid}
265
+ aria-describedby={!valid ? "url-error" : undefined}
266
+ required
267
+ />
268
+ <button
269
+ type="submit"
270
+ 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"
271
+ >
272
+ Load
273
+ </button>
274
+ </div>
275
+ {!valid && (
276
+ <p id="url-error" className="mt-2 text-sm text-red-600">
277
+ Please enter a valid http(s) URL.
278
+ </p>
279
+ )}
280
+ </form>
281
+ );
282
+ };
283
+
284
+ export default URLInput;
285
+
286
+ === pages/_app.js ===
287
+ import "@/styles/globals.css";
288
+
289
+ export default function App({ Component, pageProps }) {
290
+ return <Component {...pageProps} />;
291
+ }
292
+
293
+ === pages/index.js ===
294
+ import { useEffect, useState } from "react";
295
+ import Header from "@/components/Header";
296
+ import URLInput from "@/components/URLInput";
297
+ import Loader from "@/components/Loader";
298
+ import ErrorMessage from "@/components/ErrorMessage";
299
+
300
+ const DEFAULT_URL = "https://aistudio.google.com/apps/drive/1-7mSoYm5fqiIMDUfTcSylunPzGO9R_gG?showPreview=true&showAssistant=true&fullscreenApplet=true";
301
+
302
+ export default function Home() {
303
+ const [targetUrl, setTargetUrl] = useState(DEFAULT_URL);
304
+ const [embedUrl, setEmbedUrl] = useState("");
305
+ const [loading, setLoading] = useState(false);
306
+ const [error, setError] = useState("");
307
+
308
+ // Build an embeddable URL via the API to avoid CORS issues and validate input
309
+ useEffect(() => {
310
+ const run = async () => {
311
+ if (!targetUrl) return;
312
+ setLoading(true);
313
+ setError("");
314
+ try {
315
+ const res = await fetch(`/api/resolve?url=${encodeURIComponent(targetUrl)}`);
316
+ if (!res.ok) {
317
+ const text = await res.text();
318
+ throw new Error(text || "Failed to resolve URL");
319
+ }
320
+ const data = await res.json();
321
+ setEmbedUrl(data.embedUrl);
322
+ } catch (e) {
323
+ setError(e.message || "Unexpected error");
324
+ setEmbedUrl("");
325
+ } finally {
326
+ setLoading(false);
327
+ }
328
+ };
329
+ run();
330
+ }, [targetUrl]);
331
+
332
+ return (
333
+ <div className="min-h-screen bg-gray-50 text-gray-900">
334
+ <Header />
335
+ <main className="mx-auto max-w-7xl px-4 pb-16 pt-8 sm:px-6 lg:px-8">
336
+ <section className="mx-auto max-w-3xl">
337
+ <h1 className="text-2xl font-semibold tracking-tight text-gray-900">Embed Google AI Studio App</h1>
338
+ <p className="mt-2 text-sm text-gray-600">
339
+ Paste a Google AI Studio App URL to preview it below. We construct a safe embeddable URL and show it in an iframe.
340
+ </p>
341
+ <div className="mt-6">
342
+ <URLInput defaultUrl={DEFAULT_URL} onSubmit={setTargetUrl} />
343
+ </div>
344
+ <div className="mt-6">
345
+ {loading && <Loader label="Resolving and preparing preview..." />}
346
+ {!loading && error && <ErrorMessage message={error} />}
347
+ </div>
348
+ </section>
349
+
350
+ <section className="mt-8">
351
+ <div className="aspect-video w-full overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm">
352
+ {embedUrl ? (
353
+ <iframe
354
+ title="AI Studio App Preview"
355
+ src={embedUrl}
356
+ className="h-full w-full"
357
+ allow="clipboard-read; clipboard-write; geolocation; microphone; camera; encrypted-media; fullscreen"
358
+ sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-downloads"
359
+ />
360
+ ) : (
361
+ <div className="grid h-full place-items-center">
362
+ <p className="text-sm text-gray-500">Enter a valid AI Studio app URL to preview.</p>
363
+ </div>
364
+ )}
365
+ </div>
366
+ <p id="about" className="mt-6 text-sm text-gray-600">
367
+ Note: Some AI Studio apps may restrict embedding via X-Frame-Options or Content-Security-Policy.
368
+ If embedding is blocked, open the link directly.
369
+ </p>
370
+ </section>
371
+ </main>
372
+ <footer className="border-t border-gray-200 bg-white">
373
+ <div className="mx-auto max-w-7xl px-4 py-6 text-sm text-gray-600 sm:px-6 lg:px-8">
374
+ <div className="flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center">
375
+ <p>© {new Date().getFullYear()} AI Studio App Viewer</p>
376
+ <div className="flex items-center gap-4">
377
+ <a
378
+ href="https://aistudio.google.com/"
379
+ target="_blank"
380
+ rel="noreferrer"
381
+ className="text-gray-700 hover:text-brand-600"
382
+ >
383
+ Google AI Studio
384
+ </a>
385
+ <a
386
+ href="https://huggingface.co/spaces/akhaliq/anycoder"
387
+ target="_blank"
388
+ rel="noreferrer"
389
+ className="text-brand-700 hover:text-brand-900"
390
+ >
391
+ Built with anycoder
392
+ </a>
393
+ </div>
394
+ </div>
395
+ </div>
396
+ </footer>
397
+ </div>
398
+ );
399
+ }
400
+
401
+ === pages/api/resolve.js ===
402
+ export default function handler(req, res) {
403
+ try {
404
+ const { url } = req.query;
405
+ if (!url || typeof url !== "string") {
406
+ return res.status(400).json({ error: "Missing url parameter" });
407
+ }
408
+
409
+ let parsed;
410
+ try {
411
+ parsed = new URL(url);
412
+ } catch {
413
+ return res.status(400).json({ error: "Invalid URL" });
414
+ }
415
+
416
+ // Only allow google aistudio domain to avoid open redirect / embedding arbitrary sites
417
+ const allowedHosts = [
418
+ "aistudio.google.com",
419
+ "www.aistudio.google.com"
420
+ ];
421
+ if (!allowedHosts.includes(parsed.hostname)) {
422
+ return res.status(400).json({ error: "Only aistudio.google.com URLs are allowed" });
423
+ }
424
+
425
+ // We pass through the URL as the iframe src directly. If needed, we could transform params.
426
+ // Preserve query parameters.
427
+ const embedUrl = parsed.toString();
428
+
429
+ return res.status(200).json({ embedUrl });
430
+ } catch (e) {
431
+ return res.status(500).json({ error: "Server error" });
432
+ }
433
+ }
434
+
435
+ === styles/globals.css ===
436
+ @tailwind base;
437
+ @tailwind components;
438
+ @tailwind utilities;
439
+
440
+ /* Focus styles for better accessibility */
441
+ :focus-visible {
442
+ outline: none;
443
+ }
444
+
445
+ /* Page base */
446
+ html, body, #__next {
447
+ height: 100%;
448
+ }
449
+
450
+ body {
451
+ @apply bg-gray-50 text-gray-900 antialiased;
452
+ }
453
+
454
+ /* Utility compositions */
455
+ .container-page {
456
+ @apply mx-auto max-w-7xl px-4 sm:px-6 lg:px-8;
457
+ }