diff --git a/app/api/schemas/[id]/route.ts b/app/api/schemas/[id]/route.ts index 0cf58eb..0c4410c 100644 --- a/app/api/schemas/[id]/route.ts +++ b/app/api/schemas/[id]/route.ts @@ -16,6 +16,8 @@ const schemaUpdateSchema = z.object({ schema: z.string().min(1).optional(), schemaType: z.enum(["sql", "nosql"]).optional(), additionalInstructions: z.string().optional(), + preferredFormat: z.string().optional(), + preferredRecordCount: z.number().min(1).max(100).optional(), }); export async function GET( @@ -101,6 +103,10 @@ export async function PATCH( updates.schemaType = result.data.schemaType; if (result.data.additionalInstructions !== undefined) updates.additionalInstructions = result.data.additionalInstructions; + if (result.data.preferredFormat !== undefined) + updates.preferredFormat = result.data.preferredFormat; + if (result.data.preferredRecordCount !== undefined) + updates.preferredRecordCount = result.data.preferredRecordCount; if (Object.keys(updates).length === 0) { return NextResponse.json( diff --git a/app/api/schemas/route.ts b/app/api/schemas/route.ts index abccfbf..7a19e45 100644 --- a/app/api/schemas/route.ts +++ b/app/api/schemas/route.ts @@ -10,6 +10,8 @@ const schemaRequestSchema = z.object({ schema: z.string().min(1), schemaType: z.enum(["sql", "nosql"]), additionalInstructions: z.string().optional(), + preferredFormat: z.string().optional(), + preferredRecordCount: z.number().min(1).max(100).optional(), }); export async function GET(request: NextRequest) { @@ -66,6 +68,8 @@ export async function POST(request: NextRequest) { schema: result.data.schema, schemaType: result.data.schemaType, additionalInstructions: result.data.additionalInstructions, + preferredFormat: result.data.preferredFormat, + preferredRecordCount: result.data.preferredRecordCount, }); return NextResponse.json({ schema: savedSchema }, { status: 201 }); diff --git a/app/generator/page.tsx b/app/generator/page.tsx index b589f62..47ab13a 100644 --- a/app/generator/page.tsx +++ b/app/generator/page.tsx @@ -33,14 +33,16 @@ function GeneratorPageContent() { const [isGenerating, setIsGenerating] = useState(false); const [isLoading, setIsLoading] = useState(false); const [schema, setSchema] = useState(""); - const [schemaType, setSchemaType] = useState<"sql" | "nosql" | "sample">("sql"); + const [schemaType, setSchemaType] = useState<"sql" | "nosql" | "sample">( + "sql" + ); const [recordCount, setRecordCount] = useState(10); const [format, setFormat] = useState("json"); const [examples, setExamples] = useState(""); const [results, setResults] = useState(""); const [saveDialogOpen, setSaveDialogOpen] = useState(false); const [activeTab, setActiveTab] = useState<"schema" | "examples">("schema"); - + // swap active tab with schema data type (in step 1 and step 2) useEffect(() => { if (schemaType === "sample" && activeTab !== "examples") { @@ -49,7 +51,7 @@ function GeneratorPageContent() { setActiveTab("schema"); } }, [schemaType, activeTab]); - + const handleSchemaTypeChange = (type: "sql" | "nosql" | "sample") => { setSchemaType(type); if (type === "sample") { @@ -58,7 +60,7 @@ function GeneratorPageContent() { setActiveTab("schema"); } }; - + // Handle tab changes from the tabs component const handleTabChange = (value: string) => { const tab = value as "schema" | "examples"; @@ -79,15 +81,20 @@ function GeneratorPageContent() { const [tempMaxTokens, setTempMaxTokens] = useState(4000); const [tempHeaders, setTempHeaders] = useState>({}); const [useUserSettings, setUseUserSettings] = useState(true); - const [userSettings, setUserSettings] = useState | null>(null); + const [userSettings, setUserSettings] = + useState | null>(null); // State for schema name and loaded instructions const [schemaName, setSchemaName] = useState(null); - const [loadedInstructions, setLoadedInstructions] = useState(null); + const [loadedInstructions, setLoadedInstructions] = useState( + null + ); const [tempInstructions, setTempInstructions] = useState(""); // Reference to DailyLimitInfo component - const dailyLimitRef = useRef<{ refreshUsage: () => Promise } | null>(null); + const dailyLimitRef = useRef<{ refreshUsage: () => Promise } | null>( + null + ); // Load saved schema if schemaId is provided in URL useEffect(() => { @@ -111,7 +118,7 @@ function GeneratorPageContent() { toast({ title: "Schema Loaded", description: "Test schema loaded - ready to generate mock data", - variant: "success" + variant: "success", }); } catch (error) { console.error("Error decoding schema data:", error); @@ -142,16 +149,25 @@ function GeneratorPageContent() { setSchemaName(data.schema.name); setLoadedInstructions(data.schema.additionalInstructions || null); setTempInstructions(data.schema.additionalInstructions || ""); - + + // Use saved format and record count preferences if available + if (data.schema.preferredFormat) { + setFormat(data.schema.preferredFormat); + } + + if (data.schema.preferredRecordCount) { + setRecordCount(data.schema.preferredRecordCount); + } + // Only show the toast if not coming from saved page toast({ title: "Schema Loaded", description: `Loaded "${ data.schema.name || "Unnamed" }" schema - ready to generate mock data`, - variant: "success" - }); - } else { + variant: "success", + }); + } else { throw new Error("Schema data not found in API response"); } } catch (apiError) { @@ -181,28 +197,30 @@ function GeneratorPageContent() { // Load user settings when authenticated useEffect(() => { const fetchUserSettings = async () => { - if (status === 'authenticated' && session?.user?.id) { + if (status === "authenticated" && session?.user?.id) { try { // Import and use the localStorage utility instead of fetching from server - const { getAiSettingsFromLocal } = await import('@/lib/local-storage'); + const { getAiSettingsFromLocal } = await import( + "@/lib/local-storage" + ); const settings = getAiSettingsFromLocal(session.user.id); if (settings) { setUserSettings(settings); } else { // Fallback to API to check if settings exist - const response = await fetch('/api/user/settings'); + const response = await fetch("/api/user/settings"); if (response.ok) { // API now just returns success, it doesn't return actual settings // This is just for backward compatibility - console.log('Settings should be stored in localStorage'); + console.log("Settings should be stored in localStorage"); } } } catch (error) { - console.error('Error loading user settings:', error); + console.error("Error loading user settings:", error); } } }; - + fetchUserSettings(); }, [status, session?.user?.id]); @@ -210,7 +228,7 @@ function GeneratorPageContent() { try { setIsGenerating(true); setResults(""); - + // Determine if we're using samples based on schema type const usingSamples = schemaType === "sample"; @@ -233,29 +251,31 @@ function GeneratorPageContent() { setIsGenerating(false); return; } - + // Check if using own API key before proceeding - const isUsingOwnApiKey = hasOwnApiKey(); - - // If not using own API key, check daily limit - if (!isUsingOwnApiKey) { + const isUsingOwnApiKey = useUserSettings && !!userSettings?.apiKey; + + // Only non-authenticated users need to increment usage here + // For logged-in users, we'll let the API handle it to avoid double-counting + if (!isUsingOwnApiKey && status !== "authenticated") { try { // Call usage increment endpoint to check and increment limit - const limitResponse = await fetch('/api/usage/increment', { - method: 'POST', + const limitResponse = await fetch("/api/usage/increment", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ - usesOwnApiKey: isUsingOwnApiKey + usesOwnApiKey: isUsingOwnApiKey, }), }); - + // If hit limit, show error and stop if (!limitResponse.ok) { toast({ title: "Daily Limit Reached", - description: "You have reached your free daily generation limit. Wait for reset or use your own API key.", + description: + "You have reached your free daily generation limit. Wait for reset or use your own API key.", variant: "destructive", }); setIsGenerating(false); @@ -265,19 +285,42 @@ function GeneratorPageContent() { console.error("Error checking daily limit:", limitError); // Continue with generation if limit check fails (fail open) } + } else if (!isUsingOwnApiKey && status === "authenticated") { + // For authenticated users not using their own API key, just check the limit without incrementing + try { + const response = await fetch("/api/usage/daily"); + if (response.ok) { + const data = await response.json(); + // The daily endpoint returns {used, limit, remaining} directly, not inside limitInfo + if (data && data.remaining <= 0) { + toast({ + title: "Daily Limit Reached", + description: + "You have reached your free daily generation limit. Wait for reset or use your own API key.", + variant: "destructive", + }); + setIsGenerating(false); + return; + } + } + } catch (error) { + console.error("Error checking daily limit:", error); + // Continue with generation if limit check fails (fail open) + } } // TODO: fix this linting error // eslint-disable-next-line @typescript-eslint/no-explicit-any const requestBody: any = { - schemaType: schemaType === "sample" ? "sql" : schemaType as "sql" | "nosql", + schemaType: + schemaType === "sample" ? "sql" : (schemaType as "sql" | "nosql"), count: recordCount, format, outputFormat: format, additionalInstructions: tempInstructions || undefined, schemaName: schemaName === "New Schema" ? undefined : schemaName, }; - + // Only include AI settings if not using user's stored settings if (!useUserSettings) { if (tempApiKey) requestBody.overrideApiKey = tempApiKey; @@ -285,21 +328,30 @@ function GeneratorPageContent() { if (tempBaseUrl) requestBody.overrideBaseUrl = tempBaseUrl; if (tempTemperature) requestBody.overrideTemperature = tempTemperature; if (tempMaxTokens) requestBody.overrideMaxTokens = tempMaxTokens; - if (tempHeaders && Object.keys(tempHeaders).length > 0) requestBody.overrideHeaders = tempHeaders; + if (tempHeaders && Object.keys(tempHeaders).length > 0) + requestBody.overrideHeaders = tempHeaders; } else { // Get settings from localStorage if using user settings if (session?.user?.id) { - const { getAiSettingsFromLocal } = await import('@/lib/local-storage'); + const { getAiSettingsFromLocal } = await import( + "@/lib/local-storage" + ); const localSettings = getAiSettingsFromLocal(session.user.id); - + if (localSettings) { // Pass settings directly in the request since server can't access localStorage - if (localSettings.apiKey) requestBody.overrideApiKey = localSettings.apiKey; - if (localSettings.model) requestBody.overrideModel = localSettings.model; - if (localSettings.baseUrl) requestBody.overrideBaseUrl = localSettings.baseUrl; - if (localSettings.temperature) requestBody.overrideTemperature = localSettings.temperature; - if (localSettings.maxTokens) requestBody.overrideMaxTokens = localSettings.maxTokens; - if (localSettings.headers) requestBody.overrideHeaders = localSettings.headers; + if (localSettings.apiKey) + requestBody.overrideApiKey = localSettings.apiKey; + if (localSettings.model) + requestBody.overrideModel = localSettings.model; + if (localSettings.baseUrl) + requestBody.overrideBaseUrl = localSettings.baseUrl; + if (localSettings.temperature) + requestBody.overrideTemperature = localSettings.temperature; + if (localSettings.maxTokens) + requestBody.overrideMaxTokens = localSettings.maxTokens; + if (localSettings.headers) + requestBody.overrideHeaders = localSettings.headers; } } requestBody.useUserSettings = true; @@ -315,7 +367,7 @@ function GeneratorPageContent() { requestBody.examples = examples; } } - + // Make the API call try { const response = await fetch("/api/generate", { @@ -323,10 +375,10 @@ function GeneratorPageContent() { headers: { "Content-Type": "application/json" }, body: JSON.stringify(requestBody), }); - + if (!response.ok) { let errorMsg = `Error ${response.status}: ${response.statusText}`; - + try { const errorData = await response.json(); if (errorData && errorData.message) { @@ -335,33 +387,34 @@ function GeneratorPageContent() { } catch (parseError) { console.error("Failed to parse error response:", parseError); } - + toast({ title: `Generation Failed (${response.status})`, description: errorMsg, variant: "destructive", - action: status === 'authenticated' ? ( - - ) : undefined, + action: + status === "authenticated" ? ( + + ) : undefined, }); - + throw new Error(errorMsg); } - + const data = await response.json(); setResults(data.result); - - // After successful generation, refresh the usage counter - if (!isUsingOwnApiKey && dailyLimitRef.current) { + + // Always refresh the usage counter after generation + if (dailyLimitRef.current) { await dailyLimitRef.current.refreshUsage(); } - + if (data.warnings && data.warnings.length > 0) { toast({ title: "Generation Completed with Warnings", @@ -372,14 +425,14 @@ function GeneratorPageContent() { toast({ title: "Generation Complete", description: `Generated ${recordCount} records.`, - variant: "success" + variant: "success", }); } - + // Scroll to results after successful generation setTimeout(() => { if (resultsRef.current) { - resultsRef.current.scrollIntoView({ behavior: 'smooth' }); + resultsRef.current.scrollIntoView({ behavior: "smooth" }); } }, 100); } catch (apiError) { @@ -393,15 +446,16 @@ function GeneratorPageContent() { title: "Generation Failed", description: (error as Error).message, variant: "destructive", - action: status === 'authenticated' ? ( - - ) : undefined, + action: + status === "authenticated" ? ( + + ) : undefined, }); } } finally { @@ -409,7 +463,12 @@ function GeneratorPageContent() { } }; - const handleSave = async (name: string, description?: string) => { + const handleSave = async ( + name: string, + description?: string, + preferredFormat?: string, + preferredRecordCount?: number + ) => { if (!session) { toast({ title: "Authentication Required", @@ -418,7 +477,7 @@ function GeneratorPageContent() { }); return; } - + if (schemaType !== "sample" && !schema.trim()) { toast({ title: "Schema Required", @@ -427,7 +486,7 @@ function GeneratorPageContent() { }); return; } - + if (schemaType === "sample" && !examples.trim()) { toast({ title: "Examples Required", @@ -438,16 +497,23 @@ function GeneratorPageContent() { } try { - const response = await fetch("/api/schemas", { - method: "POST", + // Use PATCH for existing schemas, POST for new ones + const method = schemaId ? "PATCH" : "POST"; + const url = schemaId ? `/api/schemas/${schemaId}` : "/api/schemas"; + + const response = await fetch(url, { + method, headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - name, - description, - schema: schemaType !== "sample" ? schema : "/* Generated from examples */", + body: JSON.stringify({ + name, + description, + schema: + schemaType !== "sample" ? schema : "/* Generated from examples */", examples: schemaType === "sample" ? examples : undefined, schemaType: schemaType === "sample" ? "sql" : schemaType, additionalInstructions: tempInstructions || undefined, + preferredFormat: preferredFormat || format, + preferredRecordCount: preferredRecordCount || recordCount, }), }); @@ -456,11 +522,28 @@ function GeneratorPageContent() { throw new Error(errorData.message || "Failed to save schema"); } + const data = await response.json(); + toast({ - title: schemaType === "sample" ? "Examples Saved" : "Schema Saved", - description: `${schemaType === "sample" ? "Examples" : "Schema"} "${name}" saved successfully.`, - variant: "success" + title: schemaId + ? "Schema Updated" + : schemaType === "sample" + ? "Examples Saved" + : "Schema Saved", + description: `${ + schemaId ? "Updated" : "Saved" + } "${name}" successfully.`, + variant: "success", }); + + // Update the current schemaId and name if this was a new schema + if (!schemaId && data.schema && data.schema.id) { + // Update URL to include the new schema ID without full page reload + window.history.pushState({}, "", `/generator?schema=${data.schema.id}`); + // Update state variables + setSchemaName(name); + } + setSaveDialogOpen(false); } catch (error) { toast({ @@ -471,12 +554,6 @@ function GeneratorPageContent() { } }; - // Add a function to check if user has their own API key - const hasOwnApiKey = (): boolean => { - if (!useUserSettings) return !!tempApiKey; - return !!(userSettings?.apiKey); - }; - // show loading indicator if (isLoading && schemaId) { return ( @@ -528,9 +605,19 @@ function GeneratorPageContent() {
-
+
Step 2
+ {schemaId && schemaName && ( +
+
+ Loaded schema: {schemaName} +
+
+ )} @@ -566,7 +654,8 @@ function GeneratorPageContent() { Sample Data Records - Paste in example records for the AI, in any format. Several records produce better results. + Paste in example records for the AI, in any format. Several + records produce better results. @@ -603,11 +692,12 @@ function GeneratorPageContent() { Terms of Use - . You are responsible for all content generated and any API charges - incurred when using this service with your own API key. + . You are responsible for all content generated and any API + charges incurred when using this service with your own API + key.

-
@@ -615,7 +705,7 @@ function GeneratorPageContent() {
- + {session && ( )}
@@ -649,12 +745,19 @@ function GeneratorPageContent() {
-
+
Results
- Generated Results + + Generated Results + - {results ? "Your mock records are ready" : "Results will appear here after generation"} + {results + ? "Your mock records are ready" + : "Results will appear here after generation"} @@ -672,12 +775,19 @@ function GeneratorPageContent() { open={saveDialogOpen} onOpenChange={setSaveDialogOpen} onSave={handleSave} - schemaData={{ - schema: schemaType !== "sample" ? schema : "/* Using examples mode */", - schemaType: schemaType === "sample" ? "sql" : schemaType as "sql" | "nosql" + schemaData={{ + schema: + schemaType !== "sample" ? schema : "/* Using examples mode */", + schemaType: + schemaType === "sample" ? "sql" : (schemaType as "sql" | "nosql"), }} userId={session?.user?.id || ""} handleApiCall={false} + initialName={schemaName || ""} + editMode={!!schemaId} + schemaId={schemaId || ""} + preferredFormat={format} + preferredRecordCount={recordCount} />
); diff --git a/app/schema/[id]/edit/page.tsx b/app/schema/[id]/edit/page.tsx index e626742..8adc9b3 100644 --- a/app/schema/[id]/edit/page.tsx +++ b/app/schema/[id]/edit/page.tsx @@ -18,6 +18,8 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Loader2 } from "lucide-react"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Slider } from "@/components/ui/slider"; interface SchemaEditPageProps { params: Promise<{ id: string }>; @@ -34,6 +36,8 @@ export default function SchemaEditPage({ params }: SchemaEditPageProps) { const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [additionalInstructions, setAdditionalInstructions] = useState(""); + const [preferredFormat, setPreferredFormat] = useState("json"); + const [preferredRecordCount, setPreferredRecordCount] = useState(10); const fetchSchema = useCallback(async () => { if (status === "authenticated" && schemaId) { @@ -60,6 +64,8 @@ export default function SchemaEditPage({ params }: SchemaEditPageProps) { setName(data.schema.name); setDescription(data.schema.description || ""); setAdditionalInstructions(data.schema.additionalInstructions || ""); + setPreferredFormat(data.schema.preferredFormat || "json"); + setPreferredRecordCount(data.schema.preferredRecordCount || 10); } else { throw new Error("Schema data not found in response."); } @@ -123,6 +129,8 @@ export default function SchemaEditPage({ params }: SchemaEditPageProps) { schema, schemaType, additionalInstructions: additionalInstructions || undefined, + preferredFormat, + preferredRecordCount, }), }); @@ -223,6 +231,51 @@ export default function SchemaEditPage({ params }: SchemaEditPageProps) {
+ + + Default Generation Settings + + Configure how records are generated by default + + + +
+ + +
+ +
+
+ +
+ setPreferredRecordCount(value[0])} + className="w-full" + /> +
+
+
+ Schema Definition @@ -250,12 +303,6 @@ export default function SchemaEditPage({ params }: SchemaEditPageProps) { NoSQL
-
diff --git a/app/schema/new/page.tsx b/app/schema/new/page.tsx index 78a1339..ff0f38b 100644 --- a/app/schema/new/page.tsx +++ b/app/schema/new/page.tsx @@ -18,6 +18,8 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Loader2 } from "lucide-react"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Slider } from "@/components/ui/slider"; export default function NewSchemaPage() { const router = useRouter(); @@ -28,6 +30,8 @@ export default function NewSchemaPage() { const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [additionalInstructions, setAdditionalInstructions] = useState(""); + const [preferredFormat, setPreferredFormat] = useState("json"); + const [preferredRecordCount, setPreferredRecordCount] = useState(10); useEffect(() => { if (status === "unauthenticated") { @@ -62,6 +66,8 @@ export default function NewSchemaPage() { schema, schemaType, additionalInstructions: additionalInstructions || undefined, + preferredFormat, + preferredRecordCount, }) }); @@ -151,6 +157,51 @@ export default function NewSchemaPage() { + + + Default Generation Settings + + Configure how records are generated by default + + + +
+ + +
+ +
+
+ +
+ setPreferredRecordCount(value[0])} + className="w-full" + /> +
+
+
+ Schema Definition @@ -178,27 +229,6 @@ export default function NewSchemaPage() { NoSQL - diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 803aa4b..7409722 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -363,7 +363,8 @@ export default function SettingsPage() { - Your API keys are stored securely in your browser{"'"}s localStorage, not on our servers. This means your keys never leave your device, but will be lost if you clear your browser data. + Your API keys are stored securely in your browsers local storage, not on our servers. + Your settings will be lost if you clear your browser data.