diff --git a/app/RegisterSW.tsx b/app/RegisterSW.tsx new file mode 100644 index 00000000..0af4afe5 --- /dev/null +++ b/app/RegisterSW.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { useEffect } from "react"; + +export default function RegisterSW() { + useEffect(() => { + if ("serviceWorker" in navigator) { + navigator.serviceWorker + .register("/sw.js") + .then(() => console.log("SW registered")) + .catch((err) => console.error("SW registration failed:", err)); + } + }, []); + + return null; +} diff --git a/app/api/github/check-repo/[repo_name]/route.ts b/app/api/github/check-repo/[repo_name]/route.ts index a16e6c5a..78a8d1dd 100644 --- a/app/api/github/check-repo/[repo_name]/route.ts +++ b/app/api/github/check-repo/[repo_name]/route.ts @@ -1,31 +1,33 @@ -import { NextResponse } from 'next/server'; -import { checkRepositoryAvailability } from '@/lib/services/github'; +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; +import { checkRepositoryAvailability } from "@/lib/github-api"; +import { getPlainServiceToken } from "@/lib/services/tokens"; -interface RouteContext { - params: Promise<{ repo_name: string }>; -} +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; -export async function GET(_request: Request, { params }: RouteContext) { +export async function GET( + _req: NextRequest, + { params }: { params: Promise<{ repo_name: string }> } +) { try { - const { repo_name } = await params; - const result = await checkRepositoryAvailability(repo_name); - if (result.exists) { - return NextResponse.json({ available: false, username: result.username }, { status: 409 }); + const { repo_name } = await params; // πŸ”₯ μ—¬κΈ°μ„œ await ν•΄μ•Ό 함 + + const token = await getPlainServiceToken("github"); + if (!token) { + return NextResponse.json({ error: "No GitHub token" }, { status: 401 }); } - return NextResponse.json({ available: true, username: result.username }); - } catch (error) { - console.error('[API] Failed to check repository availability:', error); - const status = error instanceof Error && 'status' in error ? (error as any).status ?? 500 : 500; - return NextResponse.json( - { - success: false, - error: 'Failed to check repository availability', - message: error instanceof Error ? error.message : 'Unknown error', - }, - { status }, + + const owner = process.env.GITHUB_OWNER!; + const result = await checkRepositoryAvailability( + token, + owner, + repo_name ); + + return NextResponse.json(result); + } catch (err) { + console.error(err); + return NextResponse.json({ error: "Failed" }, { status: 500 }); } } - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; diff --git a/app/api/github/create-repo/route.ts b/app/api/github/create-repo/route.ts index f3e2d827..ce1cbda7 100644 --- a/app/api/github/create-repo/route.ts +++ b/app/api/github/create-repo/route.ts @@ -1,28 +1,70 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { createRepository, getGithubUser } from '@/lib/services/github'; +import { NextRequest, NextResponse } from "next/server"; +import { getPlainServiceToken } from "@/lib/services/tokens"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; export async function POST(request: NextRequest) { try { const body = await request.json(); - if (!body || typeof body !== 'object') { - return NextResponse.json({ success: false, error: 'Invalid payload' }, { status: 400 }); + + const repoName = body?.repo_name; + const description = body?.description ?? ""; + const isPrivate = body?.private ?? false; + + if (!repoName || typeof repoName !== "string") { + return NextResponse.json( + { success: false, error: "repo_name is required" }, + { status: 400 } + ); } - const repoName = typeof body.repo_name === 'string' ? body.repo_name : undefined; - if (!repoName) { - return NextResponse.json({ success: false, error: 'repo_name is required' }, { status: 400 }); + const token = await getPlainServiceToken("github"); + if (!token) { + return NextResponse.json( + { success: false, error: "GitHub token not configured" }, + { status: 401 } + ); } - const description = typeof body.description === 'string' ? body.description : ''; - const isPrivate = typeof body.private === 'boolean' ? body.private : false; + // 1️⃣ μ‚¬μš©μž 정보 κ°€μ Έμ˜€κΈ° + const userRes = await fetch("https://api.github.com/user", { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/vnd.github+json", + }, + }); + + if (!userRes.ok) { + throw new Error("Failed to fetch GitHub user"); + } - const repo = await createRepository({ - repoName, - description, - private: isPrivate, + const user = await userRes.json(); + + // 2️⃣ 레포 생성 + const repoRes = await fetch("https://api.github.com/user/repos", { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + Accept: "application/vnd.github+json", + }, + body: JSON.stringify({ + name: repoName, + description, + private: isPrivate, + }), }); - const user = await getGithubUser(); + if (!repoRes.ok) { + const err = await repoRes.json().catch(() => ({})); + return NextResponse.json( + { success: false, error: err?.message ?? "Failed to create repo" }, + { status: repoRes.status } + ); + } + + const repo = await repoRes.json(); return NextResponse.json({ success: true, @@ -33,18 +75,14 @@ export async function POST(request: NextRequest) { owner: user.login, }); } catch (error) { - console.error('[API] Failed to create GitHub repository:', error); - const status = error instanceof Error && 'status' in error ? (error as any).status ?? 500 : 500; + console.error("[API] Failed to create GitHub repository:", error); + return NextResponse.json( { success: false, - error: 'Failed to create GitHub repository', - message: error instanceof Error ? error.message : 'Unknown error', + error: "Failed to create GitHub repository", }, - { status }, + { status: 500 } ); } -} - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; +} \ No newline at end of file diff --git a/app/api/projects/[project_id]/files/content/route.ts b/app/api/projects/[project_id]/files/content/route.ts deleted file mode 100644 index c9d36140..00000000 --- a/app/api/projects/[project_id]/files/content/route.ts +++ /dev/null @@ -1,98 +0,0 @@ -/** - * GET /api/projects/[id]/files/content - Get file content - */ - -import { NextRequest, NextResponse } from 'next/server'; -import { - readProjectFileContent, - writeProjectFileContent, - FileBrowserError, -} from '@/lib/services/file-browser'; - -interface RouteContext { - params: Promise<{ project_id: string }>; -} - -export async function GET(request: NextRequest, { params }: RouteContext) { - try { - const { project_id } = await params; - const url = new URL(request.url); - const filePath = url.searchParams.get('path'); - - if (!filePath) { - return NextResponse.json( - { success: false, error: 'path query parameter is required' }, - { status: 400 } - ); - } - - const file = await readProjectFileContent(project_id, filePath); - - return NextResponse.json({ - success: true, - data: file, - }); - } catch (error) { - if (error instanceof FileBrowserError) { - return NextResponse.json( - { success: false, error: error.message }, - { status: error.status } - ); - } - - console.error('[API] Failed to read project file:', error); - return NextResponse.json( - { - success: false, - error: 'Failed to read project file', - }, - { status: 500 } - ); - } -} - -export async function PUT(request: NextRequest, { params }: RouteContext) { - try { - const { project_id } = await params; - const body = await request.json(); - const filePath = body.path; - const content = body.content; - - if (!filePath || typeof filePath !== 'string') { - return NextResponse.json( - { success: false, error: 'path is required' }, - { status: 400 } - ); - } - - if (typeof content !== 'string') { - return NextResponse.json( - { success: false, error: 'content must be a string' }, - { status: 400 } - ); - } - - await writeProjectFileContent(project_id, filePath, content); - - return NextResponse.json({ - success: true, - data: { path: filePath }, - }); - } catch (error) { - if (error instanceof FileBrowserError) { - return NextResponse.json( - { success: false, error: error.message }, - { status: error.status } - ); - } - - console.error('[API] Failed to write project file:', error); - return NextResponse.json( - { - success: false, - error: 'Failed to write project file', - }, - { status: 500 } - ); - } -} diff --git a/app/api/projects/[project_id]/files/route.ts b/app/api/projects/[project_id]/files/route.ts deleted file mode 100644 index 2d4ea18b..00000000 --- a/app/api/projects/[project_id]/files/route.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * GET /api/projects/[id]/files - Get project directory list - */ - -import { NextRequest, NextResponse } from 'next/server'; -import { listProjectDirectory, FileBrowserError } from '@/lib/services/file-browser'; - -interface RouteContext { - params: Promise<{ project_id: string }>; -} - -export async function GET(request: NextRequest, { params }: RouteContext) { - try { - const { project_id } = await params; - const url = new URL(request.url); - const dir = url.searchParams.get('path') ?? '.'; - - const entries = await listProjectDirectory(project_id, dir); - - return NextResponse.json({ - success: true, - data: { - entries, - }, - }); - } catch (error) { - if (error instanceof FileBrowserError) { - return NextResponse.json( - { success: false, error: error.message }, - { status: error.status } - ); - } - - console.error('[API] Failed to list project files:', error); - return NextResponse.json( - { - success: false, - error: 'Failed to list project files', - }, - { status: 500 } - ); - } -} diff --git a/app/api/projects/[project_id]/github/connect/route.ts b/app/api/projects/[project_id]/github/connect/route.ts deleted file mode 100644 index 56556250..00000000 --- a/app/api/projects/[project_id]/github/connect/route.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { connectProjectToGitHub } from '@/lib/services/github'; - -interface RouteContext { - params: Promise<{ project_id: string }>; -} - -export async function POST(request: NextRequest, { params }: RouteContext) { - try { - const { project_id } = await params; - const body = await request.json(); - if (!body || typeof body !== 'object') { - return NextResponse.json({ success: false, error: 'Invalid payload' }, { status: 400 }); - } - - const repoName = typeof body.repo_name === 'string' ? body.repo_name : undefined; - if (!repoName) { - return NextResponse.json({ success: false, error: 'repo_name is required' }, { status: 400 }); - } - - const description = typeof body.description === 'string' ? body.description : ''; - const isPrivate = typeof body.private === 'boolean' ? body.private : false; - - const result = await connectProjectToGitHub(project_id, { - repoName, - description, - private: isPrivate, - }); - - return NextResponse.json({ - success: true, - repo_url: result.repo_url, - clone_url: result.clone_url, - default_branch: result.default_branch, - owner: result.owner, - message: 'GitHub repository created and connected', - }); - } catch (error) { - console.error('[API] Failed to connect GitHub repository:', error); - const status = error instanceof Error && 'status' in error ? (error as any).status ?? 500 : 500; - return NextResponse.json( - { - success: false, - error: 'Failed to connect GitHub repository', - message: error instanceof Error ? error.message : 'Unknown error', - }, - { status }, - ); - } -} - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; diff --git a/app/api/projects/[project_id]/github/push/route.ts b/app/api/projects/[project_id]/github/push/route.ts deleted file mode 100644 index 0858a12a..00000000 --- a/app/api/projects/[project_id]/github/push/route.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NextResponse } from 'next/server'; -import { pushProjectToGitHub } from '@/lib/services/github'; - -interface RouteContext { - params: Promise<{ project_id: string }>; -} - -export async function POST(_request: Request, { params }: RouteContext) { - try { - const { project_id } = await params; - await pushProjectToGitHub(project_id); - return NextResponse.json({ success: true, message: 'Changes pushed to GitHub' }); - } catch (error) { - console.error('[API] Failed to push to GitHub:', error); - const status = error instanceof Error && 'status' in error ? (error as any).status ?? 500 : 500; - return NextResponse.json( - { - success: false, - error: 'Failed to push to GitHub', - message: error instanceof Error ? error.message : 'Unknown error', - }, - { status }, - ); - } -} - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; diff --git a/app/api/projects/[project_id]/install-dependencies/route.ts b/app/api/projects/[project_id]/install-dependencies/route.ts deleted file mode 100644 index b5bfaffa..00000000 --- a/app/api/projects/[project_id]/install-dependencies/route.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * POST /api/projects/[project_id]/install-dependencies - * Run npm install (or equivalent) for a project workspace. - */ - -import { NextResponse } from 'next/server'; -import { previewManager } from '@/lib/services/preview'; - -interface RouteContext { - params: Promise<{ project_id: string }>; -} - -export async function POST( - _request: Request, - { params }: RouteContext -) { - try { - const { project_id } = await params; - const result = await previewManager.installDependencies(project_id); - - return NextResponse.json({ - success: true, - logs: result.logs, - }); - } catch (error) { - console.error('[API] Failed to install dependencies:', error); - return NextResponse.json( - { - success: false, - error: - error instanceof Error - ? error.message - : 'Failed to install dependencies', - }, - { status: 500 } - ); - } -} - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; diff --git a/app/api/projects/[project_id]/preview/start/route.ts b/app/api/projects/[project_id]/preview/start/route.ts deleted file mode 100644 index e36399c2..00000000 --- a/app/api/projects/[project_id]/preview/start/route.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * POST /api/projects/[id]/preview/start - * Launches the development server for a project and returns the preview URL. - */ - -import { NextResponse } from 'next/server'; -import { previewManager } from '@/lib/services/preview'; - -interface RouteContext { - params: Promise<{ project_id: string }>; -} - -export async function POST( - _request: Request, - { params }: RouteContext -) { - try { - const { project_id } = await params; - const preview = await previewManager.start(project_id); - - return NextResponse.json({ - success: true, - data: preview, - }); - } catch (error) { - console.error('[API] Failed to start preview:', error); - return NextResponse.json( - { - success: false, - error: - error instanceof Error ? error.message : 'Failed to start preview', - }, - { status: 500 } - ); - } -} - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; diff --git a/app/api/projects/[project_id]/preview/status/route.ts b/app/api/projects/[project_id]/preview/status/route.ts deleted file mode 100644 index 8e54b56a..00000000 --- a/app/api/projects/[project_id]/preview/status/route.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * GET /api/projects/[id]/preview/status - * Returns the current preview status for the project. - */ - -import { NextResponse } from 'next/server'; -import { previewManager } from '@/lib/services/preview'; - -interface RouteContext { - params: Promise<{ project_id: string }>; -} - -export async function GET( - _request: Request, - { params }: RouteContext -) { - try { - const { project_id } = await params; - const preview = previewManager.getStatus(project_id); - - return NextResponse.json({ - success: true, - data: preview, - }); - } catch (error) { - console.error('[API] Failed to fetch preview status:', error); - return NextResponse.json( - { - success: false, - error: - error instanceof Error - ? error.message - : 'Failed to fetch preview status', - }, - { status: 500 } - ); - } -} - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; diff --git a/app/api/projects/[project_id]/preview/stop/route.ts b/app/api/projects/[project_id]/preview/stop/route.ts deleted file mode 100644 index 6cfb5d55..00000000 --- a/app/api/projects/[project_id]/preview/stop/route.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * POST /api/projects/[id]/preview/stop - * Stops the development server for the project if it is running. - */ - -import { NextResponse } from 'next/server'; -import { previewManager } from '@/lib/services/preview'; - -interface RouteContext { - params: Promise<{ project_id: string }>; -} - -export async function POST( - _request: Request, - { params }: RouteContext -) { - try { - const { project_id } = await params; - const preview = await previewManager.stop(project_id); - - return NextResponse.json({ - success: true, - data: preview, - }); - } catch (error) { - console.error('[API] Failed to stop preview:', error); - return NextResponse.json( - { - success: false, - error: - error instanceof Error ? error.message : 'Failed to stop preview', - }, - { status: 500 } - ); - } -} - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; diff --git a/app/api/projects/[project_id]/route.ts b/app/api/projects/[project_id]/route.ts deleted file mode 100644 index 59c0c612..00000000 --- a/app/api/projects/[project_id]/route.ts +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Single Project API Routes - * GET /api/projects/[project_id] - Retrieve project - * PUT /api/projects/[project_id] - Update project - * DELETE /api/projects/[project_id] - Delete project - */ - -import { NextRequest, NextResponse } from 'next/server'; -import { - getProjectById, - updateProject, - deleteProject, -} from '@/lib/services/project'; -import type { UpdateProjectInput } from '@/types/backend'; -import { serializeProject } from '@/lib/serializers/project'; - -interface RouteContext { - params: Promise<{ project_id: string }>; -} - -/** - * GET /api/projects/[project_id] - * Retrieve specific project - */ -export async function GET( - request: NextRequest, - { params }: RouteContext -) { - try { - const { project_id } = await params; - const project = await getProjectById(project_id); - - if (!project) { - return NextResponse.json( - { success: false, error: 'Project not found' }, - { status: 404 } - ); - } - - return NextResponse.json({ success: true, data: serializeProject(project) }); - } catch (error) { - console.error('[API] Failed to get project:', error); - return NextResponse.json( - { - success: false, - error: 'Failed to fetch project', - message: error instanceof Error ? error.message : 'Unknown error', - }, - { status: 500 } - ); - } -} - -/** - * PUT /api/projects/[project_id] - * Update project - */ -export async function PUT( - request: NextRequest, - { params }: RouteContext -) { - try { - const { project_id } = await params; - const body = await request.json(); - - const input: UpdateProjectInput = { - name: body.name, - description: body.description, - status: body.status, - previewUrl: body.previewUrl, - previewPort: body.previewPort, - preferredCli: body.preferredCli, - selectedModel: body.selectedModel, - settings: body.settings, - }; - - const project = await updateProject(project_id, input); - return NextResponse.json({ success: true, data: serializeProject(project) }); - } catch (error) { - console.error('[API] Failed to update project:', error); - - // Distinguish between different error types - if (error instanceof Error) { - if (error.message.includes('not found')) { - return NextResponse.json( - { success: false, error: 'Project not found' }, - { status: 404 } - ); - } - if (error.message.includes('validation') || error.message.includes('invalid')) { - return NextResponse.json( - { success: false, error: 'Invalid input', message: error.message }, - { status: 400 } - ); - } - } - - return NextResponse.json( - { - success: false, - error: 'Failed to update project', - message: error instanceof Error ? error.message : 'Unknown error', - }, - { status: 500 } - ); - } -} - -/** - * DELETE /api/projects/[project_id] - * Delete project - */ -export async function DELETE( - request: NextRequest, - { params }: RouteContext -) { - try { - const { project_id } = await params; - await deleteProject(project_id); - - return NextResponse.json({ - success: true, - message: 'Project deleted successfully', - }); - } catch (error) { - console.error('[API] Failed to delete project:', error); - return NextResponse.json( - { - success: false, - error: 'Failed to delete project', - message: error instanceof Error ? error.message : 'Unknown error', - }, - { status: 500 } - ); - } -} - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; diff --git a/app/api/projects/[project_id]/services/[service_id]/route.ts b/app/api/projects/[project_id]/services/[service_id]/route.ts deleted file mode 100644 index 17973b03..00000000 --- a/app/api/projects/[project_id]/services/[service_id]/route.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { NextResponse } from 'next/server'; -import { deleteProjectService } from '@/lib/services/project-services'; - -interface RouteContext { - params: Promise<{ project_id: string; service_id: string }>; -} - -export async function DELETE(_request: Request, { params }: RouteContext) { - try { - const { service_id } = await params; - const deleted = await deleteProjectService(service_id); - if (!deleted) { - return NextResponse.json({ success: false, error: 'Service not found' }, { status: 404 }); - } - - return NextResponse.json({ success: true, message: 'Service disconnected' }); - } catch (error) { - console.error('[API] Failed to delete project service:', error); - return NextResponse.json( - { - success: false, - error: 'Failed to delete project service', - message: error instanceof Error ? error.message : 'Unknown error', - }, - { status: 500 }, - ); - } -} - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; diff --git a/app/api/projects/[project_id]/services/route.ts b/app/api/projects/[project_id]/services/route.ts deleted file mode 100644 index 260fb2c7..00000000 --- a/app/api/projects/[project_id]/services/route.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { NextResponse } from 'next/server'; -import { listProjectServices } from '@/lib/services/project-services'; - -interface RouteContext { - params: Promise<{ project_id: string }>; -} - -export async function GET(_request: Request, { params }: RouteContext) { - try { - const { project_id } = await params; - const services = await listProjectServices(project_id); - const payload = services.map((service) => ({ - ...service, - service_data: service.serviceData, - })); - return NextResponse.json(payload); - } catch (error) { - console.error('[API] Failed to load project services:', error); - return NextResponse.json( - { - success: false, - error: 'Failed to load project services', - message: error instanceof Error ? error.message : 'Unknown error', - }, - { status: 500 }, - ); - } -} - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; diff --git a/app/api/projects/[project_id]/supabase/connect/route.ts b/app/api/projects/[project_id]/supabase/connect/route.ts deleted file mode 100644 index 75c6b523..00000000 --- a/app/api/projects/[project_id]/supabase/connect/route.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { connectExistingSupabase } from '@/lib/services/supabase'; - -interface RouteContext { - params: Promise<{ project_id: string }>; -} - -export async function POST(request: NextRequest, { params }: RouteContext) { - try { - const { project_id } = await params; - const body = await request.json(); - const supabaseProjectId = - typeof body?.project_id === 'string' - ? body.project_id - : typeof body?.supabase_project_id === 'string' - ? body.supabase_project_id - : undefined; - const projectUrl = typeof body?.project_url === 'string' ? body.project_url : undefined; - if (!supabaseProjectId || !projectUrl) { - return NextResponse.json( - { success: false, error: 'project_id and project_url are required' }, - { status: 400 }, - ); - } - - const result = await connectExistingSupabase(project_id, { - projectId: supabaseProjectId, - projectUrl, - projectName: typeof body?.project_name === 'string' ? body.project_name : undefined, - region: typeof body?.region === 'string' ? body.region : undefined, - }); - return NextResponse.json({ success: true, data: result }); - } catch (error) { - console.error('[API] Failed to connect Supabase project:', error); - const status = error instanceof Error && 'status' in error ? (error as any).status ?? 500 : 500; - return NextResponse.json( - { - success: false, - error: 'Failed to connect Supabase project', - message: error instanceof Error ? error.message : 'Unknown error', - }, - { status }, - ); - } -} - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; diff --git a/app/api/projects/[project_id]/vercel/connect/route.ts b/app/api/projects/[project_id]/vercel/connect/route.ts deleted file mode 100644 index a3e6ab1a..00000000 --- a/app/api/projects/[project_id]/vercel/connect/route.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { connectVercelProject } from '@/lib/services/vercel'; - -interface RouteContext { - params: Promise<{ project_id: string }>; -} - -export async function POST(request: NextRequest, { params }: RouteContext) { - try { - const { project_id } = await params; - const body = await request.json(); - const projectName = typeof body?.project_name === 'string' ? body.project_name : undefined; - if (!projectName) { - return NextResponse.json({ success: false, error: 'project_name is required' }, { status: 400 }); - } - - const teamId = - typeof body?.team_id === 'string' - ? body.team_id - : typeof body?.teamId === 'string' - ? body.teamId - : undefined; - - const result = await connectVercelProject(project_id, projectName, { - githubRepo: typeof body?.github_repo === 'string' ? body.github_repo : undefined, - teamId, - }); - return NextResponse.json({ - success: true, - data: result, - message: `Connected Vercel project ${projectName}`, - }); - } catch (error) { - console.error('[API] Failed to connect Vercel project:', error); - const status = error instanceof Error && 'status' in error ? (error as any).status ?? 500 : 500; - return NextResponse.json( - { - success: false, - error: 'Failed to connect Vercel project', - message: error instanceof Error ? error.message : 'Unknown error', - }, - { status }, - ); - } -} - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; diff --git a/app/api/projects/[project_id]/vercel/deploy/route.ts b/app/api/projects/[project_id]/vercel/deploy/route.ts deleted file mode 100644 index b1af3916..00000000 --- a/app/api/projects/[project_id]/vercel/deploy/route.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { NextResponse } from 'next/server'; -import { triggerVercelDeployment } from '@/lib/services/vercel'; - -interface RouteContext { - params: Promise<{ project_id: string }>; -} - -export async function POST(_request: Request, { params }: RouteContext) { - try { - const { project_id } = await params; - const result = await triggerVercelDeployment(project_id); - return NextResponse.json({ - success: true, - deployment_id: result.deploymentId ?? null, - deployment_url: result.deploymentUrl ?? null, - status: result.status ?? null, - }); - } catch (error) { - console.error('[API] Failed to trigger Vercel deployment:', error); - const status = error instanceof Error && 'status' in error ? (error as any).status ?? 500 : 500; - return NextResponse.json( - { - success: false, - error: 'Failed to trigger Vercel deployment', - message: error instanceof Error ? error.message : 'Unknown error', - }, - { status }, - ); - } -} - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; diff --git a/app/api/projects/[project_id]/vercel/deployment/current/route.ts b/app/api/projects/[project_id]/vercel/deployment/current/route.ts deleted file mode 100644 index 24720bae..00000000 --- a/app/api/projects/[project_id]/vercel/deployment/current/route.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NextResponse } from 'next/server'; -import { getCurrentDeploymentStatus } from '@/lib/services/vercel'; - -interface RouteContext { - params: Promise<{ project_id: string }>; -} - -export async function GET(_request: Request, { params }: RouteContext) { - try { - const { project_id } = await params; - const status = await getCurrentDeploymentStatus(project_id); - return NextResponse.json(status); - } catch (error) { - console.error('[API] Failed to get deployment status:', error); - const statusCode = error instanceof Error && 'status' in error ? (error as any).status ?? 500 : 500; - return NextResponse.json( - { - success: false, - error: 'Failed to get deployment status', - message: error instanceof Error ? error.message : 'Unknown error', - }, - { status: statusCode }, - ); - } -} - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; diff --git a/app/api/projects/route.ts b/app/api/projects/route.ts deleted file mode 100644 index 3b097ae5..00000000 --- a/app/api/projects/route.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Projects API Routes - * GET /api/projects - Get all projects - * POST /api/projects - Create new project - */ - -import { NextRequest } from 'next/server'; -import { getAllProjects, createProject } from '@/lib/services/project'; -import type { CreateProjectInput } from '@/types/backend'; -import { serializeProjects, serializeProject } from '@/lib/serializers/project'; -import { getDefaultModelForCli, normalizeModelId } from '@/lib/constants/cliModels'; -import { createSuccessResponse, createErrorResponse, handleApiError } from '@/lib/utils/api-response'; - -/** - * GET /api/projects - * Get all projects list - */ -export async function GET() { - try { - const projects = await getAllProjects(); - return createSuccessResponse(serializeProjects(projects)); - } catch (error) { - return handleApiError(error, 'API', 'Failed to fetch projects'); - } -} - -/** - * POST /api/projects - * Create new project - */ -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - const preferredCli = String(body.preferredCli || body.preferred_cli || 'claude').toLowerCase(); - const requestedModel = body.selectedModel || body.selected_model; - - const input: CreateProjectInput = { - project_id: body.project_id, - name: body.name, - initialPrompt: body.initialPrompt || body.initial_prompt, - preferredCli, - selectedModel: normalizeModelId(preferredCli, requestedModel ?? getDefaultModelForCli(preferredCli)), - description: body.description, - }; - - // Validation - if (!input.project_id || !input.name) { - return createErrorResponse('project_id and name are required', undefined, 400); - } - - const project = await createProject(input); - return createSuccessResponse(serializeProject(project), 201); - } catch (error) { - return handleApiError(error, 'API', 'Failed to create project'); - } -} - -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; diff --git a/app/api/recipe/route.ts b/app/api/recipe/route.ts new file mode 100644 index 00000000..368a37df --- /dev/null +++ b/app/api/recipe/route.ts @@ -0,0 +1,70 @@ +import { NextResponse } from "next/server"; + +export const runtime = "nodejs"; + +export async function POST(req: Request) { + try { + const { ingredients } = await req.json(); + + if (!ingredients) { + return NextResponse.json( + { recipes: "재료λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš” πŸ™‚" }, + { status: 400 } + ); + } + + const response = await fetch("https://api.openai.com/v1/chat/completions", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, + }, + body: JSON.stringify({ + model: "gpt-4o-mini", + messages: [ + { role: "system", content: "λ„ˆλŠ” μš”λ¦¬ μ „λ¬Έκ°€μ•Ό." }, + { + role: "user", + content: `λ‹€μŒ 재료둜 λ§Œλ“€ 수 μžˆλŠ” μš”λ¦¬λ₯Ό 3개 μΆ”μ²œν•΄μ€˜: ${ingredients}`, + }, + ], + }), + }); + + const data = await response.json(); + + console.log("OPENAI RAW RESPONSE πŸ‘‰", data); + + // πŸ”΄ OpenAIμ—μ„œ μ—λŸ¬κ°€ 왔을 경우 (μΏΌν„° 초과 λ“±) + if (!response.ok) { + console.error("OpenAI Error:", data); + + return NextResponse.json({ + recipes: `⚠️ ν˜„μž¬ AI μ‚¬μš©λŸ‰μ΄ μ΄ˆκ³Όλ˜μ–΄ μž„μ‹œ μΆ”μ²œμ„ λ³΄μ—¬λ“œλ¦½λ‹ˆλ‹€. + +1. ${ingredients} 볢음 +2. ${ingredients} μ˜€λ―ˆλ › +3. ${ingredients} μƒλŸ¬λ“œ`, + }); + } + + // πŸ”΄ choicesκ°€ μ—†λŠ” 경우 λ°©μ–΄ + if (!data.choices || !data.choices[0]) { + return NextResponse.json({ + recipes: "AI 응닡이 λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ μ‹œλ„ν•΄ μ£Όμ„Έμš”.", + }); + } + + // βœ… 정상 응닡 + return NextResponse.json({ + recipes: data.choices[0].message.content, + }); + + } catch (error) { + console.error("SERVER ERROR:", error); + + return NextResponse.json({ + recipes: "μ„œλ²„ 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”.", + }); + } +} diff --git a/app/layout.tsx b/app/layout.tsx index 73487b0b..0ebd0bfe 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,10 +4,12 @@ import GlobalSettingsProvider from '@/contexts/GlobalSettingsContext' import { AuthProvider } from '@/contexts/AuthContext' import Header from '@/components/layout/Header' import { Metadata } from 'next' +import RegisterSW from './RegisterSW' export const metadata: Metadata = { title: 'Claudable', description: 'Claudable Application', + manifest: '/manifest.json', icons: { icon: '/Claudable_Icon.png', }, @@ -24,6 +26,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
{children}
+ + {/* πŸ‘‡ 이 쀄 μΆ”κ°€ */} + ); diff --git a/app/page.tsx b/app/page.tsx index c06d849d..b54eb127 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,1282 +1,261 @@ "use client"; -import { useEffect, useState, useRef, useCallback } from 'react'; -import { motion } from 'framer-motion'; -import { useRouter } from 'next/navigation'; -import CreateProjectModal from '@/components/modals/CreateProjectModal'; -import DeleteProjectModal from '@/components/modals/DeleteProjectModal'; -import GlobalSettings from '@/components/settings/GlobalSettings'; -import { useGlobalSettings } from '@/contexts/GlobalSettingsContext'; -import { getDefaultModelForCli, getModelDisplayName } from '@/lib/constants/cliModels'; -import Image from 'next/image'; -import { Image as ImageIcon } from 'lucide-react'; -import type { Project as ProjectSummary } from '@/types/project'; -import { fetchCliStatusSnapshot, createCliStatusFallback } from '@/hooks/useCLI'; -import type { CLIStatus } from '@/types/cli'; -import { - ACTIVE_CLI_BRAND_COLORS, - ACTIVE_CLI_MODEL_OPTIONS, - ACTIVE_CLI_OPTIONS, - ACTIVE_CLI_OPTIONS_MAP, - DEFAULT_ACTIVE_CLI, - normalizeModelForCli, - sanitizeActiveCli, - type ActiveCliId, -} from '@/lib/utils/cliOptions'; -// Ensure fetch is available -const fetchAPI = globalThis.fetch || fetch; +import { useEffect, useState } from "react"; -const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? ''; +export default function Home() { + const [tab, setTab] = useState("home"); + const [ingredients, setIngredients] = useState(""); + const [recipes, setRecipes] = useState(""); + const [saved, setSaved] = useState([]); + const [loading, setLoading] = useState(false); + const [fade, setFade] = useState(true); -// Define assistant brand colors -const ASSISTANT_OPTIONS = ACTIVE_CLI_OPTIONS.map(({ id, name, icon }) => ({ - id, - name, - icon, -})); - -const assistantBrandColors = ACTIVE_CLI_BRAND_COLORS; - -const MODEL_OPTIONS_BY_ASSISTANT = ACTIVE_CLI_MODEL_OPTIONS; - -export default function HomePage() { - const [projects, setProjects] = useState([]); - const [showCreate, setShowCreate] = useState(false); - const [showGlobalSettings, setShowGlobalSettings] = useState(false); - const [globalSettingsTab, setGlobalSettingsTab] = useState<'general' | 'ai-assistant'>('ai-assistant'); - const [editingProject, setEditingProject] = useState(null); - const [deleteModal, setDeleteModal] = useState<{ isOpen: boolean; project: ProjectSummary | null }>({ isOpen: false, project: null }); - const [isDeleting, setIsDeleting] = useState(false); - const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null); - const [prompt, setPrompt] = useState(''); - const DEFAULT_ASSISTANT: ActiveCliId = DEFAULT_ACTIVE_CLI; - const DEFAULT_MODEL = getDefaultModelForCli(DEFAULT_ASSISTANT); - const sanitizeAssistant = useCallback( - (cli?: string | null) => sanitizeActiveCli(cli, DEFAULT_ASSISTANT), - [DEFAULT_ASSISTANT] - ); - const normalizeModelForAssistant = useCallback( - (assistant: string, model?: string | null) => normalizeModelForCli(assistant, model, DEFAULT_ASSISTANT), - [DEFAULT_ASSISTANT] - ); - - const normalizeProjectPayload = useCallback((project: any): ProjectSummary => { - const preferred = sanitizeAssistant(project?.preferredCli ?? project?.preferred_cli); - const selected = normalizeModelForAssistant(preferred, project?.selectedModel ?? project?.selected_model); - - return { - id: project.id, - name: project.name, - description: project.description ?? null, - status: project.status, - previewUrl: project.previewUrl ?? project.preview_url ?? null, - createdAt: project.createdAt ?? project.created_at ?? new Date().toISOString(), - updatedAt: project.updatedAt ?? project.updated_at, - lastActiveAt: project.lastActiveAt ?? project.last_active_at ?? null, - lastMessageAt: project.lastMessageAt ?? project.last_message_at ?? null, - initialPrompt: project.initialPrompt ?? project.initial_prompt ?? null, - services: project.services, - preferredCli: preferred as ProjectSummary['preferredCli'], - selectedModel: selected, - fallbackEnabled: project.fallbackEnabled ?? project.fallback_enabled ?? false, - }; - }, [sanitizeAssistant, normalizeModelForAssistant]); - const [selectedAssistant, setSelectedAssistant] = useState(DEFAULT_ASSISTANT); - const [selectedModel, setSelectedModel] = useState(DEFAULT_MODEL); - const [usingGlobalDefaults, setUsingGlobalDefaults] = useState(true); - const [sidebarOpen, setSidebarOpen] = useState(false); - const [cliStatus, setCLIStatus] = useState({}); - const [isInitialLoad, setIsInitialLoad] = useState(true); - const selectedAssistantOption = ACTIVE_CLI_OPTIONS_MAP[selectedAssistant]; - - // Get available models based on current assistant - const availableModels = MODEL_OPTIONS_BY_ASSISTANT[selectedAssistant] || []; - - // Sync with Global Settings (until user overrides locally) - const { settings: globalSettings } = useGlobalSettings(); - - // Check if this is a fresh page load (not navigation) - useEffect(() => { - const isPageRefresh = !sessionStorage.getItem('navigationFlag'); - - if (isPageRefresh) { - // Fresh page load or refresh - use global defaults - sessionStorage.setItem('navigationFlag', 'true'); - setIsInitialLoad(true); - setUsingGlobalDefaults(true); - } else { - // Navigation within session - check for stored selections - const storedAssistantRaw = sessionStorage.getItem('selectedAssistant'); - const storedModelRaw = sessionStorage.getItem('selectedModel'); - - if (storedModelRaw) { - const storedAssistant = sanitizeAssistant(storedAssistantRaw); - const storedModel = normalizeModelForAssistant(storedAssistant, storedModelRaw); - setSelectedAssistant(storedAssistant); - setSelectedModel(storedModel); - setUsingGlobalDefaults(false); - setIsInitialLoad(false); - return; - } - } - - // Clean up navigation flag on unmount - return () => { - // Don't clear on navigation, only on actual page unload - }; - }, [sanitizeAssistant, normalizeModelForAssistant]); - - // Apply global settings when using defaults - useEffect(() => { - if (!usingGlobalDefaults || !isInitialLoad) return; - - const cli = sanitizeAssistant(globalSettings?.default_cli); - setSelectedAssistant(cli); - const modelFromGlobal = globalSettings?.cli_settings?.[cli]?.model; - setSelectedModel(normalizeModelForAssistant(cli, modelFromGlobal)); - }, [globalSettings, usingGlobalDefaults, isInitialLoad, sanitizeAssistant, normalizeModelForAssistant]); - - // Save selections to sessionStorage when they change - useEffect(() => { - if (!isInitialLoad && selectedAssistant && selectedModel) { - const normalizedAssistant = sanitizeAssistant(selectedAssistant); - sessionStorage.setItem('selectedAssistant', normalizedAssistant); - sessionStorage.setItem('selectedModel', normalizeModelForAssistant(normalizedAssistant, selectedModel)); - } - }, [selectedAssistant, selectedModel, isInitialLoad, sanitizeAssistant, normalizeModelForAssistant]); - - // Clear navigation flag on page unload - useEffect(() => { - const handleBeforeUnload = () => { - sessionStorage.removeItem('navigationFlag'); - }; - - window.addEventListener('beforeunload', handleBeforeUnload); - return () => window.removeEventListener('beforeunload', handleBeforeUnload); - }, []); - const [showAssistantDropdown, setShowAssistantDropdown] = useState(false); - const [showModelDropdown, setShowModelDropdown] = useState(false); - const [isCreatingProject, setIsCreatingProject] = useState(false); - const [uploadedImages, setUploadedImages] = useState<{ id: string; name: string; url: string; path: string; file?: File }[]>([]); - const [isUploading, setIsUploading] = useState(false); - const [isDragOver, setIsDragOver] = useState(false); - const router = useRouter(); - const prefetchTimers = useRef>(new Map()); - const fileInputRef = useRef(null); - const assistantDropdownRef = useRef(null); - const modelDropdownRef = useRef(null); - - // Check CLI installation status - useEffect(() => { - const checkingStatus = ASSISTANT_OPTIONS.reduce((acc, cli) => { - acc[cli.id] = { - installed: false, - checking: true, - available: false, - configured: false, - }; - return acc; - }, {}); - setCLIStatus(checkingStatus); - - fetchCliStatusSnapshot() - .then((status) => setCLIStatus(status)) - .catch((error) => { - console.error('Failed to check CLI status:', error); - setCLIStatus(createCliStatusFallback()); - }); - }, []); - - // Click outside handler + // βœ… localStorage 뢈러였기 useEffect(() => { - const handleDocumentClick = (event: MouseEvent) => { - const target = event.target as Node; - - const assistantEl = assistantDropdownRef.current; - if (assistantEl && !assistantEl.contains(target)) { - setShowAssistantDropdown(false); - } - - const modelEl = modelDropdownRef.current; - if (modelEl && !modelEl.contains(target)) { - setShowModelDropdown(false); - } - }; - - document.addEventListener('mousedown', handleDocumentClick); - return () => { - document.removeEventListener('mousedown', handleDocumentClick); - }; - }, []); - - // Format time for display - const formatTime = (dateString: string | null) => { - if (!dateString) return 'Never'; - - // Server sends UTC time without 'Z' suffix, so we need to add it - // to ensure it's parsed as UTC, not local time - let utcDateString = dateString; - - // Check if the string has timezone info - const hasTimezone = dateString.endsWith('Z') || - dateString.includes('+') || - dateString.match(/[-+]\d{2}:\d{2}$/); - - if (!hasTimezone) { - // Add 'Z' to indicate UTC - utcDateString = dateString + 'Z'; - } - - // Parse the date as UTC - const date = new Date(utcDateString); - const now = new Date(); - // Calculate the actual time difference - const diffMs = now.getTime() - date.getTime(); - const diffMins = Math.floor(diffMs / (1000 * 60)); - const diffHours = Math.floor(diffMins / 60); - const diffDays = Math.floor(diffHours / 24); - - if (diffMins < 1) return 'Just now'; - if (diffMins < 60) return `${diffMins}m ago`; - if (diffHours < 24) return `${diffHours}h ago`; - if (diffDays < 30) return `${diffDays}d ago`; - - return date.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined - }); - }; - - // Format CLI and model information - const formatCliInfo = (cli?: string, model?: string) => { - const normalizedCli = sanitizeAssistant(cli); - const assistantOption = ACTIVE_CLI_OPTIONS_MAP[normalizedCli]; - const cliName = assistantOption?.name ?? 'Claude Code'; - const modelId = normalizeModelForAssistant(normalizedCli, model); - const modelLabel = getModelDisplayName(normalizedCli, modelId); - return `${cliName} β€’ ${modelLabel}`; - }; - - const formatFullTime = (dateString: string) => { - return new Date(dateString).toLocaleString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - }; - - const load = useCallback(async () => { - try { - const r = await fetchAPI(`${API_BASE}/api/projects`); - if (!r.ok) { - console.warn('Failed to load projects: HTTP', r.status); - setProjects([]); - return; - } - - const payload = await r.json(); - if (payload?.success === false) { - console.error('Failed to load projects:', payload?.error || payload?.message); - setProjects([]); - return; - } - - const items: unknown[] = Array.isArray(payload?.data) - ? payload.data - : Array.isArray(payload) - ? payload - : []; - - const normalized: ProjectSummary[] = items - .filter((project): project is Record => Boolean(project && typeof project === 'object')) - .map((project) => normalizeProjectPayload(project)); - - const sortedProjects = normalized.sort((a, b) => { - const aTime = a.lastMessageAt ?? a.createdAt; - const bTime = b.lastMessageAt ?? b.createdAt; - if (!aTime) return 1; - if (!bTime) return -1; - return new Date(bTime).getTime() - new Date(aTime).getTime(); - }); - - setProjects(sortedProjects); - } catch (error) { - console.warn('Failed to load projects:', error); - setProjects([]); - } - }, [normalizeProjectPayload]); - - async function onCreated() { await load(); } - - async function start(projectId: string) { - try { - await fetchAPI(`${API_BASE}/api/projects/${projectId}/preview/start`, { method: 'POST' }); - await load(); - } catch (error) { - console.warn('Failed to start project:', error); - } - } - - async function stop(projectId: string) { - try { - await fetchAPI(`${API_BASE}/api/projects/${projectId}/preview/stop`, { method: 'POST' }); - await load(); - } catch (error) { - console.warn('Failed to stop project:', error); + const stored = localStorage.getItem("savedRecipes"); + if (stored) { + setSaved(JSON.parse(stored)); } - } - - const showToast = useCallback((message: string, type: 'success' | 'error') => { - setToast({ message, type }); - setTimeout(() => setToast(null), 4000); }, []); - const openDeleteModal = (project: ProjectSummary) => { - setDeleteModal({ isOpen: true, project }); + // βœ… νƒ­ μ „ν™˜ μ• λ‹ˆλ©”μ΄μ…˜ + const changeTab = (newTab: string) => { + setFade(false); + setTimeout(() => { + setTab(newTab); + setFade(true); + }, 150); }; - const closeDeleteModal = () => { - setDeleteModal({ isOpen: false, project: null }); - }; + const getRecipe = async () => { + if (!ingredients) return; - async function deleteProject() { - if (!deleteModal.project) return; - - setIsDeleting(true); - try { - const response = await fetchAPI(`${API_BASE}/api/projects/${deleteModal.project.id}`, { method: 'DELETE' }); - - if (response.ok) { - showToast('Project deleted successfully', 'success'); - await load(); - closeDeleteModal(); - } else { - const errorData = await response.json().catch(() => ({ detail: 'Failed to delete project' })); - showToast(errorData.detail || 'Failed to delete project', 'error'); - } - } catch (error) { - console.warn('Failed to delete project:', error); - showToast('Failed to delete project. Please try again.', 'error'); - } finally { - setIsDeleting(false); - } - } + setLoading(true); + setRecipes(""); - async function updateProject(projectId: string, newName: string) { try { - const response = await fetchAPI(`${API_BASE}/api/projects/${projectId}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: newName }) + const res = await fetch("/api/recipe", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ingredients }), }); - - if (response.ok) { - showToast('Project updated successfully', 'success'); - await load(); - setEditingProject(null); - } else { - const errorData = await response.json().catch(() => ({ detail: 'Failed to update project' })); - showToast(errorData.detail || 'Failed to update project', 'error'); - } - } catch (error) { - console.warn('Failed to update project:', error); - showToast('Failed to update project. Please try again.', 'error'); - } - } - - // Handle files (for both drag drop and file input) - const handleFiles = useCallback(async (files: FileList | File[]) => { - setIsUploading(true); - - try { - const filesArray = Array.from(files as ArrayLike); - const imagesToAdd = filesArray - .filter(file => file.type.startsWith('image/')) - .map(file => ({ - id: crypto.randomUUID(), - name: file.name, - url: URL.createObjectURL(file), - path: '', - file, - })); - if (imagesToAdd.length > 0) { - setUploadedImages(prev => [...prev, ...imagesToAdd]); - } - } catch (error) { - console.error('Image processing failed:', error); - showToast('Failed to process image. Please try again.', 'error'); - } finally { - setIsUploading(false); - if (fileInputRef.current) { - fileInputRef.current.value = ''; - } + const data = await res.json(); + setRecipes(data.recipes); + } catch { + setRecipes("μ—λŸ¬κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€."); } - }, [showToast]); - // Handle image upload - store locally first, upload after project creation - const handleImageUpload = async (e: React.ChangeEvent) => { - const files = e.target.files; - if (!files) return; - - await handleFiles(files); + setLoading(false); }; - // Drag and drop handlers - const handleDragEnter = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragOver(true); - }; + const saveRecipe = () => { + if (!recipes) return; - const handleDragLeave = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - // Only set to false if we're leaving the container completely - if (!e.currentTarget.contains(e.relatedTarget as Node)) { - setIsDragOver(false); - } - }; - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - e.dataTransfer.dropEffect = 'copy'; - }; - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragOver(false); - - const files = e.dataTransfer.files; - if (files.length > 0) { - handleFiles(files); - } + const updated = [...saved, recipes]; + setSaved(updated); + localStorage.setItem("savedRecipes", JSON.stringify(updated)); }; - // Remove uploaded image - const removeImage = (id: string) => { - setUploadedImages(prev => { - const imageToRemove = prev.find(img => img.id === id); - if (imageToRemove) { - URL.revokeObjectURL(imageToRemove.url); - } - return prev.filter(img => img.id !== id); - }); + // βœ… μ‚­μ œ κΈ°λŠ₯ + const deleteRecipe = (index: number) => { + const updated = saved.filter((_, i) => i !== index); + setSaved(updated); + localStorage.setItem("savedRecipes", JSON.stringify(updated)); }; - const handleSubmit = async () => { - if ((!prompt.trim() && uploadedImages.length === 0) || isCreatingProject) return; - - setIsCreatingProject(true); - - // Generate a unique project ID - const projectId = `project-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - - try { - // Create a new project first - const response = await fetchAPI(`${API_BASE}/api/projects`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - project_id: projectId, - name: prompt.slice(0, 50) + (prompt.length > 50 ? '...' : ''), - initialPrompt: prompt.trim(), - preferredCli: selectedAssistant, - selectedModel - }) - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => null); - console.error('Failed to create project:', errorData); - showToast('Failed to create project', 'error'); - setIsCreatingProject(false); - return; - } - - const payload = await response.json(); - const projectData = (payload && typeof payload === 'object') ? (payload.data ?? payload) : payload; - const createdProjectId: string | undefined = projectData?.id ?? projectId; - if (!createdProjectId) { - console.error('Create project response missing id:', payload); - showToast('Failed to create project (invalid response)', 'error'); - setIsCreatingProject(false); - return; - } - if (createdProjectId !== projectId) { - console.warn('Project ID mismatch between request and response:', { - requestedId: projectId, - responseId: createdProjectId, - payload - }); - } - - // Upload images if any - let imageData: any[] = []; - - if (uploadedImages.length > 0) { - try { - for (let i = 0; i < uploadedImages.length; i++) { - const image = uploadedImages[i]; - if (!image.file) continue; - - const formData = new FormData(); - formData.append('file', image.file); - - const uploadResponse = await fetchAPI(`${API_BASE}/api/assets/${createdProjectId}/upload`, { - method: 'POST', - body: formData - }); - - if (uploadResponse.ok) { - const result = await uploadResponse.json(); - // Track image data for API - imageData.push({ - name: result.filename || image.name, - path: result.absolute_path, - public_url: typeof result.public_url === 'string' ? result.public_url : undefined - }); - } - } - } catch (uploadError) { - console.error('Image upload failed:', uploadError); - showToast('Images could not be uploaded, but project was created', 'error'); - } - } - - // Execute initial prompt directly with images - if (prompt.trim()) { - try { - const actResponse = await fetchAPI(`${API_BASE}/api/chat/${createdProjectId}/act`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - instruction: prompt.trim(), // Original prompt without image paths - images: imageData, - isInitialPrompt: true, - cliPreference: selectedAssistant, - selectedModel - }) - }); - - if (actResponse.ok) { - // Successfully kicked off ACT with image payloads - } else { - console.error('❌ ACT failed:', await actResponse.text()); - } - } catch (actError) { - console.error('❌ ACT API error:', actError); - } - } - - // Navigate to chat page with model and CLI parameters - uploadedImages.forEach(image => { - if (image.url) { - URL.revokeObjectURL(image.url); - } - }); - setUploadedImages([]); - setPrompt(''); - - const params = new URLSearchParams(); - if (selectedAssistant) params.set('cli', selectedAssistant); - if (selectedModel) params.set('model', selectedModel); - router.push(`/${createdProjectId}/chat${params.toString() ? '?' + params.toString() : ''}`); - - } catch (error) { - console.error('Failed to create project:', error); - showToast('Failed to create project', 'error'); - } finally { - setIsCreatingProject(false); - } - }; - - useEffect(() => { - load(); - - // Handle clipboard paste for images - const handlePaste = (e: ClipboardEvent) => { - const items = e.clipboardData?.items; - if (!items) return; - - const imageFiles: File[] = []; - for (let i = 0; i < items.length; i++) { - const item = items[i]; - if (item.type.startsWith('image/')) { - const file = item.getAsFile(); - if (file) { - imageFiles.push(file); - } - } - } - - if (imageFiles.length > 0) { - e.preventDefault(); - const fileList = { - length: imageFiles.length, - item: (index: number) => imageFiles[index], - [Symbol.iterator]: function* () { - for (let i = 0; i < imageFiles.length; i++) { - yield imageFiles[i]; - } - } - } as FileList; - - // Convert to FileList-like object - Object.defineProperty(fileList, 'length', { value: imageFiles.length }); - imageFiles.forEach((file, index) => { - Object.defineProperty(fileList, index, { value: file }); - }); - - handleFiles(fileList); - } - }; - - document.addEventListener('paste', handlePaste); - const timers = prefetchTimers.current; - - // Cleanup prefetch timers - return () => { - timers.forEach(timer => clearTimeout(timer)); - timers.clear(); - document.removeEventListener('paste', handlePaste); - }; - }, [selectedAssistant, handleFiles, load]); - - // Update models when assistant changes - const handleAssistantChange = (assistant: string) => { - // Don't allow selecting uninstalled CLIs - if (!cliStatus[assistant]?.installed) return; - - const sanitized = sanitizeAssistant(assistant); - setUsingGlobalDefaults(false); - setIsInitialLoad(false); - setSelectedAssistant(sanitized); - setSelectedModel(getDefaultModelForCli(sanitized)); - - setShowAssistantDropdown(false); - }; - - const handleModelChange = (modelId: string) => { - setUsingGlobalDefaults(false); - setIsInitialLoad(false); - setSelectedModel(normalizeModelForAssistant(selectedAssistant, modelId)); - setShowModelDropdown(false); - }; - - return ( -
- {/* Radial gradient background from bottom center */} -
-
-
- {/* Light mode gradient - subtle */} -
+
+
-
- - {/* Content wrapper */} -
- {/* Thin sidebar bar when closed */} -
- - - {/* Settings button when sidebar is closed */} -
- -
-
- - {/* Sidebar - Overlay style */} -
-
- {/* History header with close button */} -
-
-
-

History

-
- -
-
- -
-
- {projects.length === 0 ? ( -
-

No conversations yet

+ + {recipes && ( +
+
{recipes}
+
- ) : ( - projects.map((project) => { - const projectCli = sanitizeAssistant(project.preferredCli); - const projectColor = assistantBrandColors[projectCli] || assistantBrandColors[DEFAULT_ASSISTANT]; - return ( -
{ - e.currentTarget.style.backgroundColor = `${projectColor}15`; - }} - onMouseLeave={(e) => { - e.currentTarget.style.backgroundColor = 'transparent'; - }} - > - {editingProject?.id === project.id ? ( - // Edit mode -
{ - e.preventDefault(); - const formData = new FormData(e.target as HTMLFormElement); - const newName = formData.get('name') as string; - if (newName.trim()) { - updateProject(project.id, newName.trim()); - } - }} - className="space-y-2" - > - setEditingProject(null)} - /> -
- - -
-
- ) : ( - // View mode -
-
{ - // Pass current model selection when navigating from sidebar - const params = new URLSearchParams(); - if (selectedAssistant) params.set('cli', selectedAssistant); - if (selectedModel) params.set('model', selectedModel); - router.push(`/${project.id}/chat${params.toString() ? '?' + params.toString() : ''}`); - }} - > -

- - {project.name.length > 28 - ? `${project.name.substring(0, 28)}...` - : project.name - } - -

-
-
- {formatTime(project.lastMessageAt || project.createdAt)} -
- {project.preferredCli && ( -
- β€’ - - {formatCliInfo(projectCli, project.selectedModel ?? undefined)} - -
- )} -
-
-
- - -
-
- )} -
- ); - }) )} -
-
- -
- -
-
-
- - {/* Main Content - Not affected by sidebar */} -
-
-
-
-
-

- Claudable -

-
-

- Connect CLI Agent β€’ Build what you want β€’ Deploy instantly -

-
- - {/* Image thumbnails */} - {uploadedImages.length > 0 && ( -
- {uploadedImages.map((image, index) => ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - {image.name} -
- Image #{index + 1} -
+ + )} + + {tab === "saved" && ( + <> +

⭐ μ €μž₯된 λ ˆμ‹œν”Ό

+ + {saved.length === 0 ? ( +

μ €μž₯된 λ ˆμ‹œν”Όκ°€ μ—†μŠ΅λ‹ˆλ‹€.

+ ) : ( + saved.map((item, index) => ( +
+
{item}
- ))} -
- )} - - {/* Main Input Form */} -
{ e.preventDefault(); handleSubmit(); }} - onDragEnter={handleDragEnter} - onDragLeave={handleDragLeave} - onDragOver={handleDragOver} - onDrop={handleDrop} - className={`group flex flex-col gap-4 p-4 w-full rounded-[28px] border backdrop-blur-xl text-base shadow-xl transition-all duration-150 ease-in-out mb-6 relative overflow-visible ${ - isDragOver - ? 'border-[#DE7356] bg-[#DE7356]/10 ' - : 'border-gray-200 bg-white ' - }`} - > -
-