Delete frontend
Browse files- frontend/.gitignore +0 -35
- frontend/next.config.js +0 -31
- frontend/package-lock.json +0 -0
- frontend/package.json +0 -33
- frontend/postcss.config.js +0 -7
- frontend/public/.gitkeep +0 -2
- frontend/src/app/globals.css +0 -192
- frontend/src/app/layout.tsx +0 -23
- frontend/src/app/page.tsx +0 -1106
- frontend/src/components/ChatInterface.tsx +0 -212
- frontend/src/components/CodeEditor.tsx +0 -101
- frontend/src/components/ControlPanel.tsx +0 -244
- frontend/src/components/Header.tsx +0 -173
- frontend/src/components/LandingPage.tsx +0 -1200
- frontend/src/lib/api.ts +0 -541
- frontend/src/lib/auth.ts +0 -287
- frontend/src/types/index.ts +0 -69
- frontend/tailwind.config.js +0 -29
- frontend/tsconfig.json +0 -41
frontend/.gitignore
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
# dependencies
|
| 2 |
-
/node_modules
|
| 3 |
-
/.pnp
|
| 4 |
-
.pnp.js
|
| 5 |
-
|
| 6 |
-
# testing
|
| 7 |
-
/coverage
|
| 8 |
-
|
| 9 |
-
# next.js
|
| 10 |
-
/.next/
|
| 11 |
-
/out/
|
| 12 |
-
|
| 13 |
-
# production
|
| 14 |
-
/build
|
| 15 |
-
|
| 16 |
-
# misc
|
| 17 |
-
.DS_Store
|
| 18 |
-
*.pem
|
| 19 |
-
|
| 20 |
-
# debug
|
| 21 |
-
npm-debug.log*
|
| 22 |
-
yarn-debug.log*
|
| 23 |
-
yarn-error.log*
|
| 24 |
-
|
| 25 |
-
# local env files
|
| 26 |
-
.env*.local
|
| 27 |
-
.env
|
| 28 |
-
|
| 29 |
-
# vercel
|
| 30 |
-
.vercel
|
| 31 |
-
|
| 32 |
-
# typescript
|
| 33 |
-
*.tsbuildinfo
|
| 34 |
-
next-env.d.ts
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/next.config.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
| 1 |
-
/** @type {import('next').NextConfig} */
|
| 2 |
-
const nextConfig = {
|
| 3 |
-
output: 'standalone',
|
| 4 |
-
reactStrictMode: true,
|
| 5 |
-
// Only set NEXT_PUBLIC_API_URL if explicitly provided
|
| 6 |
-
// In Docker Space, we want it undefined so frontend uses relative URLs (proxied by Next.js)
|
| 7 |
-
...(process.env.NEXT_PUBLIC_API_URL && {
|
| 8 |
-
env: {
|
| 9 |
-
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
|
| 10 |
-
},
|
| 11 |
-
}),
|
| 12 |
-
async rewrites() {
|
| 13 |
-
// In Docker Space, proxy /api/* requests to backend on port 8000
|
| 14 |
-
// This allows frontend (7860) to communicate with backend (8000) internally
|
| 15 |
-
const API_HOST = process.env.BACKEND_HOST || 'http://localhost:8000';
|
| 16 |
-
console.log(`[Next.js] Setting up API proxy: /api/* -> ${API_HOST}/api/*`);
|
| 17 |
-
return [
|
| 18 |
-
{
|
| 19 |
-
source: '/api/:path*',
|
| 20 |
-
destination: `${API_HOST}/api/:path*`,
|
| 21 |
-
},
|
| 22 |
-
{
|
| 23 |
-
source: '/ws/:path*',
|
| 24 |
-
destination: `${API_HOST}/ws/:path*`,
|
| 25 |
-
},
|
| 26 |
-
];
|
| 27 |
-
},
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
module.exports = nextConfig
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/package-lock.json
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/package.json
DELETED
|
@@ -1,33 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"name": "anycoder-frontend",
|
| 3 |
-
"version": "1.0.0",
|
| 4 |
-
"private": true,
|
| 5 |
-
"scripts": {
|
| 6 |
-
"dev": "next dev",
|
| 7 |
-
"build": "next build",
|
| 8 |
-
"start": "next start -p 7860",
|
| 9 |
-
"lint": "next lint"
|
| 10 |
-
},
|
| 11 |
-
"dependencies": {
|
| 12 |
-
"@monaco-editor/react": "^4.6.0",
|
| 13 |
-
"axios": "^1.7.2",
|
| 14 |
-
"next": "^16.0.10",
|
| 15 |
-
"prismjs": "^1.29.0",
|
| 16 |
-
"react": "^19.2.3",
|
| 17 |
-
"react-dom": "^19.2.3",
|
| 18 |
-
"react-markdown": "^9.0.1",
|
| 19 |
-
"remark-gfm": "^4.0.0"
|
| 20 |
-
},
|
| 21 |
-
"devDependencies": {
|
| 22 |
-
"@types/node": "^20",
|
| 23 |
-
"@types/prismjs": "^1.26.4",
|
| 24 |
-
"@types/react": "^18",
|
| 25 |
-
"@types/react-dom": "^18",
|
| 26 |
-
"autoprefixer": "^10.4.19",
|
| 27 |
-
"eslint": "^9.39.2",
|
| 28 |
-
"eslint-config-next": "^16.0.10",
|
| 29 |
-
"postcss": "^8.4.39",
|
| 30 |
-
"tailwindcss": "^3.4.4",
|
| 31 |
-
"typescript": "^5"
|
| 32 |
-
}
|
| 33 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/postcss.config.js
DELETED
|
@@ -1,7 +0,0 @@
|
|
| 1 |
-
module.exports = {
|
| 2 |
-
plugins: {
|
| 3 |
-
tailwindcss: {},
|
| 4 |
-
autoprefixer: {},
|
| 5 |
-
},
|
| 6 |
-
}
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/public/.gitkeep
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
# This file ensures the public directory is tracked by git
|
| 2 |
-
|
|
|
|
|
|
|
|
|
frontend/src/app/globals.css
DELETED
|
@@ -1,192 +0,0 @@
|
|
| 1 |
-
@tailwind base;
|
| 2 |
-
@tailwind components;
|
| 3 |
-
@tailwind utilities;
|
| 4 |
-
|
| 5 |
-
* {
|
| 6 |
-
box-sizing: border-box;
|
| 7 |
-
padding: 0;
|
| 8 |
-
margin: 0;
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
html,
|
| 12 |
-
body {
|
| 13 |
-
height: 100%;
|
| 14 |
-
overflow: hidden;
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
body {
|
| 18 |
-
color: #e5e5e7;
|
| 19 |
-
background: #1d1d1f;
|
| 20 |
-
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', sans-serif;
|
| 21 |
-
-webkit-font-smoothing: antialiased;
|
| 22 |
-
-moz-osx-font-smoothing: grayscale;
|
| 23 |
-
letter-spacing: -0.01em;
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
/* Apple-style scrollbar */
|
| 27 |
-
::-webkit-scrollbar {
|
| 28 |
-
width: 12px;
|
| 29 |
-
height: 12px;
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
::-webkit-scrollbar-track {
|
| 33 |
-
background: transparent;
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
::-webkit-scrollbar-thumb {
|
| 37 |
-
background: rgba(255, 255, 255, 0.2);
|
| 38 |
-
border-radius: 10px;
|
| 39 |
-
border: 3px solid #1d1d1f;
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
::-webkit-scrollbar-thumb:hover {
|
| 43 |
-
background: rgba(255, 255, 255, 0.3);
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
::-webkit-scrollbar-corner {
|
| 47 |
-
background: #1d1d1f;
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
/* Markdown styling in chat - VS Code style */
|
| 51 |
-
.prose {
|
| 52 |
-
max-width: none;
|
| 53 |
-
color: #cccccc;
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
.prose code {
|
| 57 |
-
background-color: #2d2d30;
|
| 58 |
-
color: #d4d4d4;
|
| 59 |
-
padding: 0.2em 0.4em;
|
| 60 |
-
border-radius: 3px;
|
| 61 |
-
font-size: 0.875em;
|
| 62 |
-
font-family: 'SF Mono', 'Monaco', 'Menlo', 'Courier New', monospace;
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
.prose pre {
|
| 66 |
-
background-color: #1e1e1e;
|
| 67 |
-
padding: 1em;
|
| 68 |
-
border-radius: 4px;
|
| 69 |
-
overflow-x: auto;
|
| 70 |
-
border: 1px solid #3e3e42;
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
.prose pre code {
|
| 74 |
-
background-color: transparent;
|
| 75 |
-
padding: 0;
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
.prose p {
|
| 79 |
-
margin: 0.5em 0;
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
-
.prose a {
|
| 83 |
-
color: #3794ff;
|
| 84 |
-
text-decoration: none;
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
.prose a:hover {
|
| 88 |
-
text-decoration: underline;
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
/* Selection color - Apple style */
|
| 92 |
-
::selection {
|
| 93 |
-
background-color: rgba(0, 122, 255, 0.3);
|
| 94 |
-
color: #ffffff;
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
::-moz-selection {
|
| 98 |
-
background-color: rgba(0, 122, 255, 0.3);
|
| 99 |
-
color: #ffffff;
|
| 100 |
-
}
|
| 101 |
-
|
| 102 |
-
/* Apple-style focus rings */
|
| 103 |
-
button:focus-visible,
|
| 104 |
-
input:focus-visible,
|
| 105 |
-
select:focus-visible {
|
| 106 |
-
outline: 2px solid rgba(0, 122, 255, 0.6);
|
| 107 |
-
outline-offset: 2px;
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
/* Smooth transitions */
|
| 111 |
-
* {
|
| 112 |
-
transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
|
| 113 |
-
}
|
| 114 |
-
|
| 115 |
-
/* Animation utilities */
|
| 116 |
-
@keyframes fade-in {
|
| 117 |
-
from {
|
| 118 |
-
opacity: 0;
|
| 119 |
-
}
|
| 120 |
-
to {
|
| 121 |
-
opacity: 1;
|
| 122 |
-
}
|
| 123 |
-
}
|
| 124 |
-
|
| 125 |
-
.animate-in {
|
| 126 |
-
animation: fade-in 0.3s ease-in-out;
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
-
/* Resize handle styles */
|
| 130 |
-
.resize-handle {
|
| 131 |
-
position: relative;
|
| 132 |
-
width: 4px;
|
| 133 |
-
cursor: col-resize;
|
| 134 |
-
user-select: none;
|
| 135 |
-
background: transparent;
|
| 136 |
-
transition: background-color 0.2s ease, width 0.15s ease;
|
| 137 |
-
flex-shrink: 0;
|
| 138 |
-
display: flex;
|
| 139 |
-
align-items: center;
|
| 140 |
-
justify-content: center;
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
/* Clickable area (extends beyond visible width) */
|
| 144 |
-
.resize-handle::before {
|
| 145 |
-
content: '';
|
| 146 |
-
position: absolute;
|
| 147 |
-
inset: 0;
|
| 148 |
-
left: -4px;
|
| 149 |
-
right: -4px;
|
| 150 |
-
}
|
| 151 |
-
|
| 152 |
-
/* Visual indicator (three dots) */
|
| 153 |
-
.resize-handle::after {
|
| 154 |
-
content: '⋮';
|
| 155 |
-
position: relative;
|
| 156 |
-
z-index: 1;
|
| 157 |
-
font-size: 16px;
|
| 158 |
-
color: rgba(134, 134, 139, 0.4);
|
| 159 |
-
transition: color 0.2s ease;
|
| 160 |
-
pointer-events: none;
|
| 161 |
-
line-height: 1;
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
.resize-handle:hover {
|
| 165 |
-
background: rgba(0, 123, 255, 0.2);
|
| 166 |
-
}
|
| 167 |
-
|
| 168 |
-
.resize-handle:hover::after {
|
| 169 |
-
color: rgba(0, 123, 255, 0.8);
|
| 170 |
-
}
|
| 171 |
-
|
| 172 |
-
.resize-handle:active,
|
| 173 |
-
.resize-handle.resizing {
|
| 174 |
-
background: rgba(0, 123, 255, 0.4);
|
| 175 |
-
width: 4px;
|
| 176 |
-
}
|
| 177 |
-
|
| 178 |
-
.resize-handle:active::after,
|
| 179 |
-
.resize-handle.resizing::after {
|
| 180 |
-
color: rgba(0, 123, 255, 1);
|
| 181 |
-
}
|
| 182 |
-
|
| 183 |
-
/* Prevent text selection during resize */
|
| 184 |
-
body.resizing {
|
| 185 |
-
user-select: none !important;
|
| 186 |
-
cursor: col-resize !important;
|
| 187 |
-
}
|
| 188 |
-
|
| 189 |
-
body.resizing * {
|
| 190 |
-
cursor: col-resize !important;
|
| 191 |
-
}
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/app/layout.tsx
DELETED
|
@@ -1,23 +0,0 @@
|
|
| 1 |
-
import type { Metadata } from 'next';
|
| 2 |
-
import { Inter } from 'next/font/google';
|
| 3 |
-
import './globals.css';
|
| 4 |
-
|
| 5 |
-
const inter = Inter({ subsets: ['latin'] });
|
| 6 |
-
|
| 7 |
-
export const metadata: Metadata = {
|
| 8 |
-
title: 'AnyCoder - AI Code Generator',
|
| 9 |
-
description: 'Generate code with AI - supports HTML, Gradio, React, Streamlit, and more',
|
| 10 |
-
};
|
| 11 |
-
|
| 12 |
-
export default function RootLayout({
|
| 13 |
-
children,
|
| 14 |
-
}: {
|
| 15 |
-
children: React.ReactNode;
|
| 16 |
-
}) {
|
| 17 |
-
return (
|
| 18 |
-
<html lang="en">
|
| 19 |
-
<body className={inter.className}>{children}</body>
|
| 20 |
-
</html>
|
| 21 |
-
);
|
| 22 |
-
}
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/app/page.tsx
DELETED
|
@@ -1,1106 +0,0 @@
|
|
| 1 |
-
'use client';
|
| 2 |
-
|
| 3 |
-
import { useState, useEffect, useRef } from 'react';
|
| 4 |
-
import { flushSync } from 'react-dom';
|
| 5 |
-
import Header from '@/components/Header';
|
| 6 |
-
import LandingPage from '@/components/LandingPage';
|
| 7 |
-
import ChatInterface from '@/components/ChatInterface';
|
| 8 |
-
import CodeEditor from '@/components/CodeEditor';
|
| 9 |
-
import ControlPanel from '@/components/ControlPanel';
|
| 10 |
-
import { apiClient } from '@/lib/api';
|
| 11 |
-
import { isAuthenticated as checkIsAuthenticated, getStoredToken } from '@/lib/auth';
|
| 12 |
-
import type { Message, Language, CodeGenerationRequest, Model } from '@/types';
|
| 13 |
-
|
| 14 |
-
export default function Home() {
|
| 15 |
-
// Initialize messages as empty array (will load from localStorage in useEffect)
|
| 16 |
-
const [messages, setMessages] = useState<Message[]>([]);
|
| 17 |
-
|
| 18 |
-
const [generatedCode, setGeneratedCode] = useState('');
|
| 19 |
-
const [selectedLanguage, setSelectedLanguage] = useState<Language>('html');
|
| 20 |
-
const [selectedModel, setSelectedModel] = useState('moonshotai/Kimi-K2.6');
|
| 21 |
-
const [models, setModels] = useState<Model[]>([]);
|
| 22 |
-
const [isGenerating, setIsGenerating] = useState(false);
|
| 23 |
-
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
| 24 |
-
const [currentRepoId, setCurrentRepoId] = useState<string | null>(null); // Track imported/deployed space
|
| 25 |
-
const [username, setUsername] = useState<string | null>(null); // Track current user
|
| 26 |
-
const [pendingPR, setPendingPR] = useState<{ repoId: string; language: Language } | null>(null); // Track pending PR after redesign
|
| 27 |
-
const pendingPRRef = useRef<{ repoId: string; language: Language } | null>(null); // Ref for immediate access
|
| 28 |
-
|
| 29 |
-
// Landing page state - show landing page if no messages exist
|
| 30 |
-
const [showLandingPage, setShowLandingPage] = useState(true);
|
| 31 |
-
|
| 32 |
-
// Mobile view state: 'chat', 'editor', or 'settings' - start on chat for mobile
|
| 33 |
-
const [mobileView, setMobileView] = useState<'chat' | 'editor' | 'settings'>('chat');
|
| 34 |
-
|
| 35 |
-
// Resizable sidebar widths (in pixels)
|
| 36 |
-
const [chatSidebarWidth, setChatSidebarWidth] = useState(320);
|
| 37 |
-
const [settingsSidebarWidth, setSettingsSidebarWidth] = useState(288);
|
| 38 |
-
const [isResizingChat, setIsResizingChat] = useState(false);
|
| 39 |
-
const [isResizingSettings, setIsResizingSettings] = useState(false);
|
| 40 |
-
const [isDesktop, setIsDesktop] = useState(false);
|
| 41 |
-
|
| 42 |
-
// Debug: Log currentRepoId changes
|
| 43 |
-
useEffect(() => {
|
| 44 |
-
console.log('[App] 🔵 currentRepoId changed to:', currentRepoId);
|
| 45 |
-
}, [currentRepoId]);
|
| 46 |
-
|
| 47 |
-
// Clear cache on app startup to ensure fresh data and load models
|
| 48 |
-
useEffect(() => {
|
| 49 |
-
if (typeof window !== 'undefined') {
|
| 50 |
-
console.log('[Cache] Clearing models and languages cache on app startup');
|
| 51 |
-
localStorage.removeItem('anycoder_models');
|
| 52 |
-
localStorage.removeItem('anycoder_languages');
|
| 53 |
-
|
| 54 |
-
// Load models for checking image support
|
| 55 |
-
loadModels();
|
| 56 |
-
}
|
| 57 |
-
}, []); // Run once on mount
|
| 58 |
-
|
| 59 |
-
const loadModels = async () => {
|
| 60 |
-
try {
|
| 61 |
-
const modelsList = await apiClient.getModels();
|
| 62 |
-
console.log('[Models] Loaded models:', modelsList);
|
| 63 |
-
setModels(modelsList);
|
| 64 |
-
} catch (error) {
|
| 65 |
-
console.error('Failed to load models:', error);
|
| 66 |
-
}
|
| 67 |
-
};
|
| 68 |
-
|
| 69 |
-
// Check if current model supports images
|
| 70 |
-
// Show immediately for GLM-4.6V even before models load
|
| 71 |
-
const currentModelSupportsImages =
|
| 72 |
-
selectedModel === 'zai-org/GLM-4.6V:zai-org' ||
|
| 73 |
-
models.find(m => m.id === selectedModel)?.supports_images ||
|
| 74 |
-
false;
|
| 75 |
-
|
| 76 |
-
// Debug log for image support
|
| 77 |
-
useEffect(() => {
|
| 78 |
-
console.log('[Image Support] Selected model:', selectedModel);
|
| 79 |
-
console.log('[Image Support] Models loaded:', models.length);
|
| 80 |
-
console.log('[Image Support] Supports images:', currentModelSupportsImages);
|
| 81 |
-
}, [selectedModel, models, currentModelSupportsImages]);
|
| 82 |
-
|
| 83 |
-
// Load messages from localStorage on mount (client-side only to avoid hydration issues)
|
| 84 |
-
useEffect(() => {
|
| 85 |
-
if (typeof window !== 'undefined') {
|
| 86 |
-
const saved = localStorage.getItem('anycoder_messages');
|
| 87 |
-
if (saved) {
|
| 88 |
-
try {
|
| 89 |
-
const parsed = JSON.parse(saved);
|
| 90 |
-
console.log('[localStorage] Loaded messages from localStorage:', parsed.length, 'messages');
|
| 91 |
-
setMessages(parsed);
|
| 92 |
-
// If there are existing messages, show the full UI
|
| 93 |
-
if (parsed.length > 0) {
|
| 94 |
-
setShowLandingPage(false);
|
| 95 |
-
}
|
| 96 |
-
} catch (e) {
|
| 97 |
-
console.error('[localStorage] Failed to parse saved messages:', e);
|
| 98 |
-
}
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
// Load sidebar widths from localStorage
|
| 102 |
-
const savedChatWidth = localStorage.getItem('anycoder_chat_sidebar_width');
|
| 103 |
-
const savedSettingsWidth = localStorage.getItem('anycoder_settings_sidebar_width');
|
| 104 |
-
if (savedChatWidth) {
|
| 105 |
-
setChatSidebarWidth(parseInt(savedChatWidth, 10));
|
| 106 |
-
}
|
| 107 |
-
if (savedSettingsWidth) {
|
| 108 |
-
setSettingsSidebarWidth(parseInt(savedSettingsWidth, 10));
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
// Check if desktop on mount
|
| 112 |
-
const checkDesktop = () => {
|
| 113 |
-
setIsDesktop(window.innerWidth >= 768);
|
| 114 |
-
};
|
| 115 |
-
checkDesktop();
|
| 116 |
-
|
| 117 |
-
// Listen for window resize to update desktop status
|
| 118 |
-
window.addEventListener('resize', checkDesktop);
|
| 119 |
-
return () => window.removeEventListener('resize', checkDesktop);
|
| 120 |
-
}
|
| 121 |
-
}, []); // Empty deps = run once on mount
|
| 122 |
-
|
| 123 |
-
// Save messages to localStorage whenever they change (CRITICAL FOR PERSISTENCE!)
|
| 124 |
-
useEffect(() => {
|
| 125 |
-
if (typeof window !== 'undefined' && messages.length > 0) {
|
| 126 |
-
localStorage.setItem('anycoder_messages', JSON.stringify(messages));
|
| 127 |
-
console.log('[localStorage] Saved', messages.length, 'messages to localStorage');
|
| 128 |
-
}
|
| 129 |
-
}, [messages]);
|
| 130 |
-
|
| 131 |
-
// Track if we've attempted to fetch username to avoid repeated failures
|
| 132 |
-
const usernameFetchAttemptedRef = useRef(false);
|
| 133 |
-
// Track if backend appears to be unavailable (to avoid repeated failed requests)
|
| 134 |
-
const backendUnavailableRef = useRef(false);
|
| 135 |
-
|
| 136 |
-
// Check auth on mount and handle OAuth callback
|
| 137 |
-
useEffect(() => {
|
| 138 |
-
checkAuth();
|
| 139 |
-
|
| 140 |
-
// Check for OAuth callback in URL (handles ?session=token)
|
| 141 |
-
// initializeOAuth already handles this, but we call checkAuth to sync state
|
| 142 |
-
const urlParams = new URLSearchParams(window.location.search);
|
| 143 |
-
if (urlParams.get('session')) {
|
| 144 |
-
// OAuth callback - reset both flags and check auth after a brief delay
|
| 145 |
-
usernameFetchAttemptedRef.current = false;
|
| 146 |
-
backendUnavailableRef.current = false; // Reset backend status on OAuth callback
|
| 147 |
-
setTimeout(() => checkAuth(), 200);
|
| 148 |
-
}
|
| 149 |
-
}, []); // Only run once on mount
|
| 150 |
-
|
| 151 |
-
// Listen for storage changes (e.g., logout from another tab)
|
| 152 |
-
// Note: storage events only fire in OTHER tabs, not the current one
|
| 153 |
-
useEffect(() => {
|
| 154 |
-
const handleStorageChange = (e: StorageEvent) => {
|
| 155 |
-
if (e.key === 'hf_oauth_token' || e.key === 'hf_user_info') {
|
| 156 |
-
// Only reset username fetch if we have a token (might be logging in)
|
| 157 |
-
if (e.newValue) {
|
| 158 |
-
usernameFetchAttemptedRef.current = false;
|
| 159 |
-
backendUnavailableRef.current = false; // Reset backend status on login
|
| 160 |
-
}
|
| 161 |
-
checkAuth();
|
| 162 |
-
}
|
| 163 |
-
};
|
| 164 |
-
|
| 165 |
-
window.addEventListener('storage', handleStorageChange);
|
| 166 |
-
return () => window.removeEventListener('storage', handleStorageChange);
|
| 167 |
-
}, []);
|
| 168 |
-
|
| 169 |
-
// Listen for authentication expiration events
|
| 170 |
-
useEffect(() => {
|
| 171 |
-
const handleAuthExpired = (e: CustomEvent) => {
|
| 172 |
-
console.log('[Auth] Session expired:', e.detail?.message);
|
| 173 |
-
// Clear authentication state
|
| 174 |
-
setIsAuthenticated(false);
|
| 175 |
-
setUsername(null);
|
| 176 |
-
apiClient.setToken(null);
|
| 177 |
-
|
| 178 |
-
// Show alert to user
|
| 179 |
-
if (typeof window !== 'undefined') {
|
| 180 |
-
alert(e.detail?.message || 'Your session has expired. Please sign in again.');
|
| 181 |
-
}
|
| 182 |
-
};
|
| 183 |
-
|
| 184 |
-
window.addEventListener('auth-expired', handleAuthExpired as EventListener);
|
| 185 |
-
return () => window.removeEventListener('auth-expired', handleAuthExpired as EventListener);
|
| 186 |
-
}, []);
|
| 187 |
-
|
| 188 |
-
// Listen for window focus (user returns to tab after OAuth redirect)
|
| 189 |
-
// Only check if backend was available before or if we're authenticated with token
|
| 190 |
-
useEffect(() => {
|
| 191 |
-
const handleFocus = () => {
|
| 192 |
-
// Only reset and check if we're authenticated (might have logged in elsewhere)
|
| 193 |
-
// Don't reset if backend is known to be unavailable and we're not authenticated
|
| 194 |
-
const authenticated = checkIsAuthenticated();
|
| 195 |
-
if (authenticated) {
|
| 196 |
-
usernameFetchAttemptedRef.current = false;
|
| 197 |
-
backendUnavailableRef.current = false; // Reset backend status - might be back up
|
| 198 |
-
}
|
| 199 |
-
checkAuth();
|
| 200 |
-
};
|
| 201 |
-
|
| 202 |
-
window.addEventListener('focus', handleFocus);
|
| 203 |
-
return () => window.removeEventListener('focus', handleFocus);
|
| 204 |
-
}, []);
|
| 205 |
-
|
| 206 |
-
const checkAuth = async () => {
|
| 207 |
-
const authenticated = checkIsAuthenticated();
|
| 208 |
-
setIsAuthenticated(authenticated);
|
| 209 |
-
|
| 210 |
-
// Make sure API client has the token or clears it
|
| 211 |
-
if (authenticated) {
|
| 212 |
-
const token = getStoredToken();
|
| 213 |
-
if (token) {
|
| 214 |
-
apiClient.setToken(token);
|
| 215 |
-
|
| 216 |
-
// Get username from auth status (only if we don't have it yet and backend is available)
|
| 217 |
-
// Skip if backend is known to be unavailable to avoid repeated failed requests
|
| 218 |
-
if (!username && !usernameFetchAttemptedRef.current && !backendUnavailableRef.current) {
|
| 219 |
-
usernameFetchAttemptedRef.current = true;
|
| 220 |
-
try {
|
| 221 |
-
const authStatus = await apiClient.getAuthStatus();
|
| 222 |
-
if (authStatus.username) {
|
| 223 |
-
setUsername(authStatus.username);
|
| 224 |
-
backendUnavailableRef.current = false; // Backend is working
|
| 225 |
-
}
|
| 226 |
-
} catch (error: any) {
|
| 227 |
-
// Check if this is a connection error
|
| 228 |
-
const isConnectionError =
|
| 229 |
-
error.code === 'ECONNABORTED' ||
|
| 230 |
-
error.code === 'ECONNRESET' ||
|
| 231 |
-
error.code === 'ECONNREFUSED' ||
|
| 232 |
-
error.message?.includes('socket hang up') ||
|
| 233 |
-
error.message?.includes('timeout') ||
|
| 234 |
-
error.message?.includes('Network Error') ||
|
| 235 |
-
error.response?.status === 503 ||
|
| 236 |
-
error.response?.status === 502;
|
| 237 |
-
|
| 238 |
-
if (isConnectionError) {
|
| 239 |
-
// Mark backend as unavailable to avoid repeated requests
|
| 240 |
-
backendUnavailableRef.current = true;
|
| 241 |
-
// Don't reset attempt flag - keep it true so we don't retry until explicitly reset
|
| 242 |
-
// This prevents repeated failed requests when backend is down
|
| 243 |
-
} else {
|
| 244 |
-
// Non-connection error - log it and reset attempt flag
|
| 245 |
-
console.error('Failed to get username:', error);
|
| 246 |
-
usernameFetchAttemptedRef.current = false;
|
| 247 |
-
}
|
| 248 |
-
}
|
| 249 |
-
}
|
| 250 |
-
} else {
|
| 251 |
-
// Token missing but authenticated flag is true - clear state
|
| 252 |
-
setIsAuthenticated(false);
|
| 253 |
-
if (username) {
|
| 254 |
-
setUsername(null);
|
| 255 |
-
}
|
| 256 |
-
usernameFetchAttemptedRef.current = false;
|
| 257 |
-
backendUnavailableRef.current = false;
|
| 258 |
-
}
|
| 259 |
-
} else {
|
| 260 |
-
// Not authenticated - clear username and reset flags
|
| 261 |
-
apiClient.setToken(null);
|
| 262 |
-
if (username) {
|
| 263 |
-
setUsername(null);
|
| 264 |
-
}
|
| 265 |
-
usernameFetchAttemptedRef.current = false;
|
| 266 |
-
// Keep backendUnavailableRef as is - it's useful information even when not authenticated
|
| 267 |
-
}
|
| 268 |
-
};
|
| 269 |
-
|
| 270 |
-
const handleSendMessage = async (message: string, imageUrl?: string, overrideLanguage?: Language, overrideModel?: string, overrideRepoId?: string, shouldCreatePR?: boolean) => {
|
| 271 |
-
if (!isAuthenticated) {
|
| 272 |
-
alert('Please sign in with HuggingFace first! Click the "Sign in with Hugging Face" button in the header.');
|
| 273 |
-
return;
|
| 274 |
-
}
|
| 275 |
-
|
| 276 |
-
// Hide landing page and show full UI when first message is sent
|
| 277 |
-
if (showLandingPage) {
|
| 278 |
-
setShowLandingPage(false);
|
| 279 |
-
}
|
| 280 |
-
|
| 281 |
-
// Use override values if provided, otherwise use state
|
| 282 |
-
const language = overrideLanguage || selectedLanguage;
|
| 283 |
-
const model = overrideModel || selectedModel;
|
| 284 |
-
|
| 285 |
-
// Update state if override values provided
|
| 286 |
-
if (overrideLanguage) {
|
| 287 |
-
setSelectedLanguage(overrideLanguage);
|
| 288 |
-
}
|
| 289 |
-
if (overrideModel) {
|
| 290 |
-
setSelectedModel(overrideModel);
|
| 291 |
-
}
|
| 292 |
-
|
| 293 |
-
// If there's existing code, include it in the message context for modifications
|
| 294 |
-
let enhancedMessage = message;
|
| 295 |
-
const hasRealCode = generatedCode &&
|
| 296 |
-
generatedCode.length > 50 &&
|
| 297 |
-
!generatedCode.includes('Your generated code will appear here');
|
| 298 |
-
|
| 299 |
-
if (hasRealCode) {
|
| 300 |
-
enhancedMessage = `I have existing code in the editor. Please modify it based on my request.\n\nCurrent code:\n\`\`\`${language}\n${generatedCode}\n\`\`\`\n\nMy request: ${message}`;
|
| 301 |
-
}
|
| 302 |
-
|
| 303 |
-
// Add user message (show original message to user, but send enhanced to API)
|
| 304 |
-
console.log('[handleSendMessage] Received imageUrl:', imageUrl ? 'Yes' : 'No');
|
| 305 |
-
console.log('[handleSendMessage] Image URL length:', imageUrl?.length || 0);
|
| 306 |
-
|
| 307 |
-
const userMessage: Message = {
|
| 308 |
-
role: 'user',
|
| 309 |
-
content: message,
|
| 310 |
-
timestamp: new Date().toISOString(),
|
| 311 |
-
image_url: imageUrl,
|
| 312 |
-
};
|
| 313 |
-
setMessages((prev) => [...prev, userMessage]);
|
| 314 |
-
setIsGenerating(true);
|
| 315 |
-
|
| 316 |
-
// Clear previous code to show streaming from start
|
| 317 |
-
setGeneratedCode('');
|
| 318 |
-
|
| 319 |
-
// Prepare request with enhanced query that includes current code
|
| 320 |
-
// Use overrideRepoId if provided (from import/duplicate), otherwise use currentRepoId from state
|
| 321 |
-
const effectiveRepoId = overrideRepoId || currentRepoId || undefined;
|
| 322 |
-
|
| 323 |
-
console.log('[SendMessage] ========== GENERATION REQUEST ==========');
|
| 324 |
-
console.log('[SendMessage] overrideRepoId:', overrideRepoId);
|
| 325 |
-
console.log('[SendMessage] currentRepoId:', currentRepoId);
|
| 326 |
-
console.log('[SendMessage] effectiveRepoId (will use):', effectiveRepoId);
|
| 327 |
-
console.log('[SendMessage] ==========================================');
|
| 328 |
-
|
| 329 |
-
console.log('[Request] Building request with imageUrl:', imageUrl ? 'Yes' : 'No');
|
| 330 |
-
console.log('[Request] Image URL:', imageUrl?.substring(0, 50) + '...');
|
| 331 |
-
|
| 332 |
-
const request: CodeGenerationRequest = {
|
| 333 |
-
query: enhancedMessage,
|
| 334 |
-
language: language,
|
| 335 |
-
model_id: model,
|
| 336 |
-
provider: 'auto',
|
| 337 |
-
history: messages.map((m) => [m.role, m.content]),
|
| 338 |
-
agent_mode: false,
|
| 339 |
-
existing_repo_id: effectiveRepoId, // Pass duplicated/imported space ID for auto-deploy
|
| 340 |
-
skip_auto_deploy: !!shouldCreatePR, // Skip auto-deploy if creating PR
|
| 341 |
-
image_url: imageUrl, // For vision models like GLM-4.6V
|
| 342 |
-
};
|
| 343 |
-
|
| 344 |
-
const assistantMessage: Message = {
|
| 345 |
-
role: 'assistant',
|
| 346 |
-
content: '⏳ Generating code...',
|
| 347 |
-
timestamp: new Date().toISOString(),
|
| 348 |
-
};
|
| 349 |
-
|
| 350 |
-
// Add placeholder for assistant message
|
| 351 |
-
setMessages((prev) => [...prev, assistantMessage]);
|
| 352 |
-
|
| 353 |
-
// Stream the response
|
| 354 |
-
try {
|
| 355 |
-
apiClient.generateCodeStream(
|
| 356 |
-
request,
|
| 357 |
-
// onChunk - Update code editor in real-time with immediate flush
|
| 358 |
-
(chunk: string) => {
|
| 359 |
-
console.log('[Stream] Received chunk:', chunk.substring(0, 50), '... (length:', chunk.length, ')');
|
| 360 |
-
// Use flushSync to force immediate DOM update without React batching
|
| 361 |
-
flushSync(() => {
|
| 362 |
-
setGeneratedCode((prevCode) => {
|
| 363 |
-
const newCode = prevCode + chunk;
|
| 364 |
-
console.log('[Stream] Total code length:', newCode.length);
|
| 365 |
-
return newCode;
|
| 366 |
-
});
|
| 367 |
-
});
|
| 368 |
-
},
|
| 369 |
-
// onComplete
|
| 370 |
-
(code: string, reasoning?: string) => {
|
| 371 |
-
setGeneratedCode(code);
|
| 372 |
-
setIsGenerating(false);
|
| 373 |
-
|
| 374 |
-
// Update final message - include reasoning if available
|
| 375 |
-
setMessages((prev) => {
|
| 376 |
-
const newMessages = [...prev];
|
| 377 |
-
const content = reasoning
|
| 378 |
-
? `✅ Code generated successfully!\n\n**Reasoning:**\n${reasoning}\n\nCheck the editor →`
|
| 379 |
-
: '✅ Code generated successfully! Check the editor →';
|
| 380 |
-
|
| 381 |
-
newMessages[newMessages.length - 1] = {
|
| 382 |
-
...assistantMessage,
|
| 383 |
-
content: content,
|
| 384 |
-
};
|
| 385 |
-
return newMessages;
|
| 386 |
-
});
|
| 387 |
-
|
| 388 |
-
// Check if we need to create a PR (redesign with PR option)
|
| 389 |
-
console.log('[PR] onComplete - Checking pendingPR ref:', pendingPRRef.current);
|
| 390 |
-
console.log('[PR] onComplete - Checking pendingPR state:', pendingPR);
|
| 391 |
-
const prInfo = pendingPRRef.current;
|
| 392 |
-
if (prInfo) {
|
| 393 |
-
console.log('[PR] Creating pull request for:', prInfo.repoId);
|
| 394 |
-
createPullRequestAfterGeneration(prInfo.repoId, code, prInfo.language);
|
| 395 |
-
setPendingPR(null); // Clear state
|
| 396 |
-
pendingPRRef.current = null; // Clear ref
|
| 397 |
-
} else {
|
| 398 |
-
console.log('[PR] No pending PR - skipping PR creation');
|
| 399 |
-
}
|
| 400 |
-
},
|
| 401 |
-
// onError
|
| 402 |
-
(error: string) => {
|
| 403 |
-
setIsGenerating(false);
|
| 404 |
-
setMessages((prev) => {
|
| 405 |
-
const newMessages = [...prev];
|
| 406 |
-
newMessages[newMessages.length - 1] = {
|
| 407 |
-
...assistantMessage,
|
| 408 |
-
content: `❌ Error: ${error}`,
|
| 409 |
-
};
|
| 410 |
-
return newMessages;
|
| 411 |
-
});
|
| 412 |
-
},
|
| 413 |
-
// onDeploying
|
| 414 |
-
(message: string) => {
|
| 415 |
-
console.log('[Deploy] Deployment started:', message);
|
| 416 |
-
// Update message to show deployment in progress
|
| 417 |
-
setMessages((prev) => {
|
| 418 |
-
const newMessages = [...prev];
|
| 419 |
-
newMessages[newMessages.length - 1] = {
|
| 420 |
-
...assistantMessage,
|
| 421 |
-
content: `✅ Code generated successfully!\n\n${message}`,
|
| 422 |
-
};
|
| 423 |
-
return newMessages;
|
| 424 |
-
});
|
| 425 |
-
},
|
| 426 |
-
// onDeployed
|
| 427 |
-
(message: string, spaceUrl: string) => {
|
| 428 |
-
console.log('[Deploy] Deployment successful:', spaceUrl);
|
| 429 |
-
|
| 430 |
-
// Extract repo_id from space URL
|
| 431 |
-
const match = spaceUrl.match(/huggingface\.co\/spaces\/([^\/\s\)]+\/[^\/\s\)]+)/);
|
| 432 |
-
if (match) {
|
| 433 |
-
setCurrentRepoId(match[1]);
|
| 434 |
-
}
|
| 435 |
-
|
| 436 |
-
// Update message with deployment success - use backend message format for history tracking
|
| 437 |
-
setMessages((prev) => {
|
| 438 |
-
const newMessages = [...prev];
|
| 439 |
-
newMessages[newMessages.length - 1] = {
|
| 440 |
-
...assistantMessage,
|
| 441 |
-
content: `✅ Code generated successfully!\n\n${message}`,
|
| 442 |
-
};
|
| 443 |
-
return newMessages;
|
| 444 |
-
});
|
| 445 |
-
|
| 446 |
-
// Open the space URL in a new tab
|
| 447 |
-
window.open(spaceUrl, '_blank');
|
| 448 |
-
},
|
| 449 |
-
// onDeployError
|
| 450 |
-
(message: string) => {
|
| 451 |
-
console.log('[Deploy] Deployment error:', message);
|
| 452 |
-
// Update message to show deployment failed (but code generation succeeded)
|
| 453 |
-
setMessages((prev) => {
|
| 454 |
-
const newMessages = [...prev];
|
| 455 |
-
newMessages[newMessages.length - 1] = {
|
| 456 |
-
...assistantMessage,
|
| 457 |
-
content: `✅ Code generated successfully!\n\n${message}\n\nYou can still use the "Publish" button to deploy manually.`,
|
| 458 |
-
};
|
| 459 |
-
return newMessages;
|
| 460 |
-
});
|
| 461 |
-
}
|
| 462 |
-
);
|
| 463 |
-
} catch (error) {
|
| 464 |
-
setIsGenerating(false);
|
| 465 |
-
setMessages((prev) => {
|
| 466 |
-
const newMessages = [...prev];
|
| 467 |
-
newMessages[newMessages.length - 1] = {
|
| 468 |
-
...assistantMessage,
|
| 469 |
-
content: `❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
| 470 |
-
};
|
| 471 |
-
return newMessages;
|
| 472 |
-
});
|
| 473 |
-
}
|
| 474 |
-
};
|
| 475 |
-
|
| 476 |
-
const createPullRequestAfterGeneration = async (repoId: string, code: string, language: Language) => {
|
| 477 |
-
try {
|
| 478 |
-
console.log('[PR] Creating PR on:', repoId);
|
| 479 |
-
|
| 480 |
-
// Update message to show PR creation in progress
|
| 481 |
-
setMessages((prev) => {
|
| 482 |
-
const newMessages = [...prev];
|
| 483 |
-
newMessages[newMessages.length - 1] = {
|
| 484 |
-
...newMessages[newMessages.length - 1],
|
| 485 |
-
content: '✅ Code generated successfully!\n\n🔄 Creating Pull Request...',
|
| 486 |
-
};
|
| 487 |
-
return newMessages;
|
| 488 |
-
});
|
| 489 |
-
|
| 490 |
-
const prResult = await apiClient.createPullRequest(
|
| 491 |
-
repoId,
|
| 492 |
-
code,
|
| 493 |
-
language,
|
| 494 |
-
'🎨 Redesign from AnyCoder',
|
| 495 |
-
undefined
|
| 496 |
-
);
|
| 497 |
-
|
| 498 |
-
if (prResult.success && prResult.pr_url) {
|
| 499 |
-
console.log('[PR] Pull Request created:', prResult.pr_url);
|
| 500 |
-
|
| 501 |
-
// Update message with PR link
|
| 502 |
-
setMessages((prev) => {
|
| 503 |
-
const newMessages = [...prev];
|
| 504 |
-
newMessages[newMessages.length - 1] = {
|
| 505 |
-
...newMessages[newMessages.length - 1],
|
| 506 |
-
content: `✅ Code generated successfully!\n\n✅ Pull Request created! [View PR](${prResult.pr_url})`,
|
| 507 |
-
};
|
| 508 |
-
return newMessages;
|
| 509 |
-
});
|
| 510 |
-
|
| 511 |
-
// Open PR in new tab
|
| 512 |
-
window.open(prResult.pr_url, '_blank');
|
| 513 |
-
} else {
|
| 514 |
-
throw new Error(prResult.message || 'Failed to create Pull Request');
|
| 515 |
-
}
|
| 516 |
-
} catch (error: any) {
|
| 517 |
-
console.error('[PR] Failed to create Pull Request:', error);
|
| 518 |
-
|
| 519 |
-
// Update message with error
|
| 520 |
-
setMessages((prev) => {
|
| 521 |
-
const newMessages = [...prev];
|
| 522 |
-
newMessages[newMessages.length - 1] = {
|
| 523 |
-
...newMessages[newMessages.length - 1],
|
| 524 |
-
content: `✅ Code generated successfully!\n\n❌ Failed to create Pull Request: ${error.message || 'Unknown error'}`,
|
| 525 |
-
};
|
| 526 |
-
return newMessages;
|
| 527 |
-
});
|
| 528 |
-
}
|
| 529 |
-
};
|
| 530 |
-
|
| 531 |
-
const handleDeploy = async () => {
|
| 532 |
-
console.log('[Deploy] 🎬 handleDeploy called');
|
| 533 |
-
console.log('[Deploy] generatedCode exists?', !!generatedCode);
|
| 534 |
-
console.log('[Deploy] generatedCode length:', generatedCode?.length);
|
| 535 |
-
console.log('[Deploy] generatedCode preview:', generatedCode?.substring(0, 200));
|
| 536 |
-
|
| 537 |
-
if (!generatedCode) {
|
| 538 |
-
alert('No code to publish! Generate some code first.');
|
| 539 |
-
return;
|
| 540 |
-
}
|
| 541 |
-
|
| 542 |
-
// Get current username (fetch if not loaded)
|
| 543 |
-
let currentUsername = username;
|
| 544 |
-
if (!currentUsername) {
|
| 545 |
-
console.log('[Deploy] Username not in state, fetching from auth...');
|
| 546 |
-
try {
|
| 547 |
-
const authStatus = await apiClient.getAuthStatus();
|
| 548 |
-
if (authStatus.username) {
|
| 549 |
-
currentUsername = authStatus.username;
|
| 550 |
-
setUsername(authStatus.username);
|
| 551 |
-
console.log('[Deploy] Fetched username:', currentUsername);
|
| 552 |
-
}
|
| 553 |
-
} catch (e) {
|
| 554 |
-
console.error('[Deploy] Could not get username:', e);
|
| 555 |
-
// Don't fail - let backend handle auth
|
| 556 |
-
}
|
| 557 |
-
}
|
| 558 |
-
|
| 559 |
-
// SAME LOGIC AS GRADIO VERSION: Parse message history to find existing space
|
| 560 |
-
let existingSpace: string | null = null;
|
| 561 |
-
|
| 562 |
-
// Look for previous deployment or imported space in history
|
| 563 |
-
console.log('[Deploy] ========== DEBUG START ==========');
|
| 564 |
-
console.log('[Deploy] Total messages in history:', messages.length);
|
| 565 |
-
console.log('[Deploy] Current username:', currentUsername);
|
| 566 |
-
console.log('[Deploy] Auth status:', isAuthenticated ? 'authenticated' : 'not authenticated');
|
| 567 |
-
console.log('[Deploy] Messages:', JSON.stringify(messages, null, 2));
|
| 568 |
-
|
| 569 |
-
if (messages.length > 0 && currentUsername) {
|
| 570 |
-
console.log('[Deploy] Scanning message history FORWARD (oldest first) - MATCHING GRADIO LOGIC...');
|
| 571 |
-
console.log('[Deploy] Total messages to scan:', messages.length);
|
| 572 |
-
|
| 573 |
-
// EXACT GRADIO LOGIC: Scan forward (oldest first) and stop at first match
|
| 574 |
-
// Gradio: for user_msg, assistant_msg in history:
|
| 575 |
-
for (let i = 0; i < messages.length; i++) {
|
| 576 |
-
const msg = messages[i];
|
| 577 |
-
console.log(`[Deploy] Checking message ${i}:`, {
|
| 578 |
-
role: msg.role,
|
| 579 |
-
contentPreview: msg.content.substring(0, 100)
|
| 580 |
-
});
|
| 581 |
-
|
| 582 |
-
// Check assistant messages for deployment confirmations
|
| 583 |
-
if (msg.role === 'assistant') {
|
| 584 |
-
// Check for "✅ Deployed!" message
|
| 585 |
-
if (msg.content.includes('✅ Deployed!')) {
|
| 586 |
-
const match = msg.content.match(/huggingface\.co\/spaces\/([^\/\s\)]+\/[^\/\s\)]+)/);
|
| 587 |
-
if (match) {
|
| 588 |
-
existingSpace = match[1];
|
| 589 |
-
console.log('[Deploy] ✅ Found "✅ Deployed!" - existing_space:', existingSpace);
|
| 590 |
-
break;
|
| 591 |
-
}
|
| 592 |
-
}
|
| 593 |
-
// Check for "✅ Updated!" message
|
| 594 |
-
else if (msg.content.includes('✅ Updated!')) {
|
| 595 |
-
const match = msg.content.match(/huggingface\.co\/spaces\/([^\/\s\)]+\/[^\/\s\)]+)/);
|
| 596 |
-
if (match) {
|
| 597 |
-
existingSpace = match[1];
|
| 598 |
-
console.log('[Deploy] ✅ Found "✅ Updated!" - existing_space:', existingSpace);
|
| 599 |
-
break;
|
| 600 |
-
}
|
| 601 |
-
}
|
| 602 |
-
}
|
| 603 |
-
// Check user messages for imports
|
| 604 |
-
else if (msg.role === 'user' && msg.content.startsWith('Imported Space from')) {
|
| 605 |
-
console.log('[Deploy] 🎯 Found "Imported Space from" message');
|
| 606 |
-
const match = msg.content.match(/huggingface\.co\/spaces\/([^\/\s\)]+\/[^\/\s\)]+)/);
|
| 607 |
-
if (match) {
|
| 608 |
-
const importedSpace = match[1];
|
| 609 |
-
console.log('[Deploy] Extracted imported space:', importedSpace);
|
| 610 |
-
console.log('[Deploy] Checking ownership - user:', currentUsername, 'space:', importedSpace);
|
| 611 |
-
|
| 612 |
-
// Only use if user owns it (EXACT GRADIO LOGIC)
|
| 613 |
-
if (importedSpace.startsWith(`${currentUsername}/`)) {
|
| 614 |
-
existingSpace = importedSpace;
|
| 615 |
-
console.log('[Deploy] ✅✅✅ USER OWNS - Will update:', existingSpace);
|
| 616 |
-
break;
|
| 617 |
-
} else {
|
| 618 |
-
console.log('[Deploy] ⚠️ User does NOT own - will create new space');
|
| 619 |
-
// existing_space remains None (create new deployment)
|
| 620 |
-
}
|
| 621 |
-
}
|
| 622 |
-
}
|
| 623 |
-
}
|
| 624 |
-
|
| 625 |
-
console.log('[Deploy] Final existingSpace value:', existingSpace);
|
| 626 |
-
} else {
|
| 627 |
-
console.log('[Deploy] Skipping scan - no messages or no username');
|
| 628 |
-
console.log('[Deploy] Messages length:', messages.length);
|
| 629 |
-
console.log('[Deploy] Username:', currentUsername);
|
| 630 |
-
}
|
| 631 |
-
console.log('[Deploy] ========== DEBUG END ==========');
|
| 632 |
-
|
| 633 |
-
// TEMPORARY DEBUG: Show what will be sent
|
| 634 |
-
console.log('[Deploy] 🚀 ABOUT TO DEPLOY:');
|
| 635 |
-
console.log('[Deploy] - Language:', selectedLanguage);
|
| 636 |
-
console.log('[Deploy] - existing_repo_id:', existingSpace || 'None (new deployment)');
|
| 637 |
-
console.log('[Deploy] - Username:', currentUsername);
|
| 638 |
-
|
| 639 |
-
// Auto-generate space name (never prompt user)
|
| 640 |
-
let spaceName = undefined; // undefined = backend will auto-generate
|
| 641 |
-
|
| 642 |
-
try {
|
| 643 |
-
console.log('[Deploy] ========== DEPLOY START (Gradio-style history parsing) ==========');
|
| 644 |
-
console.log('[Deploy] Username:', currentUsername);
|
| 645 |
-
console.log('[Deploy] Existing space from history:', existingSpace);
|
| 646 |
-
console.log('[Deploy] Will create new space?', !existingSpace);
|
| 647 |
-
console.log('[Deploy] Messages count:', messages.length);
|
| 648 |
-
console.log('[Deploy] Messages (first 3):', messages.slice(0, 3).map(m => ({ role: m.role, content: m.content.substring(0, 100) })));
|
| 649 |
-
|
| 650 |
-
// CRITICAL DEBUG: Check what we're actually sending
|
| 651 |
-
const historyToSend = messages.map(msg => ({ role: msg.role, content: msg.content }));
|
| 652 |
-
console.log('[Deploy] History to send (length):', historyToSend.length);
|
| 653 |
-
console.log('[Deploy] History to send (first 2):', historyToSend.slice(0, 2));
|
| 654 |
-
console.log('[Deploy] =================================================================');
|
| 655 |
-
|
| 656 |
-
// Build deploy request, omitting undefined fields
|
| 657 |
-
const deployRequest: any = {
|
| 658 |
-
code: generatedCode,
|
| 659 |
-
language: selectedLanguage,
|
| 660 |
-
history: historyToSend // Use the variable we just logged
|
| 661 |
-
};
|
| 662 |
-
|
| 663 |
-
// Only include optional fields if they have values
|
| 664 |
-
if (spaceName) {
|
| 665 |
-
deployRequest.space_name = spaceName;
|
| 666 |
-
}
|
| 667 |
-
if (existingSpace) {
|
| 668 |
-
deployRequest.existing_repo_id = existingSpace;
|
| 669 |
-
deployRequest.commit_message = 'Update via AnyCoder';
|
| 670 |
-
}
|
| 671 |
-
|
| 672 |
-
console.log('[Deploy] 🚀 Sending to backend:', {
|
| 673 |
-
existing_repo_id: deployRequest.existing_repo_id,
|
| 674 |
-
space_name: deployRequest.space_name,
|
| 675 |
-
language: deployRequest.language,
|
| 676 |
-
has_code: !!deployRequest.code,
|
| 677 |
-
code_length: deployRequest.code?.length,
|
| 678 |
-
history_length: deployRequest.history?.length
|
| 679 |
-
});
|
| 680 |
-
console.log('[Deploy] Full request object:', JSON.stringify(deployRequest, null, 2).substring(0, 500));
|
| 681 |
-
|
| 682 |
-
const response = await apiClient.deploy(deployRequest);
|
| 683 |
-
console.log('[Deploy] ✅ Response received:', response);
|
| 684 |
-
|
| 685 |
-
if (response.success) {
|
| 686 |
-
// Update current repo ID if we got one back
|
| 687 |
-
if (response.repo_id) {
|
| 688 |
-
console.log('[Deploy] Setting currentRepoId to:', response.repo_id);
|
| 689 |
-
setCurrentRepoId(response.repo_id);
|
| 690 |
-
} else if (response.space_url) {
|
| 691 |
-
// Extract repo_id from space_url as fallback
|
| 692 |
-
const match = response.space_url.match(/huggingface\.co\/spaces\/([^\/\s\)]+\/[^\/\s\)]+)/);
|
| 693 |
-
if (match) {
|
| 694 |
-
console.log('[Deploy] Extracted repo_id from URL:', match[1]);
|
| 695 |
-
setCurrentRepoId(match[1]);
|
| 696 |
-
}
|
| 697 |
-
}
|
| 698 |
-
|
| 699 |
-
// Add deployment message to chat (EXACT format backend expects)
|
| 700 |
-
const deployMessage: Message = {
|
| 701 |
-
role: 'assistant',
|
| 702 |
-
content: existingSpace
|
| 703 |
-
? `✅ Updated! View your space at: ${response.space_url}`
|
| 704 |
-
: `✅ Deployed! View your space at: ${response.space_url}`,
|
| 705 |
-
timestamp: new Date().toISOString(),
|
| 706 |
-
};
|
| 707 |
-
setMessages((prev) => [...prev, deployMessage]);
|
| 708 |
-
|
| 709 |
-
// Open the space URL in a new tab
|
| 710 |
-
window.open(response.space_url, '_blank');
|
| 711 |
-
|
| 712 |
-
// Show success message
|
| 713 |
-
const isDev = response.dev_mode;
|
| 714 |
-
const message = isDev
|
| 715 |
-
? '🚀 Opening HuggingFace Spaces creation page...\nPlease complete the space setup in the new tab.'
|
| 716 |
-
: existingSpace
|
| 717 |
-
? `✅ Updated successfully!\n\nOpening: ${response.space_url}`
|
| 718 |
-
: `✅ Published successfully!\n\nOpening: ${response.space_url}`;
|
| 719 |
-
alert(message);
|
| 720 |
-
} else {
|
| 721 |
-
alert(`Deployment failed: ${response.message}`);
|
| 722 |
-
}
|
| 723 |
-
} catch (error: any) {
|
| 724 |
-
console.error('[Deploy] Full error object:', error);
|
| 725 |
-
console.error('[Deploy] Error response:', error.response);
|
| 726 |
-
console.error('[Deploy] Error data:', error.response?.data);
|
| 727 |
-
|
| 728 |
-
const errorMessage = error.response?.data?.detail
|
| 729 |
-
|| error.response?.data?.message
|
| 730 |
-
|| error.message
|
| 731 |
-
|| 'Unknown error';
|
| 732 |
-
|
| 733 |
-
alert(`Deployment error: ${errorMessage}\n\nCheck console for details.`);
|
| 734 |
-
}
|
| 735 |
-
};
|
| 736 |
-
|
| 737 |
-
const handleClear = () => {
|
| 738 |
-
if (confirm('Start a new chat? This will clear all messages and code.')) {
|
| 739 |
-
setMessages([]);
|
| 740 |
-
setGeneratedCode('');
|
| 741 |
-
setShowLandingPage(true);
|
| 742 |
-
// Clear localStorage to remove import history
|
| 743 |
-
if (typeof window !== 'undefined') {
|
| 744 |
-
localStorage.removeItem('anycoder_messages');
|
| 745 |
-
console.log('[localStorage] Cleared messages from localStorage');
|
| 746 |
-
}
|
| 747 |
-
}
|
| 748 |
-
};
|
| 749 |
-
|
| 750 |
-
const handleImport = (code: string, language: Language, importUrl?: string) => {
|
| 751 |
-
console.log('[Import] ========== IMPORT START ==========');
|
| 752 |
-
console.log('[Import] Language:', language);
|
| 753 |
-
console.log('[Import] Import URL:', importUrl);
|
| 754 |
-
console.log('[Import] Current username:', username);
|
| 755 |
-
console.log('[Import] Current repo before import:', currentRepoId);
|
| 756 |
-
|
| 757 |
-
// Hide landing page when importing
|
| 758 |
-
if (showLandingPage) {
|
| 759 |
-
setShowLandingPage(false);
|
| 760 |
-
}
|
| 761 |
-
|
| 762 |
-
setGeneratedCode(code);
|
| 763 |
-
setSelectedLanguage(language);
|
| 764 |
-
|
| 765 |
-
// Extract repo_id from import URL if provided
|
| 766 |
-
if (importUrl) {
|
| 767 |
-
const spaceMatch = importUrl.match(/huggingface\.co\/spaces\/([^\/\s\)]+\/[^\/\s\)]+)/);
|
| 768 |
-
console.log('[Import] Regex match result:', spaceMatch);
|
| 769 |
-
|
| 770 |
-
if (spaceMatch) {
|
| 771 |
-
const importedRepoId = spaceMatch[1];
|
| 772 |
-
const importedUsername = importedRepoId.split('/')[0];
|
| 773 |
-
|
| 774 |
-
console.log('[Import] ========================================');
|
| 775 |
-
console.log('[Import] Extracted repo_id:', importedRepoId);
|
| 776 |
-
console.log('[Import] Imported username:', importedUsername);
|
| 777 |
-
console.log('[Import] Logged-in username:', username);
|
| 778 |
-
console.log('[Import] Ownership check:', importedUsername === username);
|
| 779 |
-
console.log('[Import] ========================================');
|
| 780 |
-
|
| 781 |
-
// Only set as current repo if user owns it
|
| 782 |
-
if (username && importedRepoId.startsWith(`${username}/`)) {
|
| 783 |
-
console.log('[Import] ✅✅✅ BEFORE setCurrentRepoId - currentRepoId was:', currentRepoId);
|
| 784 |
-
setCurrentRepoId(importedRepoId);
|
| 785 |
-
console.log('[Import] ✅✅✅ CALLED setCurrentRepoId with:', importedRepoId);
|
| 786 |
-
console.log('[Import] ✅✅✅ Note: State update is async, currentRepoId will update later');
|
| 787 |
-
} else {
|
| 788 |
-
// User doesn't own the imported space, clear current repo
|
| 789 |
-
setCurrentRepoId(null);
|
| 790 |
-
if (!username) {
|
| 791 |
-
console.log('[Import] ⚠️⚠️⚠️ USERNAME IS NULL - Cannot set repo ownership!');
|
| 792 |
-
} else {
|
| 793 |
-
console.log('[Import] ⚠️ User does not own imported space:', importedRepoId, '(username:', username, ')');
|
| 794 |
-
}
|
| 795 |
-
}
|
| 796 |
-
} else {
|
| 797 |
-
console.log('[Import] ⚠️ Could not extract repo_id from URL:', importUrl);
|
| 798 |
-
}
|
| 799 |
-
} else {
|
| 800 |
-
console.log('[Import] No import URL provided');
|
| 801 |
-
}
|
| 802 |
-
|
| 803 |
-
console.log('[Import] ========== IMPORT END ==========');
|
| 804 |
-
|
| 805 |
-
// Add messages that include the imported code so LLM can see it
|
| 806 |
-
const userMessage: Message = {
|
| 807 |
-
role: 'user',
|
| 808 |
-
content: importUrl
|
| 809 |
-
? `Imported Space from ${importUrl}`
|
| 810 |
-
: `I imported a ${language} project. Here's the code that was imported.`,
|
| 811 |
-
timestamp: new Date().toISOString(),
|
| 812 |
-
};
|
| 813 |
-
|
| 814 |
-
const assistantMessage: Message = {
|
| 815 |
-
role: 'assistant',
|
| 816 |
-
content: `✅ I've loaded your ${language} project. The code is now in the editor. You can ask me to:\n\n• Modify existing features\n• Add new functionality\n• Fix bugs or improve code\n• Explain how it works\n• Publish it to HuggingFace Spaces\n\nWhat would you like me to help you with?`,
|
| 817 |
-
timestamp: new Date().toISOString(),
|
| 818 |
-
};
|
| 819 |
-
|
| 820 |
-
setMessages((prev) => [...prev, userMessage, assistantMessage]);
|
| 821 |
-
|
| 822 |
-
// Switch to editor view on mobile
|
| 823 |
-
setMobileView('editor');
|
| 824 |
-
};
|
| 825 |
-
|
| 826 |
-
// Handle landing page prompt submission
|
| 827 |
-
const handleLandingPageStart = async (prompt: string, language: Language, modelId: string, imageUrl?: string, repoId?: string, shouldCreatePR?: boolean) => {
|
| 828 |
-
console.log('[LandingPageStart] Received imageUrl:', imageUrl ? 'Yes' : 'No');
|
| 829 |
-
console.log('[LandingPageStart] Image URL length:', imageUrl?.length || 0);
|
| 830 |
-
|
| 831 |
-
// Hide landing page immediately for smooth transition
|
| 832 |
-
setShowLandingPage(false);
|
| 833 |
-
|
| 834 |
-
// If shouldCreatePR is true, set pending PR state and ref
|
| 835 |
-
if (shouldCreatePR && repoId) {
|
| 836 |
-
console.log('[PR] Setting pending PR for:', repoId);
|
| 837 |
-
const prInfo = { repoId, language };
|
| 838 |
-
setPendingPR(prInfo);
|
| 839 |
-
pendingPRRef.current = prInfo; // Set ref immediately for synchronous access
|
| 840 |
-
}
|
| 841 |
-
|
| 842 |
-
// Send the message with the selected language, model, and image
|
| 843 |
-
// Don't pass repoId to handleSendMessage when creating PR (we want to generate code first, then create PR)
|
| 844 |
-
await handleSendMessage(prompt, imageUrl, language, modelId, shouldCreatePR ? undefined : repoId, shouldCreatePR);
|
| 845 |
-
};
|
| 846 |
-
|
| 847 |
-
// Resize handlers for chat sidebar (desktop only)
|
| 848 |
-
const startResizingChat = () => {
|
| 849 |
-
if (isDesktop) {
|
| 850 |
-
setIsResizingChat(true);
|
| 851 |
-
}
|
| 852 |
-
};
|
| 853 |
-
|
| 854 |
-
const startResizingSettings = () => {
|
| 855 |
-
if (isDesktop) {
|
| 856 |
-
setIsResizingSettings(true);
|
| 857 |
-
}
|
| 858 |
-
};
|
| 859 |
-
|
| 860 |
-
// Handle mouse move for resizing (desktop only)
|
| 861 |
-
useEffect(() => {
|
| 862 |
-
const handleMouseMove = (e: MouseEvent) => {
|
| 863 |
-
if (!isDesktop) return; // Skip on mobile
|
| 864 |
-
|
| 865 |
-
if (isResizingChat) {
|
| 866 |
-
const newWidth = Math.min(Math.max(e.clientX, 250), 600); // Min 250px, max 600px
|
| 867 |
-
setChatSidebarWidth(newWidth);
|
| 868 |
-
}
|
| 869 |
-
if (isResizingSettings) {
|
| 870 |
-
const newWidth = Math.min(Math.max(window.innerWidth - e.clientX, 220), 500); // Min 220px, max 500px
|
| 871 |
-
setSettingsSidebarWidth(newWidth);
|
| 872 |
-
}
|
| 873 |
-
};
|
| 874 |
-
|
| 875 |
-
const handleMouseUp = () => {
|
| 876 |
-
if (isResizingChat) {
|
| 877 |
-
setIsResizingChat(false);
|
| 878 |
-
// Save to localStorage
|
| 879 |
-
localStorage.setItem('anycoder_chat_sidebar_width', chatSidebarWidth.toString());
|
| 880 |
-
document.body.classList.remove('resizing');
|
| 881 |
-
}
|
| 882 |
-
if (isResizingSettings) {
|
| 883 |
-
setIsResizingSettings(false);
|
| 884 |
-
// Save to localStorage
|
| 885 |
-
localStorage.setItem('anycoder_settings_sidebar_width', settingsSidebarWidth.toString());
|
| 886 |
-
document.body.classList.remove('resizing');
|
| 887 |
-
}
|
| 888 |
-
};
|
| 889 |
-
|
| 890 |
-
if (isResizingChat || isResizingSettings) {
|
| 891 |
-
document.addEventListener('mousemove', handleMouseMove);
|
| 892 |
-
document.addEventListener('mouseup', handleMouseUp);
|
| 893 |
-
// Add resizing class to body for cursor and selection styles
|
| 894 |
-
document.body.classList.add('resizing');
|
| 895 |
-
}
|
| 896 |
-
|
| 897 |
-
return () => {
|
| 898 |
-
document.removeEventListener('mousemove', handleMouseMove);
|
| 899 |
-
document.removeEventListener('mouseup', handleMouseUp);
|
| 900 |
-
document.body.classList.remove('resizing');
|
| 901 |
-
};
|
| 902 |
-
}, [isResizingChat, isResizingSettings, chatSidebarWidth, settingsSidebarWidth, isDesktop]);
|
| 903 |
-
|
| 904 |
-
// Show landing page if no messages and showLandingPage is true
|
| 905 |
-
if (showLandingPage && messages.length === 0) {
|
| 906 |
-
return (
|
| 907 |
-
<div className="min-h-screen animate-in fade-in duration-300">
|
| 908 |
-
<LandingPage
|
| 909 |
-
onStart={handleLandingPageStart}
|
| 910 |
-
onImport={handleImport}
|
| 911 |
-
isAuthenticated={isAuthenticated}
|
| 912 |
-
initialLanguage={selectedLanguage}
|
| 913 |
-
initialModel={selectedModel}
|
| 914 |
-
onAuthChange={checkAuth}
|
| 915 |
-
setPendingPR={setPendingPR}
|
| 916 |
-
pendingPRRef={pendingPRRef}
|
| 917 |
-
/>
|
| 918 |
-
</div>
|
| 919 |
-
);
|
| 920 |
-
}
|
| 921 |
-
|
| 922 |
-
return (
|
| 923 |
-
<div className="h-screen flex flex-col bg-[#000000] animate-in fade-in duration-300">
|
| 924 |
-
<Header />
|
| 925 |
-
|
| 926 |
-
{/* Apple-style layout - Responsive */}
|
| 927 |
-
<main className="flex-1 flex overflow-hidden relative">
|
| 928 |
-
{/* Left Sidebar - Chat Panel (Hidden on mobile, shown when mobileView='chat') */}
|
| 929 |
-
<div
|
| 930 |
-
className={`
|
| 931 |
-
${mobileView === 'chat' ? 'flex' : 'hidden'} md:flex
|
| 932 |
-
w-full
|
| 933 |
-
bg-[#000000] border-r border-[#424245]/30
|
| 934 |
-
flex-col
|
| 935 |
-
absolute md:relative inset-0 md:inset-auto z-10 md:z-auto
|
| 936 |
-
md:flex-shrink-0
|
| 937 |
-
`}
|
| 938 |
-
style={isDesktop ? { width: `${chatSidebarWidth}px` } : undefined}
|
| 939 |
-
>
|
| 940 |
-
{/* Panel Header */}
|
| 941 |
-
<div className="flex items-center px-4 py-3 bg-[#000000] border-b border-[#424245]/30">
|
| 942 |
-
<span className="text-sm font-medium text-[#f5f5f7]">Chat</span>
|
| 943 |
-
</div>
|
| 944 |
-
|
| 945 |
-
{/* Chat Panel */}
|
| 946 |
-
<div className="flex-1 overflow-hidden">
|
| 947 |
-
<ChatInterface
|
| 948 |
-
messages={messages}
|
| 949 |
-
onSendMessage={handleSendMessage}
|
| 950 |
-
isGenerating={isGenerating}
|
| 951 |
-
isAuthenticated={isAuthenticated}
|
| 952 |
-
supportsImages={currentModelSupportsImages}
|
| 953 |
-
/>
|
| 954 |
-
</div>
|
| 955 |
-
</div>
|
| 956 |
-
|
| 957 |
-
{/* Resize Handle for Chat Sidebar (Desktop only) */}
|
| 958 |
-
<div
|
| 959 |
-
className={`hidden md:block resize-handle ${isResizingChat ? 'resizing' : ''}`}
|
| 960 |
-
onMouseDown={startResizingChat}
|
| 961 |
-
title="Drag to resize chat panel"
|
| 962 |
-
/>
|
| 963 |
-
|
| 964 |
-
{/* Center - Editor Group (Always visible on mobile when mobileView='editor', always visible on desktop) */}
|
| 965 |
-
<div className={`
|
| 966 |
-
${mobileView === 'editor' ? 'flex' : 'hidden'} md:flex
|
| 967 |
-
flex-1 flex-col bg-[#000000]
|
| 968 |
-
absolute md:relative inset-0 md:inset-auto z-10 md:z-auto
|
| 969 |
-
md:min-w-0 overflow-hidden
|
| 970 |
-
w-full
|
| 971 |
-
`}>
|
| 972 |
-
{/* Tab Bar */}
|
| 973 |
-
<div className="flex items-center px-4 h-10 bg-[#1d1d1f] border-b border-[#424245]/30">
|
| 974 |
-
<div className="flex items-center space-x-2">
|
| 975 |
-
<div className="px-3 py-1 bg-[#2d2d2f] text-sm text-[#f5f5f7] rounded-t-lg font-normal border-t border-x border-[#424245]/50">
|
| 976 |
-
{selectedLanguage === 'html' ? 'app.html' :
|
| 977 |
-
selectedLanguage === 'gradio' || selectedLanguage === 'streamlit' ? 'app.py' :
|
| 978 |
-
selectedLanguage === 'transformers.js' ? 'app.js' :
|
| 979 |
-
selectedLanguage === 'comfyui' ? 'app.json' :
|
| 980 |
-
selectedLanguage === 'react' ? 'app.jsx' :
|
| 981 |
-
`${selectedLanguage}.txt`}
|
| 982 |
-
</div>
|
| 983 |
-
</div>
|
| 984 |
-
<div className="ml-auto flex items-center space-x-3 text-xs text-[#86868b]">
|
| 985 |
-
{isGenerating && (
|
| 986 |
-
<span className="flex items-center space-x-1.5">
|
| 987 |
-
<div className="w-1.5 h-1.5 bg-white rounded-full animate-pulse"></div>
|
| 988 |
-
<span>Generating...</span>
|
| 989 |
-
</span>
|
| 990 |
-
)}
|
| 991 |
-
<span className="font-medium">{selectedLanguage.toUpperCase()}</span>
|
| 992 |
-
</div>
|
| 993 |
-
</div>
|
| 994 |
-
|
| 995 |
-
{/* Editor */}
|
| 996 |
-
<div className="flex-1">
|
| 997 |
-
<CodeEditor
|
| 998 |
-
code={generatedCode || '// Your generated code will appear here...\n// Select a model and start chatting to generate code'}
|
| 999 |
-
language={selectedLanguage}
|
| 1000 |
-
onChange={setGeneratedCode}
|
| 1001 |
-
readOnly={isGenerating}
|
| 1002 |
-
/>
|
| 1003 |
-
</div>
|
| 1004 |
-
</div>
|
| 1005 |
-
|
| 1006 |
-
{/* Resize Handle for Settings Sidebar (Desktop only) */}
|
| 1007 |
-
<div
|
| 1008 |
-
className={`hidden md:block resize-handle ${isResizingSettings ? 'resizing' : ''}`}
|
| 1009 |
-
onMouseDown={startResizingSettings}
|
| 1010 |
-
title="Drag to resize settings panel"
|
| 1011 |
-
/>
|
| 1012 |
-
|
| 1013 |
-
{/* Right Sidebar - Configuration Panel (Hidden on mobile, shown when mobileView='settings') */}
|
| 1014 |
-
<div
|
| 1015 |
-
className={`
|
| 1016 |
-
${mobileView === 'settings' ? 'flex' : 'hidden'} md:flex
|
| 1017 |
-
w-full
|
| 1018 |
-
bg-[#000000] border-l border-[#424245]/30
|
| 1019 |
-
overflow-y-auto
|
| 1020 |
-
absolute md:relative inset-0 md:inset-auto z-10 md:z-auto
|
| 1021 |
-
flex-col
|
| 1022 |
-
md:flex-shrink-0
|
| 1023 |
-
`}
|
| 1024 |
-
style={isDesktop ? { width: `${settingsSidebarWidth}px` } : undefined}
|
| 1025 |
-
>
|
| 1026 |
-
<ControlPanel
|
| 1027 |
-
selectedLanguage={selectedLanguage}
|
| 1028 |
-
selectedModel={selectedModel}
|
| 1029 |
-
onLanguageChange={setSelectedLanguage}
|
| 1030 |
-
onModelChange={setSelectedModel}
|
| 1031 |
-
onClear={handleClear}
|
| 1032 |
-
isGenerating={isGenerating}
|
| 1033 |
-
/>
|
| 1034 |
-
</div>
|
| 1035 |
-
</main>
|
| 1036 |
-
|
| 1037 |
-
{/* Mobile Bottom Navigation (visible only on mobile) */}
|
| 1038 |
-
<nav className="md:hidden bg-[#000000]/95 backdrop-blur-xl border-t border-[#424245]/20 flex items-center justify-around h-14 px-2 safe-area-bottom">
|
| 1039 |
-
<button
|
| 1040 |
-
onClick={() => setMobileView('chat')}
|
| 1041 |
-
className={`flex flex-col items-center justify-center flex-1 py-1.5 transition-all ${mobileView === 'chat'
|
| 1042 |
-
? 'text-white'
|
| 1043 |
-
: 'text-[#86868b]'
|
| 1044 |
-
}`}
|
| 1045 |
-
>
|
| 1046 |
-
<svg className="w-5 h-5 mb-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
|
| 1047 |
-
<path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
| 1048 |
-
</svg>
|
| 1049 |
-
<span className="text-[10px]">Chat</span>
|
| 1050 |
-
</button>
|
| 1051 |
-
|
| 1052 |
-
<button
|
| 1053 |
-
onClick={() => setMobileView('editor')}
|
| 1054 |
-
className={`flex flex-col items-center justify-center flex-1 py-1.5 transition-all ${mobileView === 'editor'
|
| 1055 |
-
? 'text-white'
|
| 1056 |
-
: 'text-[#86868b]'
|
| 1057 |
-
}`}
|
| 1058 |
-
>
|
| 1059 |
-
<svg className="w-5 h-5 mb-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
|
| 1060 |
-
<path strokeLinecap="round" strokeLinejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
| 1061 |
-
</svg>
|
| 1062 |
-
<span className="text-[10px]">Code</span>
|
| 1063 |
-
</button>
|
| 1064 |
-
|
| 1065 |
-
<button
|
| 1066 |
-
onClick={() => setMobileView('settings')}
|
| 1067 |
-
className={`flex flex-col items-center justify-center flex-1 py-1.5 transition-all ${mobileView === 'settings'
|
| 1068 |
-
? 'text-white'
|
| 1069 |
-
: 'text-[#86868b]'
|
| 1070 |
-
}`}
|
| 1071 |
-
>
|
| 1072 |
-
<svg className="w-5 h-5 mb-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
|
| 1073 |
-
<path strokeLinecap="round" strokeLinejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
| 1074 |
-
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
| 1075 |
-
</svg>
|
| 1076 |
-
<span className="text-[10px]">Settings</span>
|
| 1077 |
-
</button>
|
| 1078 |
-
</nav>
|
| 1079 |
-
|
| 1080 |
-
{/* Status Bar - Apple style (hidden on mobile) */}
|
| 1081 |
-
<footer className="hidden md:flex h-6 bg-[#000000] border-t border-[#424245]/20 text-[#86868b] text-[11px] items-center px-4 justify-between">
|
| 1082 |
-
<div className="flex items-center space-x-4">
|
| 1083 |
-
<span>AnyCoder</span>
|
| 1084 |
-
<span className="flex items-center gap-1.5">
|
| 1085 |
-
{isAuthenticated ? (
|
| 1086 |
-
<>
|
| 1087 |
-
<span className="w-1.5 h-1.5 bg-[#30d158] rounded-full"></span>
|
| 1088 |
-
<span>Connected</span>
|
| 1089 |
-
</>
|
| 1090 |
-
) : (
|
| 1091 |
-
<>
|
| 1092 |
-
<span className="w-1.5 h-1.5 bg-[#ff9f0a] rounded-full"></span>
|
| 1093 |
-
<span>Not authenticated</span>
|
| 1094 |
-
</>
|
| 1095 |
-
)}
|
| 1096 |
-
</span>
|
| 1097 |
-
</div>
|
| 1098 |
-
<div className="flex items-center space-x-4">
|
| 1099 |
-
<span>{messages.length} messages</span>
|
| 1100 |
-
</div>
|
| 1101 |
-
</footer>
|
| 1102 |
-
</div>
|
| 1103 |
-
);
|
| 1104 |
-
}
|
| 1105 |
-
|
| 1106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/ChatInterface.tsx
DELETED
|
@@ -1,212 +0,0 @@
|
|
| 1 |
-
'use client';
|
| 2 |
-
|
| 3 |
-
import { useState, useRef, useEffect } from 'react';
|
| 4 |
-
import type { Message } from '@/types';
|
| 5 |
-
import ReactMarkdown from 'react-markdown';
|
| 6 |
-
import remarkGfm from 'remark-gfm';
|
| 7 |
-
import Image from 'next/image';
|
| 8 |
-
|
| 9 |
-
interface ChatInterfaceProps {
|
| 10 |
-
messages: Message[];
|
| 11 |
-
onSendMessage: (message: string, imageUrl?: string) => void;
|
| 12 |
-
isGenerating: boolean;
|
| 13 |
-
isAuthenticated?: boolean;
|
| 14 |
-
supportsImages?: boolean;
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
export default function ChatInterface({ messages, onSendMessage, isGenerating, isAuthenticated = false, supportsImages = false }: ChatInterfaceProps) {
|
| 18 |
-
const [input, setInput] = useState('');
|
| 19 |
-
const [uploadedImageUrl, setUploadedImageUrl] = useState<string | null>(null);
|
| 20 |
-
const [uploadedImageFile, setUploadedImageFile] = useState<File | null>(null);
|
| 21 |
-
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 22 |
-
const messagesEndRef = useRef<HTMLDivElement>(null);
|
| 23 |
-
|
| 24 |
-
const scrollToBottom = () => {
|
| 25 |
-
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
| 26 |
-
};
|
| 27 |
-
|
| 28 |
-
useEffect(() => {
|
| 29 |
-
scrollToBottom();
|
| 30 |
-
}, [messages]);
|
| 31 |
-
|
| 32 |
-
const handleSubmit = (e: React.FormEvent) => {
|
| 33 |
-
e.preventDefault();
|
| 34 |
-
if (input.trim() && !isGenerating) {
|
| 35 |
-
onSendMessage(input, uploadedImageUrl || undefined);
|
| 36 |
-
setInput('');
|
| 37 |
-
setUploadedImageUrl(null);
|
| 38 |
-
setUploadedImageFile(null);
|
| 39 |
-
}
|
| 40 |
-
};
|
| 41 |
-
|
| 42 |
-
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 43 |
-
const file = e.target.files?.[0];
|
| 44 |
-
if (file) {
|
| 45 |
-
// Create a data URL for the image
|
| 46 |
-
const reader = new FileReader();
|
| 47 |
-
reader.onload = (event) => {
|
| 48 |
-
const imageUrl = event.target?.result as string;
|
| 49 |
-
setUploadedImageUrl(imageUrl);
|
| 50 |
-
setUploadedImageFile(file);
|
| 51 |
-
};
|
| 52 |
-
reader.readAsDataURL(file);
|
| 53 |
-
}
|
| 54 |
-
};
|
| 55 |
-
|
| 56 |
-
const removeImage = () => {
|
| 57 |
-
setUploadedImageUrl(null);
|
| 58 |
-
setUploadedImageFile(null);
|
| 59 |
-
if (fileInputRef.current) {
|
| 60 |
-
fileInputRef.current.value = '';
|
| 61 |
-
}
|
| 62 |
-
};
|
| 63 |
-
|
| 64 |
-
return (
|
| 65 |
-
<div className="flex flex-col h-full bg-[#000000]">
|
| 66 |
-
{/* Messages */}
|
| 67 |
-
<div className="flex-1 overflow-y-auto p-4 space-y-3">
|
| 68 |
-
{messages.length === 0 ? (
|
| 69 |
-
<div className="text-center text-[#86868b] mt-12">
|
| 70 |
-
{isAuthenticated ? (
|
| 71 |
-
<>
|
| 72 |
-
<p className="text-lg font-medium text-[#f5f5f7]">Start a conversation</p>
|
| 73 |
-
<p className="text-sm mt-2 text-[#86868b]">Describe what you want to build</p>
|
| 74 |
-
</>
|
| 75 |
-
) : (
|
| 76 |
-
<>
|
| 77 |
-
<p className="text-lg font-medium text-[#f5f5f7]">Sign in to get started</p>
|
| 78 |
-
<p className="text-sm mt-2 text-[#86868b]">Use Dev Login or sign in with Hugging Face</p>
|
| 79 |
-
</>
|
| 80 |
-
)}
|
| 81 |
-
</div>
|
| 82 |
-
) : (
|
| 83 |
-
messages.map((message, index) => (
|
| 84 |
-
<div
|
| 85 |
-
key={index}
|
| 86 |
-
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
| 87 |
-
>
|
| 88 |
-
<div
|
| 89 |
-
className={`max-w-[85%] rounded-2xl px-4 py-3 ${
|
| 90 |
-
message.role === 'user'
|
| 91 |
-
? 'bg-white text-black'
|
| 92 |
-
: 'bg-[#2d2d2f] text-[#f5f5f7]'
|
| 93 |
-
}`}
|
| 94 |
-
>
|
| 95 |
-
{message.image_url && message.role === 'user' && (
|
| 96 |
-
<div className="mb-2">
|
| 97 |
-
<Image
|
| 98 |
-
src={message.image_url}
|
| 99 |
-
alt="Uploaded image"
|
| 100 |
-
width={200}
|
| 101 |
-
height={200}
|
| 102 |
-
className="rounded-lg object-cover max-w-full h-auto"
|
| 103 |
-
unoptimized
|
| 104 |
-
/>
|
| 105 |
-
</div>
|
| 106 |
-
)}
|
| 107 |
-
<div className="text-sm leading-relaxed">
|
| 108 |
-
{message.role === 'assistant' ? (
|
| 109 |
-
<ReactMarkdown
|
| 110 |
-
remarkPlugins={[remarkGfm]}
|
| 111 |
-
className="prose prose-invert prose-sm max-w-none [&>p]:my-0 [&>ul]:my-1 [&>ol]:my-1"
|
| 112 |
-
components={{
|
| 113 |
-
a: ({ node, ...props }) => <a {...props} target="_blank" rel="noopener noreferrer" />
|
| 114 |
-
}}
|
| 115 |
-
>
|
| 116 |
-
{message.content}
|
| 117 |
-
</ReactMarkdown>
|
| 118 |
-
) : (
|
| 119 |
-
<p className="whitespace-pre-wrap break-words">{message.content}</p>
|
| 120 |
-
)}
|
| 121 |
-
</div>
|
| 122 |
-
{message.timestamp && (
|
| 123 |
-
<div className="text-[10px] opacity-40 mt-2 text-right">
|
| 124 |
-
{new Date(message.timestamp).toLocaleTimeString()}
|
| 125 |
-
</div>
|
| 126 |
-
)}
|
| 127 |
-
</div>
|
| 128 |
-
</div>
|
| 129 |
-
))
|
| 130 |
-
)}
|
| 131 |
-
<div ref={messagesEndRef} />
|
| 132 |
-
</div>
|
| 133 |
-
|
| 134 |
-
{/* Input */}
|
| 135 |
-
<div className="border-t border-[#424245]/30 p-3 bg-[#000000]">
|
| 136 |
-
{/* Image Preview */}
|
| 137 |
-
{uploadedImageUrl && (
|
| 138 |
-
<div className="mb-2 relative inline-block">
|
| 139 |
-
<Image
|
| 140 |
-
src={uploadedImageUrl}
|
| 141 |
-
alt="Upload preview"
|
| 142 |
-
width={120}
|
| 143 |
-
height={120}
|
| 144 |
-
className="rounded-lg object-cover"
|
| 145 |
-
unoptimized
|
| 146 |
-
/>
|
| 147 |
-
<button
|
| 148 |
-
type="button"
|
| 149 |
-
onClick={removeImage}
|
| 150 |
-
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white rounded-full hover:bg-red-600 transition-all flex items-center justify-center text-xs font-bold"
|
| 151 |
-
>
|
| 152 |
-
×
|
| 153 |
-
</button>
|
| 154 |
-
</div>
|
| 155 |
-
)}
|
| 156 |
-
|
| 157 |
-
<form onSubmit={handleSubmit} className="flex items-center gap-2">
|
| 158 |
-
<input
|
| 159 |
-
type="text"
|
| 160 |
-
value={input}
|
| 161 |
-
onChange={(e) => setInput(e.target.value)}
|
| 162 |
-
placeholder={isAuthenticated ? "Message AnyCoder..." : "Sign in first..."}
|
| 163 |
-
disabled={isGenerating || !isAuthenticated}
|
| 164 |
-
className="flex-1 px-4 py-2.5 bg-[#2d2d2f] text-[#f5f5f7] text-sm border border-[#424245]/50 rounded-full focus:outline-none focus:border-[#424245] disabled:opacity-40 disabled:cursor-not-allowed placeholder-[#86868b]"
|
| 165 |
-
/>
|
| 166 |
-
|
| 167 |
-
{/* Image Upload Button (only show if model supports images) */}
|
| 168 |
-
{supportsImages && (
|
| 169 |
-
<>
|
| 170 |
-
<input
|
| 171 |
-
ref={fileInputRef}
|
| 172 |
-
type="file"
|
| 173 |
-
accept="image/*"
|
| 174 |
-
onChange={handleImageUpload}
|
| 175 |
-
className="hidden"
|
| 176 |
-
disabled={isGenerating || !isAuthenticated}
|
| 177 |
-
/>
|
| 178 |
-
<button
|
| 179 |
-
type="button"
|
| 180 |
-
onClick={() => fileInputRef.current?.click()}
|
| 181 |
-
disabled={isGenerating || !isAuthenticated}
|
| 182 |
-
className="p-2.5 bg-[#2d2d2f] text-[#f5f5f7] rounded-full hover:bg-[#424245] disabled:opacity-40 disabled:cursor-not-allowed transition-all active:scale-95 flex-shrink-0"
|
| 183 |
-
title="Upload image"
|
| 184 |
-
>
|
| 185 |
-
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2.5}>
|
| 186 |
-
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
|
| 187 |
-
</svg>
|
| 188 |
-
</button>
|
| 189 |
-
</>
|
| 190 |
-
)}
|
| 191 |
-
|
| 192 |
-
<button
|
| 193 |
-
type="submit"
|
| 194 |
-
disabled={isGenerating || !input.trim() || !isAuthenticated}
|
| 195 |
-
className="p-2.5 bg-white text-black rounded-full hover:bg-[#f5f5f7] disabled:bg-[#2d2d2f] disabled:text-[#86868b] disabled:cursor-not-allowed transition-all active:scale-95 flex-shrink-0"
|
| 196 |
-
>
|
| 197 |
-
{isGenerating ? (
|
| 198 |
-
<svg className="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2.5}>
|
| 199 |
-
<path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
| 200 |
-
</svg>
|
| 201 |
-
) : (
|
| 202 |
-
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2.5}>
|
| 203 |
-
<path strokeLinecap="round" strokeLinejoin="round" d="M5 12h14M12 5l7 7-7 7" />
|
| 204 |
-
</svg>
|
| 205 |
-
)}
|
| 206 |
-
</button>
|
| 207 |
-
</form>
|
| 208 |
-
</div>
|
| 209 |
-
</div>
|
| 210 |
-
);
|
| 211 |
-
}
|
| 212 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/CodeEditor.tsx
DELETED
|
@@ -1,101 +0,0 @@
|
|
| 1 |
-
'use client';
|
| 2 |
-
|
| 3 |
-
import { useEffect, useRef } from 'react';
|
| 4 |
-
import Editor from '@monaco-editor/react';
|
| 5 |
-
|
| 6 |
-
interface CodeEditorProps {
|
| 7 |
-
code: string;
|
| 8 |
-
language: string;
|
| 9 |
-
onChange?: (value: string) => void;
|
| 10 |
-
readOnly?: boolean;
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
export default function CodeEditor({ code, language, onChange, readOnly = false }: CodeEditorProps) {
|
| 14 |
-
const editorRef = useRef<any>(null);
|
| 15 |
-
const lastFormattedCodeRef = useRef<string>('');
|
| 16 |
-
const formatTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
| 17 |
-
|
| 18 |
-
// Map our language names to Monaco language IDs
|
| 19 |
-
const getMonacoLanguage = (lang: string): string => {
|
| 20 |
-
const languageMap: Record<string, string> = {
|
| 21 |
-
'html': 'html',
|
| 22 |
-
'gradio': 'python',
|
| 23 |
-
'streamlit': 'python',
|
| 24 |
-
'transformers.js': 'html', // Contains HTML, CSS, and JavaScript - HTML is primary
|
| 25 |
-
'react': 'javascriptreact', // JSX syntax highlighting
|
| 26 |
-
'comfyui': 'json',
|
| 27 |
-
};
|
| 28 |
-
return languageMap[lang] || 'plaintext';
|
| 29 |
-
};
|
| 30 |
-
|
| 31 |
-
const handleEditorDidMount = (editor: any) => {
|
| 32 |
-
editorRef.current = editor;
|
| 33 |
-
};
|
| 34 |
-
|
| 35 |
-
// Format code intelligently - only when generation appears complete
|
| 36 |
-
useEffect(() => {
|
| 37 |
-
if (editorRef.current && code && code.length > 100) {
|
| 38 |
-
// Clear existing timeout
|
| 39 |
-
if (formatTimeoutRef.current) {
|
| 40 |
-
clearTimeout(formatTimeoutRef.current);
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
// Only format if code hasn't been formatted yet or if it's different
|
| 44 |
-
if (code !== lastFormattedCodeRef.current) {
|
| 45 |
-
// Wait 1 second after code stops changing before formatting
|
| 46 |
-
formatTimeoutRef.current = setTimeout(() => {
|
| 47 |
-
if (editorRef.current) {
|
| 48 |
-
editorRef.current.getAction('editor.action.formatDocument')?.run();
|
| 49 |
-
lastFormattedCodeRef.current = code;
|
| 50 |
-
}
|
| 51 |
-
}, 1000);
|
| 52 |
-
}
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
return () => {
|
| 56 |
-
if (formatTimeoutRef.current) {
|
| 57 |
-
clearTimeout(formatTimeoutRef.current);
|
| 58 |
-
}
|
| 59 |
-
};
|
| 60 |
-
}, [code]);
|
| 61 |
-
|
| 62 |
-
return (
|
| 63 |
-
<div className="h-full overflow-hidden bg-[#1e1e1e]">
|
| 64 |
-
<Editor
|
| 65 |
-
height="100%"
|
| 66 |
-
language={getMonacoLanguage(language)}
|
| 67 |
-
value={code}
|
| 68 |
-
onChange={(value) => onChange && onChange(value || '')}
|
| 69 |
-
theme="vs-dark"
|
| 70 |
-
options={{
|
| 71 |
-
readOnly,
|
| 72 |
-
minimap: { enabled: true },
|
| 73 |
-
fontSize: 14,
|
| 74 |
-
fontFamily: "'SF Mono', 'JetBrains Mono', 'Menlo', 'Monaco', 'Courier New', monospace",
|
| 75 |
-
wordWrap: 'off',
|
| 76 |
-
lineNumbers: 'on',
|
| 77 |
-
lineNumbersMinChars: 3,
|
| 78 |
-
glyphMargin: false,
|
| 79 |
-
folding: true,
|
| 80 |
-
lineDecorationsWidth: 10,
|
| 81 |
-
scrollBeyondLastLine: false,
|
| 82 |
-
automaticLayout: true,
|
| 83 |
-
tabSize: 2,
|
| 84 |
-
insertSpaces: true,
|
| 85 |
-
padding: { top: 16, bottom: 16 },
|
| 86 |
-
lineHeight: 22,
|
| 87 |
-
letterSpacing: 0.5,
|
| 88 |
-
renderLineHighlight: 'line',
|
| 89 |
-
formatOnPaste: true,
|
| 90 |
-
formatOnType: false,
|
| 91 |
-
scrollbar: {
|
| 92 |
-
verticalScrollbarSize: 10,
|
| 93 |
-
horizontalScrollbarSize: 10,
|
| 94 |
-
},
|
| 95 |
-
}}
|
| 96 |
-
onMount={handleEditorDidMount}
|
| 97 |
-
/>
|
| 98 |
-
</div>
|
| 99 |
-
);
|
| 100 |
-
}
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/ControlPanel.tsx
DELETED
|
@@ -1,244 +0,0 @@
|
|
| 1 |
-
'use client';
|
| 2 |
-
|
| 3 |
-
import { useState, useEffect, useRef } from 'react';
|
| 4 |
-
import { apiClient } from '@/lib/api';
|
| 5 |
-
import type { Model, Language } from '@/types';
|
| 6 |
-
|
| 7 |
-
interface ControlPanelProps {
|
| 8 |
-
selectedLanguage: Language;
|
| 9 |
-
selectedModel: string;
|
| 10 |
-
onLanguageChange: (language: Language) => void;
|
| 11 |
-
onModelChange: (modelId: string) => void;
|
| 12 |
-
onClear: () => void;
|
| 13 |
-
isGenerating: boolean;
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
export default function ControlPanel({
|
| 17 |
-
selectedLanguage,
|
| 18 |
-
selectedModel,
|
| 19 |
-
onLanguageChange,
|
| 20 |
-
onModelChange,
|
| 21 |
-
onClear,
|
| 22 |
-
isGenerating,
|
| 23 |
-
}: ControlPanelProps) {
|
| 24 |
-
const [models, setModels] = useState<Model[]>([]);
|
| 25 |
-
const [languages, setLanguages] = useState<Language[]>([]);
|
| 26 |
-
const [isLoading, setIsLoading] = useState(true);
|
| 27 |
-
|
| 28 |
-
// Dropdown states
|
| 29 |
-
const [showLanguageDropdown, setShowLanguageDropdown] = useState(false);
|
| 30 |
-
const [showModelDropdown, setShowModelDropdown] = useState(false);
|
| 31 |
-
const languageDropdownRef = useRef<HTMLDivElement>(null);
|
| 32 |
-
const modelDropdownRef = useRef<HTMLDivElement>(null);
|
| 33 |
-
|
| 34 |
-
useEffect(() => {
|
| 35 |
-
loadData();
|
| 36 |
-
}, []);
|
| 37 |
-
|
| 38 |
-
// Close dropdowns when clicking outside
|
| 39 |
-
useEffect(() => {
|
| 40 |
-
const handleClickOutside = (event: MouseEvent) => {
|
| 41 |
-
if (languageDropdownRef.current && !languageDropdownRef.current.contains(event.target as Node)) {
|
| 42 |
-
setShowLanguageDropdown(false);
|
| 43 |
-
}
|
| 44 |
-
if (modelDropdownRef.current && !modelDropdownRef.current.contains(event.target as Node)) {
|
| 45 |
-
setShowModelDropdown(false);
|
| 46 |
-
}
|
| 47 |
-
};
|
| 48 |
-
|
| 49 |
-
document.addEventListener('mousedown', handleClickOutside);
|
| 50 |
-
return () => {
|
| 51 |
-
document.removeEventListener('mousedown', handleClickOutside);
|
| 52 |
-
};
|
| 53 |
-
}, []);
|
| 54 |
-
|
| 55 |
-
const loadData = async () => {
|
| 56 |
-
setIsLoading(true);
|
| 57 |
-
await Promise.all([loadModels(), loadLanguages()]);
|
| 58 |
-
setIsLoading(false);
|
| 59 |
-
};
|
| 60 |
-
|
| 61 |
-
const loadModels = async () => {
|
| 62 |
-
try {
|
| 63 |
-
console.log('Loading models...');
|
| 64 |
-
const modelsList = await apiClient.getModels();
|
| 65 |
-
console.log('Models loaded:', modelsList);
|
| 66 |
-
setModels(modelsList);
|
| 67 |
-
} catch (error) {
|
| 68 |
-
console.error('Failed to load models:', error);
|
| 69 |
-
}
|
| 70 |
-
};
|
| 71 |
-
|
| 72 |
-
const loadLanguages = async () => {
|
| 73 |
-
try {
|
| 74 |
-
console.log('Loading languages...');
|
| 75 |
-
const { languages: languagesList } = await apiClient.getLanguages();
|
| 76 |
-
console.log('Languages loaded:', languagesList);
|
| 77 |
-
setLanguages(languagesList);
|
| 78 |
-
} catch (error) {
|
| 79 |
-
console.error('Failed to load languages:', error);
|
| 80 |
-
}
|
| 81 |
-
};
|
| 82 |
-
|
| 83 |
-
const formatLanguageName = (lang: Language) => {
|
| 84 |
-
if (lang === 'html') return 'HTML';
|
| 85 |
-
if (lang === 'transformers.js') return 'Transformers.js';
|
| 86 |
-
if (lang === 'comfyui') return 'ComfyUI';
|
| 87 |
-
if (lang === 'daggr') return 'Daggr';
|
| 88 |
-
return lang.charAt(0).toUpperCase() + lang.slice(1);
|
| 89 |
-
};
|
| 90 |
-
|
| 91 |
-
const formatModelName = (name: string, id: string) => {
|
| 92 |
-
if (id === 'moonshotai/Kimi-K2.6') return 'Kimi-K2.6 ✨';
|
| 93 |
-
if (id === 'google/gemma-4-31B-it') return 'Gemma-4-31B 🤖';
|
| 94 |
-
if (id === 'zai-org/GLM-5.1') return 'GLM-5.1 🚀';
|
| 95 |
-
if (id === 'Qwen/Qwen3.5-397B-A17B') return 'Qwen3.5-397B-A17B 🤖';
|
| 96 |
-
return name;
|
| 97 |
-
};
|
| 98 |
-
|
| 99 |
-
return (
|
| 100 |
-
<div className="bg-[#000000] h-full flex flex-col">
|
| 101 |
-
{/* Panel Header */}
|
| 102 |
-
<div className="flex items-center px-4 py-3 border-b border-[#424245]/30">
|
| 103 |
-
<h3 className="text-sm font-medium text-[#f5f5f7]">Settings</h3>
|
| 104 |
-
</div>
|
| 105 |
-
|
| 106 |
-
{/* Content */}
|
| 107 |
-
<div className="flex-1 p-4 space-y-5 overflow-y-auto">
|
| 108 |
-
|
| 109 |
-
{/* Language Selection */}
|
| 110 |
-
<div className="relative" ref={languageDropdownRef}>
|
| 111 |
-
<label className="block text-xs font-medium text-[#f5f5f7] mb-2">
|
| 112 |
-
Language
|
| 113 |
-
</label>
|
| 114 |
-
<button
|
| 115 |
-
type="button"
|
| 116 |
-
onClick={() => {
|
| 117 |
-
setShowLanguageDropdown(!showLanguageDropdown);
|
| 118 |
-
setShowModelDropdown(false);
|
| 119 |
-
}}
|
| 120 |
-
disabled={isGenerating || isLoading}
|
| 121 |
-
className="w-full px-3 py-2 bg-[#1d1d1f] text-[#f5f5f7] text-sm border border-[#424245]/50 rounded-lg focus:outline-none focus:border-[#424245] disabled:opacity-40 flex items-center justify-between hover:bg-[#2d2d2f] transition-colors"
|
| 122 |
-
>
|
| 123 |
-
<span>{isLoading ? 'Loading...' : formatLanguageName(selectedLanguage)}</span>
|
| 124 |
-
<svg
|
| 125 |
-
className={`w-3.5 h-3.5 text-[#86868b] transition-transform ${showLanguageDropdown ? 'rotate-180' : ''}`}
|
| 126 |
-
fill="none"
|
| 127 |
-
stroke="currentColor"
|
| 128 |
-
viewBox="0 0 24 24"
|
| 129 |
-
strokeWidth={2.5}
|
| 130 |
-
>
|
| 131 |
-
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
| 132 |
-
</svg>
|
| 133 |
-
</button>
|
| 134 |
-
|
| 135 |
-
{/* Language Dropdown Tray */}
|
| 136 |
-
{showLanguageDropdown && !isLoading && languages.length > 0 && (
|
| 137 |
-
<div className="absolute z-50 w-full mt-1 bg-[#1d1d1f] border border-[#424245] rounded-lg shadow-xl overflow-hidden">
|
| 138 |
-
<div className="max-h-64 overflow-y-auto py-1">
|
| 139 |
-
{languages.map((lang) => (
|
| 140 |
-
<button
|
| 141 |
-
key={lang}
|
| 142 |
-
type="button"
|
| 143 |
-
onClick={() => {
|
| 144 |
-
onLanguageChange(lang);
|
| 145 |
-
setShowLanguageDropdown(false);
|
| 146 |
-
}}
|
| 147 |
-
className={`w-full px-3 py-2 text-left text-sm text-[#f5f5f7] hover:bg-[#2d2d2f] transition-colors ${selectedLanguage === lang ? 'bg-[#2d2d2f]' : ''
|
| 148 |
-
}`}
|
| 149 |
-
>
|
| 150 |
-
{formatLanguageName(lang)}
|
| 151 |
-
</button>
|
| 152 |
-
))}
|
| 153 |
-
</div>
|
| 154 |
-
</div>
|
| 155 |
-
)}
|
| 156 |
-
</div>
|
| 157 |
-
|
| 158 |
-
{/* Model Selection */}
|
| 159 |
-
<div className="relative" ref={modelDropdownRef}>
|
| 160 |
-
<label className="block text-xs font-medium text-[#f5f5f7] mb-2">
|
| 161 |
-
AI Model
|
| 162 |
-
</label>
|
| 163 |
-
<button
|
| 164 |
-
type="button"
|
| 165 |
-
onClick={() => {
|
| 166 |
-
setShowModelDropdown(!showModelDropdown);
|
| 167 |
-
setShowLanguageDropdown(false);
|
| 168 |
-
}}
|
| 169 |
-
disabled={isGenerating}
|
| 170 |
-
className="w-full px-3 py-2 bg-[#1d1d1f] text-[#f5f5f7] text-sm border border-[#424245]/50 rounded-lg focus:outline-none focus:border-[#424245] disabled:opacity-40 flex items-center justify-between hover:bg-[#2d2d2f] transition-colors"
|
| 171 |
-
>
|
| 172 |
-
{isLoading
|
| 173 |
-
? 'Loading...'
|
| 174 |
-
: formatModelName(models.find(m => m.id === selectedModel)?.name || '', selectedModel) || selectedModel || 'Select model'
|
| 175 |
-
}
|
| 176 |
-
<svg
|
| 177 |
-
className={`w-3.5 h-3.5 text-[#86868b] flex-shrink-0 ml-2 transition-transform ${showModelDropdown ? 'rotate-180' : ''}`}
|
| 178 |
-
fill="none"
|
| 179 |
-
stroke="currentColor"
|
| 180 |
-
viewBox="0 0 24 24"
|
| 181 |
-
strokeWidth={2.5}
|
| 182 |
-
>
|
| 183 |
-
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
| 184 |
-
</svg>
|
| 185 |
-
</button>
|
| 186 |
-
|
| 187 |
-
{/* Model Dropdown Tray */}
|
| 188 |
-
{showModelDropdown && models.length > 0 && (
|
| 189 |
-
<div className="absolute z-50 w-full mt-1 bg-[#1d1d1f] border border-[#424245] rounded-lg shadow-xl overflow-hidden">
|
| 190 |
-
<div className="max-h-96 overflow-y-auto py-1">
|
| 191 |
-
{models.map((model) => (
|
| 192 |
-
<button
|
| 193 |
-
key={model.id}
|
| 194 |
-
type="button"
|
| 195 |
-
onClick={() => {
|
| 196 |
-
onModelChange(model.id);
|
| 197 |
-
setShowModelDropdown(false);
|
| 198 |
-
}}
|
| 199 |
-
className={`w-full px-3 py-2 text-left transition-colors ${selectedModel === model.id
|
| 200 |
-
? 'bg-[#2d2d2f]'
|
| 201 |
-
: 'hover:bg-[#2d2d2f]'
|
| 202 |
-
}`}
|
| 203 |
-
>
|
| 204 |
-
<div className="flex items-center justify-between gap-2">
|
| 205 |
-
<span className="text-sm text-[#f5f5f7]">{formatModelName(model.name, model.id)}</span>
|
| 206 |
-
{['moonshotai/Kimi-K2.6', 'google/gemma-4-31B-it', 'zai-org/GLM-5.1', 'Qwen/Qwen3.5-397B-A17B', 'MiniMaxAI/MiniMax-M2.5'].includes(model.id) && (
|
| 207 |
-
<span className="px-1.5 py-0.5 bg-gradient-to-r from-purple-500 to-pink-500 text-white text-[9px] font-bold rounded uppercase flex-shrink-0">
|
| 208 |
-
NEW
|
| 209 |
-
</span>
|
| 210 |
-
)}
|
| 211 |
-
</div>
|
| 212 |
-
{model.description && (
|
| 213 |
-
<div className="text-[10px] text-[#86868b] mt-0.5 leading-relaxed">
|
| 214 |
-
{model.description}
|
| 215 |
-
</div>
|
| 216 |
-
)}
|
| 217 |
-
</button>
|
| 218 |
-
))}
|
| 219 |
-
</div>
|
| 220 |
-
</div>
|
| 221 |
-
)}
|
| 222 |
-
|
| 223 |
-
{/* Model Description */}
|
| 224 |
-
{!isLoading && models.find(m => m.id === selectedModel) && (
|
| 225 |
-
<p className="text-[10px] text-[#86868b] mt-2 leading-relaxed">
|
| 226 |
-
{models.find(m => m.id === selectedModel)?.description}
|
| 227 |
-
</p>
|
| 228 |
-
)}
|
| 229 |
-
</div>
|
| 230 |
-
|
| 231 |
-
{/* Action Buttons */}
|
| 232 |
-
<div className="flex flex-col space-y-2">
|
| 233 |
-
<button
|
| 234 |
-
onClick={onClear}
|
| 235 |
-
disabled={isGenerating}
|
| 236 |
-
className="w-full px-3 py-2.5 bg-[#1d1d1f] text-[#f5f5f7] text-sm rounded-full hover:bg-[#2d2d2f] disabled:opacity-30 disabled:cursor-not-allowed transition-all font-medium border border-[#424245]/50 flex items-center justify-center active:scale-95"
|
| 237 |
-
>
|
| 238 |
-
New Chat
|
| 239 |
-
</button>
|
| 240 |
-
</div>
|
| 241 |
-
</div>
|
| 242 |
-
</div>
|
| 243 |
-
);
|
| 244 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/Header.tsx
DELETED
|
@@ -1,173 +0,0 @@
|
|
| 1 |
-
'use client';
|
| 2 |
-
|
| 3 |
-
import { useState, useEffect } from 'react';
|
| 4 |
-
import {
|
| 5 |
-
initializeOAuth,
|
| 6 |
-
loginWithHuggingFace,
|
| 7 |
-
loginDevMode,
|
| 8 |
-
logout,
|
| 9 |
-
getStoredUserInfo,
|
| 10 |
-
isAuthenticated,
|
| 11 |
-
isDevelopmentMode
|
| 12 |
-
} from '@/lib/auth';
|
| 13 |
-
import { apiClient } from '@/lib/api';
|
| 14 |
-
import type { OAuthUserInfo } from '@/lib/auth';
|
| 15 |
-
|
| 16 |
-
export default function Header() {
|
| 17 |
-
const [userInfo, setUserInfo] = useState<OAuthUserInfo | null>(null);
|
| 18 |
-
const [isLoading, setIsLoading] = useState(true);
|
| 19 |
-
const [showDevLogin, setShowDevLogin] = useState(false);
|
| 20 |
-
const [devUsername, setDevUsername] = useState('');
|
| 21 |
-
const isDevMode = isDevelopmentMode();
|
| 22 |
-
|
| 23 |
-
useEffect(() => {
|
| 24 |
-
handleOAuthInit();
|
| 25 |
-
}, []);
|
| 26 |
-
|
| 27 |
-
const handleOAuthInit = async () => {
|
| 28 |
-
setIsLoading(true);
|
| 29 |
-
try {
|
| 30 |
-
const oauthResult = await initializeOAuth();
|
| 31 |
-
|
| 32 |
-
if (oauthResult) {
|
| 33 |
-
setUserInfo(oauthResult.userInfo);
|
| 34 |
-
// Set token in API client
|
| 35 |
-
apiClient.setToken(oauthResult.accessToken);
|
| 36 |
-
} else {
|
| 37 |
-
// Check if we have stored user info
|
| 38 |
-
const storedUserInfo = getStoredUserInfo();
|
| 39 |
-
if (storedUserInfo) {
|
| 40 |
-
setUserInfo(storedUserInfo);
|
| 41 |
-
}
|
| 42 |
-
}
|
| 43 |
-
} catch (error) {
|
| 44 |
-
console.error('OAuth initialization error:', error);
|
| 45 |
-
} finally {
|
| 46 |
-
setIsLoading(false);
|
| 47 |
-
}
|
| 48 |
-
};
|
| 49 |
-
|
| 50 |
-
const handleLogin = async () => {
|
| 51 |
-
try {
|
| 52 |
-
await loginWithHuggingFace();
|
| 53 |
-
} catch (error) {
|
| 54 |
-
console.error('Login failed:', error);
|
| 55 |
-
alert('Failed to start login process. Please try again.');
|
| 56 |
-
}
|
| 57 |
-
};
|
| 58 |
-
|
| 59 |
-
const handleLogout = () => {
|
| 60 |
-
logout();
|
| 61 |
-
apiClient.logout();
|
| 62 |
-
setUserInfo(null);
|
| 63 |
-
// Reload page to clear state
|
| 64 |
-
window.location.reload();
|
| 65 |
-
};
|
| 66 |
-
|
| 67 |
-
const handleDevLogin = () => {
|
| 68 |
-
if (!devUsername.trim()) {
|
| 69 |
-
alert('Please enter a username');
|
| 70 |
-
return;
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
try {
|
| 74 |
-
const result = loginDevMode(devUsername);
|
| 75 |
-
setUserInfo(result.userInfo);
|
| 76 |
-
apiClient.setToken(result.accessToken);
|
| 77 |
-
setShowDevLogin(false);
|
| 78 |
-
setDevUsername('');
|
| 79 |
-
} catch (error) {
|
| 80 |
-
console.error('Dev login failed:', error);
|
| 81 |
-
alert('Failed to login in dev mode');
|
| 82 |
-
}
|
| 83 |
-
};
|
| 84 |
-
|
| 85 |
-
return (
|
| 86 |
-
<header className="bg-[#000000]/80 backdrop-blur-xl text-white border-b border-[#424245]/30">
|
| 87 |
-
<div className="flex items-center justify-between px-3 md:px-6 h-12 md:h-14">
|
| 88 |
-
<div className="flex items-center space-x-2 md:space-x-3">
|
| 89 |
-
<h1 className="text-sm md:text-base font-medium text-[#f5f5f7]">AnyCoder</h1>
|
| 90 |
-
</div>
|
| 91 |
-
|
| 92 |
-
<div className="flex items-center space-x-3">
|
| 93 |
-
{isLoading ? (
|
| 94 |
-
<span className="text-xs text-[#86868b]">Loading...</span>
|
| 95 |
-
) : userInfo ? (
|
| 96 |
-
<div className="flex items-center space-x-2 md:space-x-3">
|
| 97 |
-
{userInfo.avatarUrl && (
|
| 98 |
-
<img
|
| 99 |
-
src={userInfo.avatarUrl}
|
| 100 |
-
alt={userInfo.name}
|
| 101 |
-
className="w-6 h-6 md:w-7 md:h-7 rounded-full"
|
| 102 |
-
/>
|
| 103 |
-
)}
|
| 104 |
-
<span className="hidden sm:inline text-xs md:text-sm text-[#f5f5f7] font-medium truncate max-w-[100px] md:max-w-none">
|
| 105 |
-
{userInfo.preferredUsername || userInfo.name}
|
| 106 |
-
</span>
|
| 107 |
-
<button
|
| 108 |
-
onClick={handleLogout}
|
| 109 |
-
className="px-3 md:px-3 py-1.5 md:py-1.5 text-[#f5f5f7] text-sm hover:text-white transition-colors"
|
| 110 |
-
>
|
| 111 |
-
Logout
|
| 112 |
-
</button>
|
| 113 |
-
</div>
|
| 114 |
-
) : (
|
| 115 |
-
<div className="flex items-center space-x-2 md:space-x-3">
|
| 116 |
-
{/* Dev Mode Login (only on localhost) */}
|
| 117 |
-
{isDevMode && (
|
| 118 |
-
<>
|
| 119 |
-
{showDevLogin ? (
|
| 120 |
-
<div className="flex items-center space-x-2">
|
| 121 |
-
<input
|
| 122 |
-
type="text"
|
| 123 |
-
value={devUsername}
|
| 124 |
-
onChange={(e) => setDevUsername(e.target.value)}
|
| 125 |
-
onKeyPress={(e) => e.key === 'Enter' && handleDevLogin()}
|
| 126 |
-
placeholder="username"
|
| 127 |
-
className="px-3 py-1.5 rounded-lg text-sm bg-[#1d1d1f] text-[#f5f5f7] border border-[#424245] focus:outline-none focus:border-white/50 w-32 font-medium"
|
| 128 |
-
autoFocus
|
| 129 |
-
/>
|
| 130 |
-
<button
|
| 131 |
-
onClick={handleDevLogin}
|
| 132 |
-
className="px-3 py-1.5 bg-white text-black rounded-lg text-sm hover:bg-[#f5f5f7] font-medium"
|
| 133 |
-
>
|
| 134 |
-
OK
|
| 135 |
-
</button>
|
| 136 |
-
<button
|
| 137 |
-
onClick={() => {
|
| 138 |
-
setShowDevLogin(false);
|
| 139 |
-
setDevUsername('');
|
| 140 |
-
}}
|
| 141 |
-
className="text-[#86868b] hover:text-[#f5f5f7] text-sm"
|
| 142 |
-
>
|
| 143 |
-
✕
|
| 144 |
-
</button>
|
| 145 |
-
</div>
|
| 146 |
-
) : (
|
| 147 |
-
<button
|
| 148 |
-
onClick={() => setShowDevLogin(true)}
|
| 149 |
-
className="px-3 py-1.5 text-sm text-[#f5f5f7] hover:text-white transition-colors"
|
| 150 |
-
title="Dev Mode"
|
| 151 |
-
>
|
| 152 |
-
Dev
|
| 153 |
-
</button>
|
| 154 |
-
)}
|
| 155 |
-
<span className="text-[#86868b] text-sm">or</span>
|
| 156 |
-
</>
|
| 157 |
-
)}
|
| 158 |
-
|
| 159 |
-
{/* OAuth Login */}
|
| 160 |
-
<button
|
| 161 |
-
onClick={handleLogin}
|
| 162 |
-
className="px-3 md:px-4 py-1.5 md:py-2 bg-white text-black rounded-full text-sm hover:bg-[#f5f5f7] transition-all font-medium"
|
| 163 |
-
>
|
| 164 |
-
Sign in
|
| 165 |
-
</button>
|
| 166 |
-
</div>
|
| 167 |
-
)}
|
| 168 |
-
</div>
|
| 169 |
-
</div>
|
| 170 |
-
</header>
|
| 171 |
-
);
|
| 172 |
-
}
|
| 173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/LandingPage.tsx
DELETED
|
@@ -1,1200 +0,0 @@
|
|
| 1 |
-
'use client';
|
| 2 |
-
|
| 3 |
-
import { useState, useEffect, useRef } from 'react';
|
| 4 |
-
import Image from 'next/image';
|
| 5 |
-
import { apiClient } from '@/lib/api';
|
| 6 |
-
import {
|
| 7 |
-
initializeOAuth,
|
| 8 |
-
loginWithHuggingFace,
|
| 9 |
-
loginDevMode,
|
| 10 |
-
logout,
|
| 11 |
-
getStoredUserInfo,
|
| 12 |
-
isAuthenticated as checkIsAuthenticated,
|
| 13 |
-
isDevelopmentMode
|
| 14 |
-
} from '@/lib/auth';
|
| 15 |
-
import type { Model, Language } from '@/types';
|
| 16 |
-
import type { OAuthUserInfo } from '@/lib/auth';
|
| 17 |
-
|
| 18 |
-
interface LandingPageProps {
|
| 19 |
-
onStart: (prompt: string, language: Language, modelId: string, imageUrl?: string, repoId?: string, shouldCreatePR?: boolean) => void;
|
| 20 |
-
onImport?: (code: string, language: Language, importUrl?: string) => void;
|
| 21 |
-
isAuthenticated: boolean;
|
| 22 |
-
initialLanguage?: Language;
|
| 23 |
-
initialModel?: string;
|
| 24 |
-
onAuthChange?: () => void;
|
| 25 |
-
setPendingPR?: (pr: { repoId: string; language: Language } | null) => void;
|
| 26 |
-
pendingPRRef?: React.MutableRefObject<{ repoId: string; language: Language } | null>;
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
export default function LandingPage({
|
| 30 |
-
onStart,
|
| 31 |
-
onImport,
|
| 32 |
-
isAuthenticated,
|
| 33 |
-
initialLanguage = 'html',
|
| 34 |
-
initialModel = 'moonshotai/Kimi-K2.6',
|
| 35 |
-
onAuthChange,
|
| 36 |
-
setPendingPR,
|
| 37 |
-
pendingPRRef
|
| 38 |
-
}: LandingPageProps) {
|
| 39 |
-
const [prompt, setPrompt] = useState('');
|
| 40 |
-
const [selectedLanguage, setSelectedLanguage] = useState<Language>(initialLanguage);
|
| 41 |
-
const [selectedModel, setSelectedModel] = useState<string>(initialModel);
|
| 42 |
-
const [models, setModels] = useState<Model[]>([]);
|
| 43 |
-
const [languages, setLanguages] = useState<Language[]>([]);
|
| 44 |
-
const [isLoading, setIsLoading] = useState(true);
|
| 45 |
-
|
| 46 |
-
// Auth states
|
| 47 |
-
const [userInfo, setUserInfo] = useState<OAuthUserInfo | null>(null);
|
| 48 |
-
const [isAuthLoading, setIsAuthLoading] = useState(true);
|
| 49 |
-
const [showDevLogin, setShowDevLogin] = useState(false);
|
| 50 |
-
const [devUsername, setDevUsername] = useState('');
|
| 51 |
-
const isDevMode = isDevelopmentMode();
|
| 52 |
-
|
| 53 |
-
// Dropdown states
|
| 54 |
-
const [showLanguageDropdown, setShowLanguageDropdown] = useState(false);
|
| 55 |
-
const [showModelDropdown, setShowModelDropdown] = useState(false);
|
| 56 |
-
const [showImportDialog, setShowImportDialog] = useState(false);
|
| 57 |
-
const [showRedesignDialog, setShowRedesignDialog] = useState(false);
|
| 58 |
-
const languageDropdownRef = useRef<HTMLDivElement>(null);
|
| 59 |
-
const modelDropdownRef = useRef<HTMLDivElement>(null);
|
| 60 |
-
const importDialogRef = useRef<HTMLDivElement>(null);
|
| 61 |
-
const redesignDialogRef = useRef<HTMLDivElement>(null);
|
| 62 |
-
|
| 63 |
-
// Trending apps state
|
| 64 |
-
const [trendingApps, setTrendingApps] = useState<any[]>([]);
|
| 65 |
-
|
| 66 |
-
// Import project state
|
| 67 |
-
const [importUrl, setImportUrl] = useState('');
|
| 68 |
-
const [isImporting, setIsImporting] = useState(false);
|
| 69 |
-
const [importError, setImportError] = useState('');
|
| 70 |
-
const [importAction, setImportAction] = useState<'duplicate' | 'update' | 'pr'>('duplicate'); // Default to duplicate
|
| 71 |
-
const [isSpaceOwner, setIsSpaceOwner] = useState(false); // Track if user owns the space
|
| 72 |
-
|
| 73 |
-
// Redesign project state
|
| 74 |
-
const [redesignUrl, setRedesignUrl] = useState('');
|
| 75 |
-
const [isRedesigning, setIsRedesigning] = useState(false);
|
| 76 |
-
const [redesignError, setRedesignError] = useState('');
|
| 77 |
-
const [createPR, setCreatePR] = useState(false); // Default to normal redesign (not PR)
|
| 78 |
-
|
| 79 |
-
// Image upload state
|
| 80 |
-
const [uploadedImageUrl, setUploadedImageUrl] = useState<string | null>(null);
|
| 81 |
-
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 82 |
-
|
| 83 |
-
// Debug effect for dropdown state
|
| 84 |
-
useEffect(() => {
|
| 85 |
-
console.log('showModelDropdown state changed to:', showModelDropdown);
|
| 86 |
-
}, [showModelDropdown]);
|
| 87 |
-
|
| 88 |
-
// Debug effect for models state
|
| 89 |
-
useEffect(() => {
|
| 90 |
-
console.log('models state changed, length:', models.length, 'models:', models);
|
| 91 |
-
}, [models]);
|
| 92 |
-
|
| 93 |
-
useEffect(() => {
|
| 94 |
-
console.log('Component mounted, initial load starting...');
|
| 95 |
-
loadData();
|
| 96 |
-
handleOAuthInit();
|
| 97 |
-
loadTrendingApps();
|
| 98 |
-
// Check auth status periodically to catch OAuth redirects
|
| 99 |
-
const interval = setInterval(() => {
|
| 100 |
-
const authenticated = checkIsAuthenticated();
|
| 101 |
-
if (authenticated && !userInfo) {
|
| 102 |
-
handleOAuthInit();
|
| 103 |
-
}
|
| 104 |
-
}, 1000);
|
| 105 |
-
return () => clearInterval(interval);
|
| 106 |
-
}, []);
|
| 107 |
-
|
| 108 |
-
const handleOAuthInit = async () => {
|
| 109 |
-
setIsAuthLoading(true);
|
| 110 |
-
try {
|
| 111 |
-
const oauthResult = await initializeOAuth();
|
| 112 |
-
|
| 113 |
-
if (oauthResult) {
|
| 114 |
-
setUserInfo(oauthResult.userInfo);
|
| 115 |
-
apiClient.setToken(oauthResult.accessToken);
|
| 116 |
-
if (onAuthChange) onAuthChange();
|
| 117 |
-
} else {
|
| 118 |
-
const storedUserInfo = getStoredUserInfo();
|
| 119 |
-
if (storedUserInfo) {
|
| 120 |
-
setUserInfo(storedUserInfo);
|
| 121 |
-
}
|
| 122 |
-
}
|
| 123 |
-
} catch (error) {
|
| 124 |
-
console.error('OAuth initialization error:', error);
|
| 125 |
-
} finally {
|
| 126 |
-
setIsAuthLoading(false);
|
| 127 |
-
}
|
| 128 |
-
};
|
| 129 |
-
|
| 130 |
-
const handleLogin = async () => {
|
| 131 |
-
try {
|
| 132 |
-
await loginWithHuggingFace();
|
| 133 |
-
} catch (error) {
|
| 134 |
-
console.error('Login failed:', error);
|
| 135 |
-
alert('Failed to start login process. Please try again.');
|
| 136 |
-
}
|
| 137 |
-
};
|
| 138 |
-
|
| 139 |
-
const handleLogout = () => {
|
| 140 |
-
logout();
|
| 141 |
-
apiClient.logout();
|
| 142 |
-
setUserInfo(null);
|
| 143 |
-
if (onAuthChange) onAuthChange();
|
| 144 |
-
window.location.reload();
|
| 145 |
-
};
|
| 146 |
-
|
| 147 |
-
const handleDevLogin = () => {
|
| 148 |
-
if (!devUsername.trim()) {
|
| 149 |
-
alert('Please enter a username');
|
| 150 |
-
return;
|
| 151 |
-
}
|
| 152 |
-
|
| 153 |
-
try {
|
| 154 |
-
const result = loginDevMode(devUsername);
|
| 155 |
-
setUserInfo(result.userInfo);
|
| 156 |
-
apiClient.setToken(result.accessToken);
|
| 157 |
-
setShowDevLogin(false);
|
| 158 |
-
setDevUsername('');
|
| 159 |
-
if (onAuthChange) onAuthChange();
|
| 160 |
-
} catch (error) {
|
| 161 |
-
console.error('Dev login failed:', error);
|
| 162 |
-
alert('Failed to login in dev mode');
|
| 163 |
-
}
|
| 164 |
-
};
|
| 165 |
-
|
| 166 |
-
// Close dropdowns when clicking outside
|
| 167 |
-
useEffect(() => {
|
| 168 |
-
const handleClickOutside = (event: MouseEvent) => {
|
| 169 |
-
if (languageDropdownRef.current && !languageDropdownRef.current.contains(event.target as Node)) {
|
| 170 |
-
setShowLanguageDropdown(false);
|
| 171 |
-
}
|
| 172 |
-
if (modelDropdownRef.current && !modelDropdownRef.current.contains(event.target as Node)) {
|
| 173 |
-
setShowModelDropdown(false);
|
| 174 |
-
}
|
| 175 |
-
if (importDialogRef.current && !importDialogRef.current.contains(event.target as Node)) {
|
| 176 |
-
setShowImportDialog(false);
|
| 177 |
-
}
|
| 178 |
-
if (redesignDialogRef.current && !redesignDialogRef.current.contains(event.target as Node)) {
|
| 179 |
-
setShowRedesignDialog(false);
|
| 180 |
-
}
|
| 181 |
-
};
|
| 182 |
-
|
| 183 |
-
document.addEventListener('mousedown', handleClickOutside);
|
| 184 |
-
return () => {
|
| 185 |
-
document.removeEventListener('mousedown', handleClickOutside);
|
| 186 |
-
};
|
| 187 |
-
}, []);
|
| 188 |
-
|
| 189 |
-
const loadData = async () => {
|
| 190 |
-
console.log('loadData called');
|
| 191 |
-
setIsLoading(true);
|
| 192 |
-
await Promise.all([loadModels(), loadLanguages()]);
|
| 193 |
-
setIsLoading(false);
|
| 194 |
-
console.log('loadData completed');
|
| 195 |
-
};
|
| 196 |
-
|
| 197 |
-
const loadModels = async () => {
|
| 198 |
-
try {
|
| 199 |
-
console.log('Loading models...');
|
| 200 |
-
const modelsList = await apiClient.getModels();
|
| 201 |
-
console.log('Models loaded successfully:', modelsList);
|
| 202 |
-
console.log('Number of models:', modelsList.length);
|
| 203 |
-
setModels(modelsList);
|
| 204 |
-
console.log('Models state updated');
|
| 205 |
-
} catch (error) {
|
| 206 |
-
console.error('Failed to load models:', error);
|
| 207 |
-
setModels([]); // Set empty array on error
|
| 208 |
-
}
|
| 209 |
-
};
|
| 210 |
-
|
| 211 |
-
const loadLanguages = async () => {
|
| 212 |
-
try {
|
| 213 |
-
const { languages: languagesList } = await apiClient.getLanguages();
|
| 214 |
-
setLanguages(languagesList);
|
| 215 |
-
} catch (error) {
|
| 216 |
-
console.error('Failed to load languages:', error);
|
| 217 |
-
}
|
| 218 |
-
};
|
| 219 |
-
|
| 220 |
-
// Check if current model supports images
|
| 221 |
-
// Show immediately for GLM-4.6V even before models load
|
| 222 |
-
const currentModelSupportsImages =
|
| 223 |
-
selectedModel === 'zai-org/GLM-4.6V:zai-org' ||
|
| 224 |
-
models.find(m => m.id === selectedModel)?.supports_images ||
|
| 225 |
-
false;
|
| 226 |
-
|
| 227 |
-
// Debug logging
|
| 228 |
-
useEffect(() => {
|
| 229 |
-
console.log('[LandingPage] Selected model:', selectedModel);
|
| 230 |
-
console.log('[LandingPage] Models loaded:', models.length);
|
| 231 |
-
console.log('[LandingPage] Supports images:', currentModelSupportsImages);
|
| 232 |
-
}, [selectedModel, models, currentModelSupportsImages]);
|
| 233 |
-
|
| 234 |
-
const loadTrendingApps = async () => {
|
| 235 |
-
try {
|
| 236 |
-
const apps = await apiClient.getTrendingAnycoderApps();
|
| 237 |
-
setTrendingApps(apps);
|
| 238 |
-
} catch (error) {
|
| 239 |
-
console.error('Failed to load trending apps:', error);
|
| 240 |
-
}
|
| 241 |
-
};
|
| 242 |
-
|
| 243 |
-
const handleSubmit = (e: React.FormEvent) => {
|
| 244 |
-
e.preventDefault();
|
| 245 |
-
if (prompt.trim() && isAuthenticated) {
|
| 246 |
-
console.log('[LandingPage Submit] Sending with image:', uploadedImageUrl ? 'Yes' : 'No');
|
| 247 |
-
console.log('[LandingPage Submit] Image URL length:', uploadedImageUrl?.length || 0);
|
| 248 |
-
onStart(prompt.trim(), selectedLanguage, selectedModel, uploadedImageUrl || undefined);
|
| 249 |
-
// Clear prompt and image after sending
|
| 250 |
-
setPrompt('');
|
| 251 |
-
setUploadedImageUrl(null);
|
| 252 |
-
} else if (!isAuthenticated) {
|
| 253 |
-
alert('Please sign in with HuggingFace first!');
|
| 254 |
-
}
|
| 255 |
-
};
|
| 256 |
-
|
| 257 |
-
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 258 |
-
const file = e.target.files?.[0];
|
| 259 |
-
if (file) {
|
| 260 |
-
const reader = new FileReader();
|
| 261 |
-
reader.onload = (event) => {
|
| 262 |
-
const imageUrl = event.target?.result as string;
|
| 263 |
-
setUploadedImageUrl(imageUrl);
|
| 264 |
-
};
|
| 265 |
-
reader.readAsDataURL(file);
|
| 266 |
-
}
|
| 267 |
-
};
|
| 268 |
-
|
| 269 |
-
const removeImage = () => {
|
| 270 |
-
setUploadedImageUrl(null);
|
| 271 |
-
if (fileInputRef.current) {
|
| 272 |
-
fileInputRef.current.value = '';
|
| 273 |
-
}
|
| 274 |
-
};
|
| 275 |
-
|
| 276 |
-
const formatLanguageName = (lang: Language) => {
|
| 277 |
-
if (lang === 'html') return 'HTML';
|
| 278 |
-
if (lang === 'transformers.js') return 'Transformers.js';
|
| 279 |
-
if (lang === 'comfyui') return 'ComfyUI';
|
| 280 |
-
if (lang === 'daggr') return 'Daggr';
|
| 281 |
-
return lang.charAt(0).toUpperCase() + lang.slice(1);
|
| 282 |
-
};
|
| 283 |
-
|
| 284 |
-
const formatModelName = (name: string, id: string) => {
|
| 285 |
-
if (id === 'moonshotai/Kimi-K2.6') return 'Kimi-K2.6 ✨';
|
| 286 |
-
if (id === 'google/gemma-4-31B-it') return 'Gemma-4-31B 🤖';
|
| 287 |
-
if (id === 'zai-org/GLM-5.1') return 'GLM-5.1 🚀';
|
| 288 |
-
if (id === 'Qwen/Qwen3.5-397B-A17B') return 'Qwen3.5-397B-A17B 🤖';
|
| 289 |
-
return name;
|
| 290 |
-
};
|
| 291 |
-
|
| 292 |
-
// Check if user owns the imported space
|
| 293 |
-
const checkSpaceOwnership = (url: string) => {
|
| 294 |
-
if (!url || !userInfo?.preferred_username) {
|
| 295 |
-
setIsSpaceOwner(false);
|
| 296 |
-
return;
|
| 297 |
-
}
|
| 298 |
-
|
| 299 |
-
const spaceMatch = url.match(/huggingface\.co\/spaces\/([^\/\s\)]+)\/[^\/\s\)]+/);
|
| 300 |
-
if (spaceMatch) {
|
| 301 |
-
const spaceOwner = spaceMatch[1];
|
| 302 |
-
const isOwner = spaceOwner === userInfo.preferred_username;
|
| 303 |
-
setIsSpaceOwner(isOwner);
|
| 304 |
-
console.log('[Import] Space owner:', spaceOwner, '| Current user:', userInfo.preferred_username, '| Is owner:', isOwner);
|
| 305 |
-
|
| 306 |
-
// Auto-select update mode if owner, otherwise duplicate
|
| 307 |
-
if (isOwner) {
|
| 308 |
-
setImportAction('update');
|
| 309 |
-
} else {
|
| 310 |
-
setImportAction('duplicate');
|
| 311 |
-
}
|
| 312 |
-
} else {
|
| 313 |
-
setIsSpaceOwner(false);
|
| 314 |
-
}
|
| 315 |
-
};
|
| 316 |
-
|
| 317 |
-
const handleImportProject = async () => {
|
| 318 |
-
if (!importUrl.trim()) {
|
| 319 |
-
setImportError('Please enter a valid URL');
|
| 320 |
-
return;
|
| 321 |
-
}
|
| 322 |
-
|
| 323 |
-
if (!isAuthenticated) {
|
| 324 |
-
alert('Please sign in with HuggingFace first!');
|
| 325 |
-
return;
|
| 326 |
-
}
|
| 327 |
-
|
| 328 |
-
setIsImporting(true);
|
| 329 |
-
setImportError('');
|
| 330 |
-
|
| 331 |
-
try {
|
| 332 |
-
console.log('[Import] ========== STARTING IMPORT ==========');
|
| 333 |
-
console.log('[Import] Import URL:', importUrl);
|
| 334 |
-
console.log('[Import] Action:', importAction);
|
| 335 |
-
|
| 336 |
-
// Extract space ID from URL
|
| 337 |
-
const spaceMatch = importUrl.match(/huggingface\.co\/spaces\/([^\/\s\)]+\/[^\/\s\)]+)/);
|
| 338 |
-
console.log('[Import] Space regex match result:', spaceMatch);
|
| 339 |
-
|
| 340 |
-
if (spaceMatch) {
|
| 341 |
-
const fromSpaceId = spaceMatch[1];
|
| 342 |
-
console.log('[Import] ✅ Detected HF Space:', fromSpaceId);
|
| 343 |
-
|
| 344 |
-
// Import the code first (always needed to load in editor)
|
| 345 |
-
const importResult = await apiClient.importProject(importUrl);
|
| 346 |
-
|
| 347 |
-
if (importResult.status !== 'success') {
|
| 348 |
-
setImportError(importResult.message || 'Failed to import project');
|
| 349 |
-
setIsImporting(false);
|
| 350 |
-
return;
|
| 351 |
-
}
|
| 352 |
-
|
| 353 |
-
// Handle different import actions
|
| 354 |
-
if (importAction === 'update' && isSpaceOwner) {
|
| 355 |
-
// Option 1: Update existing space directly (for owners)
|
| 356 |
-
console.log('[Import] Owner update - loading code for direct update to:', fromSpaceId);
|
| 357 |
-
|
| 358 |
-
if (onImport && importResult.code) {
|
| 359 |
-
// Pass the original space URL so future deployments update it
|
| 360 |
-
onImport(importResult.code, importResult.language || 'html', importUrl);
|
| 361 |
-
|
| 362 |
-
alert(`✅ Code loaded!\n\nYou can now make changes and deploy them directly to: ${importUrl}\n\nThe code has been loaded in the editor.`);
|
| 363 |
-
}
|
| 364 |
-
|
| 365 |
-
setShowImportDialog(false);
|
| 366 |
-
setImportUrl('');
|
| 367 |
-
|
| 368 |
-
} else if (importAction === 'pr') {
|
| 369 |
-
// Option 2: Create Pull Request
|
| 370 |
-
console.log('[Import] PR mode - loading code to create PR to:', fromSpaceId);
|
| 371 |
-
|
| 372 |
-
if (onImport && importResult.code) {
|
| 373 |
-
// Load code in editor with the original space for PR tracking
|
| 374 |
-
onImport(importResult.code, importResult.language || 'html', importUrl);
|
| 375 |
-
|
| 376 |
-
// Set pending PR state so any future code generation creates a PR
|
| 377 |
-
if (setPendingPR && pendingPRRef) {
|
| 378 |
-
const prInfo = { repoId: fromSpaceId, language: (importResult.language || 'html') as Language };
|
| 379 |
-
setPendingPR(prInfo);
|
| 380 |
-
pendingPRRef.current = prInfo;
|
| 381 |
-
console.log('[Import PR] Set pending PR:', prInfo);
|
| 382 |
-
}
|
| 383 |
-
|
| 384 |
-
// Show success message
|
| 385 |
-
alert(`✅ Code loaded in PR mode!\n\nYou can now:\n• Make manual edits in the editor\n• Generate new features with AI\n\nWhen you deploy, a Pull Request will be created to: ${fromSpaceId}`);
|
| 386 |
-
}
|
| 387 |
-
|
| 388 |
-
setShowImportDialog(false);
|
| 389 |
-
setImportUrl('');
|
| 390 |
-
|
| 391 |
-
} else {
|
| 392 |
-
// Option 3: Duplicate space (default)
|
| 393 |
-
console.log('[Import] Duplicate mode - will duplicate:', fromSpaceId);
|
| 394 |
-
|
| 395 |
-
const duplicateResult = await apiClient.duplicateSpace(fromSpaceId);
|
| 396 |
-
console.log('[Import] Duplicate API response:', duplicateResult);
|
| 397 |
-
|
| 398 |
-
if (duplicateResult.success) {
|
| 399 |
-
console.log('[Import] ========== DUPLICATE SUCCESS ==========');
|
| 400 |
-
console.log('[Import] Duplicated space URL:', duplicateResult.space_url);
|
| 401 |
-
console.log('[Import] Duplicated space ID:', duplicateResult.space_id);
|
| 402 |
-
console.log('[Import] ==========================================');
|
| 403 |
-
|
| 404 |
-
if (onImport && importResult.code) {
|
| 405 |
-
console.log('[Import] Calling onImport with duplicated space URL:', duplicateResult.space_url);
|
| 406 |
-
// Pass the duplicated space URL so it's tracked for future deployments
|
| 407 |
-
onImport(importResult.code, importResult.language || 'html', duplicateResult.space_url);
|
| 408 |
-
|
| 409 |
-
// Show success message with link to duplicated space
|
| 410 |
-
alert(`✅ Space duplicated successfully!\n\nYour space: ${duplicateResult.space_url}\n\nThe code has been loaded in the editor. Any changes you deploy will update this duplicated space.`);
|
| 411 |
-
}
|
| 412 |
-
|
| 413 |
-
setShowImportDialog(false);
|
| 414 |
-
setImportUrl('');
|
| 415 |
-
} else {
|
| 416 |
-
setImportError(duplicateResult.message || 'Failed to duplicate space');
|
| 417 |
-
}
|
| 418 |
-
}
|
| 419 |
-
} else {
|
| 420 |
-
// Not a Space URL - fall back to regular import
|
| 421 |
-
console.log('[Import] ❌ Not a HF Space URL - using regular import');
|
| 422 |
-
const result = await apiClient.importProject(importUrl);
|
| 423 |
-
|
| 424 |
-
if (result.status === 'success') {
|
| 425 |
-
if (onImport && result.code) {
|
| 426 |
-
onImport(result.code, result.language || 'html', importUrl);
|
| 427 |
-
} else {
|
| 428 |
-
const importMessage = `Imported from ${importUrl}`;
|
| 429 |
-
onStart(importMessage, result.language || 'html', selectedModel, undefined);
|
| 430 |
-
}
|
| 431 |
-
|
| 432 |
-
setShowImportDialog(false);
|
| 433 |
-
setImportUrl('');
|
| 434 |
-
} else {
|
| 435 |
-
setImportError(result.message || 'Failed to import project');
|
| 436 |
-
}
|
| 437 |
-
}
|
| 438 |
-
} catch (error: any) {
|
| 439 |
-
console.error('Import error:', error);
|
| 440 |
-
setImportError(error.response?.data?.message || error.message || 'Failed to import project');
|
| 441 |
-
} finally {
|
| 442 |
-
setIsImporting(false);
|
| 443 |
-
}
|
| 444 |
-
};
|
| 445 |
-
|
| 446 |
-
const handleRedesignProject = async () => {
|
| 447 |
-
if (!redesignUrl.trim()) {
|
| 448 |
-
setRedesignError('Please enter a valid URL');
|
| 449 |
-
return;
|
| 450 |
-
}
|
| 451 |
-
|
| 452 |
-
if (!isAuthenticated) {
|
| 453 |
-
alert('Please sign in with HuggingFace first!');
|
| 454 |
-
return;
|
| 455 |
-
}
|
| 456 |
-
|
| 457 |
-
setIsRedesigning(true);
|
| 458 |
-
setRedesignError('');
|
| 459 |
-
|
| 460 |
-
try {
|
| 461 |
-
// Extract space ID from URL
|
| 462 |
-
const spaceMatch = redesignUrl.match(/huggingface\.co\/spaces\/([^\/\s\)]+\/[^\/\s\)]+)/);
|
| 463 |
-
const repoId = spaceMatch ? spaceMatch[1] : null;
|
| 464 |
-
|
| 465 |
-
if (!repoId) {
|
| 466 |
-
setRedesignError('Please enter a valid HuggingFace Space URL');
|
| 467 |
-
setIsRedesigning(false);
|
| 468 |
-
return;
|
| 469 |
-
}
|
| 470 |
-
|
| 471 |
-
// Import the code first
|
| 472 |
-
const result = await apiClient.importProject(redesignUrl);
|
| 473 |
-
|
| 474 |
-
if (result.status !== 'success') {
|
| 475 |
-
setRedesignError(result.message || 'Failed to import project for redesign');
|
| 476 |
-
setIsRedesigning(false);
|
| 477 |
-
return;
|
| 478 |
-
}
|
| 479 |
-
|
| 480 |
-
if (!createPR) {
|
| 481 |
-
// Option 1: Redesign WITHOUT PR - Duplicate space first, then generate redesign
|
| 482 |
-
console.log('[Redesign] Duplicating space first:', repoId);
|
| 483 |
-
|
| 484 |
-
try {
|
| 485 |
-
const duplicateResult = await apiClient.duplicateSpace(repoId);
|
| 486 |
-
console.log('[Redesign] Duplicate result:', duplicateResult);
|
| 487 |
-
|
| 488 |
-
if (!duplicateResult.success) {
|
| 489 |
-
setRedesignError(duplicateResult.message || 'Failed to duplicate space');
|
| 490 |
-
setIsRedesigning(false);
|
| 491 |
-
return;
|
| 492 |
-
}
|
| 493 |
-
|
| 494 |
-
// Load code and trigger redesign
|
| 495 |
-
if (onImport && onStart) {
|
| 496 |
-
// Pass duplicated space URL
|
| 497 |
-
onImport(result.code, result.language || 'html', duplicateResult.space_url);
|
| 498 |
-
|
| 499 |
-
// Extract duplicated space ID to pass to generation
|
| 500 |
-
const dupSpaceMatch = duplicateResult.space_url?.match(/huggingface\.co\/spaces\/([^\/\s\)]+\/[^\/\s\)]+)/);
|
| 501 |
-
const duplicatedRepoId = dupSpaceMatch ? dupSpaceMatch[1] : undefined;
|
| 502 |
-
|
| 503 |
-
console.log('[Redesign] Duplicated space ID:', duplicatedRepoId);
|
| 504 |
-
|
| 505 |
-
setTimeout(() => {
|
| 506 |
-
const isGradio = (result.language || 'html') === 'gradio';
|
| 507 |
-
const redesignPrompt = `I have existing code in the editor from a duplicated space. Please redesign it to make it look better with minimal components needed, mobile friendly, and modern design.
|
| 508 |
-
|
| 509 |
-
Current code:
|
| 510 |
-
\`\`\`${result.language || 'html'}
|
| 511 |
-
${result.code}
|
| 512 |
-
\`\`\`
|
| 513 |
-
|
| 514 |
-
Please redesign this with:
|
| 515 |
-
- Minimal, clean components
|
| 516 |
-
- Mobile-first responsive design
|
| 517 |
-
- Modern UI/UX best practices
|
| 518 |
-
- Better visual hierarchy and spacing
|
| 519 |
-
|
| 520 |
-
${isGradio ? '\n\nIMPORTANT: Only output app.py with the redesigned UI (themes, layout, styling). Do NOT modify or output any other .py files (utils.py, models.py, etc.). Do NOT include requirements.txt or README.md.' : ''}`;
|
| 521 |
-
|
| 522 |
-
if (onStart) {
|
| 523 |
-
// Pass duplicated space ID so auto-deploy updates it
|
| 524 |
-
console.log('[Redesign] Calling onStart with duplicated repo ID:', duplicatedRepoId);
|
| 525 |
-
console.log('[Redesign] Using moonshotai/Kimi-K2.6 for redesign');
|
| 526 |
-
onStart(redesignPrompt, result.language || 'html', 'moonshotai/Kimi-K2.6', undefined, duplicatedRepoId);
|
| 527 |
-
}
|
| 528 |
-
}, 100);
|
| 529 |
-
|
| 530 |
-
// Show success message
|
| 531 |
-
alert(`✅ Space duplicated!\n\nYour space: ${duplicateResult.space_url}\n\nGenerating redesign now...`);
|
| 532 |
-
}
|
| 533 |
-
|
| 534 |
-
setShowRedesignDialog(false);
|
| 535 |
-
setRedesignUrl('');
|
| 536 |
-
|
| 537 |
-
} catch (dupError: any) {
|
| 538 |
-
console.error('[Redesign] Duplication error:', dupError);
|
| 539 |
-
setRedesignError(dupError.response?.data?.message || dupError.message || 'Failed to duplicate space');
|
| 540 |
-
setIsRedesigning(false);
|
| 541 |
-
return;
|
| 542 |
-
}
|
| 543 |
-
|
| 544 |
-
} else {
|
| 545 |
-
// Option 2: Redesign WITH PR - Import code and generate, then create PR
|
| 546 |
-
if (onImport && onStart) {
|
| 547 |
-
onImport(result.code, result.language || 'html', redesignUrl);
|
| 548 |
-
|
| 549 |
-
setTimeout(() => {
|
| 550 |
-
const isGradio = (result.language || 'html') === 'gradio';
|
| 551 |
-
const redesignPrompt = `I have existing code in the editor that I imported from ${redesignUrl}. Please redesign it to make it look better with minimal components needed, mobile friendly, and modern design.
|
| 552 |
-
|
| 553 |
-
Current code:
|
| 554 |
-
\`\`\`${result.language || 'html'}
|
| 555 |
-
${result.code}
|
| 556 |
-
\`\`\`
|
| 557 |
-
|
| 558 |
-
Please redesign this with:
|
| 559 |
-
- Minimal, clean components
|
| 560 |
-
- Mobile-first responsive design
|
| 561 |
-
- Modern UI/UX best practices
|
| 562 |
-
- Better visual hierarchy and spacing
|
| 563 |
-
|
| 564 |
-
${isGradio ? '\n\nIMPORTANT: Only output app.py with the redesigned UI (themes, layout, styling). Do NOT modify or output any other .py files (utils.py, models.py, etc.). Do NOT include requirements.txt or README.md.' : ''}
|
| 565 |
-
|
| 566 |
-
Note: After generating the redesign, I will create a Pull Request on the original space.`;
|
| 567 |
-
|
| 568 |
-
if (onStart) {
|
| 569 |
-
console.log('[Redesign] Will create PR - not passing repo ID');
|
| 570 |
-
console.log('[Redesign] Using moonshotai/Kimi-K2.6 for redesign');
|
| 571 |
-
onStart(redesignPrompt, result.language || 'html', 'moonshotai/Kimi-K2.6', undefined, repoId, true); // Pass true for shouldCreatePR
|
| 572 |
-
}
|
| 573 |
-
|
| 574 |
-
console.log('[Redesign] Will create PR after code generation completes');
|
| 575 |
-
}, 100);
|
| 576 |
-
|
| 577 |
-
setShowRedesignDialog(false);
|
| 578 |
-
setRedesignUrl('');
|
| 579 |
-
} else {
|
| 580 |
-
setRedesignError('Missing required callbacks. Please try again.');
|
| 581 |
-
}
|
| 582 |
-
}
|
| 583 |
-
} catch (error: any) {
|
| 584 |
-
console.error('Redesign error:', error);
|
| 585 |
-
setRedesignError(error.response?.data?.message || error.message || 'Failed to process redesign request');
|
| 586 |
-
} finally {
|
| 587 |
-
setIsRedesigning(false);
|
| 588 |
-
}
|
| 589 |
-
};
|
| 590 |
-
|
| 591 |
-
return (
|
| 592 |
-
<div className="h-screen flex flex-col bg-[#000000] overflow-hidden">
|
| 593 |
-
{/* Header - Apple style */}
|
| 594 |
-
<header className="flex items-center justify-between px-6 py-3 backdrop-blur-xl bg-[#000000]/80 border-b border-[#424245]/30 flex-shrink-0">
|
| 595 |
-
<a
|
| 596 |
-
href="https://huggingface.co/spaces/akhaliq/anycoder"
|
| 597 |
-
target="_blank"
|
| 598 |
-
rel="noopener noreferrer"
|
| 599 |
-
className="text-sm font-medium text-[#f5f5f7] hover:text-white transition-colors"
|
| 600 |
-
>
|
| 601 |
-
AnyCoder
|
| 602 |
-
</a>
|
| 603 |
-
|
| 604 |
-
{/* Auth Section */}
|
| 605 |
-
<div className="flex items-center space-x-3">
|
| 606 |
-
{isAuthLoading ? (
|
| 607 |
-
<span className="text-xs text-[#86868b]">Loading...</span>
|
| 608 |
-
) : userInfo ? (
|
| 609 |
-
<div className="flex items-center space-x-3">
|
| 610 |
-
{userInfo.avatarUrl && (
|
| 611 |
-
<img
|
| 612 |
-
src={userInfo.avatarUrl}
|
| 613 |
-
alt={userInfo.name}
|
| 614 |
-
className="w-7 h-7 rounded-full"
|
| 615 |
-
/>
|
| 616 |
-
)}
|
| 617 |
-
<span className="hidden sm:inline text-sm text-[#f5f5f7] truncate max-w-[120px] font-medium">
|
| 618 |
-
{userInfo.preferredUsername || userInfo.name}
|
| 619 |
-
</span>
|
| 620 |
-
<button
|
| 621 |
-
onClick={handleLogout}
|
| 622 |
-
className="px-3 py-1.5 text-sm text-[#f5f5f7] hover:text-white transition-colors"
|
| 623 |
-
>
|
| 624 |
-
Logout
|
| 625 |
-
</button>
|
| 626 |
-
</div>
|
| 627 |
-
) : (
|
| 628 |
-
<div className="flex items-center space-x-3">
|
| 629 |
-
{/* Dev Mode Login (only on localhost) */}
|
| 630 |
-
{isDevMode && (
|
| 631 |
-
<>
|
| 632 |
-
{showDevLogin ? (
|
| 633 |
-
<div className="flex items-center space-x-2">
|
| 634 |
-
<input
|
| 635 |
-
type="text"
|
| 636 |
-
value={devUsername}
|
| 637 |
-
onChange={(e) => setDevUsername(e.target.value)}
|
| 638 |
-
onKeyPress={(e) => e.key === 'Enter' && handleDevLogin()}
|
| 639 |
-
placeholder="username"
|
| 640 |
-
className="px-3 py-1.5 rounded-lg text-sm bg-[#1d1d1f] text-[#f5f5f7] border border-[#424245] focus:outline-none focus:border-white/50 w-32 font-medium"
|
| 641 |
-
autoFocus
|
| 642 |
-
/>
|
| 643 |
-
<button
|
| 644 |
-
onClick={handleDevLogin}
|
| 645 |
-
className="px-3 py-1.5 bg-white text-black rounded-lg text-sm hover:bg-[#f5f5f7] font-medium"
|
| 646 |
-
>
|
| 647 |
-
OK
|
| 648 |
-
</button>
|
| 649 |
-
<button
|
| 650 |
-
onClick={() => {
|
| 651 |
-
setShowDevLogin(false);
|
| 652 |
-
setDevUsername('');
|
| 653 |
-
}}
|
| 654 |
-
className="text-[#86868b] hover:text-[#f5f5f7] text-sm"
|
| 655 |
-
>
|
| 656 |
-
✕
|
| 657 |
-
</button>
|
| 658 |
-
</div>
|
| 659 |
-
) : (
|
| 660 |
-
<button
|
| 661 |
-
onClick={() => setShowDevLogin(true)}
|
| 662 |
-
className="px-3 py-1.5 text-sm text-[#f5f5f7] hover:text-white transition-colors"
|
| 663 |
-
title="Dev Mode"
|
| 664 |
-
>
|
| 665 |
-
Dev
|
| 666 |
-
</button>
|
| 667 |
-
)}
|
| 668 |
-
<span className="text-[#86868b] text-sm">or</span>
|
| 669 |
-
</>
|
| 670 |
-
)}
|
| 671 |
-
|
| 672 |
-
{/* OAuth Login */}
|
| 673 |
-
<button
|
| 674 |
-
onClick={handleLogin}
|
| 675 |
-
className="px-4 py-2 bg-white text-black rounded-full text-sm hover:bg-[#f5f5f7] transition-all font-medium"
|
| 676 |
-
>
|
| 677 |
-
Sign in
|
| 678 |
-
</button>
|
| 679 |
-
</div>
|
| 680 |
-
)}
|
| 681 |
-
</div>
|
| 682 |
-
</header>
|
| 683 |
-
|
| 684 |
-
{/* Main Content - Apple-style centered layout */}
|
| 685 |
-
<main className="flex-1 overflow-y-auto px-4 py-6">
|
| 686 |
-
<div className="w-full max-w-3xl mx-auto flex flex-col items-center justify-center min-h-full">
|
| 687 |
-
{/* Apple-style Headline */}
|
| 688 |
-
<div className="text-center mb-8">
|
| 689 |
-
<h2 className="text-4xl md:text-5xl font-semibold text-white mb-2 tracking-tight leading-tight">
|
| 690 |
-
Build with AnyCoder
|
| 691 |
-
</h2>
|
| 692 |
-
<p className="text-base md:text-lg text-[#86868b] font-normal">
|
| 693 |
-
Create apps with AI
|
| 694 |
-
</p>
|
| 695 |
-
</div>
|
| 696 |
-
|
| 697 |
-
{/* Simple prompt form */}
|
| 698 |
-
<form onSubmit={handleSubmit} className="relative w-full mb-8">
|
| 699 |
-
<div className="relative bg-[#2d2d30] rounded-2xl border border-[#424245] shadow-2xl">
|
| 700 |
-
{/* Image Preview */}
|
| 701 |
-
{uploadedImageUrl && (
|
| 702 |
-
<div className="px-4 pt-3">
|
| 703 |
-
<div className="relative inline-block">
|
| 704 |
-
<Image
|
| 705 |
-
src={uploadedImageUrl}
|
| 706 |
-
alt="Upload preview"
|
| 707 |
-
width={120}
|
| 708 |
-
height={120}
|
| 709 |
-
className="rounded-lg object-cover"
|
| 710 |
-
unoptimized
|
| 711 |
-
/>
|
| 712 |
-
<button
|
| 713 |
-
type="button"
|
| 714 |
-
onClick={removeImage}
|
| 715 |
-
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white rounded-full hover:bg-red-600 transition-all flex items-center justify-center text-xs font-bold"
|
| 716 |
-
>
|
| 717 |
-
×
|
| 718 |
-
</button>
|
| 719 |
-
</div>
|
| 720 |
-
</div>
|
| 721 |
-
)}
|
| 722 |
-
|
| 723 |
-
{/* Textarea */}
|
| 724 |
-
<textarea
|
| 725 |
-
value={prompt}
|
| 726 |
-
onChange={(e) => setPrompt(e.target.value)}
|
| 727 |
-
placeholder="Message AnyCoder"
|
| 728 |
-
className="w-full px-4 py-3 text-sm text-[#f5f5f7] bg-transparent placeholder:text-[#86868b] resize-none focus:outline-none min-h-[48px] font-normal"
|
| 729 |
-
rows={1}
|
| 730 |
-
onKeyDown={(e) => {
|
| 731 |
-
if (e.key === 'Enter' && !e.shiftKey) {
|
| 732 |
-
e.preventDefault();
|
| 733 |
-
handleSubmit(e);
|
| 734 |
-
}
|
| 735 |
-
}}
|
| 736 |
-
/>
|
| 737 |
-
|
| 738 |
-
{/* Bottom controls - Apple style */}
|
| 739 |
-
<div className="flex items-center justify-between px-3 pb-3 gap-2">
|
| 740 |
-
{/* Compact dropdowns on the left */}
|
| 741 |
-
<div className="flex items-center gap-2">
|
| 742 |
-
{/* Language Dropdown */}
|
| 743 |
-
<div className="relative" ref={languageDropdownRef}>
|
| 744 |
-
<button
|
| 745 |
-
type="button"
|
| 746 |
-
onClick={(e) => {
|
| 747 |
-
e.stopPropagation();
|
| 748 |
-
console.log('Language button clicked, toggling dropdown');
|
| 749 |
-
setShowLanguageDropdown(!showLanguageDropdown);
|
| 750 |
-
setShowModelDropdown(false);
|
| 751 |
-
}}
|
| 752 |
-
disabled={isLoading}
|
| 753 |
-
className="px-3 py-1.5 bg-[#1d1d1f] text-[#f5f5f7] text-xs border border-[#424245] rounded-full hover:bg-[#2d2d2f] transition-all disabled:opacity-50 flex items-center gap-1.5 font-medium"
|
| 754 |
-
>
|
| 755 |
-
<span>{isLoading ? '...' : formatLanguageName(selectedLanguage)}</span>
|
| 756 |
-
<svg
|
| 757 |
-
className={`w-3 h-3 text-[#86868b] transition-transform ${showLanguageDropdown ? 'rotate-180' : ''}`}
|
| 758 |
-
fill="none"
|
| 759 |
-
stroke="currentColor"
|
| 760 |
-
viewBox="0 0 24 24"
|
| 761 |
-
strokeWidth={2.5}
|
| 762 |
-
>
|
| 763 |
-
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
| 764 |
-
</svg>
|
| 765 |
-
</button>
|
| 766 |
-
|
| 767 |
-
{/* Language Dropdown Menu */}
|
| 768 |
-
{showLanguageDropdown && !isLoading && languages.length > 0 && (
|
| 769 |
-
<div
|
| 770 |
-
className="absolute bottom-full left-0 mb-2 w-48 bg-[#1d1d1f] border border-[#424245] rounded-xl shadow-2xl overflow-hidden backdrop-blur-xl"
|
| 771 |
-
onClick={(e) => e.stopPropagation()}
|
| 772 |
-
>
|
| 773 |
-
<div className="max-h-64 overflow-y-auto py-1">
|
| 774 |
-
{languages.map((lang) => (
|
| 775 |
-
<button
|
| 776 |
-
key={lang}
|
| 777 |
-
type="button"
|
| 778 |
-
onClick={() => {
|
| 779 |
-
setSelectedLanguage(lang);
|
| 780 |
-
setShowLanguageDropdown(false);
|
| 781 |
-
}}
|
| 782 |
-
className={`w-full px-4 py-2.5 text-left text-xs text-[#f5f5f7] hover:bg-[#2d2d2f] transition-colors font-medium ${selectedLanguage === lang ? 'bg-[#2d2d2f]' : ''
|
| 783 |
-
}`}
|
| 784 |
-
>
|
| 785 |
-
{formatLanguageName(lang)}
|
| 786 |
-
</button>
|
| 787 |
-
))}
|
| 788 |
-
</div>
|
| 789 |
-
</div>
|
| 790 |
-
)}
|
| 791 |
-
</div>
|
| 792 |
-
|
| 793 |
-
{/* Model Dropdown */}
|
| 794 |
-
<div className="relative" ref={modelDropdownRef}>
|
| 795 |
-
<button
|
| 796 |
-
type="button"
|
| 797 |
-
onClick={(e) => {
|
| 798 |
-
e.stopPropagation();
|
| 799 |
-
console.log('Model button clicked! Models length:', models.length, 'Show:', showModelDropdown);
|
| 800 |
-
setShowModelDropdown(!showModelDropdown);
|
| 801 |
-
setShowLanguageDropdown(false);
|
| 802 |
-
setShowImportDialog(false);
|
| 803 |
-
}}
|
| 804 |
-
className="px-3 py-1.5 bg-[#1d1d1f] text-[#f5f5f7] text-xs border border-[#424245] rounded-full hover:bg-[#2d2d2f] transition-all flex items-center gap-1.5 max-w-[200px] font-medium"
|
| 805 |
-
>
|
| 806 |
-
<span className="truncate">
|
| 807 |
-
{isLoading
|
| 808 |
-
? '...'
|
| 809 |
-
: formatModelName(models.find(m => m.id === selectedModel)?.name || '', selectedModel) || selectedModel || 'Model'
|
| 810 |
-
}
|
| 811 |
-
</span>
|
| 812 |
-
<svg
|
| 813 |
-
className={`w-3 h-3 text-[#86868b] flex-shrink-0 transition-transform ${showModelDropdown ? 'rotate-180' : ''}`}
|
| 814 |
-
fill="none"
|
| 815 |
-
stroke="currentColor"
|
| 816 |
-
viewBox="0 0 24 24"
|
| 817 |
-
strokeWidth={2.5}
|
| 818 |
-
>
|
| 819 |
-
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
| 820 |
-
</svg>
|
| 821 |
-
</button>
|
| 822 |
-
|
| 823 |
-
{/* Model Dropdown Menu */}
|
| 824 |
-
{showModelDropdown && models.length > 0 && (
|
| 825 |
-
<div
|
| 826 |
-
className="absolute top-full left-0 mt-2 w-56 bg-[#1d1d1f] border border-[#424245] rounded-xl shadow-2xl overflow-hidden backdrop-blur-xl z-50"
|
| 827 |
-
onClick={(e) => e.stopPropagation()}
|
| 828 |
-
>
|
| 829 |
-
<div className="max-h-96 overflow-y-auto py-1">
|
| 830 |
-
{models.map((model) => (
|
| 831 |
-
<button
|
| 832 |
-
key={model.id}
|
| 833 |
-
type="button"
|
| 834 |
-
onClick={() => {
|
| 835 |
-
setSelectedModel(model.id);
|
| 836 |
-
setShowModelDropdown(false);
|
| 837 |
-
}}
|
| 838 |
-
className={`w-full px-4 py-2 text-left transition-colors ${selectedModel === model.id
|
| 839 |
-
? 'bg-[#2d2d2f]'
|
| 840 |
-
: 'hover:bg-[#2d2d2f]'
|
| 841 |
-
}`}
|
| 842 |
-
>
|
| 843 |
-
<div className="flex items-center justify-between gap-2">
|
| 844 |
-
<span className="text-xs font-medium text-[#f5f5f7]">{formatModelName(model.name, model.id)}</span>
|
| 845 |
-
{['moonshotai/Kimi-K2.6', 'google/gemma-4-31B-it', 'zai-org/GLM-5.1', 'Qwen/Qwen3.5-397B-A17B', 'MiniMaxAI/MiniMax-M2.5'].includes(model.id) && (
|
| 846 |
-
<span className="px-1.5 py-0.5 bg-gradient-to-r from-purple-500 to-pink-500 text-white text-[9px] font-bold rounded uppercase">
|
| 847 |
-
NEW
|
| 848 |
-
</span>
|
| 849 |
-
)}
|
| 850 |
-
</div>
|
| 851 |
-
</button>
|
| 852 |
-
))}
|
| 853 |
-
</div>
|
| 854 |
-
</div>
|
| 855 |
-
)}
|
| 856 |
-
</div>
|
| 857 |
-
|
| 858 |
-
{/* Import Project Button */}
|
| 859 |
-
<div className="relative" ref={importDialogRef}>
|
| 860 |
-
<button
|
| 861 |
-
type="button"
|
| 862 |
-
onClick={(e) => {
|
| 863 |
-
e.stopPropagation();
|
| 864 |
-
setShowImportDialog(!showImportDialog);
|
| 865 |
-
setShowLanguageDropdown(false);
|
| 866 |
-
setShowModelDropdown(false);
|
| 867 |
-
setShowRedesignDialog(false);
|
| 868 |
-
setImportError('');
|
| 869 |
-
}}
|
| 870 |
-
className="px-3 py-1.5 bg-[#1d1d1f] text-[#f5f5f7] text-xs border border-[#424245] rounded-full hover:bg-[#2d2d2f] transition-all flex items-center gap-1.5 font-medium"
|
| 871 |
-
>
|
| 872 |
-
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2.5}>
|
| 873 |
-
<path strokeLinecap="round" strokeLinejoin="round" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
| 874 |
-
</svg>
|
| 875 |
-
<span>Import</span>
|
| 876 |
-
</button>
|
| 877 |
-
|
| 878 |
-
{/* Import Dialog */}
|
| 879 |
-
{showImportDialog && (
|
| 880 |
-
<div
|
| 881 |
-
className="absolute top-full left-0 mt-2 w-80 bg-[#1d1d1f] border border-[#424245] rounded-xl shadow-2xl overflow-hidden backdrop-blur-xl z-50"
|
| 882 |
-
onClick={(e) => e.stopPropagation()}
|
| 883 |
-
>
|
| 884 |
-
<div className="p-4">
|
| 885 |
-
<h3 className="text-sm font-medium text-[#f5f5f7] mb-3">Import Project</h3>
|
| 886 |
-
<input
|
| 887 |
-
type="text"
|
| 888 |
-
value={importUrl}
|
| 889 |
-
onChange={(e) => {
|
| 890 |
-
setImportUrl(e.target.value);
|
| 891 |
-
checkSpaceOwnership(e.target.value);
|
| 892 |
-
}}
|
| 893 |
-
onKeyPress={(e) => e.key === 'Enter' && handleImportProject()}
|
| 894 |
-
placeholder="https://huggingface.co/spaces/..."
|
| 895 |
-
className="w-full px-3 py-2 rounded-lg text-xs bg-[#2d2d30] text-[#f5f5f7] border border-[#424245] focus:outline-none focus:border-white/50 font-normal mb-3"
|
| 896 |
-
disabled={isImporting}
|
| 897 |
-
/>
|
| 898 |
-
|
| 899 |
-
{/* Import Action Options */}
|
| 900 |
-
{importUrl.includes('huggingface.co/spaces/') && (
|
| 901 |
-
<div className="mb-3 space-y-2">
|
| 902 |
-
<p className="text-[10px] font-medium text-[#86868b] mb-2">Import Mode:</p>
|
| 903 |
-
|
| 904 |
-
{/* Update Space (only for owners) */}
|
| 905 |
-
{isSpaceOwner && (
|
| 906 |
-
<label className="flex items-start gap-2 cursor-pointer group">
|
| 907 |
-
<input
|
| 908 |
-
type="radio"
|
| 909 |
-
checked={importAction === 'update'}
|
| 910 |
-
onChange={() => setImportAction('update')}
|
| 911 |
-
className="mt-0.5 w-3.5 h-3.5 rounded-full border-[#424245] bg-[#2d2d30] checked:bg-white checked:border-white"
|
| 912 |
-
disabled={isImporting}
|
| 913 |
-
/>
|
| 914 |
-
<div>
|
| 915 |
-
<span className="text-[11px] text-[#f5f5f7] font-medium">Update your space directly</span>
|
| 916 |
-
<p className="text-[10px] text-[#86868b] mt-0.5">
|
| 917 |
-
✅ You own this space - changes will update it
|
| 918 |
-
</p>
|
| 919 |
-
</div>
|
| 920 |
-
</label>
|
| 921 |
-
)}
|
| 922 |
-
|
| 923 |
-
{/* Duplicate Space */}
|
| 924 |
-
<label className="flex items-start gap-2 cursor-pointer group">
|
| 925 |
-
<input
|
| 926 |
-
type="radio"
|
| 927 |
-
checked={importAction === 'duplicate'}
|
| 928 |
-
onChange={() => setImportAction('duplicate')}
|
| 929 |
-
className="mt-0.5 w-3.5 h-3.5 rounded-full border-[#424245] bg-[#2d2d30] checked:bg-white checked:border-white"
|
| 930 |
-
disabled={isImporting}
|
| 931 |
-
/>
|
| 932 |
-
<div>
|
| 933 |
-
<span className="text-[11px] text-[#f5f5f7] font-medium">Duplicate to your account</span>
|
| 934 |
-
<p className="text-[10px] text-[#86868b] mt-0.5">
|
| 935 |
-
Create a copy you can freely modify
|
| 936 |
-
</p>
|
| 937 |
-
</div>
|
| 938 |
-
</label>
|
| 939 |
-
|
| 940 |
-
{/* Create PR */}
|
| 941 |
-
<label className="flex items-start gap-2 cursor-pointer group">
|
| 942 |
-
<input
|
| 943 |
-
type="radio"
|
| 944 |
-
checked={importAction === 'pr'}
|
| 945 |
-
onChange={() => setImportAction('pr')}
|
| 946 |
-
className="mt-0.5 w-3.5 h-3.5 rounded-full border-[#424245] bg-[#2d2d30] checked:bg-white checked:border-white"
|
| 947 |
-
disabled={isImporting}
|
| 948 |
-
/>
|
| 949 |
-
<div>
|
| 950 |
-
<span className="text-[11px] text-[#f5f5f7] font-medium">Create Pull Request</span>
|
| 951 |
-
<p className="text-[10px] text-[#86868b] mt-0.5">
|
| 952 |
-
Propose changes to the original space
|
| 953 |
-
</p>
|
| 954 |
-
</div>
|
| 955 |
-
</label>
|
| 956 |
-
|
| 957 |
-
{importAction === 'pr' && (
|
| 958 |
-
<p className="text-[10px] text-[#86868b] ml-6 mt-1">
|
| 959 |
-
⚠️ Requires space owner to enable PRs
|
| 960 |
-
</p>
|
| 961 |
-
)}
|
| 962 |
-
</div>
|
| 963 |
-
)}
|
| 964 |
-
|
| 965 |
-
{importError && (
|
| 966 |
-
<p className="text-xs text-red-400 mb-2">{importError}</p>
|
| 967 |
-
)}
|
| 968 |
-
|
| 969 |
-
<div className="flex gap-2">
|
| 970 |
-
<button
|
| 971 |
-
onClick={handleImportProject}
|
| 972 |
-
disabled={isImporting || !importUrl.trim()}
|
| 973 |
-
className="flex-1 px-3 py-2 bg-white text-black rounded-lg text-xs hover:bg-[#f5f5f7] disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
| 974 |
-
>
|
| 975 |
-
{isImporting ? 'Importing...' : 'Import'}
|
| 976 |
-
</button>
|
| 977 |
-
<button
|
| 978 |
-
onClick={() => {
|
| 979 |
-
setShowImportDialog(false);
|
| 980 |
-
setImportUrl('');
|
| 981 |
-
setImportError('');
|
| 982 |
-
setIsSpaceOwner(false);
|
| 983 |
-
setImportAction('duplicate');
|
| 984 |
-
}}
|
| 985 |
-
className="px-3 py-2 bg-[#2d2d30] text-[#f5f5f7] rounded-lg text-xs hover:bg-[#3d3d3f] font-medium"
|
| 986 |
-
>
|
| 987 |
-
Cancel
|
| 988 |
-
</button>
|
| 989 |
-
</div>
|
| 990 |
-
<p className="text-[10px] text-[#86868b] mt-3">
|
| 991 |
-
Import from HuggingFace Spaces, Models, or GitHub
|
| 992 |
-
</p>
|
| 993 |
-
</div>
|
| 994 |
-
</div>
|
| 995 |
-
)}
|
| 996 |
-
</div>
|
| 997 |
-
|
| 998 |
-
{/* Redesign Project Button */}
|
| 999 |
-
<div className="relative" ref={redesignDialogRef}>
|
| 1000 |
-
<button
|
| 1001 |
-
type="button"
|
| 1002 |
-
onClick={(e) => {
|
| 1003 |
-
e.stopPropagation();
|
| 1004 |
-
setShowRedesignDialog(!showRedesignDialog);
|
| 1005 |
-
setShowLanguageDropdown(false);
|
| 1006 |
-
setShowModelDropdown(false);
|
| 1007 |
-
setShowImportDialog(false);
|
| 1008 |
-
setRedesignError('');
|
| 1009 |
-
}}
|
| 1010 |
-
className="relative px-3 py-1.5 bg-[#1d1d1f] text-[#f5f5f7] text-xs border border-[#424245] rounded-full hover:bg-[#2d2d2f] transition-all flex items-center gap-1.5 font-medium overflow-visible"
|
| 1011 |
-
>
|
| 1012 |
-
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2.5}>
|
| 1013 |
-
<path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
| 1014 |
-
</svg>
|
| 1015 |
-
<span>Redesign</span>
|
| 1016 |
-
</button>
|
| 1017 |
-
|
| 1018 |
-
{/* Redesign Dialog */}
|
| 1019 |
-
{showRedesignDialog && (
|
| 1020 |
-
<div
|
| 1021 |
-
className="absolute top-full left-0 mt-2 w-80 bg-[#1d1d1f] border border-[#424245] rounded-xl shadow-2xl overflow-hidden backdrop-blur-xl z-50"
|
| 1022 |
-
onClick={(e) => e.stopPropagation()}
|
| 1023 |
-
>
|
| 1024 |
-
<div className="p-4">
|
| 1025 |
-
<h3 className="text-sm font-medium text-[#f5f5f7] mb-3">Redesign Project</h3>
|
| 1026 |
-
<input
|
| 1027 |
-
type="text"
|
| 1028 |
-
value={redesignUrl}
|
| 1029 |
-
onChange={(e) => setRedesignUrl(e.target.value)}
|
| 1030 |
-
onKeyPress={(e) => e.key === 'Enter' && handleRedesignProject()}
|
| 1031 |
-
placeholder="https://huggingface.co/spaces/..."
|
| 1032 |
-
className="w-full px-3 py-2 rounded-lg text-xs bg-[#2d2d30] text-[#f5f5f7] border border-[#424245] focus:outline-none focus:border-white/50 font-normal mb-3"
|
| 1033 |
-
disabled={isRedesigning}
|
| 1034 |
-
/>
|
| 1035 |
-
|
| 1036 |
-
{/* PR Option */}
|
| 1037 |
-
<label className="flex items-center gap-2 mb-1 cursor-pointer">
|
| 1038 |
-
<input
|
| 1039 |
-
type="checkbox"
|
| 1040 |
-
checked={createPR}
|
| 1041 |
-
onChange={(e) => setCreatePR(e.target.checked)}
|
| 1042 |
-
disabled={isRedesigning}
|
| 1043 |
-
className="w-4 h-4 rounded bg-[#2d2d30] border-[#424245] text-white focus:ring-white focus:ring-offset-0"
|
| 1044 |
-
/>
|
| 1045 |
-
<span className="text-xs text-[#f5f5f7]">
|
| 1046 |
-
Create Pull Request on original space
|
| 1047 |
-
</span>
|
| 1048 |
-
</label>
|
| 1049 |
-
|
| 1050 |
-
{createPR && (
|
| 1051 |
-
<p className="text-[10px] text-[#86868b] mb-2 ml-6">
|
| 1052 |
-
⚠️ Note: PR creation requires space owner to enable PRs. If disabled, uncheck this to duplicate the space instead.
|
| 1053 |
-
</p>
|
| 1054 |
-
)}
|
| 1055 |
-
|
| 1056 |
-
{redesignError && (
|
| 1057 |
-
<p className="text-xs text-red-400 mb-2">{redesignError}</p>
|
| 1058 |
-
)}
|
| 1059 |
-
<div className="flex gap-2">
|
| 1060 |
-
<button
|
| 1061 |
-
onClick={handleRedesignProject}
|
| 1062 |
-
disabled={isRedesigning || !redesignUrl.trim()}
|
| 1063 |
-
className="flex-1 px-3 py-2 bg-white text-black rounded-lg text-xs hover:bg-[#f5f5f7] disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
| 1064 |
-
>
|
| 1065 |
-
{isRedesigning ? 'Redesigning...' : 'Redesign'}
|
| 1066 |
-
</button>
|
| 1067 |
-
<button
|
| 1068 |
-
onClick={() => {
|
| 1069 |
-
setShowRedesignDialog(false);
|
| 1070 |
-
setRedesignUrl('');
|
| 1071 |
-
setRedesignError('');
|
| 1072 |
-
}}
|
| 1073 |
-
className="px-3 py-2 bg-[#2d2d30] text-[#f5f5f7] rounded-lg text-xs hover:bg-[#3d3d3f] font-medium"
|
| 1074 |
-
>
|
| 1075 |
-
Cancel
|
| 1076 |
-
</button>
|
| 1077 |
-
</div>
|
| 1078 |
-
<p className="text-[10px] text-[#86868b] mt-3">
|
| 1079 |
-
{createPR
|
| 1080 |
-
? 'Creates a Pull Request on the original space with your redesign'
|
| 1081 |
-
: 'Import and automatically redesign with modern, mobile-friendly design'}
|
| 1082 |
-
</p>
|
| 1083 |
-
</div>
|
| 1084 |
-
</div>
|
| 1085 |
-
)}
|
| 1086 |
-
</div>
|
| 1087 |
-
</div>
|
| 1088 |
-
|
| 1089 |
-
{/* Right side - Image upload + Send button group */}
|
| 1090 |
-
<div className="flex items-center gap-2">
|
| 1091 |
-
{/* Image Upload Button (only if model supports images) */}
|
| 1092 |
-
{currentModelSupportsImages && (
|
| 1093 |
-
<>
|
| 1094 |
-
<input
|
| 1095 |
-
ref={fileInputRef}
|
| 1096 |
-
type="file"
|
| 1097 |
-
accept="image/*"
|
| 1098 |
-
onChange={handleImageUpload}
|
| 1099 |
-
className="hidden"
|
| 1100 |
-
disabled={!isAuthenticated}
|
| 1101 |
-
/>
|
| 1102 |
-
<button
|
| 1103 |
-
type="button"
|
| 1104 |
-
onClick={() => fileInputRef.current?.click()}
|
| 1105 |
-
disabled={!isAuthenticated}
|
| 1106 |
-
className="p-2 bg-[#1d1d1f] text-[#f5f5f7] rounded-full hover:bg-[#424245] disabled:opacity-30 disabled:cursor-not-allowed transition-all active:scale-95"
|
| 1107 |
-
title="Upload image"
|
| 1108 |
-
>
|
| 1109 |
-
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2.5}>
|
| 1110 |
-
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
|
| 1111 |
-
</svg>
|
| 1112 |
-
</button>
|
| 1113 |
-
</>
|
| 1114 |
-
)}
|
| 1115 |
-
|
| 1116 |
-
{/* Send button - Apple style */}
|
| 1117 |
-
<button
|
| 1118 |
-
type="submit"
|
| 1119 |
-
disabled={!prompt.trim() || !isAuthenticated}
|
| 1120 |
-
className="p-2 bg-white text-[#1d1d1f] rounded-full hover:bg-[#f5f5f7] disabled:opacity-30 disabled:cursor-not-allowed transition-all active:scale-95 shadow-lg"
|
| 1121 |
-
title="Send"
|
| 1122 |
-
>
|
| 1123 |
-
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2.5}>
|
| 1124 |
-
<path strokeLinecap="round" strokeLinejoin="round" d="M5 12h14M12 5l7 7-7 7" />
|
| 1125 |
-
</svg>
|
| 1126 |
-
</button>
|
| 1127 |
-
</div>
|
| 1128 |
-
</div>
|
| 1129 |
-
</div>
|
| 1130 |
-
|
| 1131 |
-
{!isAuthenticated && (
|
| 1132 |
-
<div className="mt-4 text-center">
|
| 1133 |
-
<p className="text-xs text-[#86868b]">
|
| 1134 |
-
Sign in to get started
|
| 1135 |
-
</p>
|
| 1136 |
-
</div>
|
| 1137 |
-
)}
|
| 1138 |
-
</form>
|
| 1139 |
-
|
| 1140 |
-
{/* Trending Apps Section */}
|
| 1141 |
-
{trendingApps.length > 0 && (
|
| 1142 |
-
<div className="mt-8 w-full">
|
| 1143 |
-
<h3 className="text-xl font-semibold text-white mb-4 text-center">
|
| 1144 |
-
Top Trending Apps Built with AnyCoder
|
| 1145 |
-
</h3>
|
| 1146 |
-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
| 1147 |
-
{trendingApps.map((app) => (
|
| 1148 |
-
<a
|
| 1149 |
-
key={app.id}
|
| 1150 |
-
href={`https://huggingface.co/spaces/${app.id}`}
|
| 1151 |
-
target="_blank"
|
| 1152 |
-
rel="noopener noreferrer"
|
| 1153 |
-
className="group bg-[#1d1d1f] border border-[#424245] rounded-xl p-4 hover:border-white/30 transition-all hover:shadow-xl hover:scale-[1.02]">
|
| 1154 |
-
<div className="flex items-start justify-between mb-2">
|
| 1155 |
-
<div className="flex-1 min-w-0">
|
| 1156 |
-
<h4 className="text-xs font-medium text-[#f5f5f7] truncate group-hover:text-white transition-colors">
|
| 1157 |
-
{app.id.split('/')[1]}
|
| 1158 |
-
</h4>
|
| 1159 |
-
<p className="text-[10px] text-[#86868b] mt-0.5">
|
| 1160 |
-
by {app.id.split('/')[0]}
|
| 1161 |
-
</p>
|
| 1162 |
-
</div>
|
| 1163 |
-
<div className="flex items-center gap-1.5 flex-shrink-0 ml-2">
|
| 1164 |
-
<div className="flex items-center gap-0.5">
|
| 1165 |
-
<svg className="w-3 h-3 text-[#86868b]" fill="currentColor" viewBox="0 0 20 20">
|
| 1166 |
-
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
| 1167 |
-
</svg>
|
| 1168 |
-
<span className="text-[10px] text-[#86868b] font-medium">{app.likes}</span>
|
| 1169 |
-
</div>
|
| 1170 |
-
<div className="flex items-center gap-0.5">
|
| 1171 |
-
<svg className="w-3 h-3 text-[#86868b]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 1172 |
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
|
| 1173 |
-
</svg>
|
| 1174 |
-
<span className="text-[10px] text-[#86868b] font-medium">{app.trendingScore}</span>
|
| 1175 |
-
</div>
|
| 1176 |
-
</div>
|
| 1177 |
-
</div>
|
| 1178 |
-
<div className="flex flex-wrap gap-1">
|
| 1179 |
-
<span className="px-1.5 py-0.5 bg-[#2d2d30] text-[#86868b] text-[9px] rounded-full font-medium">
|
| 1180 |
-
{app.sdk}
|
| 1181 |
-
</span>
|
| 1182 |
-
{app.tags?.slice(0, 2).map((tag: string) =>
|
| 1183 |
-
tag !== 'anycoder' && tag !== app.sdk && tag !== 'region:us' && (
|
| 1184 |
-
<span key={tag} className="px-1.5 py-0.5 bg-[#2d2d30] text-[#86868b] text-[9px] rounded-full font-medium">
|
| 1185 |
-
{tag}
|
| 1186 |
-
</span>
|
| 1187 |
-
)
|
| 1188 |
-
)}
|
| 1189 |
-
</div>
|
| 1190 |
-
</a>
|
| 1191 |
-
))}
|
| 1192 |
-
</div>
|
| 1193 |
-
</div>
|
| 1194 |
-
)}
|
| 1195 |
-
</div>
|
| 1196 |
-
</main>
|
| 1197 |
-
</div>
|
| 1198 |
-
);
|
| 1199 |
-
}
|
| 1200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/lib/api.ts
DELETED
|
@@ -1,541 +0,0 @@
|
|
| 1 |
-
// API client for AnyCoder backend
|
| 2 |
-
|
| 3 |
-
import axios, { AxiosInstance } from 'axios';
|
| 4 |
-
import type {
|
| 5 |
-
Model,
|
| 6 |
-
AuthStatus,
|
| 7 |
-
CodeGenerationRequest,
|
| 8 |
-
DeploymentRequest,
|
| 9 |
-
DeploymentResponse,
|
| 10 |
-
Language,
|
| 11 |
-
} from '@/types';
|
| 12 |
-
|
| 13 |
-
// Use relative URLs in production (Next.js rewrites will proxy to backend)
|
| 14 |
-
// In local dev, use localhost:8000 for direct backend access
|
| 15 |
-
const getApiUrl = () => {
|
| 16 |
-
// If explicitly set via env var, use it (for development)
|
| 17 |
-
if (process.env.NEXT_PUBLIC_API_URL) {
|
| 18 |
-
console.log('[API Client] Using explicit API URL:', process.env.NEXT_PUBLIC_API_URL);
|
| 19 |
-
return process.env.NEXT_PUBLIC_API_URL;
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
// For server-side rendering, always use relative URLs
|
| 23 |
-
if (typeof window === 'undefined') {
|
| 24 |
-
console.log('[API Client] SSR mode: using relative URLs');
|
| 25 |
-
return '';
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
// On localhost (dev mode), use direct backend URL
|
| 29 |
-
const hostname = window.location.hostname;
|
| 30 |
-
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
| 31 |
-
console.log('[API Client] Localhost dev mode: using http://localhost:8000');
|
| 32 |
-
return 'http://localhost:8000';
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
// In production (HF Space), use relative URLs (Next.js proxies to backend)
|
| 36 |
-
console.log('[API Client] Production mode: using relative URLs (proxied by Next.js)');
|
| 37 |
-
return '';
|
| 38 |
-
};
|
| 39 |
-
|
| 40 |
-
const API_URL = getApiUrl();
|
| 41 |
-
|
| 42 |
-
class ApiClient {
|
| 43 |
-
private client: AxiosInstance;
|
| 44 |
-
private token: string | null = null;
|
| 45 |
-
|
| 46 |
-
constructor() {
|
| 47 |
-
this.client = axios.create({
|
| 48 |
-
baseURL: API_URL,
|
| 49 |
-
headers: {
|
| 50 |
-
'Content-Type': 'application/json',
|
| 51 |
-
},
|
| 52 |
-
timeout: 10000, // 10 second timeout to prevent hanging connections
|
| 53 |
-
});
|
| 54 |
-
|
| 55 |
-
// Add auth token to requests if available
|
| 56 |
-
this.client.interceptors.request.use((config) => {
|
| 57 |
-
// ALWAYS use OAuth token primarily, session token is for backend tracking only
|
| 58 |
-
if (this.token) {
|
| 59 |
-
config.headers.Authorization = `Bearer ${this.token}`;
|
| 60 |
-
}
|
| 61 |
-
return config;
|
| 62 |
-
});
|
| 63 |
-
|
| 64 |
-
// Add response interceptor to handle authentication errors
|
| 65 |
-
this.client.interceptors.response.use(
|
| 66 |
-
(response) => response,
|
| 67 |
-
(error) => {
|
| 68 |
-
// Handle 401 errors (expired/invalid authentication)
|
| 69 |
-
// ONLY log out on specific auth errors, not all 401s
|
| 70 |
-
if (error.response && error.response.status === 401) {
|
| 71 |
-
const errorData = error.response.data;
|
| 72 |
-
const errorMessage = errorData?.detail || errorData?.message || '';
|
| 73 |
-
|
| 74 |
-
// Only log out if it's an authentication/session issue
|
| 75 |
-
// Don't log out for permission errors on specific resources
|
| 76 |
-
const shouldLogout =
|
| 77 |
-
errorMessage.includes('Authentication required') ||
|
| 78 |
-
errorMessage.includes('Invalid token') ||
|
| 79 |
-
errorMessage.includes('Token expired') ||
|
| 80 |
-
errorMessage.includes('Session expired') ||
|
| 81 |
-
error.config?.url?.includes('/auth/');
|
| 82 |
-
|
| 83 |
-
if (shouldLogout && typeof window !== 'undefined') {
|
| 84 |
-
// Clear ALL authentication data including session token
|
| 85 |
-
localStorage.removeItem('hf_oauth_token');
|
| 86 |
-
localStorage.removeItem('hf_session_token');
|
| 87 |
-
localStorage.removeItem('hf_user_info');
|
| 88 |
-
this.token = null;
|
| 89 |
-
|
| 90 |
-
// Dispatch custom event to notify UI components
|
| 91 |
-
window.dispatchEvent(new CustomEvent('auth-expired', {
|
| 92 |
-
detail: { message: 'Your session has expired. Please sign in again.' }
|
| 93 |
-
}));
|
| 94 |
-
}
|
| 95 |
-
}
|
| 96 |
-
return Promise.reject(error);
|
| 97 |
-
}
|
| 98 |
-
);
|
| 99 |
-
|
| 100 |
-
// Load token from localStorage on client side
|
| 101 |
-
if (typeof window !== 'undefined') {
|
| 102 |
-
this.token = localStorage.getItem('hf_oauth_token');
|
| 103 |
-
}
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
setToken(token: string | null) {
|
| 107 |
-
this.token = token;
|
| 108 |
-
// Note: OAuth token is stored by auth.ts, not here
|
| 109 |
-
// We just keep it in memory for API calls
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
getToken(): string | null {
|
| 113 |
-
return this.token;
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
// Cache helpers
|
| 117 |
-
private getCachedData<T>(key: string, maxAgeMs: number): T | null {
|
| 118 |
-
if (typeof window === 'undefined') return null;
|
| 119 |
-
|
| 120 |
-
try {
|
| 121 |
-
const cached = localStorage.getItem(key);
|
| 122 |
-
if (!cached) return null;
|
| 123 |
-
|
| 124 |
-
const { data, timestamp } = JSON.parse(cached);
|
| 125 |
-
const age = Date.now() - timestamp;
|
| 126 |
-
|
| 127 |
-
if (age > maxAgeMs) {
|
| 128 |
-
localStorage.removeItem(key);
|
| 129 |
-
return null;
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
return data;
|
| 133 |
-
} catch (error) {
|
| 134 |
-
console.error(`Failed to get cached data for ${key}:`, error);
|
| 135 |
-
return null;
|
| 136 |
-
}
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
private setCachedData<T>(key: string, data: T): void {
|
| 140 |
-
if (typeof window === 'undefined') return;
|
| 141 |
-
|
| 142 |
-
try {
|
| 143 |
-
localStorage.setItem(key, JSON.stringify({
|
| 144 |
-
data,
|
| 145 |
-
timestamp: Date.now()
|
| 146 |
-
}));
|
| 147 |
-
} catch (error) {
|
| 148 |
-
console.error(`Failed to cache data for ${key}:`, error);
|
| 149 |
-
}
|
| 150 |
-
}
|
| 151 |
-
|
| 152 |
-
async getModels(): Promise<Model[]> {
|
| 153 |
-
// Check cache first (24 hour TTL - cache once per day)
|
| 154 |
-
const cached = this.getCachedData<Model[]>('anycoder_models', 24 * 60 * 60 * 1000);
|
| 155 |
-
if (cached) {
|
| 156 |
-
console.log('Using cached models:', cached.length, 'models');
|
| 157 |
-
return cached;
|
| 158 |
-
}
|
| 159 |
-
|
| 160 |
-
try {
|
| 161 |
-
console.log('Fetching models from API...');
|
| 162 |
-
const response = await this.client.get<Model[]>('/api/models');
|
| 163 |
-
const models = response.data;
|
| 164 |
-
|
| 165 |
-
// Cache the successful response
|
| 166 |
-
if (models && models.length > 0) {
|
| 167 |
-
this.setCachedData('anycoder_models', models);
|
| 168 |
-
console.log('Cached', models.length, 'models (valid for 24 hours)');
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
return models;
|
| 172 |
-
} catch (error: any) {
|
| 173 |
-
// Handle connection errors gracefully
|
| 174 |
-
const isConnectionError =
|
| 175 |
-
error.code === 'ECONNABORTED' ||
|
| 176 |
-
error.code === 'ECONNRESET' ||
|
| 177 |
-
error.code === 'ECONNREFUSED' ||
|
| 178 |
-
error.message?.includes('socket hang up') ||
|
| 179 |
-
error.message?.includes('timeout') ||
|
| 180 |
-
error.message?.includes('Network Error') ||
|
| 181 |
-
error.response?.status === 503 ||
|
| 182 |
-
error.response?.status === 502;
|
| 183 |
-
|
| 184 |
-
if (isConnectionError) {
|
| 185 |
-
// Try to return stale cache if available
|
| 186 |
-
const staleCache = this.getCachedData<Model[]>('anycoder_models', Infinity);
|
| 187 |
-
if (staleCache && staleCache.length > 0) {
|
| 188 |
-
console.warn('Backend not available, using stale cached models');
|
| 189 |
-
return staleCache;
|
| 190 |
-
}
|
| 191 |
-
|
| 192 |
-
console.warn('Backend not available, cannot load models');
|
| 193 |
-
return [];
|
| 194 |
-
}
|
| 195 |
-
// Re-throw other errors
|
| 196 |
-
throw error;
|
| 197 |
-
}
|
| 198 |
-
}
|
| 199 |
-
|
| 200 |
-
async getLanguages(): Promise<{ languages: Language[] }> {
|
| 201 |
-
// Check cache first (24 hour TTL - cache once per day)
|
| 202 |
-
const cached = this.getCachedData<Language[]>('anycoder_languages', 24 * 60 * 60 * 1000);
|
| 203 |
-
if (cached) {
|
| 204 |
-
console.log('Using cached languages:', cached.length, 'languages');
|
| 205 |
-
return { languages: cached };
|
| 206 |
-
}
|
| 207 |
-
|
| 208 |
-
try {
|
| 209 |
-
console.log('Fetching languages from API...');
|
| 210 |
-
const response = await this.client.get<{ languages: Language[] }>('/api/languages');
|
| 211 |
-
const languages = response.data.languages;
|
| 212 |
-
|
| 213 |
-
// Cache the successful response
|
| 214 |
-
if (languages && languages.length > 0) {
|
| 215 |
-
this.setCachedData('anycoder_languages', languages);
|
| 216 |
-
console.log('Cached', languages.length, 'languages (valid for 24 hours)');
|
| 217 |
-
}
|
| 218 |
-
|
| 219 |
-
return response.data;
|
| 220 |
-
} catch (error: any) {
|
| 221 |
-
// Handle connection errors gracefully
|
| 222 |
-
const isConnectionError =
|
| 223 |
-
error.code === 'ECONNABORTED' ||
|
| 224 |
-
error.code === 'ECONNRESET' ||
|
| 225 |
-
error.code === 'ECONNREFUSED' ||
|
| 226 |
-
error.message?.includes('socket hang up') ||
|
| 227 |
-
error.message?.includes('timeout') ||
|
| 228 |
-
error.message?.includes('Network Error') ||
|
| 229 |
-
error.response?.status === 503 ||
|
| 230 |
-
error.response?.status === 502;
|
| 231 |
-
|
| 232 |
-
if (isConnectionError) {
|
| 233 |
-
// Try to return stale cache if available
|
| 234 |
-
const staleCache = this.getCachedData<Language[]>('anycoder_languages', Infinity);
|
| 235 |
-
if (staleCache && staleCache.length > 0) {
|
| 236 |
-
console.warn('Backend not available, using stale cached languages');
|
| 237 |
-
return { languages: staleCache };
|
| 238 |
-
}
|
| 239 |
-
|
| 240 |
-
// Fall back to default languages
|
| 241 |
-
console.warn('Backend not available, using default languages');
|
| 242 |
-
return { languages: ['html', 'gradio', 'transformers.js', 'streamlit', 'comfyui', 'react'] };
|
| 243 |
-
}
|
| 244 |
-
// Re-throw other errors
|
| 245 |
-
throw error;
|
| 246 |
-
}
|
| 247 |
-
}
|
| 248 |
-
|
| 249 |
-
async getAuthStatus(): Promise<AuthStatus> {
|
| 250 |
-
try {
|
| 251 |
-
const response = await this.client.get<AuthStatus>('/api/auth/status');
|
| 252 |
-
return response.data;
|
| 253 |
-
} catch (error: any) {
|
| 254 |
-
// Silently handle connection errors - don't spam console
|
| 255 |
-
if (error.code === 'ECONNABORTED' || error.code === 'ECONNRESET' || error.message?.includes('socket hang up')) {
|
| 256 |
-
// Connection error - backend may not be ready
|
| 257 |
-
return {
|
| 258 |
-
authenticated: false,
|
| 259 |
-
username: undefined,
|
| 260 |
-
message: 'Connection error',
|
| 261 |
-
};
|
| 262 |
-
}
|
| 263 |
-
// For other errors, return not authenticated
|
| 264 |
-
return {
|
| 265 |
-
authenticated: false,
|
| 266 |
-
username: undefined,
|
| 267 |
-
message: 'Not authenticated',
|
| 268 |
-
};
|
| 269 |
-
}
|
| 270 |
-
}
|
| 271 |
-
|
| 272 |
-
// Stream-based code generation using Fetch API with streaming (supports POST)
|
| 273 |
-
generateCodeStream(
|
| 274 |
-
request: CodeGenerationRequest,
|
| 275 |
-
onChunk: (content: string) => void,
|
| 276 |
-
onComplete: (code: string, reasoning?: string) => void,
|
| 277 |
-
onError: (error: string) => void,
|
| 278 |
-
onDeploying?: (message: string) => void,
|
| 279 |
-
onDeployed?: (message: string, spaceUrl: string) => void,
|
| 280 |
-
onDeployError?: (message: string) => void
|
| 281 |
-
): () => void {
|
| 282 |
-
// Build the URL correctly whether we have a base URL or not
|
| 283 |
-
const baseUrl = API_URL || window.location.origin;
|
| 284 |
-
const url = new URL('/api/generate', baseUrl);
|
| 285 |
-
|
| 286 |
-
let abortController = new AbortController();
|
| 287 |
-
let accumulatedCode = '';
|
| 288 |
-
let buffer = ''; // Buffer for incomplete SSE lines
|
| 289 |
-
|
| 290 |
-
// Use fetch with POST to support large payloads
|
| 291 |
-
fetch(url.toString(), {
|
| 292 |
-
method: 'POST',
|
| 293 |
-
headers: {
|
| 294 |
-
'Content-Type': 'application/json',
|
| 295 |
-
...(this.token ? { 'Authorization': `Bearer ${this.token}` } : {}),
|
| 296 |
-
},
|
| 297 |
-
body: JSON.stringify(request),
|
| 298 |
-
signal: abortController.signal,
|
| 299 |
-
})
|
| 300 |
-
.then(async (response) => {
|
| 301 |
-
// Handle rate limit errors before parsing response
|
| 302 |
-
if (response.status === 429) {
|
| 303 |
-
onError('⏱️ Rate limit exceeded. Free tier allows up to 20 requests per minute. Please wait a moment and try again.');
|
| 304 |
-
return;
|
| 305 |
-
}
|
| 306 |
-
|
| 307 |
-
if (!response.ok) {
|
| 308 |
-
throw new Error(`HTTP error! status: ${response.status}`);
|
| 309 |
-
}
|
| 310 |
-
|
| 311 |
-
if (!response.body) {
|
| 312 |
-
throw new Error('Response body is null');
|
| 313 |
-
}
|
| 314 |
-
|
| 315 |
-
const reader = response.body.getReader();
|
| 316 |
-
const decoder = new TextDecoder();
|
| 317 |
-
|
| 318 |
-
while (true) {
|
| 319 |
-
const { done, value } = await reader.read();
|
| 320 |
-
|
| 321 |
-
if (done) {
|
| 322 |
-
console.log('[Stream] Stream ended, total code length:', accumulatedCode.length);
|
| 323 |
-
if (accumulatedCode) {
|
| 324 |
-
onComplete(accumulatedCode);
|
| 325 |
-
}
|
| 326 |
-
break;
|
| 327 |
-
}
|
| 328 |
-
|
| 329 |
-
// Decode chunk and add to buffer
|
| 330 |
-
buffer += decoder.decode(value, { stream: true });
|
| 331 |
-
|
| 332 |
-
// Process complete SSE messages (ending with \n\n)
|
| 333 |
-
const messages = buffer.split('\n\n');
|
| 334 |
-
|
| 335 |
-
// Keep the last incomplete message in the buffer
|
| 336 |
-
buffer = messages.pop() || '';
|
| 337 |
-
|
| 338 |
-
// Process each complete message
|
| 339 |
-
for (const message of messages) {
|
| 340 |
-
if (!message.trim()) continue;
|
| 341 |
-
|
| 342 |
-
// Parse SSE format: "data: {...}"
|
| 343 |
-
const lines = message.split('\n');
|
| 344 |
-
for (const line of lines) {
|
| 345 |
-
if (line.startsWith('data: ')) {
|
| 346 |
-
try {
|
| 347 |
-
const jsonStr = line.substring(6);
|
| 348 |
-
const data = JSON.parse(jsonStr);
|
| 349 |
-
console.log('[Stream] Received event:', data.type, data.content?.substring(0, 50));
|
| 350 |
-
|
| 351 |
-
if (data.type === 'chunk' && data.content) {
|
| 352 |
-
accumulatedCode += data.content;
|
| 353 |
-
onChunk(data.content);
|
| 354 |
-
} else if (data.type === 'complete') {
|
| 355 |
-
console.log('[Stream] Generation complete, total code length:', data.code?.length || accumulatedCode.length);
|
| 356 |
-
// Use the complete code from the message if available, otherwise use accumulated
|
| 357 |
-
const finalCode = data.code || accumulatedCode;
|
| 358 |
-
onComplete(finalCode, data.reasoning);
|
| 359 |
-
// Don't return yet - might have deployment events coming
|
| 360 |
-
} else if (data.type === 'deploying') {
|
| 361 |
-
console.log('[Stream] Deployment started:', data.message);
|
| 362 |
-
if (onDeploying) {
|
| 363 |
-
onDeploying(data.message || 'Deploying...');
|
| 364 |
-
}
|
| 365 |
-
} else if (data.type === 'deployed') {
|
| 366 |
-
console.log('[Stream] Deployment successful:', data.space_url);
|
| 367 |
-
if (onDeployed) {
|
| 368 |
-
onDeployed(data.message || 'Deployed!', data.space_url);
|
| 369 |
-
}
|
| 370 |
-
} else if (data.type === 'deploy_error') {
|
| 371 |
-
console.log('[Stream] Deployment error:', data.message);
|
| 372 |
-
if (onDeployError) {
|
| 373 |
-
onDeployError(data.message || 'Deployment failed');
|
| 374 |
-
}
|
| 375 |
-
} else if (data.type === 'error') {
|
| 376 |
-
console.error('[Stream] Error:', data.message);
|
| 377 |
-
onError(data.message || 'Unknown error occurred');
|
| 378 |
-
return; // Exit the processing loop
|
| 379 |
-
}
|
| 380 |
-
} catch (error) {
|
| 381 |
-
console.error('Error parsing SSE data:', error, 'Line:', line);
|
| 382 |
-
}
|
| 383 |
-
}
|
| 384 |
-
}
|
| 385 |
-
}
|
| 386 |
-
}
|
| 387 |
-
})
|
| 388 |
-
.catch((error) => {
|
| 389 |
-
if (error.name === 'AbortError') {
|
| 390 |
-
console.log('[Stream] Request aborted');
|
| 391 |
-
return;
|
| 392 |
-
}
|
| 393 |
-
console.error('[Stream] Fetch error:', error);
|
| 394 |
-
onError(error.message || 'Connection error occurred');
|
| 395 |
-
});
|
| 396 |
-
|
| 397 |
-
// Return cleanup function
|
| 398 |
-
return () => {
|
| 399 |
-
abortController.abort();
|
| 400 |
-
};
|
| 401 |
-
}
|
| 402 |
-
|
| 403 |
-
// Alternative: WebSocket-based generation
|
| 404 |
-
generateCodeWebSocket(
|
| 405 |
-
request: CodeGenerationRequest,
|
| 406 |
-
onChunk: (content: string) => void,
|
| 407 |
-
onComplete: (code: string) => void,
|
| 408 |
-
onError: (error: string) => void
|
| 409 |
-
): WebSocket {
|
| 410 |
-
// Build WebSocket URL correctly for both dev and production
|
| 411 |
-
const baseUrl = API_URL || window.location.origin;
|
| 412 |
-
const wsUrl = baseUrl.replace('http', 'ws') + '/ws/generate';
|
| 413 |
-
const ws = new WebSocket(wsUrl);
|
| 414 |
-
|
| 415 |
-
ws.onopen = () => {
|
| 416 |
-
ws.send(JSON.stringify(request));
|
| 417 |
-
};
|
| 418 |
-
|
| 419 |
-
ws.onmessage = (event) => {
|
| 420 |
-
try {
|
| 421 |
-
const data = JSON.parse(event.data);
|
| 422 |
-
|
| 423 |
-
if (data.type === 'chunk' && data.content) {
|
| 424 |
-
onChunk(data.content);
|
| 425 |
-
} else if (data.type === 'complete' && data.code) {
|
| 426 |
-
onComplete(data.code);
|
| 427 |
-
ws.close();
|
| 428 |
-
} else if (data.type === 'error') {
|
| 429 |
-
onError(data.message || 'Unknown error occurred');
|
| 430 |
-
ws.close();
|
| 431 |
-
}
|
| 432 |
-
} catch (error) {
|
| 433 |
-
console.error('Error parsing WebSocket data:', error);
|
| 434 |
-
}
|
| 435 |
-
};
|
| 436 |
-
|
| 437 |
-
ws.onerror = (error) => {
|
| 438 |
-
console.error('WebSocket error:', error);
|
| 439 |
-
onError('Connection error occurred');
|
| 440 |
-
};
|
| 441 |
-
|
| 442 |
-
return ws;
|
| 443 |
-
}
|
| 444 |
-
|
| 445 |
-
async deploy(request: DeploymentRequest): Promise<DeploymentResponse> {
|
| 446 |
-
console.log('[API Client] Deploy request:', {
|
| 447 |
-
endpoint: '/api/deploy',
|
| 448 |
-
method: 'POST',
|
| 449 |
-
baseURL: API_URL,
|
| 450 |
-
hasToken: !!this.token,
|
| 451 |
-
language: request.language,
|
| 452 |
-
code_length: request.code?.length,
|
| 453 |
-
space_name: request.space_name,
|
| 454 |
-
existing_repo_id: request.existing_repo_id,
|
| 455 |
-
});
|
| 456 |
-
|
| 457 |
-
try {
|
| 458 |
-
const response = await this.client.post<DeploymentResponse>('/api/deploy', request);
|
| 459 |
-
console.log('[API Client] Deploy response:', response.status, response.data);
|
| 460 |
-
return response.data;
|
| 461 |
-
} catch (error: any) {
|
| 462 |
-
console.error('[API Client] Deploy error:', {
|
| 463 |
-
status: error.response?.status,
|
| 464 |
-
statusText: error.response?.statusText,
|
| 465 |
-
data: error.response?.data,
|
| 466 |
-
message: error.message,
|
| 467 |
-
});
|
| 468 |
-
throw error;
|
| 469 |
-
}
|
| 470 |
-
}
|
| 471 |
-
|
| 472 |
-
async importProject(url: string, preferLocal: boolean = false): Promise<any> {
|
| 473 |
-
const response = await this.client.post('/api/import', { url, prefer_local: preferLocal });
|
| 474 |
-
return response.data;
|
| 475 |
-
}
|
| 476 |
-
|
| 477 |
-
async importSpace(username: string, spaceName: string): Promise<any> {
|
| 478 |
-
const response = await this.client.get(`/api/import/space/${username}/${spaceName}`);
|
| 479 |
-
return response.data;
|
| 480 |
-
}
|
| 481 |
-
|
| 482 |
-
async importModel(modelId: string, preferLocal: boolean = false): Promise<any> {
|
| 483 |
-
const response = await this.client.get(`/api/import/model/${modelId}`, {
|
| 484 |
-
params: { prefer_local: preferLocal }
|
| 485 |
-
});
|
| 486 |
-
return response.data;
|
| 487 |
-
}
|
| 488 |
-
|
| 489 |
-
async importGithub(owner: string, repo: string): Promise<any> {
|
| 490 |
-
const response = await this.client.get(`/api/import/github/${owner}/${repo}`);
|
| 491 |
-
return response.data;
|
| 492 |
-
}
|
| 493 |
-
|
| 494 |
-
async createPullRequest(repoId: string, code: string, language: string, prTitle?: string, prDescription?: string): Promise<any> {
|
| 495 |
-
const response = await this.client.post('/api/create-pr', {
|
| 496 |
-
repo_id: repoId,
|
| 497 |
-
code,
|
| 498 |
-
language,
|
| 499 |
-
pr_title: prTitle,
|
| 500 |
-
pr_description: prDescription
|
| 501 |
-
});
|
| 502 |
-
return response.data;
|
| 503 |
-
}
|
| 504 |
-
|
| 505 |
-
async duplicateSpace(fromSpaceId: string, toSpaceName?: string, isPrivate: boolean = false): Promise<any> {
|
| 506 |
-
const response = await this.client.post('/api/duplicate-space', {
|
| 507 |
-
from_space_id: fromSpaceId,
|
| 508 |
-
to_space_name: toSpaceName,
|
| 509 |
-
private: isPrivate
|
| 510 |
-
});
|
| 511 |
-
return response.data;
|
| 512 |
-
}
|
| 513 |
-
|
| 514 |
-
logout() {
|
| 515 |
-
this.token = null;
|
| 516 |
-
}
|
| 517 |
-
|
| 518 |
-
async getTrendingAnycoderApps(): Promise<any[]> {
|
| 519 |
-
try {
|
| 520 |
-
// Fetch from HuggingFace API directly
|
| 521 |
-
const response = await axios.get('https://huggingface.co/api/spaces', {
|
| 522 |
-
timeout: 5000,
|
| 523 |
-
});
|
| 524 |
-
|
| 525 |
-
// Filter for apps with 'anycoder' tag and sort by trendingScore
|
| 526 |
-
const anycoderApps = response.data
|
| 527 |
-
.filter((space: any) => space.tags && space.tags.includes('anycoder'))
|
| 528 |
-
.sort((a: any, b: any) => (b.trendingScore || 0) - (a.trendingScore || 0))
|
| 529 |
-
.slice(0, 6);
|
| 530 |
-
|
| 531 |
-
return anycoderApps;
|
| 532 |
-
} catch (error) {
|
| 533 |
-
console.error('Failed to fetch trending anycoder apps:', error);
|
| 534 |
-
return [];
|
| 535 |
-
}
|
| 536 |
-
}
|
| 537 |
-
}
|
| 538 |
-
|
| 539 |
-
// Export singleton instance
|
| 540 |
-
export const apiClient = new ApiClient();
|
| 541 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/lib/auth.ts
DELETED
|
@@ -1,287 +0,0 @@
|
|
| 1 |
-
// HuggingFace OAuth authentication utilities (Server-side flow for Docker Spaces)
|
| 2 |
-
|
| 3 |
-
const STORAGE_KEY = 'hf_oauth_token';
|
| 4 |
-
const SESSION_KEY = 'hf_session_token'; // NEW: Store session UUID
|
| 5 |
-
const USER_INFO_KEY = 'hf_user_info';
|
| 6 |
-
const DEV_MODE_KEY = 'hf_dev_mode';
|
| 7 |
-
const API_BASE = '/api';
|
| 8 |
-
|
| 9 |
-
// Check if we're in development mode (localhost)
|
| 10 |
-
const isDevelopment = typeof window !== 'undefined' &&
|
| 11 |
-
(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
|
| 12 |
-
|
| 13 |
-
export interface OAuthUserInfo {
|
| 14 |
-
id?: string;
|
| 15 |
-
sub?: string;
|
| 16 |
-
name: string;
|
| 17 |
-
preferred_username?: string;
|
| 18 |
-
preferredUsername?: string;
|
| 19 |
-
picture?: string;
|
| 20 |
-
avatarUrl?: string;
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
export interface OAuthResult {
|
| 24 |
-
accessToken: string;
|
| 25 |
-
accessTokenExpiresAt: Date;
|
| 26 |
-
userInfo: OAuthUserInfo;
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
/**
|
| 30 |
-
* Initialize OAuth and check if user is logged in
|
| 31 |
-
* Returns OAuth result if user is already logged in
|
| 32 |
-
*/
|
| 33 |
-
export async function initializeOAuth(): Promise<OAuthResult | null> {
|
| 34 |
-
try {
|
| 35 |
-
// In development mode, check for dev mode login first
|
| 36 |
-
if (isDevelopment && isDevModeEnabled()) {
|
| 37 |
-
const storedToken = getStoredToken();
|
| 38 |
-
const storedUserInfo = getStoredUserInfo();
|
| 39 |
-
|
| 40 |
-
if (storedToken && storedUserInfo) {
|
| 41 |
-
return {
|
| 42 |
-
accessToken: storedToken,
|
| 43 |
-
accessTokenExpiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
| 44 |
-
userInfo: storedUserInfo,
|
| 45 |
-
};
|
| 46 |
-
}
|
| 47 |
-
return null;
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
// Check if we're handling an OAuth callback (session parameter in URL)
|
| 51 |
-
const urlParams = new URLSearchParams(window.location.search);
|
| 52 |
-
const sessionToken = urlParams.get('session');
|
| 53 |
-
|
| 54 |
-
if (sessionToken) {
|
| 55 |
-
// Fetch session data from backend
|
| 56 |
-
try {
|
| 57 |
-
const response = await fetch(`${API_BASE}/auth/session?session=${sessionToken}`);
|
| 58 |
-
if (response.ok) {
|
| 59 |
-
const data = await response.json();
|
| 60 |
-
|
| 61 |
-
// Normalize user info
|
| 62 |
-
const userInfo: OAuthUserInfo = {
|
| 63 |
-
id: data.user_info.sub || data.user_info.id,
|
| 64 |
-
name: data.user_info.name,
|
| 65 |
-
preferredUsername: data.user_info.preferred_username || data.user_info.preferredUsername,
|
| 66 |
-
avatarUrl: data.user_info.picture || data.user_info.avatarUrl,
|
| 67 |
-
};
|
| 68 |
-
|
| 69 |
-
const oauthResult: OAuthResult = {
|
| 70 |
-
accessToken: data.access_token,
|
| 71 |
-
accessTokenExpiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
| 72 |
-
userInfo,
|
| 73 |
-
};
|
| 74 |
-
|
| 75 |
-
// Store the OAuth result AND session token
|
| 76 |
-
storeOAuthData(oauthResult);
|
| 77 |
-
storeSessionToken(sessionToken); // NEW: Store session UUID
|
| 78 |
-
|
| 79 |
-
// Clean up URL
|
| 80 |
-
window.history.replaceState({}, document.title, window.location.pathname);
|
| 81 |
-
|
| 82 |
-
return oauthResult;
|
| 83 |
-
}
|
| 84 |
-
} catch (error) {
|
| 85 |
-
console.error('Failed to fetch session:', error);
|
| 86 |
-
}
|
| 87 |
-
}
|
| 88 |
-
|
| 89 |
-
// Check if we have stored credentials
|
| 90 |
-
const storedToken = getStoredToken();
|
| 91 |
-
const storedUserInfo = getStoredUserInfo();
|
| 92 |
-
|
| 93 |
-
if (storedToken && storedUserInfo) {
|
| 94 |
-
return {
|
| 95 |
-
accessToken: storedToken,
|
| 96 |
-
accessTokenExpiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
| 97 |
-
userInfo: storedUserInfo,
|
| 98 |
-
};
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
return null;
|
| 102 |
-
} catch (error) {
|
| 103 |
-
console.error('OAuth initialization error:', error);
|
| 104 |
-
return null;
|
| 105 |
-
}
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
/**
|
| 109 |
-
* Redirect to HuggingFace OAuth login page (via backend)
|
| 110 |
-
*/
|
| 111 |
-
export async function loginWithHuggingFace(): Promise<void> {
|
| 112 |
-
try {
|
| 113 |
-
// Call backend to get OAuth URL
|
| 114 |
-
const response = await fetch(`${API_BASE}/auth/login`);
|
| 115 |
-
if (!response.ok) {
|
| 116 |
-
throw new Error('Failed to get login URL');
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
const data = await response.json();
|
| 120 |
-
// Redirect to the OAuth authorization URL
|
| 121 |
-
window.location.href = data.login_url;
|
| 122 |
-
} catch (error) {
|
| 123 |
-
console.error('Failed to initiate OAuth login:', error);
|
| 124 |
-
throw new Error('Failed to start login process');
|
| 125 |
-
}
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
/**
|
| 129 |
-
* Logout and clear stored credentials
|
| 130 |
-
*/
|
| 131 |
-
export function logout(): void {
|
| 132 |
-
if (typeof window !== 'undefined') {
|
| 133 |
-
localStorage.removeItem(STORAGE_KEY);
|
| 134 |
-
localStorage.removeItem(SESSION_KEY); // NEW: Clear session token
|
| 135 |
-
localStorage.removeItem(USER_INFO_KEY);
|
| 136 |
-
localStorage.removeItem(DEV_MODE_KEY);
|
| 137 |
-
}
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
/**
|
| 141 |
-
* Store OAuth data in localStorage
|
| 142 |
-
*/
|
| 143 |
-
function storeOAuthData(result: OAuthResult): void {
|
| 144 |
-
if (typeof window !== 'undefined') {
|
| 145 |
-
localStorage.setItem(STORAGE_KEY, result.accessToken);
|
| 146 |
-
localStorage.setItem(USER_INFO_KEY, JSON.stringify(result.userInfo));
|
| 147 |
-
}
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
/**
|
| 151 |
-
* Store session token in localStorage
|
| 152 |
-
*/
|
| 153 |
-
function storeSessionToken(sessionToken: string): void {
|
| 154 |
-
if (typeof window !== 'undefined') {
|
| 155 |
-
localStorage.setItem(SESSION_KEY, sessionToken);
|
| 156 |
-
}
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
/**
|
| 160 |
-
* Get stored session token
|
| 161 |
-
*/
|
| 162 |
-
export function getStoredSessionToken(): string | null {
|
| 163 |
-
if (typeof window !== 'undefined') {
|
| 164 |
-
return localStorage.getItem(SESSION_KEY);
|
| 165 |
-
}
|
| 166 |
-
return null;
|
| 167 |
-
}
|
| 168 |
-
|
| 169 |
-
/**
|
| 170 |
-
* Get stored access token
|
| 171 |
-
*/
|
| 172 |
-
export function getStoredToken(): string | null {
|
| 173 |
-
if (typeof window !== 'undefined') {
|
| 174 |
-
return localStorage.getItem(STORAGE_KEY);
|
| 175 |
-
}
|
| 176 |
-
return null;
|
| 177 |
-
}
|
| 178 |
-
|
| 179 |
-
/**
|
| 180 |
-
* Get stored user info
|
| 181 |
-
*/
|
| 182 |
-
export function getStoredUserInfo(): OAuthUserInfo | null {
|
| 183 |
-
if (typeof window !== 'undefined') {
|
| 184 |
-
const userInfoStr = localStorage.getItem(USER_INFO_KEY);
|
| 185 |
-
if (userInfoStr) {
|
| 186 |
-
try {
|
| 187 |
-
return JSON.parse(userInfoStr);
|
| 188 |
-
} catch {
|
| 189 |
-
return null;
|
| 190 |
-
}
|
| 191 |
-
}
|
| 192 |
-
}
|
| 193 |
-
return null;
|
| 194 |
-
}
|
| 195 |
-
|
| 196 |
-
/**
|
| 197 |
-
* Check if user is authenticated
|
| 198 |
-
*/
|
| 199 |
-
export function isAuthenticated(): boolean {
|
| 200 |
-
return getStoredToken() !== null;
|
| 201 |
-
}
|
| 202 |
-
|
| 203 |
-
/**
|
| 204 |
-
* Validate authentication with backend
|
| 205 |
-
* Returns true if authenticated, false if session expired
|
| 206 |
-
*/
|
| 207 |
-
export async function validateAuthentication(): Promise<boolean> {
|
| 208 |
-
const token = getStoredToken();
|
| 209 |
-
if (!token) {
|
| 210 |
-
return false;
|
| 211 |
-
}
|
| 212 |
-
|
| 213 |
-
// Skip validation for dev mode tokens
|
| 214 |
-
if (isDevelopment && token.startsWith('dev_token_')) {
|
| 215 |
-
return true;
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
try {
|
| 219 |
-
const response = await fetch(`${API_BASE}/auth/status`, {
|
| 220 |
-
headers: {
|
| 221 |
-
'Authorization': `Bearer ${token}`,
|
| 222 |
-
},
|
| 223 |
-
});
|
| 224 |
-
|
| 225 |
-
if (response.status === 401) {
|
| 226 |
-
// Session expired, clean up
|
| 227 |
-
logout();
|
| 228 |
-
return false;
|
| 229 |
-
}
|
| 230 |
-
|
| 231 |
-
if (!response.ok) {
|
| 232 |
-
return false;
|
| 233 |
-
}
|
| 234 |
-
|
| 235 |
-
const data = await response.json();
|
| 236 |
-
return data.authenticated === true;
|
| 237 |
-
} catch (error) {
|
| 238 |
-
console.error('Failed to validate authentication:', error);
|
| 239 |
-
return false;
|
| 240 |
-
}
|
| 241 |
-
}
|
| 242 |
-
|
| 243 |
-
/**
|
| 244 |
-
* Development mode login (mock authentication)
|
| 245 |
-
*/
|
| 246 |
-
export function loginDevMode(username: string): OAuthResult {
|
| 247 |
-
const mockToken = `dev_token_${username}_${Date.now()}`;
|
| 248 |
-
const mockUserInfo: OAuthUserInfo = {
|
| 249 |
-
id: `dev_${Date.now()}`,
|
| 250 |
-
name: username,
|
| 251 |
-
preferredUsername: username.toLowerCase().replace(/\s+/g, '_'),
|
| 252 |
-
avatarUrl: `https://ui-avatars.com/api/?name=${encodeURIComponent(username)}&background=random&size=128`,
|
| 253 |
-
};
|
| 254 |
-
|
| 255 |
-
const result: OAuthResult = {
|
| 256 |
-
accessToken: mockToken,
|
| 257 |
-
accessTokenExpiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
| 258 |
-
userInfo: mockUserInfo,
|
| 259 |
-
};
|
| 260 |
-
|
| 261 |
-
// Store the mock data
|
| 262 |
-
storeOAuthData(result);
|
| 263 |
-
// Mark as dev mode
|
| 264 |
-
if (typeof window !== 'undefined') {
|
| 265 |
-
localStorage.setItem(DEV_MODE_KEY, 'true');
|
| 266 |
-
}
|
| 267 |
-
|
| 268 |
-
return result;
|
| 269 |
-
}
|
| 270 |
-
|
| 271 |
-
/**
|
| 272 |
-
* Check if dev mode is enabled
|
| 273 |
-
*/
|
| 274 |
-
export function isDevModeEnabled(): boolean {
|
| 275 |
-
if (typeof window !== 'undefined') {
|
| 276 |
-
return localStorage.getItem(DEV_MODE_KEY) === 'true';
|
| 277 |
-
}
|
| 278 |
-
return false;
|
| 279 |
-
}
|
| 280 |
-
|
| 281 |
-
/**
|
| 282 |
-
* Check if we're in development environment
|
| 283 |
-
*/
|
| 284 |
-
export function isDevelopmentMode(): boolean {
|
| 285 |
-
return isDevelopment;
|
| 286 |
-
}
|
| 287 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/types/index.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
| 1 |
-
// Type definitions for AnyCoder frontend
|
| 2 |
-
|
| 3 |
-
export interface Model {
|
| 4 |
-
name: string;
|
| 5 |
-
id: string;
|
| 6 |
-
description: string;
|
| 7 |
-
supports_images?: boolean;
|
| 8 |
-
}
|
| 9 |
-
|
| 10 |
-
export interface Message {
|
| 11 |
-
role: 'user' | 'assistant' | 'system';
|
| 12 |
-
content: string;
|
| 13 |
-
timestamp?: string;
|
| 14 |
-
image_url?: string; // For vision models
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
export interface CodeGenerationRequest {
|
| 18 |
-
query: string;
|
| 19 |
-
language: string;
|
| 20 |
-
model_id: string;
|
| 21 |
-
provider: string;
|
| 22 |
-
history: string[][];
|
| 23 |
-
agent_mode: boolean;
|
| 24 |
-
existing_repo_id?: string; // For auto-deploy to update existing space
|
| 25 |
-
skip_auto_deploy?: boolean; // Skip auto-deploy (for PR creation)
|
| 26 |
-
image_url?: string; // For vision models like GLM-4.6V
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
export interface CodeGenerationResponse {
|
| 30 |
-
code: string;
|
| 31 |
-
history: string[][];
|
| 32 |
-
status: string;
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
export interface StreamChunk {
|
| 36 |
-
type: 'chunk' | 'complete' | 'error' | 'status';
|
| 37 |
-
content?: string;
|
| 38 |
-
code?: string;
|
| 39 |
-
message?: string;
|
| 40 |
-
progress?: number;
|
| 41 |
-
timestamp?: string;
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
export interface AuthStatus {
|
| 45 |
-
authenticated: boolean;
|
| 46 |
-
username?: string;
|
| 47 |
-
message: string;
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
export interface DeploymentRequest {
|
| 51 |
-
code: string;
|
| 52 |
-
space_name?: string;
|
| 53 |
-
language: string;
|
| 54 |
-
requirements?: string;
|
| 55 |
-
existing_repo_id?: string; // For updating existing spaces
|
| 56 |
-
commit_message?: string;
|
| 57 |
-
history?: Array<{ role: string; content: string }>; // Chat history for tracking
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
export interface DeploymentResponse {
|
| 61 |
-
success: boolean;
|
| 62 |
-
space_url?: string;
|
| 63 |
-
message: string;
|
| 64 |
-
dev_mode?: boolean;
|
| 65 |
-
repo_id?: string;
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
export type Language = 'html' | 'gradio' | 'transformers.js' | 'streamlit' | 'comfyui' | 'react' | 'daggr';
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/tailwind.config.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
| 1 |
-
/** @type {import('tailwindcss').Config} */
|
| 2 |
-
module.exports = {
|
| 3 |
-
content: [
|
| 4 |
-
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
| 5 |
-
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
| 6 |
-
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
| 7 |
-
],
|
| 8 |
-
theme: {
|
| 9 |
-
extend: {
|
| 10 |
-
colors: {
|
| 11 |
-
primary: {
|
| 12 |
-
50: '#eff6ff',
|
| 13 |
-
100: '#dbeafe',
|
| 14 |
-
200: '#bfdbfe',
|
| 15 |
-
300: '#93c5fd',
|
| 16 |
-
400: '#60a5fa',
|
| 17 |
-
500: '#3b82f6',
|
| 18 |
-
600: '#2563eb',
|
| 19 |
-
700: '#1d4ed8',
|
| 20 |
-
800: '#1e40af',
|
| 21 |
-
900: '#1e3a8a',
|
| 22 |
-
},
|
| 23 |
-
},
|
| 24 |
-
},
|
| 25 |
-
},
|
| 26 |
-
plugins: [],
|
| 27 |
-
darkMode: 'class',
|
| 28 |
-
}
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/tsconfig.json
DELETED
|
@@ -1,41 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"compilerOptions": {
|
| 3 |
-
"lib": [
|
| 4 |
-
"dom",
|
| 5 |
-
"dom.iterable",
|
| 6 |
-
"esnext"
|
| 7 |
-
],
|
| 8 |
-
"allowJs": true,
|
| 9 |
-
"skipLibCheck": true,
|
| 10 |
-
"strict": true,
|
| 11 |
-
"noEmit": true,
|
| 12 |
-
"esModuleInterop": true,
|
| 13 |
-
"module": "esnext",
|
| 14 |
-
"moduleResolution": "bundler",
|
| 15 |
-
"resolveJsonModule": true,
|
| 16 |
-
"isolatedModules": true,
|
| 17 |
-
"jsx": "react-jsx",
|
| 18 |
-
"incremental": true,
|
| 19 |
-
"plugins": [
|
| 20 |
-
{
|
| 21 |
-
"name": "next"
|
| 22 |
-
}
|
| 23 |
-
],
|
| 24 |
-
"paths": {
|
| 25 |
-
"@/*": [
|
| 26 |
-
"./src/*"
|
| 27 |
-
]
|
| 28 |
-
},
|
| 29 |
-
"target": "ES2017"
|
| 30 |
-
},
|
| 31 |
-
"include": [
|
| 32 |
-
"next-env.d.ts",
|
| 33 |
-
"**/*.ts",
|
| 34 |
-
"**/*.tsx",
|
| 35 |
-
".next/types/**/*.ts",
|
| 36 |
-
".next/dev/types/**/*.ts"
|
| 37 |
-
],
|
| 38 |
-
"exclude": [
|
| 39 |
-
"node_modules"
|
| 40 |
-
]
|
| 41 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|