github-actions[bot] commited on
Commit
e6ce630
·
0 Parent(s):

GitHub deploy: 8ac466cec7cb18a3cdc40223ab11ee9b5f5f569b

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .eslintrc.json +3 -0
  2. .github/workflows/deploy-to-hf-spaces.yaml +58 -0
  3. .gitignore +41 -0
  4. Dockerfile +42 -0
  5. README.md +50 -0
  6. app/api/__init__.py +0 -0
  7. app/api/config.py +17 -0
  8. app/api/main.py +24 -0
  9. app/api/models/__init__.py +0 -0
  10. app/api/models/database.py +13 -0
  11. app/api/routes/__init__.py +0 -0
  12. app/api/routes/auth.py +16 -0
  13. app/api/routes/enhance_prompt.py +13 -0
  14. app/api/routes/generate_image.py +159 -0
  15. app/api/services/__init__.py +0 -0
  16. app/api/services/openai_service.py +13 -0
  17. app/favicon.ico +0 -0
  18. app/globals.css +76 -0
  19. app/layout.tsx +28 -0
  20. app/page.tsx +150 -0
  21. app/password/page.tsx +64 -0
  22. components/Gallery/BatchMenu.tsx +67 -0
  23. components/Gallery/ImageBatch.tsx +157 -0
  24. components/Gallery/ImageCard.tsx +82 -0
  25. components/Gallery/ImageGrid.tsx +20 -0
  26. components/Header.tsx +47 -0
  27. components/ImageGenerator/AspectRatioSelector.tsx +34 -0
  28. components/ImageGenerator/GeneratorForm.tsx +157 -0
  29. components/ImageGenerator/ImageCountSlider.tsx +29 -0
  30. components/ImageGenerator/ModelSelector.tsx +33 -0
  31. components/ImageGenerator/PromptInput.tsx +101 -0
  32. components/Layout.tsx +15 -0
  33. components/MainContent.tsx +27 -0
  34. components/Navbar.tsx +72 -0
  35. components/SearchInput.tsx +17 -0
  36. components/Sidebar.tsx +59 -0
  37. components/ThemeToggle.tsx +31 -0
  38. components/User/CreditDisplay.tsx +1 -0
  39. components/ui/button.tsx +56 -0
  40. components/ui/input.tsx +25 -0
  41. components/ui/select.tsx +160 -0
  42. components/ui/slider.tsx +136 -0
  43. components/ui/tooltip.tsx +30 -0
  44. lib/utils.ts +6 -0
  45. middleware.ts +21 -0
  46. next.config.mjs +37 -0
  47. nyxbui.json +17 -0
  48. package-lock.json +0 -0
  49. package.json +50 -0
  50. pnpm-lock.yaml +0 -0
.eslintrc.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "extends": "next/core-web-vitals"
3
+ }
.github/workflows/deploy-to-hf-spaces.yaml ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy to HuggingFace Spaces
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - dev
7
+ - main
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ check-secret:
12
+ runs-on: ubuntu-latest
13
+ outputs:
14
+ token-set: ${{ steps.check-key.outputs.defined }}
15
+ steps:
16
+ - id: check-key
17
+ env:
18
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
19
+ if: "${{ env.HF_TOKEN != '' }}"
20
+ run: echo "defined=true" >> $GITHUB_OUTPUT
21
+
22
+ deploy:
23
+ runs-on: ubuntu-latest
24
+ needs: [check-secret]
25
+ if: needs.check-secret.outputs.token-set == 'true'
26
+ env:
27
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
28
+ steps:
29
+ - name: Checkout repository
30
+ uses: actions/checkout@v4
31
+
32
+ - name: Remove git history
33
+ run: rm -rf .git
34
+
35
+ - name: Prepend YAML front matter to README.md
36
+ run: |
37
+ echo "---" > temp_readme.md
38
+ echo "title: Imagine" >> temp_readme.md
39
+ echo "emoji: 🐳" >> temp_readme.md
40
+ echo "colorFrom: purple" >> temp_readme.md
41
+ echo "colorTo: gray" >> temp_readme.md
42
+ echo "sdk: docker" >> temp_readme.md
43
+ echo "pinned: false" >> temp_readme.md
44
+ echo "app_port: 3000" >> temp_readme.md
45
+ echo "---" >> temp_readme.md
46
+ cat README.md >> temp_readme.md
47
+ mv temp_readme.md README.md
48
+
49
+ - name: Configure git
50
+ run: |
51
+ git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
52
+ git config --global user.name "github-actions[bot]"
53
+ - name: Set up Git and push to Space
54
+ run: |
55
+ git init --initial-branch=main
56
+ git add .
57
+ git commit -m "GitHub deploy: ${{ github.sha }}"
58
+ git push --force https://arcticaurora:${HF_TOKEN}@huggingface.co/spaces/arcticaurora/imagine main
.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.js
7
+ .yarn/install-state.gz
8
+
9
+ # testing
10
+ /coverage
11
+
12
+ # next.js
13
+ /.next/
14
+ /out/
15
+
16
+ # production
17
+ /build
18
+
19
+ # misc
20
+ .DS_Store
21
+ *.pem
22
+
23
+ # debug
24
+ npm-debug.log*
25
+ yarn-debug.log*
26
+ yarn-error.log*
27
+
28
+ # local env files
29
+ .env*.local
30
+
31
+ # vercel
32
+ .vercel
33
+
34
+ # typescript
35
+ *.tsbuildinfo
36
+ next-env.d.ts
37
+ __pycache__/
38
+ .aider*
39
+ # ignore env file
40
+ .env*
41
+ test.py
Dockerfile ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Set up the Python environment
2
+ FROM python:3.12 AS python-base
3
+ WORKDIR /app
4
+
5
+ # Install Python dependencies
6
+ COPY requirements.txt .
7
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
8
+
9
+ # Set up Node.js environment
10
+ FROM node:20 AS node-base
11
+ WORKDIR /app
12
+
13
+ # Copy the frontend code
14
+ COPY package*.json ./
15
+ RUN npm install
16
+ COPY . .
17
+
18
+ # Combine Python and Node.js
19
+ FROM python-base
20
+ WORKDIR /app
21
+
22
+ # Copy Node.js files and set up environment
23
+ COPY --from=node-base /app /app
24
+ COPY --from=node-base /usr/local/bin/node /usr/local/bin/
25
+ COPY --from=node-base /usr/local/lib/node_modules /usr/local/lib/node_modules
26
+ RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \
27
+ ln -s /usr/local/bin/node /usr/bin/node && \
28
+ ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/bin/npm
29
+
30
+ # Copy the backend code
31
+ COPY app ./app
32
+
33
+ # Set up a non-root user
34
+ RUN useradd -m -u 1000 appuser
35
+ RUN chown -R appuser:appuser /app
36
+ USER appuser
37
+
38
+ # Expose the ports the apps run on
39
+ EXPOSE 3000 8000
40
+
41
+ # Start both the FastAPI backend and Next.js frontend in development mode.
42
+ CMD ["sh", "-c", "npm run dev & uvicorn app.api.main:app --host 0.0.0.0 --port 8000"]
README.md ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Imagine
3
+ emoji: 🐳
4
+ colorFrom: purple
5
+ colorTo: gray
6
+ sdk: docker
7
+ pinned: false
8
+ app_port: 3000
9
+ ---
10
+ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
11
+
12
+ ## Getting Started
13
+
14
+ First, install the dependencies:
15
+ ```bash
16
+ npm install
17
+ ```
18
+
19
+ Second, run the development server:
20
+
21
+ ```bash
22
+ npm run dev
23
+ # or
24
+ yarn dev
25
+ # or
26
+ pnpm dev
27
+ # or
28
+ bun dev
29
+ ```
30
+
31
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
32
+
33
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
34
+
35
+ This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
36
+
37
+ ## Learn More
38
+
39
+ To learn more about Next.js, take a look at the following resources:
40
+
41
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
42
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
43
+
44
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
45
+
46
+ ## Deploy on Vercel
47
+
48
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
49
+
50
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
app/api/__init__.py ADDED
File without changes
app/api/config.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ load_dotenv()
5
+
6
+ class Settings:
7
+ DB_USER = os.getenv("DB_USER")
8
+ DB_PASSWORD = os.getenv("DB_PASSWORD")
9
+ DB_HOST = os.getenv("DB_HOST")
10
+ DB_PORT = os.getenv("DB_PORT")
11
+ DB_NAME = os.getenv("DB_NAME")
12
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
13
+ OPENAI_API_BASE = os.getenv("OPENAI_API_BASE")
14
+ NEXT_PUBLIC_API_URL = os.getenv("NEXT_PUBLIC_API_URL")
15
+ APP_PASSWORD = os.getenv("APP_PASSWORD")
16
+
17
+ settings = Settings()
app/api/main.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from .routes import generate_image, enhance_prompt, auth
4
+
5
+ app = FastAPI()
6
+
7
+ # Configure CORS
8
+ app.add_middleware(
9
+ CORSMiddleware,
10
+ allow_origins=["*"], # Adjust this in production
11
+ allow_credentials=True,
12
+ allow_methods=["*"],
13
+ allow_headers=["*"],
14
+ )
15
+
16
+ # Include routers
17
+ app.include_router(generate_image.router, prefix="/api")
18
+ app.include_router(enhance_prompt.router, prefix="/api")
19
+ app.include_router(auth.router, prefix="/api")
20
+
21
+ @app.get("/")
22
+ async def root():
23
+ return {"message": "Welcome to the Imagine API"}
24
+
app/api/models/__init__.py ADDED
File without changes
app/api/models/database.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import psycopg2
2
+ from ..config import settings
3
+
4
+ def get_db_connection():
5
+ conn = psycopg2.connect(
6
+ dbname=settings.DB_NAME,
7
+ user=settings.DB_USER,
8
+ password=settings.DB_PASSWORD,
9
+ host=settings.DB_HOST,
10
+ port=settings.DB_PORT,
11
+ sslmode='require'
12
+ )
13
+ return conn
app/api/routes/__init__.py ADDED
File without changes
app/api/routes/auth.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException
2
+ from pydantic import BaseModel
3
+ from ..config import settings
4
+
5
+ router = APIRouter()
6
+
7
+ class PasswordRequest(BaseModel):
8
+ password: str
9
+
10
+ @router.post("/check-password")
11
+ async def check_password(request: PasswordRequest):
12
+ correct_password = settings.APP_PASSWORD
13
+ if request.password == correct_password:
14
+ return {"success": True}
15
+ else:
16
+ raise HTTPException(status_code=401, detail="Incorrect password")
app/api/routes/enhance_prompt.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+ from pydantic import BaseModel
3
+ from ..services.openai_service import enhance_prompt
4
+
5
+ router = APIRouter()
6
+
7
+ class PromptRequest(BaseModel):
8
+ prompt: str
9
+
10
+ @router.post("/enhance-prompt")
11
+ async def enhance_prompt_route(request: PromptRequest):
12
+ enhanced_prompt = enhance_prompt(request.prompt)
13
+ return {"enhancedPrompt": enhanced_prompt}
app/api/routes/generate_image.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException
2
+ from fastapi.responses import StreamingResponse
3
+ import asyncio
4
+ import json
5
+ from pydantic import BaseModel
6
+ import requests
7
+ from typing import List, Optional
8
+ from ..models.database import get_db_connection
9
+ from ..config import settings
10
+ import psycopg2
11
+ from psycopg2.extras import RealDictCursor
12
+
13
+ router = APIRouter()
14
+
15
+ class ImageGenerationRequest(BaseModel):
16
+ prompt: str
17
+ width: int
18
+ height: int
19
+ model: str
20
+ number_results: int
21
+ placeholderId: Optional[str] = None
22
+
23
+ class Image(BaseModel):
24
+ url: str
25
+
26
+ class Batch(BaseModel):
27
+ id: int
28
+ prompt: str
29
+ width: int
30
+ height: int
31
+ model: str
32
+ images: List[Image]
33
+ status: str
34
+ tempId: Optional[str] = None
35
+ createdAt: Optional[str] = None
36
+
37
+ async def insert_batch(prompt: str, width: int, height: int, model: str, urls: List[str]) -> int:
38
+ conn = get_db_connection()
39
+ try:
40
+ with conn.cursor() as cur:
41
+ cur.execute('BEGIN')
42
+ cur.execute(
43
+ 'INSERT INTO batches (prompt, width, height, model) VALUES (%s, %s, %s, %s) RETURNING id',
44
+ (prompt, width, height, model)
45
+ )
46
+ batch_id = cur.fetchone()[0]
47
+
48
+ for url in urls:
49
+ cur.execute(
50
+ 'INSERT INTO images (batch_id, url) VALUES (%s, %s)',
51
+ (batch_id, url)
52
+ )
53
+
54
+ conn.commit()
55
+ return batch_id
56
+ except Exception as e:
57
+ conn.rollback()
58
+ raise e
59
+ finally:
60
+ conn.close()
61
+
62
+ async def generate_images(request: ImageGenerationRequest):
63
+ try:
64
+ response = requests.post(
65
+ f"{settings.NEXT_PUBLIC_API_URL}/generate-image",
66
+ json=request.dict(exclude={'placeholderId'})
67
+ )
68
+ response.raise_for_status()
69
+ data = response.json()
70
+ image_urls = [image['url'] for image in data['batch']['images']]
71
+ batch_id = await insert_batch(request.prompt, request.width, request.height, request.model, image_urls)
72
+
73
+ return Batch(
74
+ id=batch_id,
75
+ prompt=request.prompt,
76
+ width=request.width,
77
+ height=request.height,
78
+ model=request.model,
79
+ images=[Image(url=url) for url in image_urls],
80
+ status='completed',
81
+ tempId=request.placeholderId
82
+ )
83
+ except Exception as e:
84
+ return Batch(id=request.placeholderId, status='error')
85
+
86
+ async def stream_response(request: ImageGenerationRequest):
87
+ batch = await generate_images(request)
88
+ yield json.dumps({"batch": batch.dict()}).encode() + b'\n'
89
+
90
+ @router.post("/generate-image")
91
+ async def generate_image(request: ImageGenerationRequest):
92
+ return StreamingResponse(stream_response(request), media_type="application/json")
93
+
94
+ @router.get("/generate-image")
95
+ async def get_batches(page: int = 1, limit: int = 10):
96
+ offset = (page - 1) * limit
97
+ conn = get_db_connection()
98
+ try:
99
+ with conn.cursor(cursor_factory=RealDictCursor) as cur:
100
+ cur.execute("""
101
+ SELECT b.id, b.prompt, b.width, b.height, b.model, array_agg(i.url) as image_urls, b.created_at
102
+ FROM batches b
103
+ JOIN images i ON i.batch_id = b.id
104
+ GROUP BY b.id, b.prompt, b.width, b.height, b.model, b.created_at
105
+ ORDER BY b.created_at DESC
106
+ LIMIT %s OFFSET %s
107
+ """, (limit, offset))
108
+ batches = cur.fetchall()
109
+
110
+ cur.execute('SELECT COUNT(*) FROM batches')
111
+ total_count = cur.fetchone()['count']
112
+
113
+ batches = [
114
+ Batch(
115
+ id=row['id'],
116
+ prompt=row['prompt'],
117
+ width=row['width'],
118
+ height=row['height'],
119
+ model=row['model'],
120
+ images=[Image(url=url) for url in row['image_urls']],
121
+ status='completed',
122
+ createdAt=row['created_at'].isoformat() if row['created_at'] else None
123
+ ) for row in batches
124
+ ]
125
+
126
+ has_more = total_count > page * limit
127
+
128
+ return {"batches": batches, "hasMore": has_more}
129
+ except Exception as e:
130
+ raise HTTPException(status_code=500, detail=f"Failed to fetch batches: {str(e)}")
131
+ finally:
132
+ conn.close()
133
+
134
+ @router.delete("/generate-image")
135
+ async def delete_batch(id: str):
136
+ conn = get_db_connection()
137
+ try:
138
+ with conn.cursor() as cur:
139
+ # Check if the id is a UUID (tempId) or a number (actual batch id)
140
+ is_uuid = len(id) == 36 and '-' in id
141
+
142
+ if is_uuid:
143
+ # If it's a UUID, it means the batch hasn't been saved to the database yet
144
+ # So we don't need to delete anything from the database
145
+ return {"success": True}
146
+
147
+ # Delete associated images first
148
+ cur.execute('DELETE FROM images WHERE batch_id = %s', (id,))
149
+
150
+ # Then delete the batch
151
+ cur.execute('DELETE FROM batches WHERE id = %s', (id,))
152
+
153
+ conn.commit()
154
+ return {"success": True}
155
+ except Exception as e:
156
+ conn.rollback()
157
+ raise HTTPException(status_code=500, detail=f"Failed to delete batch: {str(e)}")
158
+ finally:
159
+ conn.close()
app/api/services/__init__.py ADDED
File without changes
app/api/services/openai_service.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from openai import OpenAI
2
+ from ..config import settings
3
+
4
+ def enhance_prompt(prompt: str) -> str:
5
+ client = OpenAI(api_key=settings.OPENAI_API_KEY, base_url=settings.OPENAI_API_BASE)
6
+ completion = client.chat.completions.create(
7
+ model="gemini-1.5-flash-latest",
8
+ messages=[
9
+ {"role": "system", "content": "You are an AI assistant that enhances image generation prompts. Your task is to take a user's prompt and make it more detailed and descriptive, suitable for high-quality image generation."},
10
+ {"role": "user", "content": f"Enhance this image generation prompt: {prompt}. Reply with the enhanced prompt only."}
11
+ ]
12
+ )
13
+ return completion.choices[0].message.content
app/favicon.ico ADDED
app/globals.css ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --background: 0 0% 100%;
8
+ --foreground: 222.2 84% 4.9%;
9
+
10
+ --card: 0 0% 100%;
11
+ --card-foreground: 222.2 84% 4.9%;
12
+
13
+ --popover: 0 0% 100%;
14
+ --popover-foreground: 222.2 84% 4.9%;
15
+
16
+ --primary: 222.2 47.4% 11.2%;
17
+ --primary-foreground: 210 40% 98%;
18
+
19
+ --secondary: 210 40% 96.1%;
20
+ --secondary-foreground: 222.2 47.4% 11.2%;
21
+
22
+ --muted: 210 40% 96.1%;
23
+ --muted-foreground: 215.4 16.3% 46.9%;
24
+
25
+ --accent: 210 40% 96.1%;
26
+ --accent-foreground: 222.2 47.4% 11.2%;
27
+
28
+ --destructive: 0 84.2% 60.2%;
29
+ --destructive-foreground: 210 40% 98%;
30
+
31
+ --border: 214.3 31.8% 91.4%;
32
+ --input: 214.3 31.8% 91.4%;
33
+ --ring: 222.2 84% 4.9%;
34
+
35
+ --radius: 0.5rem;
36
+ }
37
+
38
+ .dark {
39
+ --background: 222.2 84% 4.9%;
40
+ --foreground: 210 40% 98%;
41
+
42
+ --card: 222.2 84% 4.9%;
43
+ --card-foreground: 210 40% 98%;
44
+
45
+ --popover: 222.2 84% 4.9%;
46
+ --popover-foreground: 210 40% 98%;
47
+
48
+ --primary: 210 40% 98%;
49
+ --primary-foreground: 222.2 47.4% 11.2%;
50
+
51
+ --secondary: 217.2 32.6% 17.5%;
52
+ --secondary-foreground: 210 40% 98%;
53
+
54
+ --muted: 217.2 32.6% 17.5%;
55
+ --muted-foreground: 215 20.2% 65.1%;
56
+
57
+ --accent: 217.2 32.6% 17.5%;
58
+ --accent-foreground: 210 40% 98%;
59
+
60
+ --destructive: 0 62.8% 30.6%;
61
+ --destructive-foreground: 210 40% 98%;
62
+
63
+ --border: 217.2 32.6% 17.5%;
64
+ --input: 217.2 32.6% 17.5%;
65
+ --ring: 212.7 26.8% 83.9%;
66
+ }
67
+ }
68
+
69
+ @layer base {
70
+ * {
71
+ @apply border-border;
72
+ }
73
+ body {
74
+ @apply bg-background text-foreground;
75
+ }
76
+ }
app/layout.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import './globals.css'
2
+ import { Inter } from 'next/font/google'
3
+ import Layout from '../components/Layout'
4
+
5
+ const inter = Inter({ subsets: ['latin'],
6
+ variable: "--font-sans",
7
+ })
8
+
9
+ export const metadata = {
10
+ title: 'Imagine - AI Image Generation',
11
+ description: 'Generate amazing images using AI',
12
+ }
13
+
14
+ export default function RootLayout({
15
+ children,
16
+ }: {
17
+ children: React.ReactNode
18
+ }) {
19
+ return (
20
+ <html lang="en" suppressHydrationWarning>
21
+ <body className={`${inter.className} bg-neutral-50 dark:bg-amoled-black text-[#141414] dark:text-white`}>
22
+ <Layout>
23
+ {children}
24
+ </Layout>
25
+ </body>
26
+ </html>
27
+ )
28
+ }
app/page.tsx ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import GeneratorForm from '../components/ImageGenerator/GeneratorForm';
5
+ import ImageBatch from '../components/Gallery/ImageBatch';
6
+ import { Button } from '../components/ui/button';
7
+ import Navbar from '../components/Navbar';
8
+
9
+ interface Image {
10
+ url: string;
11
+ }
12
+
13
+ interface Batch {
14
+ id: number | string;
15
+ prompt: string;
16
+ width: number;
17
+ height: number;
18
+ model: string;
19
+ images: Image[];
20
+ createdAt?: string;
21
+ status?: 'pending' | 'completed' | 'error';
22
+ tempId?: string;
23
+ }
24
+
25
+ export default function Home() {
26
+ const [batches, setBatches] = useState<Batch[]>([]);
27
+ const [remixBatch, setRemixBatch] = useState<Batch | null>(null);
28
+ const [page, setPage] = useState(1);
29
+ const [hasMore, setHasMore] = useState(true);
30
+ const [isLoading, setIsLoading] = useState(false);
31
+
32
+ useEffect(() => {
33
+ fetchPreviousBatches();
34
+ }, []);
35
+
36
+ const fetchPreviousBatches = async (loadMore = false) => {
37
+ if (isLoading) return;
38
+ setIsLoading(true);
39
+ try {
40
+ const response = await fetch(`/api/generate-image?page=${loadMore ? page + 1 : 1}&limit=10`);
41
+ if (response.ok) {
42
+ const data = await response.json();
43
+ if (loadMore) {
44
+ setBatches(prev => [...prev, ...data.batches]);
45
+ setPage(prev => prev + 1);
46
+ } else {
47
+ setBatches(data.batches);
48
+ }
49
+ setHasMore(data.hasMore);
50
+ }
51
+ } catch (error) {
52
+ console.error('Error fetching previous batches:', error);
53
+ } finally {
54
+ setIsLoading(false);
55
+ }
56
+ };
57
+
58
+ const handleGenerate = useCallback((newBatch: Batch & { tempId?: string }, isPlaceholder: boolean) => {
59
+ const batchWithTimestamp = {
60
+ ...newBatch,
61
+ createdAt: newBatch.createdAt || new Date().toISOString()
62
+ };
63
+
64
+ setBatches((prev) => {
65
+ const index = prev.findIndex(b =>
66
+ (typeof b.id === 'number' && b.id === newBatch.id) ||
67
+ (b.tempId && b.tempId === newBatch.tempId)
68
+ );
69
+ if (index !== -1) {
70
+ // Update existing batch
71
+ const updatedBatches = [...prev];
72
+ updatedBatches[index] = {
73
+ ...batchWithTimestamp,
74
+ id: typeof newBatch.id === 'number' ? newBatch.id : prev[index].id, // Keep the numeric id if it exists
75
+ tempId: newBatch.tempId || prev[index].tempId // Keep the tempId for reference
76
+ };
77
+ return updatedBatches;
78
+ } else {
79
+ // Add new batch
80
+ return [batchWithTimestamp, ...prev];
81
+ }
82
+ });
83
+ }, []);
84
+
85
+ const handleDelete = async (id: number | string) => {
86
+ // Optimistically remove the batch from the UI
87
+ setBatches((prev) => prev.filter((batch) =>
88
+ (typeof batch.id === 'number' && batch.id !== id) &&
89
+ (batch.tempId !== id)
90
+ ));
91
+
92
+ // Then send the delete request to the server
93
+ try {
94
+ const response = await fetch(`/api/generate-image?id=${id}`, {
95
+ method: 'DELETE',
96
+ });
97
+ if (!response.ok) {
98
+ console.error('Failed to delete batch');
99
+ // If the deletion fails on the server, you might want to add the batch back to the UI
100
+ // This would require keeping a copy of the deleted batch or fetching it again from the server
101
+ }
102
+ } catch (error) {
103
+ console.error('Error deleting batch:', error);
104
+ // Similarly, handle the error case here if you want to revert the UI change
105
+ }
106
+ };
107
+
108
+ const handleRemix = (batch: Batch) => {
109
+ setRemixBatch(batch);
110
+ };
111
+
112
+ const handleLoadMore = () => {
113
+ fetchPreviousBatches(true);
114
+ };
115
+
116
+ const handleReload = useCallback(() => {
117
+ setPage(1);
118
+ fetchPreviousBatches();
119
+ }, []);
120
+
121
+ return (
122
+ <>
123
+ <Navbar onReload={handleReload} />
124
+ <div className="flex flex-col md:flex-row flex-1 justify-start py-5">
125
+ <div className="w-full md:w-80 px-4 md:px-6 mb-6 md:mb-0">
126
+ <GeneratorForm onGenerate={handleGenerate} remixBatch={remixBatch} />
127
+ </div>
128
+ <div className="flex-1 px-4 md:px-6 overflow-x-auto">
129
+ <div className="layout-content-container flex flex-col w-full">
130
+ {batches.map((batch) => (
131
+ <ImageBatch key={batch.id} batch={batch} onDelete={handleDelete} onRemix={handleRemix} />
132
+ ))}
133
+ {hasMore && (
134
+ <div className="flex justify-center mt-4">
135
+ <Button
136
+ onClick={handleLoadMore}
137
+ disabled={isLoading}
138
+ variant="outline"
139
+ className="bg-white dark:bg-gray-800 text-[#141414] dark:text-white font-bold"
140
+ >
141
+ {isLoading ? 'Loading...' : 'Load More'}
142
+ </Button>
143
+ </div>
144
+ )}
145
+ </div>
146
+ </div>
147
+ </div>
148
+ </>
149
+ );
150
+ }
app/password/page.tsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useState } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Input } from '../../components/ui/input';
7
+
8
+ export default function PasswordPage() {
9
+ const [password, setPassword] = useState('');
10
+ const [error, setError] = useState('');
11
+ const [isLoading, setIsLoading] = useState(false);
12
+ const router = useRouter();
13
+
14
+ const handleSubmit = async (e: React.FormEvent) => {
15
+ e.preventDefault();
16
+ setIsLoading(true);
17
+ setError('');
18
+
19
+ try {
20
+ const response = await fetch('/api/check-password', {
21
+ method: 'POST',
22
+ headers: { 'Content-Type': 'application/json' },
23
+ body: JSON.stringify({ password }),
24
+ });
25
+
26
+ if (response.ok) {
27
+ document.cookie = 'isAuthenticated=true; path=/; max-age=604800'; // Set cookie for 7 days
28
+ router.push('/');
29
+ } else {
30
+ setError('Incorrect password');
31
+ }
32
+ } catch (error) {
33
+ console.error('Error checking password:', error);
34
+ setError('An error occurred. Please try again.');
35
+ } finally {
36
+ setIsLoading(false);
37
+ }
38
+ };
39
+
40
+ return (
41
+ <div className="flex flex-col items-center justify-center min-h-screen bg-neutral-50 dark:bg-amoled-black">
42
+ <form onSubmit={handleSubmit} className="w-full max-w-md p-8 bg-white dark:bg-gray-800 rounded-2xl shadow-lg">
43
+ <h1 className="text-3xl font-bold mb-6 text-center text-gray-800 dark:text-white">Enter Password</h1>
44
+ <div className="mb-6">
45
+ <Input
46
+ type="password"
47
+ value={password}
48
+ onChange={(e) => setPassword(e.target.value)}
49
+ className="w-full p-3 text-lg"
50
+ placeholder="Password"
51
+ />
52
+ </div>
53
+ {error && <p className="text-red-500 mb-4 text-center">{error}</p>}
54
+ <Button
55
+ type="submit"
56
+ className="w-full p-3 text-lg font-semibold"
57
+ disabled={isLoading}
58
+ >
59
+ {isLoading ? 'Checking...' : 'Submit'}
60
+ </Button>
61
+ </form>
62
+ </div>
63
+ );
64
+ }
components/Gallery/BatchMenu.tsx ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { MoreHorizontal, Trash2, RefreshCw } from 'lucide-react';
3
+ import { Button } from '../ui/button';
4
+
5
+ interface BatchMenuProps {
6
+ onDelete: () => void;
7
+ onRemix: () => void;
8
+ }
9
+
10
+ export default function BatchMenu({ onDelete, onRemix }: BatchMenuProps) {
11
+ const [isOpen, setIsOpen] = useState(false);
12
+ const menuRef = useRef<HTMLDivElement>(null);
13
+
14
+ useEffect(() => {
15
+ function handleClickOutside(event: MouseEvent) {
16
+ if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
17
+ setIsOpen(false);
18
+ }
19
+ }
20
+
21
+ document.addEventListener('mousedown', handleClickOutside);
22
+ return () => {
23
+ document.removeEventListener('mousedown', handleClickOutside);
24
+ };
25
+ }, []);
26
+
27
+ return (
28
+ <div className="relative" ref={menuRef}>
29
+ <Button
30
+ variant="ghost"
31
+ size="icon"
32
+ className="h-8 w-8"
33
+ onClick={() => setIsOpen(!isOpen)}
34
+ >
35
+ <MoreHorizontal className="h-4 w-4" />
36
+ </Button>
37
+ {isOpen && (
38
+ <div className="absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 z-50">
39
+ <div className="py-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
40
+ <button
41
+ onClick={() => {
42
+ onRemix();
43
+ setIsOpen(false);
44
+ }}
45
+ className="flex items-center px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-900 w-full text-left"
46
+ role="menuitem"
47
+ >
48
+ <RefreshCw className="mr-2 h-4 w-4" />
49
+ <span>Remix</span>
50
+ </button>
51
+ <button
52
+ onClick={() => {
53
+ onDelete();
54
+ setIsOpen(false);
55
+ }}
56
+ className="flex items-center px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-900 w-full text-left"
57
+ role="menuitem"
58
+ >
59
+ <Trash2 className="mr-2 h-4 w-4" />
60
+ <span>Delete</span>
61
+ </button>
62
+ </div>
63
+ </div>
64
+ )}
65
+ </div>
66
+ );
67
+ }
components/Gallery/ImageBatch.tsx ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef, useEffect } from 'react';
2
+ import ImageCard from './ImageCard';
3
+ import BatchMenu from './BatchMenu';
4
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"
5
+ import { Copy, Check } from 'lucide-react';
6
+ import { Button } from '../ui/button';
7
+
8
+ const formatTimestamp = (timestamp: string | undefined) => {
9
+ if (!timestamp) return '';
10
+
11
+ const date = new Date(timestamp);
12
+ if (isNaN(date.getTime())) return '';
13
+
14
+ const now = new Date();
15
+ const hours = date.getHours().toString().padStart(2, '0');
16
+ const minutes = date.getMinutes().toString().padStart(2, '0');
17
+ const day = date.getDate().toString().padStart(2, '0');
18
+ const month = (date.getMonth() + 1).toString().padStart(2, '0');
19
+ const year = date.getFullYear();
20
+
21
+ const timeString = `${hours}:${minutes}`;
22
+ const dateString = date.getFullYear() === now.getFullYear()
23
+ ? `${day}-${month}`
24
+ : `${day}-${month}-${year}`;
25
+
26
+ return `${timeString} ${dateString}`;
27
+ };
28
+
29
+ interface Image {
30
+ url: string;
31
+ }
32
+
33
+ interface Batch {
34
+ id: number;
35
+ prompt: string;
36
+ width: number;
37
+ height: number;
38
+ model: string;
39
+ images: Image[];
40
+ createdAt?: string;
41
+ status?: string;
42
+ }
43
+
44
+ const modelNames: { [key: string]: string } = {
45
+ 'runware:100@1': 'FLUX SCHNELL',
46
+ 'runware:101@1': 'FLUX DEV'
47
+ };
48
+
49
+ interface ImageBatchProps {
50
+ batch: Batch;
51
+ onDelete: (id: number) => void;
52
+ onRemix: (batch: Batch) => void;
53
+ }
54
+
55
+ export default function ImageBatch({ batch, onDelete, onRemix }: ImageBatchProps) {
56
+ const [copied, setCopied] = useState(false);
57
+ const [isPromptTruncated, setIsPromptTruncated] = useState(false);
58
+ const promptRef = useRef<HTMLParagraphElement>(null);
59
+ const modelName = modelNames[batch.model] || batch.model;
60
+ const [elapsedTime, setElapsedTime] = useState(0);
61
+
62
+ useEffect(() => {
63
+ const checkTruncation = () => {
64
+ if (promptRef.current) {
65
+ setIsPromptTruncated(
66
+ promptRef.current.scrollWidth > promptRef.current.clientWidth
67
+ );
68
+ }
69
+ };
70
+
71
+ checkTruncation();
72
+ window.addEventListener('resize', checkTruncation);
73
+
74
+ return () => {
75
+ window.removeEventListener('resize', checkTruncation);
76
+ };
77
+ }, [batch.prompt]);
78
+
79
+ useEffect(() => {
80
+ let interval: NodeJS.Timeout;
81
+ if (batch.status === 'pending') {
82
+ const startTime = new Date(batch.createdAt || Date.now()).getTime();
83
+ interval = setInterval(() => {
84
+ const now = Date.now();
85
+ setElapsedTime((now - startTime) / 1000);
86
+ }, 100);
87
+ }
88
+ return () => clearInterval(interval);
89
+ }, [batch.status, batch.createdAt]);
90
+
91
+ const handleDelete = () => {
92
+ onDelete(batch.id);
93
+ };
94
+
95
+ const handleRemix = () => {
96
+ onRemix(batch);
97
+ };
98
+
99
+ const copyToClipboard = () => {
100
+ navigator.clipboard.writeText(batch.prompt).then(() => {
101
+ setCopied(true);
102
+ setTimeout(() => setCopied(false), 2000);
103
+ });
104
+ };
105
+
106
+ return (
107
+ <div className="mb-4 rounded-xl p-3 bg-[#ededed] dark:bg-gray-900">
108
+ <div className="flex flex-col">
109
+ <div className="flex items-center justify-between">
110
+ <TooltipProvider delayDuration={0}>
111
+ <Tooltip>
112
+ <TooltipTrigger asChild>
113
+ <p ref={promptRef} className="text-[#141414] dark:text-white text-xs font-medium truncate cursor-default max-w-[95%]">{batch.prompt}</p>
114
+ </TooltipTrigger>
115
+ {isPromptTruncated && (
116
+ <TooltipContent side="bottom" align="center" className="max-w-md">
117
+ <p className="text-sm">{batch.prompt}</p>
118
+ </TooltipContent>
119
+ )}
120
+ </Tooltip>
121
+ </TooltipProvider>
122
+ <div className="flex items-center gap-2">
123
+ <Button
124
+ variant="ghost"
125
+ size="icon"
126
+ className="h-6 w-6 flex-shrink-0"
127
+ onClick={copyToClipboard}
128
+ >
129
+ {copied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
130
+ </Button>
131
+ <BatchMenu onDelete={handleDelete} onRemix={handleRemix} />
132
+ </div>
133
+ </div>
134
+ <p className="text-[#141414] dark:text-white text-[10px] mt-0">
135
+ {modelName} | {batch.width}x{batch.height}{formatTimestamp(batch.createdAt) && ` • ${formatTimestamp(batch.createdAt)}`}
136
+ </p>
137
+ </div>
138
+ <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2 mt-2">
139
+ {batch.images.map((image, index) => (
140
+ <ImageCard
141
+ key={index}
142
+ image={image}
143
+ batchImages={batch.images}
144
+ batchId={batch.id}
145
+ status={batch.status}
146
+ width={batch.width}
147
+ height={batch.height}
148
+ elapsedTime={elapsedTime}
149
+ />
150
+ ))}
151
+ </div>
152
+ {batch.status === 'error' && (
153
+ <p className="text-red-500 mt-2">Error generating images. Please try again.</p>
154
+ )}
155
+ </div>
156
+ );
157
+ }
components/Gallery/ImageCard.tsx ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect } from 'react';
2
+ import { Fancybox } from "@fancyapps/ui";
3
+ import "@fancyapps/ui/dist/fancybox/fancybox.css";
4
+
5
+ interface Image {
6
+ url: string;
7
+ }
8
+
9
+ interface ImageCardProps {
10
+ image: Image;
11
+ batchImages: Image[];
12
+ batchId: number;
13
+ status?: 'pending' | 'error' | 'completed';
14
+ width: number;
15
+ height: number;
16
+ elapsedTime: number;
17
+ }
18
+
19
+ export default function ImageCard({ image, batchImages, batchId, status, width, height, elapsedTime }: ImageCardProps) {
20
+ useEffect(() => {
21
+ Fancybox.bind(`[data-fancybox="gallery-${batchId}"]`, {
22
+ contentClick: "iterateZoom",
23
+ Images: {
24
+ Panzoom: {
25
+ maxScale: 8,
26
+ },
27
+ },
28
+ Toolbar: {
29
+ display: {
30
+ left: ["infobar"],
31
+ middle: [],
32
+ right: ["iterateZoom", "fullscreen", "download", "thumbs", "close"],
33
+ }
34
+ },
35
+ Thumbs: {
36
+ type: "classic",
37
+ },
38
+ });
39
+
40
+ return () => {
41
+ Fancybox.destroy();
42
+ };
43
+ }, [batchId]);
44
+
45
+ const aspectRatio = `${width} / ${height}`;
46
+
47
+ return (
48
+ <a
49
+ href={image.url}
50
+ data-fancybox={`gallery-${batchId}`}
51
+ data-src={image.url}
52
+ className="block relative"
53
+ style={{ aspectRatio }}
54
+ >
55
+ <div
56
+ className={`w-full h-full bg-center bg-no-repeat bg-cover rounded-xl cursor-pointer overflow-hidden ${
57
+ status === 'pending' ? 'animate-pulse bg-gray-300 dark:bg-gray-700' : ''
58
+ }`}
59
+ style={status !== 'pending' ? {backgroundImage: `url("${image.url}")`} : {}}
60
+ >
61
+ {status === 'pending' && (
62
+ <div className="absolute inset-0 flex items-center justify-center">
63
+ <div className="relative w-16 h-16">
64
+ <svg className="animate-spin h-16 w-16 text-primary" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
65
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
66
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
67
+ </svg>
68
+ <div className="absolute inset-0 flex items-center justify-center">
69
+ <span className="text-primary font-bold">{elapsedTime.toFixed(1)}s</span>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ )}
74
+ {status === 'error' && (
75
+ <div className="flex items-center justify-center h-full bg-red-200 dark:bg-red-900 rounded-xl">
76
+ <span className="text-red-500 dark:text-red-300">Error</span>
77
+ </div>
78
+ )}
79
+ </div>
80
+ </a>
81
+ );
82
+ }
components/Gallery/ImageGrid.tsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ImageCard from './ImageCard';
2
+
3
+ interface Image {
4
+ url: string;
5
+ }
6
+
7
+ export default function ImageGrid({ title, images, batchId }: { title: string; images: Image[]; batchId: number }) {
8
+ return (
9
+ <div className="mb-8">
10
+ <h2 className="text-[#141414] dark:text-white text-[22px] font-bold leading-tight tracking-[-0.015em] px-4 pb-3 pt-5">{title}</h2>
11
+ <div className="flex overflow-x-auto pb-4">
12
+ <div className="flex gap-3 md:gap-4 px-4">
13
+ {images.map((image, index) => (
14
+ <ImageCard key={index} image={image} batchImages={images} batchId={batchId} />
15
+ ))}
16
+ </div>
17
+ </div>
18
+ </div>
19
+ );
20
+ }
components/Header.tsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Image from 'next/image';
2
+ import Link from 'next/link';
3
+
4
+ export default function Header() {
5
+ return (
6
+ <header className="flex items-center justify-between whitespace-nowrap border-b border-solid border-b-[#ededed] px-10 py-3">
7
+ <div className="flex items-center gap-8">
8
+ <div className="flex items-center gap-4 text-[#141414]">
9
+ <div className="size-4">
10
+ <svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
11
+ <path fillRule="evenodd" clipRule="evenodd" d="M24 18.4228L42 11.475V34.3663C42 34.7796 41.7457 35.1504 41.3601 35.2992L24 42V18.4228Z" fill="currentColor"></path>
12
+ <path fillRule="evenodd" clipRule="evenodd" d="M24 8.18819L33.4123 11.574L24 15.2071L14.5877 11.574L24 8.18819ZM9 15.8487L21 20.4805V37.6263L9 32.9945V15.8487ZM27 37.6263V20.4805L39 15.8487V32.9945L27 37.6263ZM25.354 2.29885C24.4788 1.98402 23.5212 1.98402 22.646 2.29885L4.98454 8.65208C3.7939 9.08038 3 10.2097 3 11.475V34.3663C3 36.0196 4.01719 37.5026 5.55962 38.098L22.9197 44.7987C23.6149 45.0671 24.3851 45.0671 25.0803 44.7987L42.4404 38.098C43.9828 37.5026 45 36.0196 45 34.3663V11.475C45 10.2097 44.2061 9.08038 43.0155 8.65208L25.354 2.29885Z" fill="currentColor"></path>
13
+ </svg>
14
+ </div>
15
+ <h2 className="text-[#141414] text-lg font-bold leading-tight tracking-[-0.015em]">ImageGen</h2>
16
+ </div>
17
+ <div className="flex items-center gap-9">
18
+ <Link href="#" className="text-[#141414] text-sm font-medium leading-normal">Home</Link>
19
+ <Link href="#" className="text-[#141414] text-sm font-medium leading-normal">Explore</Link>
20
+ <Link href="#" className="text-[#141414] text-sm font-medium leading-normal">Create</Link>
21
+ </div>
22
+ </div>
23
+ <div className="flex flex-1 justify-end gap-8">
24
+ <label className="flex flex-col min-w-40 !h-10 max-w-64">
25
+ <div className="flex w-full flex-1 items-stretch rounded-xl h-full">
26
+ <div className="text-neutral-500 flex border-none bg-[#ededed] items-center justify-center pl-4 rounded-l-xl border-r-0">
27
+ <svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" fill="currentColor" viewBox="0 0 256 256">
28
+ <path d="M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"></path>
29
+ </svg>
30
+ </div>
31
+ <input
32
+ placeholder="Search"
33
+ className="form-input flex w-full min-w-0 flex-1 resize-none overflow-hidden rounded-xl text-[#141414] focus:outline-0 focus:ring-0 border-none bg-[#ededed] focus:border-none h-full placeholder:text-neutral-500 px-4 rounded-l-none border-l-0 pl-2 text-base font-normal leading-normal"
34
+ />
35
+ </div>
36
+ </label>
37
+ <button className="flex min-w-[84px] max-w-[480px] cursor-pointer items-center justify-center overflow-hidden rounded-xl h-10 px-4 bg-[#ededed] text-[#141414] text-sm font-bold leading-normal tracking-[0.015em]">
38
+ <span className="truncate">4.6k credits</span>
39
+ </button>
40
+ <div
41
+ className="bg-center bg-no-repeat aspect-square bg-cover rounded-full size-10"
42
+ style={{backgroundImage: 'url("https://cdn.usegalileo.ai/sdxl10/a879f542-e73c-43af-9e70-cc7859996bc5.png")'}}
43
+ ></div>
44
+ </div>
45
+ </header>
46
+ );
47
+ }
components/ImageGenerator/AspectRatioSelector.tsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import {
4
+ Select,
5
+ SelectContent,
6
+ SelectItem,
7
+ SelectTrigger,
8
+ SelectValue,
9
+ } from '../ui/select'
10
+
11
+ interface AspectRatioSelectorProps {
12
+ value: string;
13
+ onChange: (value: string) => void;
14
+ }
15
+
16
+ export default function AspectRatioSelector({ value, onChange }: AspectRatioSelectorProps) {
17
+ return (
18
+ <div className="flex max-w-[480px] flex-wrap items-end gap-4 px-4 py-3">
19
+ <label className="flex flex-col min-w-40 flex-1">
20
+ <p className="text-[#141414] dark:text-white text-base font-medium leading-normal pb-2">Aspect Ratio</p>
21
+ <Select value={value} onValueChange={onChange}>
22
+ <SelectTrigger className="w-full bg-[#ededed] dark:bg-gray-900 border-none rounded-2xl h-14 px-4">
23
+ <SelectValue placeholder="Choose aspect ratio" />
24
+ </SelectTrigger>
25
+ <SelectContent className="bg-[#ededed] dark:bg-gray-900 rounded-2xl">
26
+ <SelectItem value="square">Square (1024x1024)</SelectItem>
27
+ <SelectItem value="portrait">Portrait (832x1216)</SelectItem>
28
+ <SelectItem value="landscape">Landscape (1216x832)</SelectItem>
29
+ </SelectContent>
30
+ </Select>
31
+ </label>
32
+ </div>
33
+ );
34
+ }
components/ImageGenerator/GeneratorForm.tsx ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { Loader2 } from 'lucide-react';
3
+ import ModelSelector from './ModelSelector';
4
+ import PromptInput from './PromptInput';
5
+ import AspectRatioSelector from './AspectRatioSelector';
6
+ import ImageCountSlider from './ImageCountSlider';
7
+ import { Button } from '../ui/button';
8
+ import { v4 as uuidv4 } from 'uuid';
9
+
10
+ interface Image {
11
+ url: string;
12
+ }
13
+
14
+ interface Batch {
15
+ id: number | string; // Allow string for tempId
16
+ prompt: string;
17
+ width: number;
18
+ height: number;
19
+ model: string;
20
+ images: Image[];
21
+ status?: string;
22
+ tempId?: string;
23
+ }
24
+
25
+ export default function GeneratorForm({ onGenerate, remixBatch }: { onGenerate: (batch: Batch, isPlaceholder: boolean) => void, remixBatch: Batch | null }) {
26
+ const [prompt, setPrompt] = useState('');
27
+ const [model, setModel] = useState('runware:100@1'); // FLUX SCHNELL as default
28
+ const [aspectRatio, setAspectRatio] = useState('square');
29
+ const [imageCount, setImageCount] = useState(1);
30
+ const [isLoading, setIsLoading] = useState(false);
31
+ const [error, setError] = useState<string | null>(null);
32
+
33
+ useEffect(() => {
34
+ if (remixBatch) {
35
+ setPrompt(remixBatch.prompt);
36
+ setModel(remixBatch.model);
37
+ setAspectRatio(getAspectRatioFromDimensions(remixBatch.width, remixBatch.height));
38
+ setImageCount(remixBatch.images.length);
39
+ }
40
+ }, [remixBatch]);
41
+
42
+ const getAspectRatioFromDimensions = (width: number, height: number) => {
43
+ if (width === height) return 'square';
44
+ if (width === 832 && height === 1216) return 'portrait';
45
+ if (width === 1216 && height === 832) return 'landscape';
46
+ return 'square'; // Default to square if dimensions don't match known ratios
47
+ };
48
+
49
+ const aspectRatios: { [key: string]: { width: number; height: number } } = {
50
+ square: { width: 1024, height: 1024 },
51
+ landscape: { width: 1216, height: 832 },
52
+ portrait: { width: 832, height: 1216 }
53
+ };
54
+
55
+ const handleGenerate = useCallback(async () => {
56
+ setError(null);
57
+ const placeholderId = uuidv4();
58
+ const placeholderBatch: Batch = {
59
+ id: placeholderId,
60
+ prompt,
61
+ width: aspectRatios[aspectRatio as keyof typeof aspectRatios].width,
62
+ height: aspectRatios[aspectRatio as keyof typeof aspectRatios].height,
63
+ model,
64
+ images: Array(imageCount).fill({ url: '/placeholder-image.png' }),
65
+ status: 'pending',
66
+ tempId: placeholderId
67
+ };
68
+ onGenerate(placeholderBatch, true);
69
+
70
+ try {
71
+ const response = await fetch('/api/generate-image', {
72
+ method: 'POST',
73
+ headers: {
74
+ 'Content-Type': 'application/json',
75
+ },
76
+ body: JSON.stringify({
77
+ prompt,
78
+ width: placeholderBatch.width,
79
+ height: placeholderBatch.height,
80
+ model,
81
+ number_results: imageCount,
82
+ placeholderId,
83
+ }),
84
+ });
85
+
86
+ if (!response.ok) {
87
+ throw new Error('Failed to generate image');
88
+ }
89
+
90
+ const reader = response.body?.getReader();
91
+ if (!reader) {
92
+ throw new Error('Failed to read response');
93
+ }
94
+
95
+ while (true) {
96
+ const { done, value } = await reader.read();
97
+ if (done) break;
98
+
99
+ const chunk = new TextDecoder().decode(value);
100
+ const data = JSON.parse(chunk);
101
+
102
+ if (data.batch) {
103
+ onGenerate({ ...data.batch, tempId: placeholderId }, false);
104
+ }
105
+ }
106
+ } catch (error) {
107
+ console.error('Error generating image:', error);
108
+ setError(error instanceof Error ? error.message : 'An unknown error occurred');
109
+ onGenerate({ ...placeholderBatch, status: 'error' }, false);
110
+ }
111
+ }, [aspectRatio, prompt, model, imageCount, onGenerate, aspectRatios]);
112
+
113
+ useEffect(() => {
114
+ const handleKeyDown = (event: KeyboardEvent) => {
115
+ if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
116
+ event.preventDefault();
117
+ handleGenerate();
118
+ }
119
+ };
120
+
121
+ document.addEventListener('keydown', handleKeyDown);
122
+ return () => {
123
+ document.removeEventListener('keydown', handleKeyDown);
124
+ };
125
+ }, [handleGenerate]);
126
+
127
+ return (
128
+ <div className="layout-content-container flex flex-col w-full md:w-80">
129
+ <PromptInput value={prompt} onChange={setPrompt} />
130
+ <ModelSelector value={model} onChange={setModel} />
131
+ <AspectRatioSelector value={aspectRatio} onChange={setAspectRatio} />
132
+ <ImageCountSlider value={imageCount} onChange={setImageCount} />
133
+ {error && (
134
+ <div className="px-4 py-2 mb-3 text-red-500 bg-red-100 dark:bg-red-900 dark:text-red-100 rounded-md">
135
+ {error}
136
+ </div>
137
+ )}
138
+ <div className="flex px-4 py-3">
139
+ <Button
140
+ variant="outline"
141
+ className="w-full justify-center bg-white dark:bg-gray-800 text-[#141414] dark:text-white font-bold"
142
+ onClick={handleGenerate}
143
+ disabled={isLoading}
144
+ >
145
+ {isLoading ? (
146
+ <>
147
+ <Loader2 className="mr-2 size-4 animate-spin" />
148
+ Generating...
149
+ </>
150
+ ) : (
151
+ 'Generate'
152
+ )}
153
+ </Button>
154
+ </div>
155
+ </div>
156
+ );
157
+ }
components/ImageGenerator/ImageCountSlider.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React from 'react';
4
+ import { Slider } from '../ui/slider';
5
+
6
+ interface ImageCountSliderProps {
7
+ value: number;
8
+ onChange: (value: number) => void;
9
+ }
10
+
11
+ export default function ImageCountSlider({ value, onChange }: ImageCountSliderProps) {
12
+ return (
13
+ <div className="flex flex-col px-4 py-3">
14
+ <div className="flex justify-between items-center mb-2">
15
+ <p className="text-[#141414] dark:text-white text-base font-medium leading-normal">Image Count</p>
16
+ <p className="text-[#141414] dark:text-white text-sm font-normal leading-normal">{value}</p>
17
+ </div>
18
+ <Slider
19
+ min={1}
20
+ max={4}
21
+ step={1}
22
+ value={[value]}
23
+ onValueChange={(newValue) => onChange(newValue[0])}
24
+ className="w-full"
25
+ formatLabel={(value) => `${value}`}
26
+ />
27
+ </div>
28
+ );
29
+ }
components/ImageGenerator/ModelSelector.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import {
4
+ Select,
5
+ SelectContent,
6
+ SelectItem,
7
+ SelectTrigger,
8
+ SelectValue,
9
+ } from '../ui/select'
10
+
11
+ interface ModelSelectorProps {
12
+ value: string;
13
+ onChange: (value: string) => void;
14
+ }
15
+
16
+ export default function ModelSelector({ value, onChange }: ModelSelectorProps) {
17
+ return (
18
+ <div className="flex max-w-[480px] flex-wrap items-end gap-4 px-4 py-3">
19
+ <label className="flex flex-col min-w-40 flex-1">
20
+ <p className="text-[#141414] dark:text-white text-base font-medium leading-normal pb-2">Model</p>
21
+ <Select value={value} onValueChange={onChange}>
22
+ <SelectTrigger className="w-full bg-[#ededed] dark:bg-gray-900 border-none rounded-2xl h-14 px-4">
23
+ <SelectValue placeholder="Choose model" />
24
+ </SelectTrigger>
25
+ <SelectContent className="bg-[#ededed] dark:bg-gray-900 rounded-2xl">
26
+ <SelectItem value="runware:100@1">FLUX SCHNELL</SelectItem>
27
+ <SelectItem value="runware:101@1">FLUX DEV</SelectItem>
28
+ </SelectContent>
29
+ </Select>
30
+ </label>
31
+ </div>
32
+ )
33
+ }
components/ImageGenerator/PromptInput.tsx ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { Wand2, Undo2 } from 'lucide-react';
3
+ import { Button } from '../ui/button';
4
+
5
+ interface PromptInputProps {
6
+ value: string;
7
+ onChange: (value: string) => void;
8
+ }
9
+
10
+ export default function PromptInput({ value, onChange }: PromptInputProps) {
11
+ const [isEnhancing, setIsEnhancing] = useState(false);
12
+ const [previousPrompt, setPreviousPrompt] = useState('');
13
+ const [isEnhanced, setIsEnhanced] = useState(false);
14
+
15
+ const enhancePrompt = useCallback(async () => {
16
+ if (!value.trim() || isEnhancing) return;
17
+
18
+ setIsEnhancing(true);
19
+ setPreviousPrompt(value);
20
+ try {
21
+ const response = await fetch('/api/enhance-prompt', {
22
+ method: 'POST',
23
+ headers: {
24
+ 'Content-Type': 'application/json',
25
+ },
26
+ body: JSON.stringify({ prompt: value }),
27
+ });
28
+
29
+ if (!response.ok) {
30
+ throw new Error('Failed to enhance prompt');
31
+ }
32
+
33
+ const data = await response.json();
34
+ onChange(data.enhancedPrompt);
35
+ setIsEnhanced(true);
36
+ } catch (error) {
37
+ console.error('Error enhancing prompt:', error);
38
+ } finally {
39
+ setIsEnhancing(false);
40
+ }
41
+ }, [value, onChange]);
42
+
43
+ const undoEnhance = useCallback(() => {
44
+ onChange(previousPrompt);
45
+ setIsEnhanced(false);
46
+ }, [previousPrompt, onChange]);
47
+
48
+ useEffect(() => {
49
+ const handleKeyDown = (event: KeyboardEvent) => {
50
+ if ((event.ctrlKey || event.metaKey) && event.key === 'i') {
51
+ event.preventDefault();
52
+ enhancePrompt();
53
+ }
54
+ };
55
+
56
+ document.addEventListener('keydown', handleKeyDown);
57
+ return () => {
58
+ document.removeEventListener('keydown', handleKeyDown);
59
+ };
60
+ }, [enhancePrompt]);
61
+
62
+ return (
63
+ <div className="flex flex-col w-full gap-2 px-4 py-3">
64
+ <div className="flex justify-between items-center">
65
+ <label htmlFor="prompt-input" className="text-[#141414] dark:text-white text-base font-medium leading-normal">
66
+ Prompt
67
+ </label>
68
+ <div className="flex gap-2">
69
+ {isEnhanced && (
70
+ <Button
71
+ variant="ghost"
72
+ size="icon"
73
+ onClick={undoEnhance}
74
+ title="Undo enhance"
75
+ className="h-8 w-8"
76
+ >
77
+ <Undo2 className="h-4 w-4" />
78
+ </Button>
79
+ )}
80
+ <Button
81
+ variant="ghost"
82
+ size="icon"
83
+ onClick={enhancePrompt}
84
+ disabled={isEnhancing || !value.trim()}
85
+ title="Enhance prompt (Ctrl+I / Cmd+I)"
86
+ className="h-8 w-8"
87
+ >
88
+ <Wand2 className={`h-4 w-4 ${isEnhancing ? 'animate-pulse' : ''}`} />
89
+ </Button>
90
+ </div>
91
+ </div>
92
+ <textarea
93
+ id="prompt-input"
94
+ value={value}
95
+ onChange={(e) => onChange(e.target.value)}
96
+ placeholder="Type your prompt here"
97
+ className="w-full min-h-[150px] p-4 rounded-xl text-[#141414] dark:text-white focus:outline-none focus:ring-0 border-none bg-[#ededed] dark:bg-gray-900 placeholder:text-neutral-500 dark:placeholder:text-gray-400 text-base font-normal leading-normal resize-none"
98
+ />
99
+ </div>
100
+ );
101
+ }
components/Layout.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { ThemeProvider } from 'next-themes'
4
+
5
+ export default function Layout({ children }: { children: React.ReactNode }) {
6
+ return (
7
+ <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
8
+ <div className="relative flex min-h-screen flex-col bg-neutral-50 dark:bg-amoled-black overflow-x-hidden">
9
+ <main className="flex-grow">
10
+ {children}
11
+ </main>
12
+ </div>
13
+ </ThemeProvider>
14
+ );
15
+ }
components/MainContent.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ImageGrid from './Gallery/ImageGrid';
2
+
3
+ export default function MainContent() {
4
+ const currentImages = [
5
+ { url: "https://cdn.usegalileo.ai/stability/01f4939b-b0b4-4a14-ba61-a2f049aa4cde.png" },
6
+ { url: "https://cdn.usegalileo.ai/stability/43b6ac61-1427-4ddf-8c99-ab43c5cb8746.png" },
7
+ { url: "https://cdn.usegalileo.ai/stability/84b80f3d-decd-468c-ae69-6deabde1c7b2.png" }
8
+ ];
9
+
10
+ const previousImages = [
11
+ { url: "https://cdn.usegalileo.ai/stability/318d9396-efa7-485d-8433-2e2de66d869b.png" },
12
+ { url: "https://cdn.usegalileo.ai/stability/3a91c59c-6f67-4624-b28b-ebba9aea7a60.png" },
13
+ { url: "https://cdn.usegalileo.ai/stability/aa91a32b-9d69-4382-afdf-f9ca99cef62a.png" }
14
+ ];
15
+
16
+ return (
17
+ <div className="layout-content-container flex flex-col max-w-[960px] flex-1">
18
+ <div className="flex flex-wrap justify-between gap-3 p-4">
19
+ <p className="text-[#141414] text-4xl font-black leading-tight tracking-[-0.033em] min-w-72">
20
+ Fantasy castle on a hilltop, surrounded by rolling hills and a beautiful sunset, magical, serene, high details, romantic, stylized
21
+ </p>
22
+ </div>
23
+ <ImageGrid title="Current Generation" images={currentImages} batchId={1} />
24
+ <ImageGrid title="Previously Generated" images={previousImages} batchId={2} />
25
+ </div>
26
+ );
27
+ }
components/Navbar.tsx ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { ThemeToggle } from './ThemeToggle';
4
+ import { Button } from './ui/button';
5
+ import { useRouter, usePathname } from 'next/navigation';
6
+ import { LogOut, RefreshCw, ImagePlus } from 'lucide-react';
7
+ import { useState } from 'react';
8
+ import Link from 'next/link';
9
+
10
+ interface NavbarProps {
11
+ onReload: () => void;
12
+ }
13
+
14
+ export default function Navbar({ onReload }: NavbarProps) {
15
+ const router = useRouter();
16
+ const pathname = usePathname();
17
+ const [isReloading, setIsReloading] = useState(false);
18
+
19
+ const handleLogout = () => {
20
+ // Clear the authentication cookie
21
+ document.cookie = 'isAuthenticated=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT;';
22
+ // Redirect to the password page
23
+ router.push('/password');
24
+ };
25
+
26
+ const handleReload = () => {
27
+ setIsReloading(true);
28
+ onReload();
29
+ setTimeout(() => setIsReloading(false), 500); // Reset after 0.5 seconds
30
+ };
31
+
32
+ const handleHomeClick = () => {
33
+ if (pathname === '/') {
34
+ window.location.reload();
35
+ } else {
36
+ router.push('/');
37
+ }
38
+ };
39
+
40
+ const isPasswordPage = pathname === '/password';
41
+
42
+ return (
43
+ <header className="flex items-center justify-between whitespace-nowrap border-b border-solid border-b-[#ededed] dark:border-b-gray-800 px-10 py-0.5 bg-white dark:bg-amoled-black text-[#141414] dark:text-white">
44
+ <div className="flex items-center gap-4 cursor-pointer" onClick={handleHomeClick}>
45
+ <ImagePlus className="h-6 w-6 text-primary" />
46
+ <h2 className="text-lg font-bold leading-tight tracking-[-0.015em]">Imagine</h2>
47
+ </div>
48
+ <div className="flex items-center gap-4">
49
+ {!isPasswordPage && (
50
+ <>
51
+ <Button
52
+ onClick={handleReload}
53
+ variant="ghost"
54
+ size="icon"
55
+ title="Reload batches"
56
+ className={`transition-transform duration-500 ease-in-out ${isReloading ? 'rotate-180' : ''}`}
57
+ disabled={isReloading}
58
+ >
59
+ <RefreshCw className={`h-[1.2rem] w-[1.2rem] ${isReloading ? 'animate-spin' : ''}`} />
60
+ <span className="sr-only">Reload</span>
61
+ </Button>
62
+ <ThemeToggle />
63
+ <Button onClick={handleLogout} variant="ghost" size="icon">
64
+ <LogOut className="h-[1.2rem] w-[1.2rem]" />
65
+ <span className="sr-only">Logout</span>
66
+ </Button>
67
+ </>
68
+ )}
69
+ </div>
70
+ </header>
71
+ );
72
+ }
components/SearchInput.tsx ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function SearchInput() {
2
+ return (
3
+ <label className="flex flex-col min-w-40 !h-10 max-w-64">
4
+ <div className="flex w-full flex-1 items-stretch rounded-xl h-full">
5
+ <div className="text-neutral-500 dark:text-gray-400 flex border-none bg-[#ededed] dark:bg-gray-900 items-center justify-center pl-4 rounded-l-xl border-r-0">
6
+ <svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" fill="currentColor" viewBox="0 0 256 256">
7
+ <path d="M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"></path>
8
+ </svg>
9
+ </div>
10
+ <input
11
+ placeholder="Search"
12
+ className="form-input flex w-full min-w-0 flex-1 resize-none overflow-hidden rounded-xl text-[#141414] dark:text-white focus:outline-0 focus:ring-0 border-none bg-[#ededed] dark:bg-gray-900 focus:border-none h-full placeholder:text-neutral-500 dark:placeholder:text-gray-400 px-4 rounded-l-none border-l-0 pl-2 text-base font-normal leading-normal"
13
+ />
14
+ </div>
15
+ </label>
16
+ );
17
+ }
components/Sidebar.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function Sidebar() {
2
+ return (
3
+ <div className="layout-content-container flex flex-col w-80">
4
+ <h2 className="text-[#141414] text-[22px] font-bold leading-tight tracking-[-0.015em] px-4 pb-3 pt-5">Create</h2>
5
+ <div className="flex max-w-[480px] flex-wrap items-end gap-4 px-4 py-3">
6
+ <label className="flex flex-col min-w-40 flex-1">
7
+ <p className="text-[#141414] text-base font-medium leading-normal pb-2">Model</p>
8
+ <select className="form-input flex w-full min-w-0 flex-1 resize-none overflow-hidden rounded-xl text-[#141414] focus:outline-0 focus:ring-0 border-none bg-[#ededed] focus:border-none h-14 bg-[image:--select-button-svg] placeholder:text-neutral-500 p-4 text-base font-normal leading-normal">
9
+ <option value="one">Choose model</option>
10
+ <option value="two">two</option>
11
+ <option value="three">three</option>
12
+ </select>
13
+ </label>
14
+ </div>
15
+ <div className="flex max-w-[480px] flex-wrap items-end gap-4 px-4 py-3">
16
+ <label className="flex flex-col min-w-40 flex-1">
17
+ <p className="text-[#141414] text-base font-medium leading-normal pb-2">Prompt</p>
18
+ <textarea
19
+ placeholder="Type your prompt here"
20
+ className="form-input flex w-full min-w-0 flex-1 resize-none overflow-hidden rounded-xl text-[#141414] focus:outline-0 focus:ring-0 border-none bg-[#ededed] focus:border-none min-h-36 placeholder:text-neutral-500 p-4 text-base font-normal leading-normal"
21
+ ></textarea>
22
+ </label>
23
+ </div>
24
+ <div className="flex max-w-[480px] flex-wrap items-end gap-4 px-4 py-3">
25
+ <label className="flex flex-col min-w-40 flex-1">
26
+ <p className="text-[#141414] text-base font-medium leading-normal pb-2">Aspect Ratio</p>
27
+ <select className="form-input flex w-full min-w-0 flex-1 resize-none overflow-hidden rounded-xl text-[#141414] focus:outline-0 focus:ring-0 border-none bg-[#ededed] focus:border-none h-14 bg-[image:--select-button-svg] placeholder:text-neutral-500 p-4 text-base font-normal leading-normal">
28
+ <option value="one">Choose aspect ratio</option>
29
+ <option value="two">two</option>
30
+ <option value="three">three</option>
31
+ </select>
32
+ </label>
33
+ </div>
34
+ <div className="@container">
35
+ <div className="relative flex w-full flex-col items-start justify-between gap-3 p-4 @[480px]:flex-row @[480px]:items-center">
36
+ <div className="flex w-full shrink-[3] items-center justify-between">
37
+ <p className="text-[#141414] text-base font-medium leading-normal">Image Count</p>
38
+ <p className="text-[#141414] text-sm font-normal leading-normal @[480px]:hidden">3</p>
39
+ </div>
40
+ <div className="flex h-4 w-full items-center gap-4">
41
+ <div className="flex h-1 flex-1 rounded-sm bg-[#dbdbdb]">
42
+ <div className="h-full w-[32%] rounded-sm bg-white"></div>
43
+ <div className="relative"><div className="absolute -left-2 -top-1.5 size-4 rounded-full bg-white"></div></div>
44
+ </div>
45
+ <p className="text-[#141414] text-sm font-normal leading-normal hidden @[480px]:block">3</p>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ <div className="flex px-4 py-3">
50
+ <button className="flex min-w-[84px] max-w-[480px] cursor-pointer items-center justify-center overflow-hidden rounded-xl h-10 px-4 flex-1 bg-white text-[#141414] gap-2 pl-4 text-sm font-bold leading-normal tracking-[0.015em]">
51
+ <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" fill="currentColor" viewBox="0 0 256 256">
52
+ <path d="M233.54,142.23a8,8,0,0,0-8-2,88.08,88.08,0,0,1-109.8-109.8,8,8,0,0,0-10-10,104.84,104.84,0,0,0-52.91,37A104,104,0,0,0,136,224a103.09,103.09,0,0,0,62.52-20.88,104.84,104.84,0,0,0,37-52.91A8,8,0,0,0,233.54,142.23ZM188.9,190.34A88,88,0,0,1,65.66,67.11a89,89,0,0,1,31.4-26A106,106,0,0,0,96,56,104.11,104.11,0,0,0,200,160a106,106,0,0,0,14.92-1.06A89,89,0,0,1,188.9,190.34Z"></path>
53
+ </svg>
54
+ <span className="truncate">Dream (0.91)</span>
55
+ </button>
56
+ </div>
57
+ </div>
58
+ );
59
+ }
components/ThemeToggle.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { useTheme } from 'next-themes'
5
+ import { Moon, Sun } from 'lucide-react'
6
+ import { Button } from './ui/button'
7
+
8
+ export function ThemeToggle() {
9
+ const [mounted, setMounted] = useState(false)
10
+ const { theme, setTheme } = useTheme()
11
+
12
+ useEffect(() => {
13
+ setMounted(true)
14
+ }, [])
15
+
16
+ if (!mounted) {
17
+ return null
18
+ }
19
+
20
+ return (
21
+ <Button
22
+ variant="ghost"
23
+ size="icon"
24
+ onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
25
+ >
26
+ <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
27
+ <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
28
+ <span className="sr-only">Toggle theme</span>
29
+ </Button>
30
+ )
31
+ }
components/User/CreditDisplay.tsx ADDED
@@ -0,0 +1 @@
 
 
1
+ // This component is no longer needed and can be removed.
components/ui/button.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+ import { Slot } from '@radix-ui/react-slot'
3
+ import { type VariantProps, cva } from 'class-variance-authority'
4
+
5
+ import { ny } from '@/lib/utils'
6
+
7
+ const buttonVariants = cva(
8
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13
+ destructive:
14
+ 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
15
+ outline:
16
+ 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
17
+ secondary:
18
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
19
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
20
+ link: 'text-primary underline-offset-4 hover:underline',
21
+ },
22
+ size: {
23
+ default: 'h-10 px-4 py-2',
24
+ sm: 'h-9 rounded-md px-3',
25
+ lg: 'h-11 rounded-md px-8',
26
+ icon: 'h-10 w-10',
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: 'default',
31
+ size: 'default',
32
+ },
33
+ },
34
+ )
35
+
36
+ export interface ButtonProps
37
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
+ VariantProps<typeof buttonVariants> {
39
+ asChild?: boolean
40
+ }
41
+
42
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
43
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
44
+ const Comp = asChild ? Slot : 'button'
45
+ return (
46
+ <Comp
47
+ className={ny(buttonVariants({ variant, size, className }))}
48
+ ref={ref}
49
+ {...props}
50
+ />
51
+ )
52
+ },
53
+ )
54
+ Button.displayName = 'Button'
55
+
56
+ export { Button, buttonVariants }
components/ui/input.tsx ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { ny } from "@/lib/utils"
4
+
5
+ export interface InputProps
6
+ extends React.InputHTMLAttributes<HTMLInputElement> {}
7
+
8
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
9
+ ({ className, type, ...props }, ref) => {
10
+ return (
11
+ <input
12
+ type={type}
13
+ className={ny(
14
+ "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
15
+ className
16
+ )}
17
+ ref={ref}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+ )
23
+ Input.displayName = "Input"
24
+
25
+ export { Input }
components/ui/select.tsx ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import * as SelectPrimitive from '@radix-ui/react-select'
5
+ import { Check, ChevronDown, ChevronUp } from 'lucide-react'
6
+
7
+ import { ny } from '@/lib/utils'
8
+
9
+ const Select = SelectPrimitive.Root
10
+
11
+ const SelectGroup = SelectPrimitive.Group
12
+
13
+ const SelectValue = SelectPrimitive.Value
14
+
15
+ const SelectTrigger = React.forwardRef<
16
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
17
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
18
+ >(({ className, children, ...props }, ref) => (
19
+ <SelectPrimitive.Trigger
20
+ ref={ref}
21
+ className={ny(
22
+ 'border-input bg-background ring-offset-background placeholder:data-[placeholder]:text-muted-foreground focus:ring-ring flex h-10 w-full items-center justify-between rounded-full border px-3 py-2 text-left text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
23
+ className,
24
+ )}
25
+ {...props}
26
+ >
27
+ {children}
28
+ <SelectPrimitive.Icon asChild>
29
+ <ChevronDown className="size-4 shrink-0 opacity-50" />
30
+ </SelectPrimitive.Icon>
31
+ </SelectPrimitive.Trigger>
32
+ ))
33
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34
+
35
+ const SelectScrollUpButton = React.forwardRef<
36
+ React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
37
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
38
+ >(({ className, ...props }, ref) => (
39
+ <SelectPrimitive.ScrollUpButton
40
+ ref={ref}
41
+ className={ny(
42
+ 'flex cursor-default items-center justify-center py-1',
43
+ className,
44
+ )}
45
+ {...props}
46
+ >
47
+ <ChevronUp className="size-4" />
48
+ </SelectPrimitive.ScrollUpButton>
49
+ ))
50
+ SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
51
+
52
+ const SelectScrollDownButton = React.forwardRef<
53
+ React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
54
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
55
+ >(({ className, ...props }, ref) => (
56
+ <SelectPrimitive.ScrollDownButton
57
+ ref={ref}
58
+ className={ny(
59
+ 'flex cursor-default items-center justify-center py-1',
60
+ className,
61
+ )}
62
+ {...props}
63
+ >
64
+ <ChevronDown className="size-4" />
65
+ </SelectPrimitive.ScrollDownButton>
66
+ ))
67
+ SelectScrollDownButton.displayName
68
+ = SelectPrimitive.ScrollDownButton.displayName
69
+
70
+ const SelectContent = React.forwardRef<
71
+ React.ElementRef<typeof SelectPrimitive.Content>,
72
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
73
+ >(({ className, children, position = 'popper', ...props }, ref) => (
74
+ <SelectPrimitive.Portal>
75
+ <SelectPrimitive.Content
76
+ ref={ref}
77
+ className={ny(
78
+ 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-32 overflow-hidden rounded-2xl border shadow-md',
79
+ position === 'popper'
80
+ && 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
81
+ className,
82
+ )}
83
+ position={position}
84
+ {...props}
85
+ >
86
+ <SelectScrollUpButton />
87
+ <SelectPrimitive.Viewport
88
+ className={ny(
89
+ 'p-1',
90
+ position === 'popper'
91
+ && 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
92
+ )}
93
+ >
94
+ {children}
95
+ </SelectPrimitive.Viewport>
96
+ <SelectScrollDownButton />
97
+ </SelectPrimitive.Content>
98
+ </SelectPrimitive.Portal>
99
+ ))
100
+ SelectContent.displayName = SelectPrimitive.Content.displayName
101
+
102
+ const SelectLabel = React.forwardRef<
103
+ React.ElementRef<typeof SelectPrimitive.Label>,
104
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
105
+ >(({ className, ...props }, ref) => (
106
+ <SelectPrimitive.Label
107
+ ref={ref}
108
+ className={ny('py-1.5 pl-8 pr-2 text-sm font-semibold', className)}
109
+ {...props}
110
+ />
111
+ ))
112
+ SelectLabel.displayName = SelectPrimitive.Label.displayName
113
+
114
+ const SelectItem = React.forwardRef<
115
+ React.ElementRef<typeof SelectPrimitive.Item>,
116
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
117
+ >(({ className, children, ...props }, ref) => (
118
+ <SelectPrimitive.Item
119
+ ref={ref}
120
+ className={ny(
121
+ 'focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
122
+ className,
123
+ )}
124
+ {...props}
125
+ >
126
+ <span className="absolute left-2 flex size-3.5 items-center justify-center">
127
+ <SelectPrimitive.ItemIndicator>
128
+ <Check className="size-4" />
129
+ </SelectPrimitive.ItemIndicator>
130
+ </span>
131
+
132
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
133
+ </SelectPrimitive.Item>
134
+ ))
135
+ SelectItem.displayName = SelectPrimitive.Item.displayName
136
+
137
+ const SelectSeparator = React.forwardRef<
138
+ React.ElementRef<typeof SelectPrimitive.Separator>,
139
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
140
+ >(({ className, ...props }, ref) => (
141
+ <SelectPrimitive.Separator
142
+ ref={ref}
143
+ className={ny('bg-muted -mx-1 my-1 h-px', className)}
144
+ {...props}
145
+ />
146
+ ))
147
+ SelectSeparator.displayName = SelectPrimitive.Separator.displayName
148
+
149
+ export {
150
+ Select,
151
+ SelectGroup,
152
+ SelectValue,
153
+ SelectTrigger,
154
+ SelectContent,
155
+ SelectLabel,
156
+ SelectItem,
157
+ SelectSeparator,
158
+ SelectScrollUpButton,
159
+ SelectScrollDownButton,
160
+ }
components/ui/slider.tsx ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import * as SliderPrimitive from '@radix-ui/react-slider'
5
+
6
+ import { ny } from '@/lib/utils'
7
+
8
+ interface SliderProps extends React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> {
9
+ showSteps?: 'none' | 'half' | 'full'
10
+ formatLabel?: (value: number) => string
11
+ formatLabelSide?: string
12
+ }
13
+
14
+ const Slider = React.forwardRef<
15
+ React.ElementRef<typeof SliderPrimitive.Root>,
16
+ SliderProps
17
+ >(({ className, showSteps = 'none', formatLabel, formatLabelSide = 'top', ...props }, ref) => {
18
+ const { min = 0, max = 100, step = 1, orientation = 'horizontal', value, defaultValue, onValueChange } = props
19
+ const [hoveredThumbIndex, setHoveredThumbIndex] = React.useState<boolean>(false)
20
+ const numberOfSteps = Math.floor((max - min) / step)
21
+ const stepLines = Array.from({ length: numberOfSteps }, (_, index) => index * step + min)
22
+
23
+ const initialValue = Array.isArray(value) ? value : (Array.isArray(defaultValue) ? defaultValue : [min, max])
24
+ const [localValues, setLocalValues] = React.useState<number[]>(initialValue)
25
+
26
+ React.useEffect(() => {
27
+ if (!isEqual(value, localValues))
28
+ setLocalValues(Array.isArray(value) ? value : (Array.isArray(defaultValue) ? defaultValue : [min, max]))
29
+ }, [min, max, value])
30
+
31
+ const handleValueChange = (newValues: number[]) => {
32
+ setLocalValues(newValues)
33
+ if (onValueChange)
34
+ onValueChange(newValues)
35
+ }
36
+
37
+ function isEqual(array1: number[] | undefined, array2: number[] | undefined) {
38
+ array1 = array1 ?? []
39
+ array2 = array2 ?? []
40
+
41
+ if (array1.length !== array2.length)
42
+ return false
43
+
44
+ for (let i = 0; i < array1.length; i++) {
45
+ if (array1[i] !== array2[i])
46
+ return false
47
+ }
48
+
49
+ return true
50
+ }
51
+
52
+ return (
53
+ <SliderPrimitive.Root
54
+ ref={ref}
55
+ className={ny(
56
+ 'relative flex cursor-pointer touch-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
57
+ orientation === 'horizontal' ? 'w-full items-center' : 'h-full justify-center',
58
+ className,
59
+ )}
60
+ min={min}
61
+ max={max}
62
+ step={step}
63
+ value={localValues}
64
+ onValueChange={value => handleValueChange(value)}
65
+ {...props}
66
+ onFocus={() => setHoveredThumbIndex(true)}
67
+ onBlur={() => setHoveredThumbIndex(false)}
68
+ >
69
+ <SliderPrimitive.Track className={ny(
70
+ 'bg-primary/20 relative grow overflow-hidden rounded-full',
71
+ orientation === 'horizontal' ? 'h-1.5 w-full' : 'h-full w-1.5',
72
+ )}
73
+ >
74
+ <SliderPrimitive.Range className={ny(
75
+ 'bg-primary absolute',
76
+ orientation === 'horizontal' ? 'h-full' : 'w-full',
77
+ )}
78
+ />
79
+ {showSteps !== undefined && showSteps !== 'none' && stepLines.map((value, index) => {
80
+ if (value === min || value === max)
81
+ return null
82
+
83
+ const positionPercentage = ((value - min) / (max - min)) * 100
84
+ const adjustedPosition = 50 + (positionPercentage - 50) * 0.96
85
+ return (
86
+ <div
87
+ key={index}
88
+ className={ny(
89
+ { 'w-0.5 h-2': orientation !== 'vertical', 'w-2 h-0.5': orientation === 'vertical' },
90
+ 'bg-muted-foreground absolute',
91
+ {
92
+ 'left-1': orientation === 'vertical' && showSteps === 'half',
93
+ 'top-1': orientation !== 'vertical' && showSteps === 'half',
94
+ 'left-0': orientation === 'vertical' && showSteps === 'full',
95
+ 'top-0': orientation !== 'vertical' && showSteps === 'full',
96
+ '-translate-x-1/2': orientation !== 'vertical',
97
+ '-translate-y-1/2': orientation === 'vertical',
98
+ },
99
+ )}
100
+ style={{
101
+ [orientation === 'vertical' ? 'bottom' : 'left']: `${adjustedPosition}%`,
102
+ }}
103
+ />
104
+ )
105
+ })}
106
+
107
+ </SliderPrimitive.Track>
108
+ {localValues.map((numberStep, index) => (
109
+ <SliderPrimitive.Thumb
110
+ key={index}
111
+ className={ny(
112
+ 'border-primary/50 bg-background focus-visible:ring-ring block size-4 rounded-full border shadow transition-colors focus-visible:outline-none focus-visible:ring-1',
113
+ )}
114
+ >
115
+ {hoveredThumbIndex && formatLabel && (
116
+ <div
117
+ className={ny(
118
+ { 'bottom-8 left-1/2 -translate-x-1/2': formatLabelSide === 'top' },
119
+ { 'top-8 left-1/2 -translate-x-1/2': formatLabelSide === 'bottom' },
120
+ { 'right-8 -translate-y-1/4': formatLabelSide === 'left' },
121
+ { 'left-8 -translate-y-1/4': formatLabelSide === 'right' },
122
+ 'bg-popover text-popover-foreground absolute z-30 w-max items-center justify-items-center rounded-md border px-2 py-1 text-center shadow-sm',
123
+ )}
124
+ >
125
+ {formatLabel(numberStep)}
126
+ </div>
127
+ )}
128
+ </SliderPrimitive.Thumb>
129
+ ))}
130
+ </SliderPrimitive.Root>
131
+ )
132
+ })
133
+
134
+ Slider.displayName = SliderPrimitive.Root.displayName
135
+
136
+ export { Slider }
components/ui/tooltip.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5
+
6
+ import { ny } from "@/lib/utils"
7
+
8
+ const TooltipProvider = TooltipPrimitive.Provider
9
+
10
+ const Tooltip = TooltipPrimitive.Root
11
+
12
+ const TooltipTrigger = TooltipPrimitive.Trigger
13
+
14
+ const TooltipContent = React.forwardRef<
15
+ React.ElementRef<typeof TooltipPrimitive.Content>,
16
+ React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
17
+ >(({ className, sideOffset = 4, ...props }, ref) => (
18
+ <TooltipPrimitive.Content
19
+ ref={ref}
20
+ sideOffset={sideOffset}
21
+ className={ny(
22
+ "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ ))
28
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName
29
+
30
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
lib/utils.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { type ClassValue, clsx } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function ny(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
middleware.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from 'next/server';
2
+ import type { NextRequest } from 'next/server';
3
+
4
+ export function middleware(request: NextRequest) {
5
+ const isAuthenticated = request.cookies.get('isAuthenticated')?.value === 'true';
6
+ const isPasswordPage = request.nextUrl.pathname === '/password';
7
+
8
+ if (!isAuthenticated && !isPasswordPage) {
9
+ return NextResponse.redirect(new URL('/password', request.url));
10
+ }
11
+
12
+ if (isAuthenticated && isPasswordPage) {
13
+ return NextResponse.redirect(new URL('/', request.url));
14
+ }
15
+
16
+ return NextResponse.next();
17
+ }
18
+
19
+ export const config = {
20
+ matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
21
+ };
next.config.mjs ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ typescript: {
4
+ // !! WARN !!
5
+ // Dangerously allow production builds to successfully complete even if
6
+ // your project has type errors.
7
+ // !! WARN !!
8
+ ignoreBuildErrors: true,
9
+ },
10
+ rewrites: async () => {
11
+ return [
12
+ {
13
+ source: "/api/:path*",
14
+ destination:
15
+ process.env.NODE_ENV === "development"
16
+ ? "http://127.0.0.1:8000/api/:path*"
17
+ : "/api/",
18
+ },
19
+ {
20
+ source: "/docs",
21
+ destination:
22
+ process.env.NODE_ENV === "development"
23
+ ? "http://127.0.0.1:8000/docs"
24
+ : "/api/docs",
25
+ },
26
+ {
27
+ source: "/openapi.json",
28
+ destination:
29
+ process.env.NODE_ENV === "development"
30
+ ? "http://127.0.0.1:8000/openapi.json"
31
+ : "/api/openapi.json",
32
+ },
33
+ ];
34
+ },
35
+ };
36
+
37
+ export default nextConfig;
nyxbui.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://nyxbui.design/schema.json",
3
+ "style": "default",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.ts",
8
+ "css": "app/globals.css",
9
+ "baseColor": "slate",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils"
16
+ }
17
+ }
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "fastapi-corrector",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "fastapi-dev": "pip install -r requirements.txt && uvicorn app.api.main:app --reload",
7
+ "next-dev": "next dev",
8
+ "dev": "next dev",
9
+ "build": "next build",
10
+ "start": "next start",
11
+ "lint": "next lint"
12
+ },
13
+ "dependencies": {
14
+ "@fancyapps/ui": "^5.0.36",
15
+ "@radix-ui/react-select": "^2.1.1",
16
+ "@radix-ui/react-slider": "^1.2.0",
17
+ "@radix-ui/react-slot": "^1.1.0",
18
+ "@radix-ui/react-tooltip": "^1.1.2",
19
+ "class-variance-authority": "^0.7.0",
20
+ "clsx": "^2.1.1",
21
+ "cobe": "^0.6.3",
22
+ "concurrently": "^9.0.0",
23
+ "framer-motion": "^11.5.4",
24
+ "lucide-react": "^0.439.0",
25
+ "next": "14.2.9",
26
+ "next-themes": "^0.3.0",
27
+ "openai": "^4.5.0",
28
+ "pg": "^8.12.0",
29
+ "react": "^18",
30
+ "react-dom": "^18",
31
+ "react-spring": "^9.7.4",
32
+ "tailwind-merge": "^2.5.2",
33
+ "tailwindcss-animate": "^1.0.7",
34
+ "uuid": "^10.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@tailwindcss/container-queries": "^0.1.1",
38
+ "@tailwindcss/forms": "^0.5.8",
39
+ "@types/node": "^20",
40
+ "@types/react": "^18",
41
+ "@types/react-dom": "^18",
42
+ "autoprefixer": "^10.4.20",
43
+ "eslint": "^8",
44
+ "eslint-config-next": "14.2.9",
45
+ "postcss": "^8",
46
+ "tailwindcss": "^3.4.10",
47
+ "typescript": "^5"
48
+ },
49
+ "packageManager": "pnpm@9.10.0"
50
+ }
pnpm-lock.yaml ADDED
The diff for this file is too large to render. See raw diff