Leon4gr45/builder
0
1'use client';2 3import React, { useState, useCallback, useEffect, useRef } from 'react';4import { Button } from '@/components/ui/button';5import { Play, Loader2 } from 'lucide-react';6import { toast } from 'sonner';7import { SchemaViewer } from '@/components/database-manager/schema-viewer';8import { SqlEditor } from '@/components/database-manager/sql-editor';9 10interface SchemaEditorProps {11 projectId: string;12 enabled: boolean;13 onSchemaChange?: (schema: string) => void;14 workspaceId?: string;15}16 17// Keep these exports — used by vfs/index.ts for transient file generation18export function getProjectSchema(projectId: string): string {19 if (typeof window === 'undefined') return '';20 return localStorage.getItem(`osw-db-schema-${projectId}`) || '';21}22 23export function setProjectSchema(projectId: string, schema: string): void {24 if (typeof window === 'undefined') return;25 if (schema) {26 localStorage.setItem(`osw-db-schema-${projectId}`, schema);27 } else {28 localStorage.removeItem(`osw-db-schema-${projectId}`);29 }30}31 32/**33 * Save schema to localStorage and apply DDL to the project database (Server Mode only).34 * Used by project-manager and template-manager during project creation.35 */36export async function applyProjectDatabaseSchema(projectId: string, ddl: string, workspaceId?: string): Promise<void> {37 setProjectSchema(projectId, ddl);38 if (process.env.NEXT_PUBLIC_SERVER_MODE === 'true') {39 try {40 const apiBase = workspaceId ? `/api/w/${workspaceId}` : '/api';41 const res = await fetch(`${apiBase}/projects/${projectId}/database/query`, {42 method: 'POST',43 headers: { 'Content-Type': 'application/json' },44 body: JSON.stringify({ sql: ddl }),45 });46 if (!res.ok) {47 console.warn('[Schema] DDL apply failed — will auto-heal on Schema tab open');48 }49 } catch {50 // Non-fatal — auto-apply on Schema tab open will recover51 }52 }53}54 55type SubTab = 'tables' | 'sql' | 'ddl';56 57export function SchemaEditor({ projectId, enabled, onSchemaChange, workspaceId }: SchemaEditorProps) {58 const apiBase = workspaceId ? `/api/w/${workspaceId}` : '/api';59 const [activeSubTab, setActiveSubTab] = useState<SubTab>('tables');60 const [ddl, setDdl] = useState('');61 const [applying, setApplying] = useState(false);62 const [schemaKey, setSchemaKey] = useState(0);63 const autoAppliedRef = useRef<string | null>(null);64 65 const schemaEndpoint = `${apiBase}/projects/${projectId}/database/schema`;66 const queryEndpoint = `${apiBase}/projects/${projectId}/database/query`;67 68 // Auto-apply: if localStorage has schema DDL but the project database has no tables,69 // apply the DDL automatically. This self-heals when the initial application during70 // project creation failed (e.g., project not yet synced to SQLite, server restart).71 useEffect(() => {72 if (!enabled) return;73 // Only auto-apply once per projectId74 if (autoAppliedRef.current === projectId) return;75 76 const storedSchema = getProjectSchema(projectId);77 if (!storedSchema) return;78 79 const tryAutoApply = async () => {80 try {81 // Check if database already has tables82 const schemaRes = await fetch(schemaEndpoint);83 if (!schemaRes.ok) return;84 const schemaData = await schemaRes.json();85 if (schemaData.tables && schemaData.tables.length > 0) {86 autoAppliedRef.current = projectId;87 return; // Already has tables, nothing to do88 }89 90 // Database is empty but localStorage has DDL — apply it91 const res = await fetch(queryEndpoint, {92 method: 'POST',93 headers: { 'Content-Type': 'application/json' },94 body: JSON.stringify({ sql: storedSchema }),95 });96 97 if (res.ok) {98 autoAppliedRef.current = projectId;99 setSchemaKey(prev => prev + 1);100 }101 } catch {102 // Non-fatal — user can manually apply via DDL tab103 }104 };105 106 tryAutoApply();107 }, [enabled, projectId, schemaEndpoint, queryEndpoint]);108 109 const applyDDL = useCallback(async () => {110 if (!ddl.trim()) return;111 112 setApplying(true);113 try {114 const res = await fetch(queryEndpoint, {115 method: 'POST',116 headers: { 'Content-Type': 'application/json' },117 body: JSON.stringify({ sql: ddl.trim() }),118 });119 120 const data = await res.json();121 if (!res.ok) {122 toast.error(data.error || 'Failed to apply DDL');123 return;124 }125 126 toast.success('DDL applied successfully');127 128 // Update localStorage schema (append DDL) so AI server context stays in sync129 const existing = getProjectSchema(projectId);130 const updated = existing ? `${existing}\n\n${ddl.trim()}` : ddl.trim();131 setProjectSchema(projectId, updated);132 onSchemaChange?.(updated);133 134 // Refresh SchemaViewer135 setSchemaKey(prev => prev + 1);136 setDdl('');137 } catch (err) {138 toast.error(err instanceof Error ? err.message : 'Failed to apply DDL');139 } finally {140 setApplying(false);141 }142 }, [ddl, queryEndpoint, projectId, onSchemaChange]);143 144 if (!enabled) {145 return null;146 }147 148 return (149 <div className="h-full flex flex-col">150 {/* Sub-tab buttons */}151 <div className="flex items-center gap-1 mb-3 border-b pb-2">152 {(['tables', 'sql', 'ddl'] as const).map(tab => (153 <button154 key={tab}155 onClick={() => setActiveSubTab(tab)}156 className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${157 activeSubTab === tab158 ? 'bg-primary text-primary-foreground'159 : 'text-muted-foreground hover:text-foreground hover:bg-muted'160 }`}161 >162 {tab === 'tables' ? 'Tables' : tab === 'sql' ? 'SQL' : 'DDL'}163 </button>164 ))}165 </div>166 167 {/* Sub-tab content */}168 <div className="flex-1 min-h-0">169 {activeSubTab === 'tables' && (170 <SchemaViewer171 key={schemaKey}172 schemaEndpoint={schemaEndpoint}173 showSystemTablesToggle={false}174 />175 )}176 177 {activeSubTab === 'sql' && (178 <SqlEditor queryEndpoint={queryEndpoint} />179 )}180 181 {activeSubTab === 'ddl' && (182 <div className="h-full flex flex-col gap-3">183 <div className="flex items-center justify-between">184 <div>185 <h4 className="text-sm font-medium">Apply DDL</h4>186 <p className="text-xs text-muted-foreground mt-0.5">187 CREATE TABLE, ALTER TABLE, and other DDL statements188 </p>189 </div>190 <Button191 size="sm"192 className="h-7 px-2 text-xs"193 onClick={applyDDL}194 disabled={applying || !ddl.trim()}195 >196 {applying ? (197 <Loader2 className="h-3 w-3 mr-1 animate-spin" />198 ) : (199 <Play className="h-3 w-3 mr-1" />200 )}201 Apply202 </Button>203 </div>204 <textarea205 data-schema-editor206 className="flex-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm font-mono resize-none focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring placeholder:text-muted-foreground"207 placeholder={`-- Create or modify tables\nCREATE TABLE IF NOT EXISTS example (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP\n);`}208 value={ddl}209 onChange={(e) => setDdl(e.target.value)}210 spellCheck={false}211 />212 </div>213 )}214 </div>215 </div>216 );217}218 