Spaces:
Sleeping
Sleeping
Your Name commited on
Commit ·
d7c622e
1
Parent(s): 5e99860
Deploy AutoForge to Hugging Face Space
Browse files- backend/api/auth.py +20 -0
- frontend/src/App.tsx +5 -11
- frontend/src/api/client.ts +4 -0
- frontend/src/components/WorkflowTimeline.tsx +64 -13
- frontend/src/context/AuthContext.tsx +16 -28
- frontend/src/index.css +32 -32
- frontend/src/pages/DashboardPage.tsx +49 -35
- frontend/src/pages/LoginPage.tsx +0 -41
- frontend/src/pages/NewWorkflowPage.tsx +0 -100
- frontend/src/pages/SignupPage.tsx +0 -44
backend/api/auth.py
CHANGED
|
@@ -45,6 +45,26 @@ async def login(
|
|
| 45 |
return TokenResponse(access_token=token)
|
| 46 |
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
@router.get("/me", response_model=UserResponse)
|
| 49 |
async def me(current_user: User = Depends(get_current_user)) -> User:
|
| 50 |
return current_user
|
|
|
|
| 45 |
return TokenResponse(access_token=token)
|
| 46 |
|
| 47 |
|
| 48 |
+
@router.post("/guest", response_model=TokenResponse)
|
| 49 |
+
async def guest_login(
|
| 50 |
+
repo: WorkflowRepository = Depends(get_repository),
|
| 51 |
+
) -> TokenResponse:
|
| 52 |
+
import uuid
|
| 53 |
+
guest_id = str(uuid.uuid4())
|
| 54 |
+
guest_email = f"guest-{guest_id}@autoforge.local"
|
| 55 |
+
guest_username = f"Guest_{guest_id[:8]}"
|
| 56 |
+
|
| 57 |
+
# Auto-create the guest user with a random password
|
| 58 |
+
user = await repo.create_user(
|
| 59 |
+
email=guest_email,
|
| 60 |
+
username=guest_username,
|
| 61 |
+
password_hash=hash_password(guest_id),
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
token = create_access_token(str(user.id))
|
| 65 |
+
return TokenResponse(access_token=token)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
@router.get("/me", response_model=UserResponse)
|
| 69 |
async def me(current_user: User = Depends(get_current_user)) -> User:
|
| 70 |
return current_user
|
frontend/src/App.tsx
CHANGED
|
@@ -1,17 +1,14 @@
|
|
| 1 |
import { Navigate, Route, Routes } from "react-router-dom";
|
| 2 |
import { AuthProvider, useAuth } from "./context/AuthContext";
|
| 3 |
import Layout from "./components/Layout";
|
| 4 |
-
import LoginPage from "./pages/LoginPage";
|
| 5 |
-
import SignupPage from "./pages/SignupPage";
|
| 6 |
import DashboardPage from "./pages/DashboardPage";
|
| 7 |
-
import NewWorkflowPage from "./pages/NewWorkflowPage";
|
| 8 |
import WorkflowPage from "./pages/WorkflowPage";
|
| 9 |
import SettingsPage from "./pages/SettingsPage";
|
| 10 |
|
| 11 |
-
function
|
| 12 |
const { user, loading } = useAuth();
|
| 13 |
-
if (loading) return <p>
|
| 14 |
-
if (!user) return <
|
| 15 |
return <>{children}</>;
|
| 16 |
}
|
| 17 |
|
|
@@ -19,19 +16,16 @@ export default function App() {
|
|
| 19 |
return (
|
| 20 |
<AuthProvider>
|
| 21 |
<Routes>
|
| 22 |
-
<Route path="/login" element={<LoginPage />} />
|
| 23 |
-
<Route path="/signup" element={<SignupPage />} />
|
| 24 |
<Route
|
| 25 |
path="/"
|
| 26 |
element={
|
| 27 |
-
<
|
| 28 |
<Layout />
|
| 29 |
-
</
|
| 30 |
}
|
| 31 |
>
|
| 32 |
<Route index element={<Navigate to="/dashboard" replace />} />
|
| 33 |
<Route path="dashboard" element={<DashboardPage />} />
|
| 34 |
-
<Route path="workflows/new" element={<NewWorkflowPage />} />
|
| 35 |
<Route path="workflows/:id" element={<WorkflowPage />} />
|
| 36 |
<Route path="settings" element={<SettingsPage />} />
|
| 37 |
</Route>
|
|
|
|
| 1 |
import { Navigate, Route, Routes } from "react-router-dom";
|
| 2 |
import { AuthProvider, useAuth } from "./context/AuthContext";
|
| 3 |
import Layout from "./components/Layout";
|
|
|
|
|
|
|
| 4 |
import DashboardPage from "./pages/DashboardPage";
|
|
|
|
| 5 |
import WorkflowPage from "./pages/WorkflowPage";
|
| 6 |
import SettingsPage from "./pages/SettingsPage";
|
| 7 |
|
| 8 |
+
function AuthWrapper({ children }: { children: React.ReactNode }) {
|
| 9 |
const { user, loading } = useAuth();
|
| 10 |
+
if (loading) return <div className="loading-fullscreen"><div className="spinner spinner-lg"></div><p>Initializing...</p></div>;
|
| 11 |
+
if (!user) return <p>Failed to initialize guest session.</p>;
|
| 12 |
return <>{children}</>;
|
| 13 |
}
|
| 14 |
|
|
|
|
| 16 |
return (
|
| 17 |
<AuthProvider>
|
| 18 |
<Routes>
|
|
|
|
|
|
|
| 19 |
<Route
|
| 20 |
path="/"
|
| 21 |
element={
|
| 22 |
+
<AuthWrapper>
|
| 23 |
<Layout />
|
| 24 |
+
</AuthWrapper>
|
| 25 |
}
|
| 26 |
>
|
| 27 |
<Route index element={<Navigate to="/dashboard" replace />} />
|
| 28 |
<Route path="dashboard" element={<DashboardPage />} />
|
|
|
|
| 29 |
<Route path="workflows/:id" element={<WorkflowPage />} />
|
| 30 |
<Route path="settings" element={<SettingsPage />} />
|
| 31 |
</Route>
|
frontend/src/api/client.ts
CHANGED
|
@@ -55,6 +55,10 @@ export const api = {
|
|
| 55 |
method: "POST",
|
| 56 |
body: JSON.stringify({ email, password }),
|
| 57 |
}),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
me: () => request<User>("/auth/me"),
|
| 59 |
listProviders: () =>
|
| 60 |
request<{ providers: ProviderInfo[] }>("/settings/providers"),
|
|
|
|
| 55 |
method: "POST",
|
| 56 |
body: JSON.stringify({ email, password }),
|
| 57 |
}),
|
| 58 |
+
guestLogin: () =>
|
| 59 |
+
request<{ access_token: string }>("/auth/guest", {
|
| 60 |
+
method: "POST",
|
| 61 |
+
}),
|
| 62 |
me: () => request<User>("/auth/me"),
|
| 63 |
listProviders: () =>
|
| 64 |
request<{ providers: ProviderInfo[] }>("/settings/providers"),
|
frontend/src/components/WorkflowTimeline.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import type { WorkflowEvent } from "../types";
|
| 2 |
|
| 3 |
const EVENT_ICONS: Record<string, string> = {
|
|
@@ -26,19 +27,69 @@ const EVENT_ICONS: Record<string, string> = {
|
|
| 26 |
};
|
| 27 |
|
| 28 |
export default function WorkflowTimeline({ events }: { events: WorkflowEvent[] }) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
return (
|
| 30 |
-
<
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
);
|
| 44 |
}
|
|
|
|
| 1 |
+
import { useState, useEffect } from "react";
|
| 2 |
import type { WorkflowEvent } from "../types";
|
| 3 |
|
| 4 |
const EVENT_ICONS: Record<string, string> = {
|
|
|
|
| 27 |
};
|
| 28 |
|
| 29 |
export default function WorkflowTimeline({ events }: { events: WorkflowEvent[] }) {
|
| 30 |
+
const [isOpen, setIsOpen] = useState(true);
|
| 31 |
+
|
| 32 |
+
const isComplete = events.some(e => e.event_type === "WORKFLOW_COMPLETED" || e.event_type === "WORKFLOW_FAILED");
|
| 33 |
+
|
| 34 |
+
useEffect(() => {
|
| 35 |
+
if (isComplete) setIsOpen(false);
|
| 36 |
+
}, [isComplete]);
|
| 37 |
+
|
| 38 |
return (
|
| 39 |
+
<details
|
| 40 |
+
open={isOpen}
|
| 41 |
+
onToggle={(e) => setIsOpen(e.currentTarget.open)}
|
| 42 |
+
style={{
|
| 43 |
+
background: "var(--bg-elevated)",
|
| 44 |
+
borderRadius: "var(--radius-md)",
|
| 45 |
+
border: "1px solid var(--border-subtle)",
|
| 46 |
+
marginBottom: "2rem",
|
| 47 |
+
overflow: "hidden"
|
| 48 |
+
}}
|
| 49 |
+
>
|
| 50 |
+
<summary style={{
|
| 51 |
+
padding: "1rem 1.5rem",
|
| 52 |
+
cursor: "pointer",
|
| 53 |
+
fontWeight: 600,
|
| 54 |
+
display: "flex",
|
| 55 |
+
alignItems: "center",
|
| 56 |
+
gap: "0.5rem",
|
| 57 |
+
listStyle: "none",
|
| 58 |
+
borderBottom: isOpen ? "1px solid var(--border-subtle)" : "none",
|
| 59 |
+
userSelect: "none"
|
| 60 |
+
}}>
|
| 61 |
+
<span style={{ transition: "transform 0.2s", transform: isOpen ? "rotate(90deg)" : "rotate(0deg)" }}>▶</span>
|
| 62 |
+
{isComplete ? "Agent finished thinking" : "Agent is thinking..."}
|
| 63 |
+
</summary>
|
| 64 |
+
|
| 65 |
+
<div style={{ padding: "1.5rem", maxHeight: "400px", overflowY: "auto" }}>
|
| 66 |
+
<ul className="timeline">
|
| 67 |
+
{events.map((event) => (
|
| 68 |
+
<li key={event.id} style={{ marginBottom: "1rem", display: "flex", gap: "1rem", alignItems: "flex-start" }}>
|
| 69 |
+
<span className="icon" style={{
|
| 70 |
+
background: "var(--bg-surface)",
|
| 71 |
+
width: "32px", height: "32px",
|
| 72 |
+
display: "flex", alignItems: "center", justifyContent: "center",
|
| 73 |
+
borderRadius: "50%",
|
| 74 |
+
border: "1px solid var(--border-subtle)",
|
| 75 |
+
flexShrink: 0
|
| 76 |
+
}}>
|
| 77 |
+
{EVENT_ICONS[event.event_type] || "•"}
|
| 78 |
+
</span>
|
| 79 |
+
<div>
|
| 80 |
+
<strong style={{ display: "block", color: "var(--text-primary)", fontSize: "0.9rem" }}>
|
| 81 |
+
{event.event_type.replace(/_/g, " ")}
|
| 82 |
+
</strong>
|
| 83 |
+
<div style={{ color: "var(--text-secondary)", fontSize: "0.85rem", marginTop: "0.2rem" }}>{event.message}</div>
|
| 84 |
+
<div className="time" style={{ color: "var(--text-muted)", fontSize: "0.75rem", marginTop: "0.2rem" }}>
|
| 85 |
+
{new Date(event.timestamp).toLocaleTimeString()}
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
</li>
|
| 89 |
+
))}
|
| 90 |
+
{events.length === 0 && <li>Initializing workflow engine...</li>}
|
| 91 |
+
</ul>
|
| 92 |
+
</div>
|
| 93 |
+
</details>
|
| 94 |
);
|
| 95 |
}
|
frontend/src/context/AuthContext.tsx
CHANGED
|
@@ -5,9 +5,6 @@ import type { User } from "../types";
|
|
| 5 |
interface AuthContextValue {
|
| 6 |
user: User | null;
|
| 7 |
loading: boolean;
|
| 8 |
-
login: (email: string, password: string) => Promise<void>;
|
| 9 |
-
signup: (email: string, username: string, password: string) => Promise<void>;
|
| 10 |
-
logout: () => void;
|
| 11 |
}
|
| 12 |
|
| 13 |
const AuthContext = createContext<AuthContextValue | null>(null);
|
|
@@ -17,34 +14,25 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|
| 17 |
const [loading, setLoading] = useState(true);
|
| 18 |
|
| 19 |
useEffect(() => {
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
}, []);
|
| 29 |
|
| 30 |
-
const login = async (email: string, password: string) => {
|
| 31 |
-
const { access_token } = await api.login(email, password);
|
| 32 |
-
setToken(access_token);
|
| 33 |
-
setUser(await api.me());
|
| 34 |
-
};
|
| 35 |
-
|
| 36 |
-
const signup = async (email: string, username: string, password: string) => {
|
| 37 |
-
await api.signup(email, username, password);
|
| 38 |
-
await login(email, password);
|
| 39 |
-
};
|
| 40 |
-
|
| 41 |
-
const logout = () => {
|
| 42 |
-
setToken(null);
|
| 43 |
-
setUser(null);
|
| 44 |
-
};
|
| 45 |
-
|
| 46 |
return (
|
| 47 |
-
<AuthContext.Provider value={{ user, loading
|
| 48 |
{children}
|
| 49 |
</AuthContext.Provider>
|
| 50 |
);
|
|
|
|
| 5 |
interface AuthContextValue {
|
| 6 |
user: User | null;
|
| 7 |
loading: boolean;
|
|
|
|
|
|
|
|
|
|
| 8 |
}
|
| 9 |
|
| 10 |
const AuthContext = createContext<AuthContextValue | null>(null);
|
|
|
|
| 14 |
const [loading, setLoading] = useState(true);
|
| 15 |
|
| 16 |
useEffect(() => {
|
| 17 |
+
const initializeAuth = async () => {
|
| 18 |
+
try {
|
| 19 |
+
if (!getToken()) {
|
| 20 |
+
const { access_token } = await api.guestLogin();
|
| 21 |
+
setToken(access_token);
|
| 22 |
+
}
|
| 23 |
+
setUser(await api.me());
|
| 24 |
+
} catch (err) {
|
| 25 |
+
console.error("Failed to authenticate guest", err);
|
| 26 |
+
setToken(null);
|
| 27 |
+
} finally {
|
| 28 |
+
setLoading(false);
|
| 29 |
+
}
|
| 30 |
+
};
|
| 31 |
+
initializeAuth();
|
| 32 |
}, []);
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
return (
|
| 35 |
+
<AuthContext.Provider value={{ user, loading }}>
|
| 36 |
{children}
|
| 37 |
</AuthContext.Provider>
|
| 38 |
);
|
frontend/src/index.css
CHANGED
|
@@ -3,34 +3,34 @@
|
|
| 3 |
============================================================ */
|
| 4 |
|
| 5 |
:root {
|
| 6 |
-
/* Color palette */
|
| 7 |
-
--bg-base: #
|
| 8 |
-
--bg-surface: #
|
| 9 |
-
--bg-elevated: #
|
| 10 |
-
--bg-sidebar: #
|
| 11 |
-
--bg-input: #
|
| 12 |
-
|
| 13 |
-
--border-subtle: #
|
| 14 |
-
--border-default: #
|
| 15 |
-
--border-strong: #
|
| 16 |
-
|
| 17 |
-
--text-primary: #
|
| 18 |
-
--text-secondary: #
|
| 19 |
-
--text-muted: #
|
| 20 |
-
--text-inverse: #
|
| 21 |
-
|
| 22 |
-
--accent-primary: #
|
| 23 |
-
--accent-primary-hover: #
|
| 24 |
-
--accent-primary-glow: rgba(
|
| 25 |
-
|
| 26 |
-
--accent-success: #
|
| 27 |
-
--accent-success-bg: rgba(
|
| 28 |
-
--accent-warning: #
|
| 29 |
-
--accent-warning-bg: rgba(
|
| 30 |
-
--accent-danger: #
|
| 31 |
-
--accent-danger-bg: rgba(
|
| 32 |
-
--accent-info: #
|
| 33 |
-
--accent-info-bg: rgba(
|
| 34 |
|
| 35 |
--sidebar-width: 248px;
|
| 36 |
--radius-sm: 6px;
|
|
@@ -38,10 +38,10 @@
|
|
| 38 |
--radius-lg: 14px;
|
| 39 |
--radius-xl: 20px;
|
| 40 |
|
| 41 |
-
--shadow-sm: 0 1px 3px rgba(0,0,0,0.
|
| 42 |
-
--shadow-md: 0 4px 16px rgba(0,0,0,0.
|
| 43 |
-
--shadow-lg: 0 12px 40px rgba(0,0,0,0.
|
| 44 |
-
--shadow-glow: 0 0 24px
|
| 45 |
|
| 46 |
--transition-fast: 120ms ease;
|
| 47 |
--transition-base: 200ms ease;
|
|
|
|
| 3 |
============================================================ */
|
| 4 |
|
| 5 |
:root {
|
| 6 |
+
/* Color palette - Light Theme */
|
| 7 |
+
--bg-base: #f9fafb;
|
| 8 |
+
--bg-surface: #ffffff;
|
| 9 |
+
--bg-elevated: #f3f4f6;
|
| 10 |
+
--bg-sidebar: #ffffff;
|
| 11 |
+
--bg-input: #ffffff;
|
| 12 |
+
|
| 13 |
+
--border-subtle: #e5e7eb;
|
| 14 |
+
--border-default: #d1d5db;
|
| 15 |
+
--border-strong: #9ca3af;
|
| 16 |
+
|
| 17 |
+
--text-primary: #111827;
|
| 18 |
+
--text-secondary: #374151;
|
| 19 |
+
--text-muted: #6b7280;
|
| 20 |
+
--text-inverse: #ffffff;
|
| 21 |
+
|
| 22 |
+
--accent-primary: #000000;
|
| 23 |
+
--accent-primary-hover: #374151;
|
| 24 |
+
--accent-primary-glow: rgba(0, 0, 0, 0.08);
|
| 25 |
+
|
| 26 |
+
--accent-success: #059669;
|
| 27 |
+
--accent-success-bg: rgba(5, 150, 105, 0.12);
|
| 28 |
+
--accent-warning: #d97706;
|
| 29 |
+
--accent-warning-bg: rgba(217, 119, 6, 0.12);
|
| 30 |
+
--accent-danger: #dc2626;
|
| 31 |
+
--accent-danger-bg: rgba(220, 38, 38, 0.12);
|
| 32 |
+
--accent-info: #4f46e5;
|
| 33 |
+
--accent-info-bg: rgba(79, 70, 229, 0.12);
|
| 34 |
|
| 35 |
--sidebar-width: 248px;
|
| 36 |
--radius-sm: 6px;
|
|
|
|
| 38 |
--radius-lg: 14px;
|
| 39 |
--radius-xl: 20px;
|
| 40 |
|
| 41 |
+
--shadow-sm: 0 1px 3px rgba(0,0,0,0.05);
|
| 42 |
+
--shadow-md: 0 4px 16px rgba(0,0,0,0.08);
|
| 43 |
+
--shadow-lg: 0 12px 40px rgba(0,0,0,0.12);
|
| 44 |
+
--shadow-glow: 0 0 24px rgba(0,0,0,0.06);
|
| 45 |
|
| 46 |
--transition-fast: 120ms ease;
|
| 47 |
--transition-base: 200ms ease;
|
frontend/src/pages/DashboardPage.tsx
CHANGED
|
@@ -1,45 +1,59 @@
|
|
| 1 |
-
import {
|
| 2 |
-
import {
|
| 3 |
import { api } from "../api/client";
|
| 4 |
-
import type { Workflow } from "../types";
|
| 5 |
|
| 6 |
export default function DashboardPage() {
|
| 7 |
-
const [
|
|
|
|
| 8 |
const [error, setError] = useState("");
|
|
|
|
|
|
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
return (
|
| 17 |
-
<div>
|
| 18 |
-
<
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
</
|
| 43 |
</div>
|
| 44 |
);
|
| 45 |
}
|
|
|
|
| 1 |
+
import { useState } from "react";
|
| 2 |
+
import { useNavigate } from "react-router-dom";
|
| 3 |
import { api } from "../api/client";
|
|
|
|
| 4 |
|
| 5 |
export default function DashboardPage() {
|
| 6 |
+
const [prompt, setPrompt] = useState("");
|
| 7 |
+
const [apiKey, setApiKey] = useState("");
|
| 8 |
const [error, setError] = useState("");
|
| 9 |
+
const [loading, setLoading] = useState(false);
|
| 10 |
+
const navigate = useNavigate();
|
| 11 |
|
| 12 |
+
const handleSubmit = async (e: React.FormEvent) => {
|
| 13 |
+
e.preventDefault();
|
| 14 |
+
if (!prompt.trim()) return setError("Prompt cannot be empty");
|
| 15 |
+
if (!apiKey.trim()) return setError("API Key is required");
|
| 16 |
+
|
| 17 |
+
setLoading(true);
|
| 18 |
+
setError("");
|
| 19 |
+
|
| 20 |
+
try {
|
| 21 |
+
const { workflow_id } = await api.createWorkflow(prompt.trim(), "gemini", apiKey.trim());
|
| 22 |
+
await api.startWorkflow(workflow_id);
|
| 23 |
+
navigate(`/workflows/${workflow_id}`);
|
| 24 |
+
} catch (err: any) {
|
| 25 |
+
setError(err.message || "Failed to start workflow");
|
| 26 |
+
setLoading(false);
|
| 27 |
+
}
|
| 28 |
+
};
|
| 29 |
|
| 30 |
return (
|
| 31 |
+
<div style={{ maxWidth: "800px", margin: "4rem auto", textAlign: "center" }}>
|
| 32 |
+
<h1 style={{ fontSize: "2rem", marginBottom: "1rem", fontWeight: 600 }}>What would you like to build?</h1>
|
| 33 |
+
<p style={{ color: "var(--text-secondary)", marginBottom: "3rem" }}>AutoForge AI will autonomously plan, write, and test your application.</p>
|
| 34 |
+
|
| 35 |
+
{error && <div className="alert alert-error">{error}</div>}
|
| 36 |
+
|
| 37 |
+
<form onSubmit={handleSubmit} style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
| 38 |
+
<input
|
| 39 |
+
type="password"
|
| 40 |
+
placeholder="Enter your Gemini API Key..."
|
| 41 |
+
value={apiKey}
|
| 42 |
+
onChange={(e) => setApiKey(e.target.value)}
|
| 43 |
+
style={{ padding: "1rem", fontSize: "1rem", borderRadius: "var(--radius-lg)", boxShadow: "var(--shadow-sm)" }}
|
| 44 |
+
/>
|
| 45 |
+
|
| 46 |
+
<textarea
|
| 47 |
+
placeholder="E.g. Create a simple Python FastAPI server that serves a React to-do app..."
|
| 48 |
+
value={prompt}
|
| 49 |
+
onChange={(e) => setPrompt(e.target.value)}
|
| 50 |
+
style={{ minHeight: "150px", padding: "1rem", fontSize: "1.1rem", borderRadius: "var(--radius-lg)", boxShadow: "var(--shadow-md)" }}
|
| 51 |
+
/>
|
| 52 |
+
|
| 53 |
+
<button type="submit" className="primary" disabled={loading} style={{ alignSelf: "flex-end", padding: "0.8rem 2rem", fontSize: "1.1rem" }}>
|
| 54 |
+
{loading ? "Starting..." : "Generate Code"}
|
| 55 |
+
</button>
|
| 56 |
+
</form>
|
| 57 |
</div>
|
| 58 |
);
|
| 59 |
}
|
frontend/src/pages/LoginPage.tsx
DELETED
|
@@ -1,41 +0,0 @@
|
|
| 1 |
-
import { useState } from "react";
|
| 2 |
-
import { Link, useNavigate } from "react-router-dom";
|
| 3 |
-
import { useAuth } from "../context/AuthContext";
|
| 4 |
-
|
| 5 |
-
export default function LoginPage() {
|
| 6 |
-
const { login } = useAuth();
|
| 7 |
-
const navigate = useNavigate();
|
| 8 |
-
const [email, setEmail] = useState("");
|
| 9 |
-
const [password, setPassword] = useState("");
|
| 10 |
-
const [error, setError] = useState("");
|
| 11 |
-
|
| 12 |
-
const submit = async (e: React.FormEvent) => {
|
| 13 |
-
e.preventDefault();
|
| 14 |
-
setError("");
|
| 15 |
-
try {
|
| 16 |
-
await login(email, password);
|
| 17 |
-
navigate("/dashboard");
|
| 18 |
-
} catch (err) {
|
| 19 |
-
setError(err instanceof Error ? err.message : "Login failed");
|
| 20 |
-
}
|
| 21 |
-
};
|
| 22 |
-
|
| 23 |
-
return (
|
| 24 |
-
<div className="auth-page">
|
| 25 |
-
<div className="card auth-card">
|
| 26 |
-
<h2>Login to AutoForge</h2>
|
| 27 |
-
{error && <p className="error">{error}</p>}
|
| 28 |
-
<form onSubmit={submit}>
|
| 29 |
-
<label>Email</label>
|
| 30 |
-
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
|
| 31 |
-
<label>Password</label>
|
| 32 |
-
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
|
| 33 |
-
<button className="primary" type="submit">Login</button>
|
| 34 |
-
</form>
|
| 35 |
-
<p style={{ marginTop: "1rem" }}>
|
| 36 |
-
No account? <Link to="/signup">Sign up</Link>
|
| 37 |
-
</p>
|
| 38 |
-
</div>
|
| 39 |
-
</div>
|
| 40 |
-
);
|
| 41 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/pages/NewWorkflowPage.tsx
DELETED
|
@@ -1,100 +0,0 @@
|
|
| 1 |
-
import { useEffect, useState } from "react";
|
| 2 |
-
import { useNavigate } from "react-router-dom";
|
| 3 |
-
import { api } from "../api/client";
|
| 4 |
-
import type { ProviderInfo } from "../types";
|
| 5 |
-
|
| 6 |
-
export default function NewWorkflowPage() {
|
| 7 |
-
const navigate = useNavigate();
|
| 8 |
-
const [providers, setProviders] = useState<ProviderInfo[]>([]);
|
| 9 |
-
const [provider, setProvider] = useState("openai");
|
| 10 |
-
const [model, setModel] = useState("");
|
| 11 |
-
const [apiKey, setApiKey] = useState("");
|
| 12 |
-
const [prompt, setPrompt] = useState("");
|
| 13 |
-
const [error, setError] = useState("");
|
| 14 |
-
const [loading, setLoading] = useState(false);
|
| 15 |
-
|
| 16 |
-
useEffect(() => {
|
| 17 |
-
api.listProviders().then((data) => {
|
| 18 |
-
setProviders(data.providers);
|
| 19 |
-
if (data.providers.length) {
|
| 20 |
-
setProvider(data.providers[0].id);
|
| 21 |
-
setModel(data.providers[0].models[0] || "");
|
| 22 |
-
}
|
| 23 |
-
});
|
| 24 |
-
}, []);
|
| 25 |
-
|
| 26 |
-
const selectedProvider = providers.find((p) => p.id === provider);
|
| 27 |
-
|
| 28 |
-
const submit = async (e: React.FormEvent) => {
|
| 29 |
-
e.preventDefault();
|
| 30 |
-
setLoading(true);
|
| 31 |
-
setError("");
|
| 32 |
-
try {
|
| 33 |
-
const wf = await api.createWorkflow({
|
| 34 |
-
user_prompt: prompt,
|
| 35 |
-
llm_provider: provider,
|
| 36 |
-
llm_model: model,
|
| 37 |
-
api_key: apiKey || undefined,
|
| 38 |
-
});
|
| 39 |
-
await api.startWorkflow(wf.workflow_id, apiKey || undefined);
|
| 40 |
-
navigate(`/workflows/${wf.workflow_id}`);
|
| 41 |
-
} catch (err) {
|
| 42 |
-
setError(err instanceof Error ? err.message : "Failed to create workflow");
|
| 43 |
-
} finally {
|
| 44 |
-
setLoading(false);
|
| 45 |
-
}
|
| 46 |
-
};
|
| 47 |
-
|
| 48 |
-
return (
|
| 49 |
-
<div>
|
| 50 |
-
<h2>New Workflow</h2>
|
| 51 |
-
<div className="card">
|
| 52 |
-
{error && <p className="error">{error}</p>}
|
| 53 |
-
<form onSubmit={submit}>
|
| 54 |
-
<label>Prompt</label>
|
| 55 |
-
<textarea
|
| 56 |
-
value={prompt}
|
| 57 |
-
onChange={(e) => setPrompt(e.target.value)}
|
| 58 |
-
placeholder="Build a FastAPI calculator with add and subtract endpoints..."
|
| 59 |
-
required
|
| 60 |
-
/>
|
| 61 |
-
|
| 62 |
-
<div className="grid-2">
|
| 63 |
-
<div>
|
| 64 |
-
<label>LLM Provider</label>
|
| 65 |
-
<select value={provider} onChange={(e) => {
|
| 66 |
-
setProvider(e.target.value);
|
| 67 |
-
const p = providers.find((x) => x.id === e.target.value);
|
| 68 |
-
setModel(p?.models[0] || "");
|
| 69 |
-
}}>
|
| 70 |
-
{providers.map((p) => (
|
| 71 |
-
<option key={p.id} value={p.id}>{p.name}</option>
|
| 72 |
-
))}
|
| 73 |
-
</select>
|
| 74 |
-
</div>
|
| 75 |
-
<div>
|
| 76 |
-
<label>Model</label>
|
| 77 |
-
<select value={model} onChange={(e) => setModel(e.target.value)} required>
|
| 78 |
-
{(selectedProvider?.models || []).map((m) => (
|
| 79 |
-
<option key={m} value={m}>{m}</option>
|
| 80 |
-
))}
|
| 81 |
-
</select>
|
| 82 |
-
</div>
|
| 83 |
-
</div>
|
| 84 |
-
|
| 85 |
-
<label>API Key (optional if saved in settings)</label>
|
| 86 |
-
<input
|
| 87 |
-
type="password"
|
| 88 |
-
value={apiKey}
|
| 89 |
-
onChange={(e) => setApiKey(e.target.value)}
|
| 90 |
-
placeholder="sk-..."
|
| 91 |
-
/>
|
| 92 |
-
|
| 93 |
-
<button className="primary" type="submit" disabled={loading}>
|
| 94 |
-
{loading ? "Starting…" : "Start Workflow"}
|
| 95 |
-
</button>
|
| 96 |
-
</form>
|
| 97 |
-
</div>
|
| 98 |
-
</div>
|
| 99 |
-
);
|
| 100 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/pages/SignupPage.tsx
DELETED
|
@@ -1,44 +0,0 @@
|
|
| 1 |
-
import { useState } from "react";
|
| 2 |
-
import { Link, useNavigate } from "react-router-dom";
|
| 3 |
-
import { useAuth } from "../context/AuthContext";
|
| 4 |
-
|
| 5 |
-
export default function SignupPage() {
|
| 6 |
-
const { signup } = useAuth();
|
| 7 |
-
const navigate = useNavigate();
|
| 8 |
-
const [email, setEmail] = useState("");
|
| 9 |
-
const [username, setUsername] = useState("");
|
| 10 |
-
const [password, setPassword] = useState("");
|
| 11 |
-
const [error, setError] = useState("");
|
| 12 |
-
|
| 13 |
-
const submit = async (e: React.FormEvent) => {
|
| 14 |
-
e.preventDefault();
|
| 15 |
-
setError("");
|
| 16 |
-
try {
|
| 17 |
-
await signup(email, username, password);
|
| 18 |
-
navigate("/dashboard");
|
| 19 |
-
} catch (err) {
|
| 20 |
-
setError(err instanceof Error ? err.message : "Signup failed");
|
| 21 |
-
}
|
| 22 |
-
};
|
| 23 |
-
|
| 24 |
-
return (
|
| 25 |
-
<div className="auth-page">
|
| 26 |
-
<div className="card auth-card">
|
| 27 |
-
<h2>Create Account</h2>
|
| 28 |
-
{error && <p className="error">{error}</p>}
|
| 29 |
-
<form onSubmit={submit}>
|
| 30 |
-
<label>Email</label>
|
| 31 |
-
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
|
| 32 |
-
<label>Username</label>
|
| 33 |
-
<input value={username} onChange={(e) => setUsername(e.target.value)} required />
|
| 34 |
-
<label>Password</label>
|
| 35 |
-
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required minLength={8} />
|
| 36 |
-
<button className="primary" type="submit">Sign Up</button>
|
| 37 |
-
</form>
|
| 38 |
-
<p style={{ marginTop: "1rem" }}>
|
| 39 |
-
Have an account? <Link to="/login">Login</Link>
|
| 40 |
-
</p>
|
| 41 |
-
</div>
|
| 42 |
-
</div>
|
| 43 |
-
);
|
| 44 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|