-- VibeCoder AI Roblox Studio Plugin -- Drop this file into a plugin project, or paste into Studio plugin source. local HttpService = game:GetService("HttpService") local Selection = game:GetService("Selection") local ChangeHistoryService = game:GetService("ChangeHistoryService") local CollectionService = game:GetService("CollectionService") local ReplicatedStorage = game:GetService("ReplicatedStorage") local ServerScriptService = game:GetService("ServerScriptService") local StudioService = game:GetService("StudioService") local TweenService = game:GetService("TweenService") local LogService = game:GetService("LogService") local RunService = game:GetService("RunService") local TextService = game:GetService("TextService") local toolbar = plugin:CreateToolbar("VibeCoder AI") local button = toolbar:CreateButton("VibeCoder AI", "Generate Roblox code with AI", "rbxassetid://4458901886") -- Prefer IPv4 loopback; "localhost" may resolve to IPv6 (::1) on Windows. local API_BASE = "https://rolbox.duckdns.org" -- Same origin as the web app (landing + Stripe checkout). Change for production, e.g. https://your-domain.com -- Base URL shown in the out-of-credits banner (append #pricing in refreshUpgradeUrlHint). local WEB_APP_URL = "https://roblox-vibe-coding-web.vercel.app" -- Optional default; UI "API token" box overrides this when filled. local AUTH_TOKEN = "" -- Slightly faster polls = smoother “live” feel (still light on the API; /jobs is not rate-limited). local POLL_INTERVAL = 0.22 -- Auto-fix settings local AUTO_FIX_ENABLED = true local AUTO_FIX_MAX_ATTEMPTS = 3 local AUTO_FIX_WINDOW_SEC = 120 local AUTO_FIX_DEBOUNCE_SEC = 1.2 local TOKEN_SETTING_KEY = "VibeCoderAuthToken" local PROJECT_SETTING_KEY = "VibeCoderLastProjectJson" local TAG_NAME = "VibeCoderAI" local MODE_SETTING_KEY = "VibeCoderGenerationMode" -- Dock to the right by default (not floating in the center). Change to .Left if you prefer the left dock. local widgetInfo = DockWidgetPluginGuiInfo.new( Enum.InitialDockState.Right, true, false, 420, 520, 320, 380 ) local widget = plugin:CreateDockWidgetPluginGui("VibeCoderAIWidget", widgetInfo) widget.Title = "VibeCoder AI TEST NEW" local function uiRound(uicornerParent, radius) local c = Instance.new("UICorner") c.CornerRadius = UDim.new(0, radius or 12) c.Parent = uicornerParent return c end local function uiStroke(parent, transparency) local s = Instance.new("UIStroke") s.Thickness = 1 s.Transparency = transparency or 0.6 s.Color = Color3.fromRGB(255, 255, 255) s.Parent = parent return s end -- Full-widget backdrop: UIGradient must NOT live on the ScrollingFrame (Studio can fail to composite -- children correctly — middle of the panel stays blank while the top token row still shows). local backdrop = Instance.new("Frame") backdrop.BorderSizePixel = 0 backdrop.BackgroundColor3 = Color3.fromRGB(18, 21, 35) backdrop.Size = UDim2.fromScale(1, 1) backdrop.ZIndex = 0 backdrop.Parent = widget local gradient = Instance.new("UIGradient") gradient.Color = ColorSequence.new({ ColorSequenceKeypoint.new(0, Color3.fromRGB(18, 21, 35)), ColorSequenceKeypoint.new(1, Color3.fromRGB(8, 10, 18)), }) gradient.Rotation = 35 gradient.Parent = backdrop -- Make the whole plugin UI scrollable (small Studio windows). local rootScroll = Instance.new("ScrollingFrame") rootScroll.Name = "RootScroll" rootScroll.BackgroundTransparency = 1 rootScroll.BorderSizePixel = 0 rootScroll.Size = UDim2.fromScale(1, 1) rootScroll.ZIndex = 1 rootScroll.CanvasSize = UDim2.new(0, 0, 0, 0) rootScroll.AutomaticCanvasSize = Enum.AutomaticSize.None -- Show a global scrollbar so controls don't appear "missing" when pushed below the fold. rootScroll.ScrollBarThickness = 8 rootScroll.ScrollBarImageTransparency = 0.35 rootScroll.ScrollBarImageColor3 = Color3.fromRGB(170, 175, 200) rootScroll.ScrollingDirection = Enum.ScrollingDirection.Y rootScroll.ScrollingEnabled = true rootScroll.Parent = widget local root = Instance.new("Frame") root.BackgroundTransparency = 1 root.BorderSizePixel = 0 root.Size = UDim2.new(1, 0, 0, 0) root.AutomaticSize = Enum.AutomaticSize.Y root.ZIndex = 1 root.Parent = rootScroll -- Padding on `root` so it is part of the measured column (AutomaticCanvasSize accounts for it). local padding = Instance.new("UIPadding") padding.PaddingTop = UDim.new(0, 14) padding.PaddingBottom = UDim.new(0, 14) padding.PaddingLeft = UDim.new(0, 14) padding.PaddingRight = UDim.new(0, 14) padding.Parent = root local layout = Instance.new("UIListLayout") layout.FillDirection = Enum.FillDirection.Vertical layout.HorizontalAlignment = Enum.HorizontalAlignment.Center layout.SortOrder = Enum.SortOrder.LayoutOrder layout.Padding = UDim.new(0, 10) layout.Parent = root -- Some Studio/plugin environments fail to update AutomaticCanvasSize reliably. -- Use AbsoluteContentSize for a deterministic CanvasSize update (no task.defer to avoid re-entrancy). local function refreshCanvas() local h = math.ceil(layout.AbsoluteContentSize.Y + padding.PaddingTop.Offset + padding.PaddingBottom.Offset + 2) rootScroll.CanvasSize = UDim2.new(0, 0, 0, h) end layout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(refreshCanvas) refreshCanvas() local title = Instance.new("TextLabel") title.BackgroundTransparency = 1 title.Size = UDim2.new(1, 0, 0, 28) title.Font = Enum.Font.GothamSemibold title.TextSize = 18 title.TextColor3 = Color3.fromRGB(240, 240, 255) title.Text = "Vibe Coder AI" title.Parent = root -- Minimal UI: no badges or helper text. -- Horizontal scroll container for the token box (tokens are long; make left↔right scrolling easy). local tokenScroll = Instance.new("ScrollingFrame") tokenScroll.BackgroundColor3 = Color3.fromRGB(14, 16, 28) tokenScroll.BorderSizePixel = 0 tokenScroll.Size = UDim2.new(1, 0, 0, 44) tokenScroll.CanvasSize = UDim2.new(0, 0, 0, 0) tokenScroll.AutomaticCanvasSize = Enum.AutomaticSize.None tokenScroll.ScrollBarThickness = 6 tokenScroll.ScrollingDirection = Enum.ScrollingDirection.X tokenScroll.ScrollingEnabled = true tokenScroll.Parent = root uiRound(tokenScroll, 10) uiStroke(tokenScroll, 0.78) local tokenBox = Instance.new("TextBox") tokenBox.Size = UDim2.new(1, 0, 1, 0) tokenBox.ClearTextOnFocus = false tokenBox.MultiLine = true tokenBox.TextWrapped = false tokenBox.ClipsDescendants = true tokenBox.TextXAlignment = Enum.TextXAlignment.Left tokenBox.TextYAlignment = Enum.TextYAlignment.Top tokenBox.Font = Enum.Font.Code tokenBox.TextSize = 11 tokenBox.TextColor3 = Color3.fromRGB(220, 225, 245) tokenBox.PlaceholderText = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." tokenBox.BackgroundTransparency = 1 tokenBox.BorderSizePixel = 0 tokenBox.Parent = tokenScroll local tokenPad = Instance.new("UIPadding") tokenPad.PaddingLeft = UDim.new(0, 10) tokenPad.PaddingRight = UDim.new(0, 10) tokenPad.Parent = tokenBox local function refreshTokenScroll() -- Ensure the inner TextBox is wide enough for horizontal scrolling. local s = tokenBox.Text or tokenBox.PlaceholderText or "" local sz = TextService:GetTextSize(s, tokenBox.TextSize, tokenBox.Font, Vector2.new(10000, 44)) local innerW = math.max(tokenScroll.AbsoluteSize.X, math.ceil(sz.X) + tokenPad.PaddingLeft.Offset + tokenPad.PaddingRight.Offset + 20) tokenBox.Size = UDim2.new(0, innerW, 1, 0) tokenScroll.CanvasSize = UDim2.new(0, innerW, 0, 0) end tokenScroll:GetPropertyChangedSignal("AbsoluteSize"):Connect(refreshTokenScroll) tokenBox:GetPropertyChangedSignal("Text"):Connect(refreshTokenScroll) refreshTokenScroll() local okLoad, savedToken = pcall(function() return plugin:GetSetting(TOKEN_SETTING_KEY) end) if okLoad and typeof(savedToken) == "string" then tokenBox.Text = savedToken end tokenBox.FocusLost:Connect(function() pcall(function() plugin:SetSetting(TOKEN_SETTING_KEY, tokenBox.Text) end) end) -- Shown when the API returns 402 (not enough credits). Copy the URL below into your browser (Studio cannot open it reliably from a plugin). local upgradeFrame = Instance.new("Frame") upgradeFrame.BackgroundColor3 = Color3.fromRGB(28, 22, 42) upgradeFrame.BorderSizePixel = 0 upgradeFrame.Size = UDim2.new(1, 0, 0, 0) -- When hidden, keep height at 0 so the rest of the UI isn't pushed off-screen. upgradeFrame.AutomaticSize = Enum.AutomaticSize.None upgradeFrame.Visible = false uiRound(upgradeFrame, 12) uiStroke(upgradeFrame, 0.75) upgradeFrame.Parent = root local upgradePad = Instance.new("UIPadding") upgradePad.PaddingTop = UDim.new(0, 10) upgradePad.PaddingBottom = UDim.new(0, 10) upgradePad.PaddingLeft = UDim.new(0, 12) upgradePad.PaddingRight = UDim.new(0, 12) upgradePad.Parent = upgradeFrame local upgradeLbl = Instance.new("TextLabel") upgradeLbl.BackgroundTransparency = 1 upgradeLbl.Size = UDim2.new(1, 0, 0, 0) upgradeLbl.AutomaticSize = Enum.AutomaticSize.Y upgradeLbl.Font = Enum.Font.GothamMedium upgradeLbl.TextSize = 12 upgradeLbl.TextColor3 = Color3.fromRGB(250, 220, 235) upgradeLbl.TextWrapped = true upgradeLbl.TextXAlignment = Enum.TextXAlignment.Left upgradeLbl.Text = "You're out of credits. Upgrade on the website (Pro / Ultra), complete checkout, then keep the same API token here. Subscribers: use the site’s billing portal for invoices, card on file, cancel, and auto-renew settings.\n\nRoblox Studio cannot open this link from the plugin—select the address below, copy (Ctrl+C), and paste it into Chrome, Edge, or another browser." upgradeLbl.Parent = upgradeFrame -- Read-only TextBox so the URL can be selected + Ctrl+C. local upgradeUrlHint = Instance.new("TextBox") upgradeUrlHint.BackgroundTransparency = 1 upgradeUrlHint.Size = UDim2.new(1, 0, 0, 0) upgradeUrlHint.AutomaticSize = Enum.AutomaticSize.Y upgradeUrlHint.Font = Enum.Font.Code upgradeUrlHint.TextSize = 10 upgradeUrlHint.TextColor3 = Color3.fromRGB(160, 165, 195) upgradeUrlHint.TextWrapped = true upgradeUrlHint.TextXAlignment = Enum.TextXAlignment.Left upgradeUrlHint.TextEditable = false upgradeUrlHint.ClearTextOnFocus = false upgradeUrlHint.Text = "" upgradeUrlHint.Parent = upgradeFrame local upgradeContentLayout = Instance.new("UIListLayout") upgradeContentLayout.FillDirection = Enum.FillDirection.Vertical upgradeContentLayout.SortOrder = Enum.SortOrder.LayoutOrder upgradeContentLayout.Padding = UDim.new(0, 8) upgradeContentLayout.Parent = upgradeFrame upgradeLbl.LayoutOrder = 1 upgradeUrlHint.LayoutOrder = 2 local function refreshUpgradeUrlHint() local base = (WEB_APP_URL:gsub("%s+", "")):gsub("/+$", "") local withHash = base .. "#pricing" upgradeUrlHint.Text = withHash end refreshUpgradeUrlHint() local function showUpgradeBanner(visible) upgradeFrame.Visible = visible if visible then upgradeFrame.AutomaticSize = Enum.AutomaticSize.Y else upgradeFrame.AutomaticSize = Enum.AutomaticSize.None upgradeFrame.Size = UDim2.new(1, 0, 0, 0) end end local function isPaymentRequired(errStr) local s = tostring(errStr or "") return string.sub(s, 1, 3) == "402" end local function getToken() local t = (tokenBox.Text or ""):gsub("^%s+", ""):gsub("%s+$", "") if t ~= "" then return t end if AUTH_TOKEN ~= "" then return AUTH_TOKEN end return "" end -- Job source for /feedback (must match API z.enum). Declared before HTTP helpers. local lastJobSource = "plugin_generate" local function httpJson(url, method, body) local headers = { ["Content-Type"] = "application/json", } local tok = getToken() if tok ~= "" then headers["Authorization"] = "Bearer " .. tok end local ok, res = pcall(function() return HttpService:RequestAsync({ Url = url, Method = method, Headers = headers, Body = body and HttpService:JSONEncode(body) or nil, }) end) if not ok then return false, tostring(res) end if not res.Success then local detail = "" if typeof(res.Body) == "string" and res.Body ~= "" then detail = " — " .. res.Body end return false, tostring(res.StatusCode) .. " " .. tostring(res.StatusMessage or "Request failed") .. detail end return true, res.Body end local function sendFeedback(rating, usdPerMonth, note) local payload = { source = lastJobSource, rating = rating, } if typeof(usdPerMonth) == "number" then payload.willingMonthlyUsd = usdPerMonth end if typeof(note) == "string" and note ~= "" then payload.note = note end return httpJson(API_BASE .. "/feedback", "POST", payload) end -- Prompt gets its own vertical scrollbar (not the global/root scrollbar). local promptScroll = Instance.new("ScrollingFrame") promptScroll.BackgroundColor3 = Color3.fromRGB(14, 16, 28) promptScroll.BorderSizePixel = 0 promptScroll.Size = UDim2.new(1, 0, 0, 120) promptScroll.CanvasSize = UDim2.new(0, 0, 0, 0) promptScroll.AutomaticCanvasSize = Enum.AutomaticSize.None promptScroll.ScrollBarThickness = 6 promptScroll.ScrollingDirection = Enum.ScrollingDirection.Y promptScroll.ScrollingEnabled = true promptScroll.Parent = root uiRound(promptScroll, 14) uiStroke(promptScroll, 0.78) local promptBox = Instance.new("TextBox") promptBox.Size = UDim2.new(1, 0, 1, 0) promptBox.ClearTextOnFocus = false promptBox.MultiLine = true promptBox.Text = "" promptBox.TextWrapped = true promptBox.ClipsDescendants = true promptBox.TextXAlignment = Enum.TextXAlignment.Left promptBox.TextYAlignment = Enum.TextYAlignment.Top promptBox.Font = Enum.Font.Gotham promptBox.TextSize = 14 promptBox.TextColor3 = Color3.fromRGB(235, 235, 245) promptBox.PlaceholderText = "First run: describe your game. After Apply: type only the change (e.g. add score UI) — same Generate button improves the last build." promptBox.BackgroundTransparency = 1 promptBox.BorderSizePixel = 0 promptBox.Parent = promptScroll local promptPad = Instance.new("UIPadding") promptPad.PaddingTop = UDim.new(0, 10) promptPad.PaddingBottom = UDim.new(0, 10) promptPad.PaddingLeft = UDim.new(0, 12) promptPad.PaddingRight = UDim.new(0, 12) promptPad.Parent = promptBox local function refreshPromptScroll() local w = math.max(40, promptScroll.AbsoluteSize.X - promptPad.PaddingLeft.Offset - promptPad.PaddingRight.Offset - 6) local text = promptBox.Text if text == "" then text = promptBox.PlaceholderText or "" end local sz = TextService:GetTextSize(text, promptBox.TextSize, promptBox.Font, Vector2.new(w, 100000)) local innerH = math.max(promptScroll.AbsoluteSize.Y, math.ceil(sz.Y) + promptPad.PaddingTop.Offset + promptPad.PaddingBottom.Offset + 12) promptBox.Size = UDim2.new(1, 0, 0, innerH) promptScroll.CanvasSize = UDim2.new(0, 0, 0, innerH) end promptScroll:GetPropertyChangedSignal("AbsoluteSize"):Connect(refreshPromptScroll) promptBox:GetPropertyChangedSignal("Text"):Connect(refreshPromptScroll) refreshPromptScroll() local promptHint = Instance.new("TextLabel") promptHint.BackgroundTransparency = 1 promptHint.Size = UDim2.new(1, 0, 0, 0) promptHint.AutomaticSize = Enum.AutomaticSize.Y promptHint.Font = Enum.Font.Gotham promptHint.TextSize = 11 promptHint.TextColor3 = Color3.fromRGB(155, 162, 195) promptHint.TextWrapped = true promptHint.TextXAlignment = Enum.TextXAlignment.Left promptHint.Text = "" promptHint.Visible = false promptHint.Parent = root local function getMode() local ok, v = pcall(function() return plugin:GetSetting(MODE_SETTING_KEY) end) if ok and typeof(v) == "string" and (v == "fast" or v == "full") then return v end return "fast" end local function setMode(mode) if mode ~= "fast" and mode ~= "full" then return end pcall(function() plugin:SetSetting(MODE_SETTING_KEY, mode) end) end local currentMode = getMode() local modeRow = Instance.new("Frame") modeRow.BackgroundTransparency = 1 modeRow.Size = UDim2.new(1, 0, 0, 34) modeRow.Parent = root local modeLayout = Instance.new("UIListLayout") modeLayout.FillDirection = Enum.FillDirection.Horizontal modeLayout.HorizontalAlignment = Enum.HorizontalAlignment.Left modeLayout.SortOrder = Enum.SortOrder.LayoutOrder modeLayout.Padding = UDim.new(0, 8) modeLayout.Parent = modeRow local modeBtn = Instance.new("TextButton") modeBtn.Size = UDim2.new(0, 150, 0, 28) modeBtn.AutoButtonColor = false modeBtn.Font = Enum.Font.GothamSemibold modeBtn.TextSize = 12 modeBtn.TextColor3 = Color3.fromRGB(255, 255, 255) modeBtn.BackgroundColor3 = Color3.fromRGB(60, 64, 90) modeBtn.BorderSizePixel = 0 uiRound(modeBtn, 12) uiStroke(modeBtn, 0.8) modeBtn.Parent = modeRow local modeHelp = Instance.new("TextLabel") modeHelp.BackgroundTransparency = 1 modeHelp.Size = UDim2.new(1, -160, 0, 28) modeHelp.Font = Enum.Font.Gotham modeHelp.TextSize = 11 modeHelp.TextColor3 = Color3.fromRGB(170, 175, 200) modeHelp.TextXAlignment = Enum.TextXAlignment.Left modeHelp.Text = "Fast: 1-script playable. Full: richer, slower." modeHelp.Parent = modeRow local function refreshModeUi() if currentMode == "full" then modeBtn.Text = "Mode: FULL (slower)" else modeBtn.Text = "Mode: FAST" end end refreshModeUi() modeBtn.MouseButton1Click:Connect(function() if currentMode == "fast" then currentMode = "full" else currentMode = "fast" end setMode(currentMode) refreshModeUi() end) local buttonRow = Instance.new("Frame") buttonRow.BackgroundTransparency = 1 buttonRow.Size = UDim2.new(1, 0, 0, 42) buttonRow.Parent = root local rowLayout = Instance.new("UIListLayout") rowLayout.FillDirection = Enum.FillDirection.Horizontal rowLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center rowLayout.SortOrder = Enum.SortOrder.LayoutOrder rowLayout.Padding = UDim.new(0, 8) rowLayout.Parent = buttonRow local function makeBtn(text, bg) local b = Instance.new("TextButton") b.Size = UDim2.new(0, 92, 0, 36) b.AutoButtonColor = false b.Text = text b.Font = Enum.Font.GothamSemibold b.TextSize = 13 b.TextColor3 = Color3.fromRGB(255, 255, 255) b.BackgroundColor3 = bg b.BorderSizePixel = 0 uiRound(b, 14) uiStroke(b, 0.75) return b end local genBtn = makeBtn("Generate ⚡", Color3.fromRGB(124, 58, 237)) genBtn.Parent = buttonRow local clearBtn = makeBtn("Reset 🧹", Color3.fromRGB(239, 68, 68)) clearBtn.Parent = buttonRow local enhBtn = makeBtn("Enhance 🧠", Color3.fromRGB(34, 211, 238)) enhBtn.Parent = buttonRow local stopBtn = makeBtn("Stop ⛔", Color3.fromRGB(60, 64, 90)) stopBtn.Parent = buttonRow -- Apply is intentionally separate from Generate (stateless generation). local applyBtn = makeBtn("Apply 🚀", Color3.fromRGB(99, 102, 241)) applyBtn.Parent = buttonRow applyBtn.LayoutOrder = 2 clearBtn.LayoutOrder = 5 enhBtn.LayoutOrder = 4 stopBtn.LayoutOrder = 6 genBtn.LayoutOrder = 1 -- Replace Previous toggle local replaceRow = Instance.new("Frame") replaceRow.BackgroundTransparency = 1 replaceRow.Size = UDim2.new(1, 0, 0, 26) replaceRow.Parent = root local replaceLayout = Instance.new("UIListLayout") replaceLayout.FillDirection = Enum.FillDirection.Horizontal replaceLayout.HorizontalAlignment = Enum.HorizontalAlignment.Left replaceLayout.SortOrder = Enum.SortOrder.LayoutOrder replaceLayout.Padding = UDim.new(0, 8) replaceLayout.Parent = replaceRow local replaceToggle = Instance.new("TextButton") replaceToggle.Size = UDim2.new(0, 220, 0, 24) replaceToggle.AutoButtonColor = false replaceToggle.Font = Enum.Font.GothamMedium replaceToggle.TextSize = 12 replaceToggle.TextColor3 = Color3.fromRGB(235, 235, 245) replaceToggle.BackgroundColor3 = Color3.fromRGB(14, 16, 28) replaceToggle.BorderSizePixel = 0 uiRound(replaceToggle, 12) uiStroke(replaceToggle, 0.82) replaceToggle.Parent = replaceRow local replacePrevious = true local function refreshReplaceToggle() replaceToggle.Text = (replacePrevious and "✔ " or "☐ ") .. "Replace Previous Generation" end refreshReplaceToggle() replaceToggle.MouseButton1Click:Connect(function() replacePrevious = not replacePrevious refreshReplaceToggle() end) -- Feedback panel (shown after a successful insert) -- Feedback UI removed. -- Fixed-height output (scale Y was filling the dock and left a giant empty panel). Read-only TextBox scrolls + selects for Ctrl+C. local OUT_TEXT_PX = 168 local outFrame = Instance.new("Frame") outFrame.BackgroundColor3 = Color3.fromRGB(12, 14, 24) outFrame.BorderSizePixel = 0 outFrame.Size = UDim2.new(1, 0, 0, 0) outFrame.AutomaticSize = Enum.AutomaticSize.Y outFrame.ClipsDescendants = true uiRound(outFrame, 14) uiStroke(outFrame, 0.82) outFrame.Parent = root local outFramePad = Instance.new("UIPadding") outFramePad.PaddingTop = UDim.new(0, 8) outFramePad.PaddingBottom = UDim.new(0, 8) outFramePad.PaddingLeft = UDim.new(0, 10) outFramePad.PaddingRight = UDim.new(0, 10) outFramePad.Parent = outFrame local outVLayout = Instance.new("UIListLayout") outVLayout.FillDirection = Enum.FillDirection.Vertical outVLayout.SortOrder = Enum.SortOrder.LayoutOrder outVLayout.Padding = UDim.new(0, 6) outVLayout.Parent = outFrame local outToolbar = Instance.new("Frame") outToolbar.BackgroundTransparency = 1 outToolbar.Size = UDim2.new(1, 0, 0, 24) outToolbar.Parent = outFrame local outTitle = Instance.new("TextLabel") outTitle.BackgroundTransparency = 1 outTitle.Size = UDim2.new(0.55, 0, 1, 0) outTitle.Position = UDim2.new(0, 0, 0, 0) outTitle.Font = Enum.Font.GothamSemibold outTitle.TextSize = 12 outTitle.TextColor3 = Color3.fromRGB(200, 205, 235) outTitle.TextXAlignment = Enum.TextXAlignment.Left outTitle.Text = "Output" outTitle.Parent = outToolbar -- Tabs: Preview (default) + Code local tabBar = Instance.new("Frame") tabBar.BackgroundTransparency = 1 tabBar.Size = UDim2.new(0, 190, 0, 22) tabBar.Position = UDim2.new(0, 54, 0, 1) tabBar.Parent = outToolbar local tabLayout = Instance.new("UIListLayout") tabLayout.FillDirection = Enum.FillDirection.Horizontal tabLayout.HorizontalAlignment = Enum.HorizontalAlignment.Left tabLayout.VerticalAlignment = Enum.VerticalAlignment.Center tabLayout.SortOrder = Enum.SortOrder.LayoutOrder tabLayout.Padding = UDim.new(0, 6) tabLayout.Parent = tabBar local function makeTab(text) local b = Instance.new("TextButton") b.Size = UDim2.new(0, 88, 0, 22) b.AutoButtonColor = false b.Font = Enum.Font.GothamMedium b.TextSize = 11 b.TextColor3 = Color3.fromRGB(235, 235, 250) b.Text = text b.BackgroundColor3 = Color3.fromRGB(45, 48, 72) b.BorderSizePixel = 0 uiRound(b, 8) return b end local previewTabBtn = makeTab("Preview 👁️") previewTabBtn.Parent = tabBar local codeTabBtn = makeTab("Code 💻") codeTabBtn.Parent = tabBar local btnStack = Instance.new("Frame") btnStack.BackgroundTransparency = 1 -- Copy buttons removed; keep an empty placeholder with zero width. btnStack.Size = UDim2.new(0, 0, 0, 22) btnStack.Position = UDim2.new(1, -4, 0, 0) btnStack.AnchorPoint = Vector2.new(1, 0) btnStack.Parent = outToolbar local stackLayout = Instance.new("UIListLayout") stackLayout.FillDirection = Enum.FillDirection.Horizontal stackLayout.HorizontalAlignment = Enum.HorizontalAlignment.Right stackLayout.VerticalAlignment = Enum.VerticalAlignment.Center stackLayout.Padding = UDim.new(0, 6) stackLayout.Parent = btnStack local copyOutputBtn = Instance.new("TextButton") copyOutputBtn.Size = UDim2.new(0, 92, 0, 22) copyOutputBtn.AutoButtonColor = false copyOutputBtn.Font = Enum.Font.GothamMedium copyOutputBtn.TextSize = 11 copyOutputBtn.TextColor3 = Color3.fromRGB(235, 235, 250) copyOutputBtn.Text = "Copy output" copyOutputBtn.BackgroundColor3 = Color3.fromRGB(45, 48, 72) copyOutputBtn.BorderSizePixel = 0 uiRound(copyOutputBtn, 8) copyOutputBtn.Parent = btnStack copyOutputBtn.Visible = false local copyPromptBtn = Instance.new("TextButton") copyPromptBtn.Size = UDim2.new(0, 94, 0, 22) copyPromptBtn.AutoButtonColor = false copyPromptBtn.Font = Enum.Font.GothamMedium copyPromptBtn.TextSize = 11 copyPromptBtn.TextColor3 = Color3.fromRGB(235, 235, 250) copyPromptBtn.Text = "Copy prompt" copyPromptBtn.BackgroundColor3 = Color3.fromRGB(45, 48, 72) copyPromptBtn.BorderSizePixel = 0 uiRound(copyPromptBtn, 8) copyPromptBtn.Parent = btnStack copyPromptBtn.Visible = false -- Output gets its own vertical scrollbar. local outContent = Instance.new("ScrollingFrame") outContent.BackgroundColor3 = Color3.fromRGB(8, 10, 18) outContent.BorderSizePixel = 0 outContent.Size = UDim2.new(1, 0, 0, OUT_TEXT_PX) outContent.CanvasSize = UDim2.new(0, 0, 0, 0) outContent.AutomaticCanvasSize = Enum.AutomaticSize.None outContent.ScrollBarThickness = 6 outContent.ScrollingDirection = Enum.ScrollingDirection.Y outContent.ScrollingEnabled = true outContent.Parent = outFrame uiRound(outContent, 10) uiStroke(outContent, 0.75) local previewBox = Instance.new("TextLabel") previewBox.Size = UDim2.new(1, 0, 1, 0) previewBox.BackgroundTransparency = 1 previewBox.Text = "" previewBox.TextWrapped = true previewBox.TextYAlignment = Enum.TextYAlignment.Top previewBox.TextXAlignment = Enum.TextXAlignment.Left previewBox.Font = Enum.Font.Gotham previewBox.TextSize = 13 previewBox.TextColor3 = Color3.fromRGB(220, 225, 245) previewBox.BorderSizePixel = 0 previewBox.Parent = outContent local outBox = Instance.new("TextBox") outBox.Size = UDim2.new(1, 0, 0, OUT_TEXT_PX) outBox.BackgroundTransparency = 1 outBox.Text = "" outBox.PlaceholderText = "Code + log…" outBox.ClearTextOnFocus = false outBox.MultiLine = true outBox.TextWrapped = true outBox.TextEditable = false outBox.Font = Enum.Font.Code outBox.TextSize = 13 outBox.TextColor3 = Color3.fromRGB(220, 225, 245) outBox.TextXAlignment = Enum.TextXAlignment.Left outBox.TextYAlignment = Enum.TextYAlignment.Top outBox.ClipsDescendants = true outBox.BorderSizePixel = 0 outBox.Visible = false outBox.Parent = outContent local outPad = Instance.new("UIPadding") outPad.PaddingTop = UDim.new(0, 8) outPad.PaddingBottom = UDim.new(0, 8) outPad.PaddingLeft = UDim.new(0, 10) outPad.PaddingRight = UDim.new(0, 10) outPad.Parent = outContent local function refreshOutScroll() local w = math.max(40, outContent.AbsoluteSize.X - outPad.PaddingLeft.Offset - outPad.PaddingRight.Offset - 6) local text = outBox.Text if text == "" then text = outBox.PlaceholderText or "" end local sz = TextService:GetTextSize(text, outBox.TextSize, outBox.Font, Vector2.new(w, 100000)) local innerH = math.max(outContent.AbsoluteSize.Y, math.ceil(sz.Y) + outPad.PaddingTop.Offset + outPad.PaddingBottom.Offset + 12) outBox.Size = UDim2.new(1, 0, 0, innerH) previewBox.Size = UDim2.new(1, 0, 0, innerH) outContent.CanvasSize = UDim2.new(0, 0, 0, innerH) end outContent:GetPropertyChangedSignal("AbsoluteSize"):Connect(refreshOutScroll) outBox:GetPropertyChangedSignal("Text"):Connect(refreshOutScroll) refreshOutScroll() local outCopyHint = Instance.new("TextLabel") outCopyHint.BackgroundTransparency = 1 outCopyHint.Size = UDim2.new(1, 0, 0, 0) outCopyHint.AutomaticSize = Enum.AutomaticSize.Y outCopyHint.Font = Enum.Font.Gotham outCopyHint.TextSize = 10 outCopyHint.TextColor3 = Color3.fromRGB(130, 135, 165) outCopyHint.TextXAlignment = Enum.TextXAlignment.Left outCopyHint.TextWrapped = true local DEFAULT_COPY_HINT = "Tip: select text in a field, then press Ctrl+C." outCopyHint.Text = DEFAULT_COPY_HINT outCopyHint.Parent = outFrame local function copyToClipboard(text) text = tostring(text or "") if text == "" then return false end -- Studio supports setclipboard() in plugin/editor contexts. if typeof(setclipboard) == "function" then local ok = pcall(function() setclipboard(text) end) return ok end return false end local function flashCopyHint(message) outCopyHint.Text = message task.delay(2.0, function() -- Avoid stomping other updates if this label is repurposed later. if outCopyHint.Text == message then outCopyHint.Text = DEFAULT_COPY_HINT end end) end -- Copy buttons removed (select text + Ctrl+C instead). local function setOutput(t) -- Keep the output scrolled to bottom during streaming unless the user is actively selecting text. local oldText = outBox.Text or "" local oldCursor = outBox.CursorPosition local oldSel = outBox.SelectionStart local wasAtEnd = false if typeof(oldCursor) == "number" and oldCursor > 0 then wasAtEnd = oldCursor >= (#oldText - 2) end outBox.Text = t or "" -- If the user isn't actively selecting, auto-follow the tail (log-like). local selecting = typeof(oldSel) == "number" and oldSel ~= -1 and oldSel ~= oldCursor if not selecting and wasAtEnd then local n = #(outBox.Text or "") outBox.CursorPosition = n + 1 outBox.SelectionStart = n + 1 -- Also scroll the container to the bottom (visible scrollbar). if outContent and outContent:IsA("ScrollingFrame") then outContent.CanvasPosition = Vector2.new(0, math.max(0, outContent.CanvasSize.Y.Offset - outContent.AbsoluteSize.Y)) end else -- Best-effort: keep the prior cursor where it was. if typeof(oldCursor) == "number" and oldCursor > 0 then outBox.CursorPosition = math.min(oldCursor, #(outBox.Text or "") + 1) end if typeof(oldSel) == "number" and oldSel > 0 then outBox.SelectionStart = math.min(oldSel, #(outBox.Text or "") + 1) end end end -- Single-view UI: always show code (Luau). Hide the Preview tab entirely. local activeTab = "code" local function setTab(_tab) activeTab = "code" previewBox.Visible = false outBox.Visible = true end previewTabBtn.Visible = false codeTabBtn.Visible = false setTab("code") local function setPreview(text) previewBox.Text = text or "" end local function setOutOfCreditsOutput(rawErr) showUpgradeBanner(true) local detail = tostring(rawErr or "") setOutput( "⚠️ Not enough credits\n\n" .. "Your balance can't cover this generation. On the website, sign in and upgrade to Pro or Ultra (Stripe Checkout in the browser). After you subscribe, the billing portal is where you update payment details, download invoices, cancel, or turn off renewal—same API token in this plugin.\n\n" .. "Technical: " .. detail ) end local cancelled = false -- Studio log capture (for auto-fix) local LOG_RING_MAX = 220 local logRing = {} :: { string } local errRing = {} :: { string } local logWatchEnabled = false local logWatchUntil = 0 local lastErrorAt = 0 local autoFixInProgress = false local autoFixAttempts = 0 local lastProcessedErrCount = 0 local lastAutoFixTriggerAt = 0 local autoFixHeartbeatConn = nil -- Forward declare so Heartbeat/task.delay closures resolve to this local (not a nil global). local runAutoFixIfNeeded -- Used by persisted JSON helpers below; must appear before getLastProjectJson (Lua local visibility). local function trim(s) return tostring(s or ""):gsub("^%s+", ""):gsub("%s+$", "") end -- Declared before runAutoFixIfNeeded: that function calls us from Heartbeat while this line is still -- "above" the old local function site — Lua would otherwise resolve getLastProjectJson as a nil global. local function getLastProjectJson() local ok, v = pcall(function() return plugin:GetSetting(PROJECT_SETTING_KEY) end) if ok and typeof(v) == "string" and trim(v) ~= "" then return v end return nil end local function setLastProjectJson(projectJson) if typeof(projectJson) ~= "string" then return end -- Allow empty string to clear persisted JSON (Reset must not leave stale improve context). if trim(projectJson) == "" then pcall(function() plugin:SetSetting(PROJECT_SETTING_KEY, "") end) return end pcall(function() plugin:SetSetting(PROJECT_SETTING_KEY, projectJson) end) end local function pushRing(t, s) table.insert(t, s) while #t > LOG_RING_MAX do table.remove(t, 1) end end local function formatLogLine(message, messageType) local kind = tostring(messageType or "") local prefix = "LOG" if kind:find("Error") then prefix = "ERR" elseif kind:find("Warning") then prefix = "WARN" end return os.date("!%H:%M:%S") .. " [" .. prefix .. "] " .. tostring(message or "") end LogService.MessageOut:Connect(function(message, messageType) local line = formatLogLine(message, messageType) pushRing(logRing, line) if tostring(messageType):find("Error") then pushRing(errRing, line) lastErrorAt = tick() end end) local function stopLogWatch() logWatchEnabled = false logWatchUntil = 0 lastProcessedErrCount = #errRing if autoFixHeartbeatConn then pcall(function() autoFixHeartbeatConn:Disconnect() end) autoFixHeartbeatConn = nil end end local function startLogWatch() logWatchEnabled = true logWatchUntil = tick() + AUTO_FIX_WINDOW_SEC lastProcessedErrCount = #errRing autoFixAttempts = 0 autoFixInProgress = false lastAutoFixTriggerAt = 0 if not autoFixHeartbeatConn then autoFixHeartbeatConn = RunService.Heartbeat:Connect(function() -- Keep it lightweight; runAutoFixIfNeeded returns quickly when idle. runAutoFixIfNeeded() end) end end local function makeAutoFixRequest(errorLines) return table.concat({ "Fix runtime errors and keep gameplay the same.", "", "ERROR_LOG:", errorLines, "", "CONSTRAINTS:", "- Do not introduce obby/parkour/lava/finish/checkpoints unless explicitly requested.", "- Ensure every referenced Workspace object exists at the exact path/name; create missing Folders/Parts/Models if needed.", "- Avoid workspace.Foo direct indexing; use workspace:WaitForChild(\"Foo\", 10) with nil-check + create-if-missing.", "- Never call methods/events on nil; guard all WaitForChild results.", "- Use only valid Enum.Material values; never use Enum.Material.Gold (simulate with Metal + yellow color).", }, "\n") end runAutoFixIfNeeded = function() if not AUTO_FIX_ENABLED then return end if not logWatchEnabled or tick() > logWatchUntil then stopLogWatch() return end if autoFixInProgress then return end if autoFixAttempts >= AUTO_FIX_MAX_ATTEMPTS then stopLogWatch() return end if #errRing <= lastProcessedErrCount then return end -- Debounce bursts of errors. if (tick() - lastErrorAt) < AUTO_FIX_DEBOUNCE_SEC then return end if (tick() - lastAutoFixTriggerAt) < AUTO_FIX_DEBOUNCE_SEC then return end local projectJson = getLastProjectJson() if not projectJson then stopLogWatch() return end autoFixInProgress = true autoFixAttempts += 1 lastAutoFixTriggerAt = tick() lastProcessedErrCount = #errRing local startIdx = math.max(1, #errRing - 20) local slice = {} for i = startIdx, #errRing do table.insert(slice, errRing[i]) end local reqText = makeAutoFixRequest(table.concat(slice, "\n")) showToast("🛠 Auto-fix: improving…") uiState.isGenerating = true uiState.canApply = false uiState.lastRawOutput = nil uiState.lastProjectJson = nil uiState.lastDecoded = nil uiState.lastLuau = nil refreshActionStates() local ok, body = httpJson(API_BASE .. "/improve-job", "POST", { format = "json", projectJson = projectJson, request = reqText, mode = currentMode, }) if not ok then uiState.isGenerating = false refreshActionStates() autoFixInProgress = false showToast("❌ Auto-fix failed") return end local decoded = HttpService:JSONDecode(body) local jobId = decoded.jobId if not jobId then uiState.isGenerating = false refreshActionStates() autoFixInProgress = false showToast("❌ Auto-fix failed") return end pollJob(jobId, "🛠 Auto-fix job started.\nApplying a fix for runtime errors…", "plugin_autofix") uiState.isGenerating = false refreshActionStates() if uiState.canApply then showToast("🛠 Auto-fix: re-applying…") applyToGame() end autoFixInProgress = false end -- UI + state (future-ready for credits/model selection/usage tracking) local uiState = { isGenerating = false, canApply = false, lastJobId = nil, lastRawOutput = nil, -- full output text (JSON+optional wrappers) lastProjectJson = nil, -- extracted JSON only lastDecoded = nil, -- JSONDecode table lastLuau = nil, -- Luau-only output (zero JSON mode) lastPrompt = nil, -- raw prompt used for the last generation lastRoute = nil, -- "visual_build" | "gameplay" from API/worker (or client fallback) fakePhase = "idle", } local function setButtonEnabled(btn, enabled) btn.Active = enabled btn.AutoButtonColor = false btn.TextTransparency = enabled and 0 or 0.35 btn.BackgroundTransparency = enabled and 0 or 0.25 end local function refreshActionStates() local g = not uiState.isGenerating setButtonEnabled(genBtn, g) setButtonEnabled(enhBtn, g) setButtonEnabled(clearBtn, true) setButtonEnabled(stopBtn, uiState.isGenerating) setButtonEnabled(applyBtn, (not uiState.isGenerating) and uiState.canApply) end refreshActionStates() -- Toast local toast = Instance.new("Frame") toast.BackgroundColor3 = Color3.fromRGB(20, 24, 40) toast.BorderSizePixel = 0 toast.Size = UDim2.new(1, 0, 0, 34) toast.AnchorPoint = Vector2.new(0.5, 0) toast.Position = UDim2.new(0.5, 0, 0, -40) toast.Visible = true uiRound(toast, 12) uiStroke(toast, 0.8) toast.Parent = root local toastLabel = Instance.new("TextLabel") toastLabel.BackgroundTransparency = 1 toastLabel.Size = UDim2.new(1, -20, 1, 0) toastLabel.Position = UDim2.new(0, 10, 0, 0) toastLabel.Font = Enum.Font.GothamMedium toastLabel.TextSize = 12 toastLabel.TextColor3 = Color3.fromRGB(235, 235, 245) toastLabel.TextXAlignment = Enum.TextXAlignment.Left toastLabel.Text = "" toastLabel.Parent = toast local toastTween = nil local function showToast(msg) toastLabel.Text = tostring(msg or "") if toastTween then pcall(function() toastTween:Cancel() end) end toast.Position = UDim2.new(0.5, 0, 0, -40) local tIn = TweenService:Create(toast, TweenInfo.new(0.18, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), { Position = UDim2.new(0.5, 0, 0, 0), }) tIn:Play() toastTween = tIn task.delay(1.6, function() local tOut = TweenService:Create(toast, TweenInfo.new(0.22, Enum.EasingStyle.Quad, Enum.EasingDirection.In), { Position = UDim2.new(0.5, 0, 0, -40), }) tOut:Play() toastTween = tOut end) end -- Feedback UI removed. -- True when the worker output is plain Luau (gameplay stream), not project JSON. -- Must run before extractJsonBlob: Lua table literals use { } and would fool brace scanning. local function looksLikeLuauSource(raw) local s = tostring(raw or ""):gsub("^%s+", ""):gsub("%s+$", "") if s == "" then return false end -- Our project JSON always starts with "{" at the first non-space char. if s:sub(1, 1) == "{" then return false end if s:match("^%-%-") then return true end if s:match("^local") then return true end return false end local function extractJsonBlob(raw) local s = tostring(raw or "") local inner = s:match("```json%s*(.-)%s*```") or s:match("```%s*(.-)%s*```") if inner then s = inner end local startIdx = s:find("{", 1, true) if not startIdx then return nil end local depth = 0 for i = startIdx, #s do local c = s:sub(i, i) if c == "{" then depth += 1 elseif c == "}" then depth -= 1 if depth == 0 then return s:sub(startIdx, i) end end end return nil end -- Prefer showing real Luau in the UI by extracting scripts[].code from the project JSON. -- Returns nil if the JSON is incomplete/invalid or contains no scripts. local function tryExtractLuau(raw) if looksLikeLuauSource(raw) then local s = tostring(raw or ""):gsub("%s+$", "") return (s ~= "" and s) or nil end local blob = extractJsonBlob(raw) if not blob then -- Luau-only mode: raw is already Luau (no leading "local", e.g. starts with "game:GetService"). local s = tostring(raw or ""):gsub("%s+$", "") if s == "" then return nil end if s:find("\n") or s:find("local ", 1, true) or s:find("function ", 1, true) then return s end return nil end local ok, decoded = pcall(function() return HttpService:JSONDecode(blob) end) if not ok or type(decoded) ~= "table" then return nil end local scripts = decoded.scripts if type(scripts) ~= "table" or #scripts == 0 then return nil end local out = "" for _, s in ipairs(scripts) do if type(s) == "table" and type(s.code) == "string" and s.code ~= "" then local p = tostring(s.path or "") local n = tostring(s.name or "Script") local k = tostring(s.kind or "Script") out ..= ("-- %s (%s)\n"):format((p ~= "" and (p .. "/") or "") .. n, k) out ..= s.code if not out:match("\n$") then out ..= "\n" end out ..= "\n" end end out = out:gsub("%s+$", "") if out == "" then return nil end return out end -- Best-effort extraction of scripts[].code while JSON is still streaming/incomplete. -- Returns nil until it can find at least one "code":"..." string. local function tryExtractLuauStreaming(raw) local s = tostring(raw or "") if looksLikeLuauSource(s) then local t = s:gsub("%s+$", "") return (t ~= "" and t) or nil end -- Prefer extracting from the JSON blob if present; otherwise treat raw as plain text stream. local blob = extractJsonBlob(s) or s local codes = {} local i = 1 while true do local keyStart = blob:find([["code"]], i, true) if not keyStart then break end local colon = blob:find(":", keyStart + 6, true) if not colon then break end local q = blob:find('"', colon + 1, true) if not q then break end -- Scan a JSON string, allowing incomplete tail (no closing quote yet). local j = q + 1 local out = {} while j <= #blob do local c = blob:sub(j, j) if c == '"' then -- End of string j += 1 break elseif c == "\\" then local n = blob:sub(j + 1, j + 1) if n == "" then break end if n == "n" then table.insert(out, "\n") j += 2 elseif n == "r" then table.insert(out, "\r") j += 2 elseif n == "t" then table.insert(out, "\t") j += 2 elseif n == "\\" or n == '"' or n == "/" then table.insert(out, n) j += 2 elseif n == "u" then -- Skip \uXXXX sequences (rare in Luau). Keep them as-is if incomplete. local hex = blob:sub(j + 2, j + 5) if #hex < 4 or not hex:match("^[0-9a-fA-F]+$") then break end -- Preserve as raw sequence to avoid unicode issues in Studio textbox. table.insert(out, "\\u" .. hex) j += 6 else -- Unknown escape; include literal char and continue. table.insert(out, n) j += 2 end else table.insert(out, c) j += 1 end end local code = table.concat(out) if code ~= "" then table.insert(codes, code) end i = math.max(j, keyStart + 6) end if #codes == 0 then return nil end return table.concat(codes, "\n\n") end local function applyLuauToGame(luauCode) local code = tostring(luauCode or ""):gsub("%s+$", "") if code == "" then return false, "No Luau code to apply." end -- replacePrevious cleanup runs in applyToGame (this function is declared before deletePreviousGeneratedFolders / clearGenerated exist). -- If we're applying Luau-only output (no project JSON), ensure any referenced "world" pieces exist -- so the result is still playable and not an empty baseplate. local function ensureFolder(parent, name) local f = parent:FindFirstChild(name) if f and f:IsA("Folder") then return f end if f then f.Name = name .. "_BadType" end local nf = Instance.new("Folder") nf.Name = name nf.Parent = parent pcall(function() CollectionService:AddTag(nf, TAG_NAME) end) return nf end local function ensureWinGui() local starterGui = game:GetService("StarterGui") local guiNames = { "WinGui", "WinMessageGui" } local labelNames = { "WinMessage", "WinMessageLabel" } for _, guiName in ipairs(guiNames) do local winGui = starterGui:FindFirstChild(guiName) if winGui and not winGui:IsA("ScreenGui") then winGui.Name = guiName .. "_BadType" winGui = nil end if not winGui then winGui = Instance.new("ScreenGui") winGui.Name = guiName winGui.ResetOnSpawn = false winGui.Enabled = false winGui.Parent = starterGui pcall(function() CollectionService:AddTag(winGui, TAG_NAME) end) end for _, labelName in ipairs(labelNames) do local lbl = winGui:FindFirstChild(labelName) if lbl and not lbl:IsA("TextLabel") then lbl.Name = labelName .. "_BadType" lbl = nil end if not lbl then lbl = Instance.new("TextLabel") lbl.Name = labelName lbl.Size = UDim2.new(1, 0, 0, 60) lbl.Position = UDim2.new(0, 0, 0, 24) lbl.BackgroundTransparency = 1 lbl.Visible = false lbl.Text = "You win!" lbl.Font = Enum.Font.GothamBlack lbl.TextScaled = true lbl.TextColor3 = Color3.fromRGB(255, 255, 255) lbl.Parent = winGui pcall(function() CollectionService:AddTag(lbl, TAG_NAME) end) end end end end local function ensureCoinWorld() local vcWorld = ensureFolder(workspace, "VC_World") local assets = ensureFolder(vcWorld, "Assets") local platform = assets:FindFirstChild("FloatingPlatform") if platform and not platform:IsA("BasePart") then platform.Name = "FloatingPlatform_BadType" platform = nil end if not platform then local p = Instance.new("Part") p.Name = "FloatingPlatform" p.Anchored = true p.Size = Vector3.new(40, 2, 40) p.Position = Vector3.new(0, 10, 0) p.Material = Enum.Material.SmoothPlastic p.Color = Color3.fromRGB(90, 180, 255) p.TopSurface = Enum.SurfaceType.Smooth p.BottomSurface = Enum.SurfaceType.Smooth p.Parent = assets pcall(function() CollectionService:AddTag(p, TAG_NAME) end) platform = p end local coinPositions = { Vector3.new(-12, 12, -12), Vector3.new(-12, 12, 12), Vector3.new(12, 12, -12), Vector3.new(12, 12, 12), Vector3.new(0, 12, 0), } for i = 1, 5 do local name = "Coin" .. tostring(i) local c = assets:FindFirstChild(name) if c and not c:IsA("BasePart") then c.Name = name .. "_BadType" c = nil end if not c then local coin = Instance.new("Part") coin.Name = name coin.Anchored = true coin.CanCollide = false coin.Shape = Enum.PartType.Cylinder coin.Size = Vector3.new(0.6, 2.0, 2.0) coin.Material = Enum.Material.Neon coin.Color = Color3.fromRGB(255, 221, 76) coin.CFrame = CFrame.new(coinPositions[i]) * CFrame.Angles(0, 0, math.rad(90)) coin.Parent = assets pcall(function() CollectionService:AddTag(coin, TAG_NAME) end) end end end local folder = ServerScriptService:FindFirstChild("VibeCoderAI") if not folder then folder = Instance.new("Folder") folder.Name = "VibeCoderAI" folder.Parent = ServerScriptService end local scriptName = "VC_Main" local s = folder:FindFirstChild(scriptName) if not s or not s:IsA("Script") then if s then pcall(function() s:Destroy() end) end s = Instance.new("Script") s.Name = scriptName s.Parent = folder pcall(function() CollectionService:AddTag(s, TAG_NAME) end) end local okSrc, srcErr = pcall(function() s.Source = code end) if not okSrc then return false, "Could not write script source: " .. tostring(srcErr) end ChangeHistoryService:SetWaypoint("VibeCoder AI — applied Luau", false) return true, "✅ Applied Luau script to ServerScriptService/VibeCoderAI/VC_Main" end local function safeDecodeProjectJson(raw) if looksLikeLuauSource(raw) then return nil, nil end local blob = extractJsonBlob(raw) if not blob then return nil, nil end local ok, decoded = pcall(function() return HttpService:JSONDecode(blob) end) if not ok or type(decoded) ~= "table" then return nil, nil end -- Reject junk: brace-balanced Lua snippets decode as odd tables without our shape. if decoded.schemaVersion == nil and type(decoded.instances) ~= "table" and type(decoded.scripts) ~= "table" then return nil, nil end return blob, decoded end local function formatPreview(decoded) if type(decoded) ~= "table" then return "⚠️ No structured project found yet.\n\nSwitch to Code tab to see raw output." end local title = tostring(decoded.title or "Generation Ready") local summary = tostring(decoded.summary or "") local lines = {} table.insert(lines, "✅ " .. title .. " ready") if summary ~= "" then table.insert(lines, "") table.insert(lines, summary) end table.insert(lines, "") local hasSpawn, hasFinish, hasLava = false, false, false local parts = 0 if type(decoded.instances) == "table" then for _, inst in ipairs(decoded.instances) do if type(inst) == "table" then local n = tostring(inst.name or "") local k = tostring(inst.kind or "") if k == "SpawnLocation" or n == "VC_Spawn" then hasSpawn = true end if n == "VC_Finish" then hasFinish = true end if n == "VC_Lava" then hasLava = true end if k == "Part" or k == "WedgePart" or k == "TrussPart" or k == "SpawnLocation" or k == "MeshPart" then parts += 1 end end end end local scriptCount = 0 if type(decoded.scripts) == "table" then scriptCount = #decoded.scripts end local terrainOps = 0 if type(decoded.terrain) == "table" then if type(decoded.terrain.fillBlocks) == "table" then terrainOps += #decoded.terrain.fillBlocks end if type(decoded.terrain.fillBalls) == "table" then terrainOps += #decoded.terrain.fillBalls end if type(decoded.terrain.fillRegions) == "table" then terrainOps += #decoded.terrain.fillRegions end end if hasSpawn then table.insert(lines, "• Spawn created") end if hasLava then table.insert(lines, "• Lava floor added") end if hasFinish then table.insert(lines, "• Finish platform added") end if (not hasSpawn) and (not hasLava) and (not hasFinish) then table.insert(lines, "• Parts: " .. tostring(parts)) end if terrainOps > 0 then table.insert(lines, "• Terrain ops: " .. tostring(terrainOps)) end if scriptCount > 0 then table.insert(lines, "• Scripts: " .. tostring(scriptCount)) end table.insert(lines, "") table.insert(lines, "Next: click Apply 🚀 to insert into your game.") return table.concat(lines, "\n") end -- Short summary for VisualBuild: no JSON, no script listing — user sees viewport + this text only. local function formatVisualBuildSummary(decoded) if type(decoded) ~= "table" then return "⚡ Building visuals…" end -- Model JSON "title"/"summary" can drift (e.g. example "Red Block") while instances match the prompt. -- Prefer the user's actual prompt for the headline so the panel always matches what they typed. local userLine = type(uiState.lastPrompt) == "string" and trim(uiState.lastPrompt) or "" local title = tostring(decoded.title or "Build") if userLine ~= "" then title = userLine if #title > 120 then title = title:sub(1, 117) .. "..." end end local schemaTitle = tostring(decoded.title or "") local summary = tostring(decoded.summary or "") local n = 0 if type(decoded.instances) == "table" then n = #decoded.instances end local terrainOps = 0 if type(decoded.terrain) == "table" then local tr = decoded.terrain if type(tr.fillBlocks) == "table" then terrainOps += #tr.fillBlocks end if type(tr.fillBalls) == "table" then terrainOps += #tr.fillBalls end if type(tr.fillRegions) == "table" then terrainOps += #tr.fillRegions end end local lines = {} table.insert(lines, "✅ " .. title) if userLine ~= "" and schemaTitle ~= "" and schemaTitle ~= userLine and schemaTitle ~= "Build" then table.insert(lines, ("(model title was: %s)"):format(schemaTitle)) end if summary ~= "" then table.insert(lines, "") table.insert(lines, summary) end table.insert(lines, "") table.insert(lines, ("• Objects in build: %d"):format(n)) if terrainOps > 0 then table.insert(lines, ("• Terrain updates: %d"):format(terrainOps)) end table.insert(lines, "") table.insert(lines, "Live preview updates in the 3D view. Click Apply 🚀 to finalize placement.") return table.concat(lines, "\n") end local function clearGenerated() local removed = 0 ChangeHistoryService:SetWaypoint("VibeCoder AI — clear build", false) -- Delete all tagged instances. Sort deepest-first so children delete before parents. local items = CollectionService:GetTagged(TAG_NAME) table.sort(items, function(a, b) local da = 0 local p = a while p do da += 1 p = p.Parent end local db = 0 p = b while p do db += 1 p = p.Parent end return da > db end) for _, inst in ipairs(items) do if inst and inst.Parent then pcall(function() inst:Destroy() end) removed += 1 end end ChangeHistoryService:SetWaypoint("VibeCoder AI — clear done", false) return removed end local function splitPath(pathStr) local parts = {} for part in string.gmatch(pathStr, "[^/]+") do table.insert(parts, part) end return parts end local function resolveParentFromPath(pathStr) local parts = splitPath(pathStr) if #parts < 1 then return nil end local serviceName = parts[1] local okSvc, svc = pcall(function() return game:GetService(serviceName) end) if not okSvc or not svc then return nil end local parent = svc for i = 2, #parts do local fname = parts[i] local f = parent:FindFirstChild(fname) if not f then f = Instance.new("Folder") f.Name = fname f.Parent = parent pcall(function() CollectionService:AddTag(f, TAG_NAME) end) end parent = f end return parent end local function vec3FromArray(a) if type(a) ~= "table" then return nil end local x = tonumber(a[1]) local y = tonumber(a[2]) local z = tonumber(a[3]) if not x or not y or not z then return nil end return Vector3.new(x, y, z) end local function color3FromArray(a) if type(a) ~= "table" then return nil end local r = tonumber(a[1]) local g = tonumber(a[2]) local b = tonumber(a[3]) if not r or not g or not b then return nil end if r <= 1 and g <= 1 and b <= 1 then return Color3.new(r, g, b) end return Color3.fromRGB(math.clamp(math.floor(r + 0.5), 0, 255), math.clamp(math.floor(g + 0.5), 0, 255), math.clamp(math.floor(b + 0.5), 0, 255)) end local function materialFromString(s) if type(s) ~= "string" then return nil end local ok, mat = pcall(function() return Enum.Material[s] end) if ok then return mat end return nil end -- Ensure a shared runtime library exists for generated scripts. local function ensureRuntimeLibrary() local root = ReplicatedStorage:FindFirstChild("VibeCoderAI") if not root then root = Instance.new("Folder") root.Name = "VibeCoderAI" root.Parent = ReplicatedStorage end local runtime = root:FindFirstChild("Runtime") if not runtime then runtime = Instance.new("Folder") runtime.Name = "Runtime" runtime.Parent = root end local util = runtime:FindFirstChild("VC_Util") if not util or not util:IsA("ModuleScript") then if util then pcall(function() util:Destroy() end) end util = Instance.new("ModuleScript") util.Name = "VC_Util" util.Parent = runtime end -- Keep this module tiny + stable; generators can rely on it. util.Source = [[ local Util = {} function Util.getOrCreate(parent: Instance, className: string, name: string): Instance local child = parent:FindFirstChild(name) if child and child.ClassName == className then return child end if child then child.Name = name .. "_BadType" end local inst = Instance.new(className) inst.Name = name inst.Parent = parent return inst end function Util.waitOrCreate(parent: Instance, className: string, name: string, timeoutSec: number?): Instance local t = tonumber(timeoutSec) or 5 local child = parent:WaitForChild(name, t) if child and child.ClassName == className then return child end return Util.getOrCreate(parent, className, name) end function Util.safeConnect(inst: any, signalName: string, fn) if not inst then return nil end local sig = inst[signalName] if typeof(sig) ~= "RBXScriptSignal" then return nil end return sig:Connect(fn) end function Util.materialFromName(name: any) local s = typeof(name) == "string" and name or "" -- Never allow non-existent materials like "Gold" if s:lower() == "gold" then return Enum.Material.Metal, Color3.fromRGB(255, 210, 70) end local ok, mat = pcall(function() return Enum.Material[s] end) if ok and mat then return mat, nil end return Enum.Material.SmoothPlastic, nil end return Util ]] return true end local INSTANCE_KIND_TO_CLASS = { Part = "Part", WedgePart = "WedgePart", TrussPart = "TrussPart", SpawnLocation = "SpawnLocation", MeshPart = "MeshPart", Model = "Model", Folder = "Folder", } local TERRAIN_OP_BUDGET = 24 local function isRobloxAssetId(s) if type(s) ~= "string" then return false end return string.sub(s, 1, 13) == "rbxassetid://" end local function region3FromCorners(minArr, maxArr) local av = vec3FromArray(minArr) local bv = vec3FromArray(maxArr) if not av or not bv then return nil end local minV = Vector3.new(math.min(av.X, bv.X), math.min(av.Y, bv.Y), math.min(av.Z, bv.Z)) local maxV = Vector3.new(math.max(av.X, bv.X), math.max(av.Y, bv.Y), math.max(av.Z, bv.Z)) return Region3.new(minV, maxV) end --- workspace.Terrain: FillBlock / FillBall / FillRegion from project JSON (schema v2). local function applyTerrain(decoded) local t = decoded.terrain if type(t) ~= "table" then return 0 end local terrain = workspace:FindFirstChildOfClass("Terrain") if not terrain then return 0 end local done = 0 local function oneOp(fn) if done >= TERRAIN_OP_BUDGET then return end local ok = pcall(fn) if ok then done += 1 end end if type(t.fillBlocks) == "table" then for _, fb in ipairs(t.fillBlocks) do if type(fb) == "table" and done < TERRAIN_OP_BUDGET then local pos = vec3FromArray(fb.position) local size = vec3FromArray(fb.size) local mat = materialFromString(fb.material) if pos and size and mat then oneOp(function() terrain:FillBlock(CFrame.new(pos), size, mat) end) end end end end if type(t.fillBalls) == "table" then for _, fb in ipairs(t.fillBalls) do if type(fb) == "table" and done < TERRAIN_OP_BUDGET then local center = vec3FromArray(fb.center) local rad = tonumber(fb.radius) local mat = materialFromString(fb.material) if center and rad and rad > 0 and mat then oneOp(function() terrain:FillBall(center, rad, mat) end) end end end end if type(t.fillRegions) == "table" then for _, fr in ipairs(t.fillRegions) do if type(fr) == "table" and done < TERRAIN_OP_BUDGET then local reg = region3FromCorners(fr.min, fr.max) local mat = materialFromString(fr.material) local res = tonumber(fr.resolution) if not res or res <= 0 then res = 4 end if reg and mat then oneOp(function() terrain:FillRegion(reg, res, mat) end) end end end end return done end local function mergeInstanceSpecs(decoded) local list = {} if type(decoded.instances) == "table" then for _, s in ipairs(decoded.instances) do table.insert(list, s) end end if type(decoded.meshAssets) == "table" then for _, s in ipairs(decoded.meshAssets) do table.insert(list, s) end end return list end local function insertInstances(decoded) local instances = mergeInstanceSpecs(decoded) if #instances == 0 then return 0 end local function prop(props, key) if type(props) ~= "table" then return nil end -- Support both schema styles: lowerCamel (size/position/color/material) and UpperCamel (Size/Position/Color/Material). local v = props[key] if v ~= nil then return v end local up = key:sub(1, 1):upper() .. key:sub(2) return props[up] end local created = 0 for _, spec in ipairs(instances) do if type(spec) == "table" then local pathStr = tostring(spec.path or "") local kind = tostring(spec.kind or "") local name = tostring(spec.name or "") local props = spec.props if pathStr ~= "" and kind ~= "" and name ~= "" then local skipInvalidMesh = kind == "MeshPart" and not isRobloxAssetId(tostring(spec.meshId or "")) if not skipInvalidMesh then local parent = resolveParentFromPath(pathStr) if parent then local className = INSTANCE_KIND_TO_CLASS[kind] if className then local inst = parent:FindFirstChild(name) if inst and inst.ClassName ~= className then inst:Destroy() inst = nil end if not inst then inst = Instance.new(className) inst.Name = name inst.Parent = parent pcall(function() CollectionService:AddTag(inst, TAG_NAME) end) created += 1 end if inst:IsA("BasePart") then if type(props) == "table" then local size = vec3FromArray(prop(props, "size")) if size then inst.Size = size end local pos = vec3FromArray(prop(props, "position")) if pos then inst.Position = pos end local col = color3FromArray(prop(props, "color")) if col then inst.Color = col end local anchored = prop(props, "anchored") if typeof(anchored) == "boolean" then inst.Anchored = anchored end local canCollide = prop(props, "canCollide") if typeof(canCollide) == "boolean" then inst.CanCollide = canCollide end local mat = materialFromString(prop(props, "material")) if mat then inst.Material = mat end local tr = tonumber(prop(props, "transparency")) if tr then inst.Transparency = math.clamp(tr, 0, 1) end -- Optional: simple part shapes for better visuals (trees often want Ball leaves). if inst:IsA("Part") then local shape = tostring(prop(props, "shape") or "") if shape ~= "" then local s = shape:lower() if s == "ball" or s == "sphere" then inst.Shape = Enum.PartType.Ball elseif s == "cylinder" then inst.Shape = Enum.PartType.Cylinder elseif s == "block" then inst.Shape = Enum.PartType.Block end end end end end if inst:IsA("MeshPart") then local mid = spec.meshId if type(mid) == "string" and isRobloxAssetId(mid) then pcall(function() inst.MeshId = mid end) end local tid = spec.textureId if type(tid) == "string" and isRobloxAssetId(tid) then pcall(function() inst.TextureID = tid end) end end end end end end end end return created end local function ensureWorkspaceCoreParts(decoded) local function ensurePart(name, defaults) local existing = workspace:FindFirstChild(name) if existing and not existing:IsA("BasePart") then -- If the model/plugin accidentally created a Folder/Model with the reserved name, -- move it aside so the runtime script won't crash. existing.Name = name .. "_BadType" existing = nil end if not existing then local p = Instance.new("Part") p.Name = name p.Anchored = true p.TopSurface = Enum.SurfaceType.Smooth p.BottomSurface = Enum.SurfaceType.Smooth p.Parent = workspace pcall(function() CollectionService:AddTag(p, TAG_NAME) end) existing = p end local part = existing :: BasePart if defaults.size then part.Size = defaults.size end if defaults.position then part.Position = defaults.position end if defaults.color then part.Color = defaults.color end if defaults.material then part.Material = defaults.material end if defaults.canCollide ~= nil then part.CanCollide = defaults.canCollide end if defaults.transparency ~= nil then part.Transparency = defaults.transparency end return part end -- Only create VC_Spawn when the build is likely to be played (scripts/gameplay), -- or when the output explicitly references a spawn. local shouldEnsureSpawn = false if type(decoded) == "table" then if type(decoded.instances) == "table" then for _, inst in ipairs(decoded.instances) do if type(inst) == "table" then local n = tostring(inst.name or ""):lower() local k = tostring(inst.kind or ""):lower() if k == "spawnlocation" or n == "vc_spawn" or n:find("spawn", 1, true) then shouldEnsureSpawn = true break end end end end if not shouldEnsureSpawn and type(decoded.scripts) == "table" and #decoded.scripts > 0 then shouldEnsureSpawn = true end -- Do not infer spawn from title/summary; only create spawn if explicitly needed. end if shouldEnsureSpawn then ensurePart("VC_Spawn", { size = Vector3.new(18, 1, 18), position = Vector3.new(0, 6, 0), color = Color3.fromRGB(80, 160, 255), material = Enum.Material.SmoothPlastic, canCollide = true, transparency = 0, }) end -- Only create VC_Finish / VC_Lava if the project actually needs them (obby/parkour). local needFinishOrLava = false if type(decoded) == "table" then if type(decoded.title) == "string" and decoded.title:lower():find("obby", 1, true) then needFinishOrLava = true end if type(decoded.summary) == "string" and decoded.summary:lower():find("obby", 1, true) then needFinishOrLava = true end if type(decoded.instances) == "table" then for _, inst in ipairs(decoded.instances) do if type(inst) == "table" then local n = tostring(inst.name or "") if n == "VC_Finish" or n == "VC_Lava" then needFinishOrLava = true break end end end end if not needFinishOrLava and type(decoded.scripts) == "table" then for _, s in ipairs(decoded.scripts) do if type(s) == "table" then local code = tostring(s.code or "") if code:find("VC_Finish", 1, true) or code:find("VC_Lava", 1, true) then needFinishOrLava = true break end end end end end if needFinishOrLava then ensurePart("VC_Finish", { size = Vector3.new(14, 1, 14), position = Vector3.new(0, 6, 140), color = Color3.fromRGB(60, 200, 90), material = Enum.Material.Neon, canCollide = true, transparency = 0, }) local lava = ensurePart("VC_Lava", { size = Vector3.new(400, 2, 400), position = Vector3.new(0, -50, 0), color = Color3.fromRGB(220, 60, 60), material = Enum.Material.Neon, canCollide = false, transparency = 0.05, }) lava.Locked = true end end -- Forward-compatible project JSON: keep in sync with apps/api/src/lib/projectSchema.ts local PLUGIN_SCHEMA_MAX = 2 local function nonEmptyTable(t) return type(t) == "table" and next(t) ~= nil end local function reservedBlocksNotice(decoded) if nonEmptyTable(decoded.assetPipeline) then return " Reserved: assetPipeline is not applied in this plugin build." end return "" end local function schemaVersionNotice(decoded) local v = tonumber(decoded.schemaVersion) if v == nil then return " Note: schemaVersion missing (treated as 1)." end if v > PLUGIN_SCHEMA_MAX then return " Warning: schemaVersion=" .. tostring(v) .. " (this plugin supports up to " .. tostring(PLUGIN_SCHEMA_MAX) .. ") — update the plugin for full support." end return "" end -- Parses API JSON (scripts[]) and creates Script / LocalScript / ModuleScript under the right services. local function insertScriptsFromPayload(raw) local blob = extractJsonBlob(raw) if not blob then return false, "Could not find JSON in the output (model may have returned plain text)." end local okd, decoded = pcall(function() return HttpService:JSONDecode(blob) end) if not okd or type(decoded) ~= "table" then return false, "JSON decode failed." end local scripts = decoded.scripts if type(scripts) ~= "table" or #scripts == 0 then return false, 'No "scripts" array in JSON.' end ChangeHistoryService:SetWaypoint("VibeCoder AI — insert scripts", false) local terrainCount = applyTerrain(decoded) local instanceCount = insertInstances(decoded) -- Safety net: ensure spawn exists, and obby parts only if needed. ensureWorkspaceCoreParts(decoded) local created = 0 for _, spec in ipairs(scripts) do if type(spec) == "table" then local pathStr = tostring(spec.path or "") local instName = tostring(spec.name or "Generated") local kind = tostring(spec.kind or "Script") local code = tostring(spec.code or "") if pathStr ~= "" and code ~= "" then local parent = resolveParentFromPath(pathStr) if parent then local className = "Script" if kind == "LocalScript" then className = "LocalScript" elseif kind == "ModuleScript" then className = "ModuleScript" end local inst = parent:FindFirstChild(instName) if inst and inst.ClassName ~= className then inst:Destroy() inst = nil end if not inst then inst = Instance.new(className) inst.Name = instName inst.Parent = parent pcall(function() CollectionService:AddTag(inst, TAG_NAME) end) end if inst:IsA("LuaSourceContainer") then inst.Source = code created += 1 end end end end end ChangeHistoryService:SetWaypoint("VibeCoder AI — insert done", false) if created == 0 and instanceCount == 0 and terrainCount == 0 then return false, "Nothing was inserted (no recognized scripts, instances, or terrain)." end local msg = "Inserted " .. tostring(created) .. " script(s)" if terrainCount > 0 then msg ..= " + " .. tostring(terrainCount) .. " terrain op(s)" end if instanceCount > 0 then msg ..= " + " .. tostring(instanceCount) .. " instance(s)" end msg ..= "." msg ..= schemaVersionNotice(decoded) msg ..= reservedBlocksNotice(decoded) return true, msg end local GENERATED_PREFIX = "Generated_" local function deletePreviousGeneratedFolders() local removed = 0 local function clearIn(parent) if not parent then return end for _, ch in ipairs(parent:GetChildren()) do if ch and ch.Name and string.sub(ch.Name, 1, #GENERATED_PREFIX) == GENERATED_PREFIX then removed += 1 pcall(function() ch:Destroy() end) end end end local function clearLegacyNamedIn(parent, names) if not parent then return end for _, ch in ipairs(parent:GetChildren()) do if ch and ch.Name then for _, legacyName in ipairs(names) do if ch.Name == legacyName then removed += 1 pcall(function() ch:Destroy() end) break end end end end end clearIn(workspace) clearIn(ServerScriptService:FindFirstChild("VibeCoderAI")) clearIn(game:GetService("StarterGui")) clearIn(game:GetService("ReplicatedStorage")) local starterPlayer = game:GetService("StarterPlayer") clearIn(starterPlayer:FindFirstChild("StarterPlayerScripts")) clearIn(starterPlayer:FindFirstChild("StarterCharacterScripts")) -- Clean up legacy generations from older plugin/model outputs that were not tagged. clearLegacyNamedIn(workspace, { "AI_Build", "VC_World", "VibeCoderAI", "VC_Spawn" }) clearLegacyNamedIn(game:GetService("StarterGui"), { "WinGui", "WinMessageGui", "CoinCounterGui", "AI_Build" }) clearLegacyNamedIn(starterPlayer:FindFirstChild("StarterPlayerScripts"), { "AI_Build", "CoinCollector", "CoinCollectorClient", "MonetizationClient", "VibeCoderAI", }) clearLegacyNamedIn(ServerScriptService, { "AI_Build", "LoadAssets", "LoadCoinAssets", "LoadNatureAssets", "MonetizationServer", "VibeCoderAI", }) return removed end local function makeGenerationRoot() local stamp = os.date("!%Y%m%d_%H%M%S") local name = GENERATED_PREFIX .. stamp local rootFolder = Instance.new("Folder") rootFolder.Name = name rootFolder.Parent = workspace pcall(function() CollectionService:AddTag(rootFolder, TAG_NAME) end) local svcRoot = ServerScriptService:FindFirstChild("VibeCoderAI") if not svcRoot then svcRoot = Instance.new("Folder") svcRoot.Name = "VibeCoderAI" svcRoot.Parent = ServerScriptService end local scriptsRoot = Instance.new("Folder") scriptsRoot.Name = name scriptsRoot.Parent = svcRoot pcall(function() CollectionService:AddTag(scriptsRoot, TAG_NAME) end) return name, rootFolder, scriptsRoot end local function resolveParentWithGeneration(rootWorkspaceFolder, rootScriptsFolder, pathStr) local parts = splitPath(pathStr) if #parts < 1 then return nil end local serviceName = parts[1] -- Workspace content goes inside the generation folder. if serviceName == "Workspace" then local parent = rootWorkspaceFolder for i = 2, #parts do local fname = parts[i] local f = parent:FindFirstChild(fname) if not f then f = Instance.new("Folder") f.Name = fname f.Parent = parent pcall(function() CollectionService:AddTag(f, TAG_NAME) end) end parent = f end return parent end -- Script services go inside ServerScriptService/VibeCoderAI/Generated_/... local okSvc, svc = pcall(function() return game:GetService(serviceName) end) if okSvc and svc and (svc == ServerScriptService or svc == ReplicatedStorage or svc == StarterPlayerScripts or svc == StarterGui) then local parent = rootScriptsFolder for i = 2, #parts do local fname = parts[i] local f = parent:FindFirstChild(fname) if not f then f = Instance.new("Folder") f.Name = fname f.Parent = parent pcall(function() CollectionService:AddTag(f, TAG_NAME) end) end parent = f end return parent end -- Fallback: do not write outside generation roots. return nil end local function textHasAny(haystack, terms) for _, term in ipairs(terms) do if haystack:find(term, 1, true) then return true end end return false end -- Heuristic: short, visual-only prompts ("create a red block") should not auto-add spawn/floor/world. local function looksLikeSimpleVisualPrompt(rawPrompt) local p = trim(rawPrompt):lower() if p == "" then return false end if #p > 160 then return false end local hasLogicSignals = textHasAny(p, { "ui", "gui", "hud", "button", "click", "touch", "interact", "door", "script", "system", "logic", "inventory", "shop", "quest", "enemy", "npc", "combat", "weapon", "datastore", "save", }) if hasLogicSignals then return false end return textHasAny(p, { "block", "part", "baseplate", "platform", "wall", "room", "house", "building", "city", "town", "skyline", "village", "terrain", "tree", "trees", "lava", "color", "red", "blue", "green", }) end -- Remove model-added spawn/floor when we are in VisualBuild / simple-visual mode (instances-only). local function stripVisualBuildPlayableDefaults(d) if type(d) ~= "table" or type(d.instances) ~= "table" then return d end local removeNames = { VC_World = true, VC_Spawn = true, VC_Floor = true, } local cleaned = {} for _, inst in ipairs(d.instances) do if type(inst) == "table" then local name = tostring(inst.name or "") local kind = tostring(inst.kind or "") if not (removeNames[name] or kind == "SpawnLocation") then table.insert(cleaned, inst) end end end d.instances = cleaned return d end local function applyDecodedToGame(decoded) if type(decoded) ~= "table" then return false, "No decoded project to apply." end -- For simple visual-only prompts, strip "make it playable" defaults if they appear in JSON. -- This prevents unexpected VC_Spawn/VC_Floor/VC_World from being inserted when the user asked for a single part. local function stripPlayableDefaultsIfNeeded(d) local prompt = uiState.lastPrompt local visualMode = (uiState.lastRoute == "visual_build") or (type(prompt) == "string" and looksLikeSimpleVisualPrompt(prompt)) if not visualMode then return d end -- Only do this when there are no scripts (instances-only generation). if type(d.scripts) == "table" and #d.scripts > 0 then return d end if type(d.instances) ~= "table" then return d end return stripVisualBuildPlayableDefaults(d) end decoded = stripPlayableDefaultsIfNeeded(decoded) -- Heuristic safety: if the model forgot to include visible instances (platform/coins/UI), -- create the minimum required objects referenced by scripts so the result is playable. local function decodedScriptsText(d) if type(d) ~= "table" or type(d.scripts) ~= "table" then return "" end local out = {} for _, s in ipairs(d.scripts) do if type(s) == "table" and type(s.code) == "string" then table.insert(out, s.code) end end return table.concat(out, "\n\n") end local function ensureFolder(parent, name) local f = parent:FindFirstChild(name) if f and f:IsA("Folder") then return f end if f then f.Name = name .. "_BadType" end local nf = Instance.new("Folder") nf.Name = name nf.Parent = parent pcall(function() CollectionService:AddTag(nf, TAG_NAME) end) return nf end local function ensureWinGui() local starterGui = game:GetService("StarterGui") local winGui = starterGui:FindFirstChild("WinGui") if winGui and not winGui:IsA("ScreenGui") then winGui.Name = "WinGui_BadType" winGui = nil end if not winGui then winGui = Instance.new("ScreenGui") winGui.Name = "WinGui" winGui.ResetOnSpawn = false winGui.Enabled = false winGui.Parent = starterGui pcall(function() CollectionService:AddTag(winGui, TAG_NAME) end) end local lbl = winGui:FindFirstChild("WinMessage") if lbl and not lbl:IsA("TextLabel") then lbl.Name = "WinMessage_BadType" lbl = nil end if not lbl then lbl = Instance.new("TextLabel") lbl.Name = "WinMessage" lbl.Size = UDim2.new(1, 0, 0, 60) lbl.Position = UDim2.new(0, 0, 0, 24) lbl.BackgroundTransparency = 1 lbl.Text = "You win!" lbl.Font = Enum.Font.GothamBlack lbl.TextScaled = true lbl.TextColor3 = Color3.fromRGB(255, 255, 255) lbl.Parent = winGui pcall(function() CollectionService:AddTag(lbl, TAG_NAME) end) end end local function ensureCoinWorld() local vcWorld = ensureFolder(workspace, "VC_World") local assets = ensureFolder(vcWorld, "Assets") local platform = assets:FindFirstChild("FloatingPlatform") if platform and not platform:IsA("BasePart") then platform.Name = "FloatingPlatform_BadType" platform = nil end if not platform then local p = Instance.new("Part") p.Name = "FloatingPlatform" p.Anchored = true p.Size = Vector3.new(40, 2, 40) p.Position = Vector3.new(0, 10, 0) p.Material = Enum.Material.SmoothPlastic p.Color = Color3.fromRGB(90, 180, 255) p.TopSurface = Enum.SurfaceType.Smooth p.BottomSurface = Enum.SurfaceType.Smooth p.Parent = assets pcall(function() CollectionService:AddTag(p, TAG_NAME) end) platform = p end local coinPositions = { Vector3.new(-12, 12, -12), Vector3.new(-12, 12, 12), Vector3.new(12, 12, -12), Vector3.new(12, 12, 12), Vector3.new(0, 12, 0), } for i = 1, 5 do local name = "Coin" .. tostring(i) local c = assets:FindFirstChild(name) if c and not c:IsA("BasePart") then c.Name = name .. "_BadType" c = nil end if not c then local coin = Instance.new("Part") coin.Name = name coin.Anchored = true coin.CanCollide = false coin.Shape = Enum.PartType.Cylinder coin.Size = Vector3.new(0.6, 2.0, 2.0) coin.Material = Enum.Material.Neon coin.Color = Color3.fromRGB(255, 221, 76) coin.CFrame = CFrame.new(coinPositions[i]) * CFrame.Angles(0, 0, math.rad(90)) coin.Parent = assets pcall(function() CollectionService:AddTag(coin, TAG_NAME) end) end end end ensureRuntimeLibrary() if replacePrevious then deletePreviousGeneratedFolders() -- Also clear tagged instances as a safety net (older builds). clearGenerated() end -- Apply directly to the requested paths (Workspace/..., ServerScriptService/...). -- This avoids breaking scripts that do workspace:WaitForChild("X") expecting root-level names. ChangeHistoryService:SetWaypoint("VibeCoder AI — apply build", false) local terrainCount = applyTerrain(decoded) local instanceCount = insertInstances(decoded) -- Safety net: ensure spawn exists, and only create obby parts when needed. local visualInstancesOnly = type(decoded.scripts) == "table" and #decoded.scripts == 0 and (uiState.lastRoute == "visual_build" or (type(uiState.lastPrompt) == "string" and looksLikeSimpleVisualPrompt(uiState.lastPrompt))) if not visualInstancesOnly then ensureWorkspaceCoreParts(decoded) end -- Scripts local createdScripts = 0 if type(decoded.scripts) == "table" then for _, spec in ipairs(decoded.scripts) do if type(spec) == "table" then local pathStr = tostring(spec.path or "") local instName = tostring(spec.name or "Generated") local kind = tostring(spec.kind or "Script") local code = tostring(spec.code or "") if pathStr ~= "" and code ~= "" then local parent = resolveParentFromPath(pathStr) if parent then local className = "Script" if kind == "LocalScript" then className = "LocalScript" elseif kind == "ModuleScript" then className = "ModuleScript" end local inst = Instance.new(className) inst.Name = instName inst.Parent = parent pcall(function() CollectionService:AddTag(inst, TAG_NAME) end) if inst:IsA("LuaSourceContainer") then inst.Source = code createdScripts += 1 end end end end end end ChangeHistoryService:SetWaypoint("VibeCoder AI — applied build", false) if createdScripts == 0 and instanceCount == 0 and terrainCount == 0 then return false, "Nothing to apply." end return true, ("✅ Applied (scripts=%d, instances=%d, terrainOps=%d)"):format(createdScripts, instanceCount, terrainCount) end -- Must match API/worker classifyGenerationRoute lists. local function classifyGenerationRoute(rawPrompt) local p = trim(rawPrompt):lower() if p == "" then return "gameplay" end -- "round" must be whole-word only — substring matches "ground", "surround", "background", etc. local function hasWholeWord(hay, word) for token in string.gmatch(hay, "%a+") do if token == word then return true end end return false end if hasWholeWord(p, "round") or hasWholeWord(p, "rounds") then return "gameplay" end if textHasAny(p, { "obby", "parkour", "coin", "coins", "collect", "collection", "simulator", "tycoon", "quest", "quests", "shop", "inventory", "combat", "enemy", "enemies", "npc", "npcs", "weapon", "weapons", "wave", "waves", "leaderboard", "datastore", "data store", "save", "saving", "gameplay", "minigame", "raid", "boss", "upgrade", "currency", "monetization", "pets", "pet", "grind", "survival", "pvp", "battle royale", "tower defense", "td ", }) then return "gameplay" end if textHasAny(p, { "block", "blocks", "part", "parts", "tree", "trees", "house", "home", "building", "build", "mansion", "castle", "bridge", "road", "car", "cars", "wall", "walls", "floor", "platform", "terrain", "map", "model", "models", "prop", "props", "decoration", "decor", "room", "scene", "environment", "baseplate", "statue", "fountain", "garden", "park", "city", "cities", "town", "towns", "skyline", "village", "lamp", "roof", "door", "window", "windows", "stairs", "stair", "lava", "lavas", }) then return "visual_build" end if looksLikeSimpleVisualPrompt(rawPrompt) then return "visual_build" end return "gameplay" end local function isExplicitCodeOnlyPrompt(rawPrompt) local p = trim(rawPrompt):lower() if p == "" then return false end return p:find("code only", 1, true) or p:find("luau only", 1, true) or p:find("lua only", 1, true) or p:find("script only", 1, true) or p:find("only code", 1, true) or p:find("only luau", 1, true) or p:find("only lua", 1, true) end local function chooseRequestFormat(rawPrompt) if isExplicitCodeOnlyPrompt(rawPrompt) then return "luau" end -- Match API: visual/build → JSON (instances in Edit); gameplay → Luau (code stream). local route = classifyGenerationRoute(rawPrompt) if route == "gameplay" then return "luau" end return "json" end -- User wants a brand-new generation, not an improve on the last build. local function wantsExplicitFreshStart(rawPrompt) local p = trim(rawPrompt):lower() if p == "" then return false end return p:find("start over", 1, true) or p:find("from scratch", 1, true) or p:find("brand new", 1, true) or p:find("new game", 1, true) or p:find("different game", 1, true) or p:find("ignore previous", 1, true) or p:find("ignore the last", 1, true) or p:find("reset everything", 1, true) end -- When we already have a JSON project or Luau from the last Apply, treat short follow-ups as improve. local function shouldStartFresh(rawPrompt, haveContext) local p = trim(rawPrompt):lower() if p == "" then return true end if not haveContext then return true end local wantsImprove = p:match("^change%s*:") or p:match("^update%s*:") or p:match("^modify%s*:") or p:match("^improve%s*:") or p:match("^edit%s*:") or p:match("^continue%s*:") or p:find("improve the previous", 1, true) or p:find("update the previous", 1, true) or p:find("modify the previous", 1, true) or p:find("change the previous", 1, true) or p:find("keep the previous", 1, true) or p:find("same game", 1, true) if wantsImprove then return false end -- Common follow-ups (e.g. "add score UI") without re-pasting the whole game idea. if p:match("^add%s") or p:match("^also%s") or p:match("^now%s") or p:match("^include%s") then return false end if p:find("add score", 1, true) or p:find("add ui", 1, true) or p:find("score ui", 1, true) then return false end if p:find("leaderboard", 1, true) or p:find("on%-screen", 1, true) or p:find("onscreen", 1, true) then return false end if p:find("screen gui", 1, true) or p:find("screengui", 1, true) or p:find("startergui", 1, true) then return false end if p:find("please add", 1, true) or p:find("add a ", 1, true) or p:find("add an ", 1, true) then return false end if p:find("make it ", 1, true) or p:find("can you add", 1, true) then return false end if p:find("^fix", 1, true) or p:find("^tweak", 1, true) or p:find("^remove", 1, true) or p:find("^faster", 1, true) or p:find("^slower", 1, true) then return false end if p:find("^delete", 1, true) or p:find("^undo", 1, true) or p:find("^strip", 1, true) or p:find("^drop", 1, true) then return false end if p:find("take out", 1, true) or p:find("get rid of", 1, true) or p:find("no more", 1, true) or p:find("remove the", 1, true) then return false end if p:find("extra ui", 1, true) or p:find("remove ui", 1, true) or p:find("remove shop", 1, true) or p:find("without the", 1, true) then return false end if p:find("more coins", 1, true) or p:find("less coins", 1, true) or p:find("coin value", 1, true) then return false end if p:find("^make ", 1, true) or p:find("^make the ", 1, true) then return false end return true end local function getEnhancedPrompt(rawPrompt) local raw = trim(rawPrompt) if raw == "" then return raw end local ok, body = httpJson(API_BASE .. "/enhance", "POST", { prompt = raw }) if not ok then return raw end local decoded = nil local okd = pcall(function() decoded = HttpService:JSONDecode(body) end) if not okd or type(decoded) ~= "table" then return raw end return trim(decoded.enhanced or raw) end -- Live preview: instances + terrain only (no scripts). Caller handles debounce / clear. local function applyPreviewInstances(decoded) if type(decoded) ~= "table" then return end if type(decoded.instances) ~= "table" or #decoded.instances == 0 then return end local d = table.clone(decoded) d.scripts = {} if uiState.lastRoute == "visual_build" then stripVisualBuildPlayableDefaults(d) end pcall(function() applyTerrain(d) end) pcall(function() insertInstances(d) end) end -- NOTE: Roblox HttpService does not support true streaming/SSE. -- We use job + polling to simulate streaming in Studio. local function pollJob(jobId, headerText, source) local cursor = 0 local buffer = "" -- Keep server "enhanced" text out of `buffer` so JSON decode / Luau extract stay valid. local enhancedText = "" local enhancedShown = false local lastPreviewUpdate = 0 local lastPreviewApplyAt = 0 local previewCleared = false uiState.lastJobId = jobId local function maybeApplyLivePreview(decoded) if type(decoded) ~= "table" then return end if type(decoded.instances) ~= "table" or #decoded.instances == 0 then return end local now = tick() if (now - lastPreviewApplyAt) < 0.35 then return end lastPreviewApplyAt = now if replacePrevious and not previewCleared then clearGenerated() previewCleared = true end applyPreviewInstances(decoded) end -- Fake instant feel (Preview tab) even if backend is slow. uiState.fakePhase = "start" setTab("code") if uiState.lastRoute == "visual_build" then setPreview("⚡ Building visuals in viewport…") elseif uiState.lastRoute == "gameplay" then setPreview("⚡ Streaming Luau…") else setPreview("⚡ Generating…") end task.delay(0.3, function() if uiState.isGenerating and uiState.lastJobId == jobId then uiState.fakePhase = "placing" if uiState.lastRoute == "visual_build" then setPreview("⚡ Placing parts in viewport…") else setPreview("⚡ Generating Luau…") end end end) task.delay(0.6, function() if uiState.isGenerating and uiState.lastJobId == jobId then uiState.fakePhase = "scripts" if uiState.lastRoute == "visual_build" then setPreview("⚡ Finishing visuals…") else setPreview("⚡ Streaming code…") end end end) -- Live preview in Explorer while streaming (disabled so it never runs). local function getOrCreateLivePreviewScript() local folder = ServerScriptService:FindFirstChild("VibeCoderAI") if not folder then folder = Instance.new("Folder") folder.Name = "VibeCoderAI" folder.Parent = ServerScriptService end local s = folder:FindFirstChild("VC_LiveStream") if not s or not s:IsA("Script") then if s then pcall(function() s:Destroy() end) end s = Instance.new("Script") s.Name = "VC_LiveStream" s.Disabled = true s.Source = "-- Live stream preview (disabled)\n" s.Parent = folder end return s end local livePreviewScript = getOrCreateLivePreviewScript() local function updateLivePreview(now) -- Throttle Explorer updates; frequent Source writes can stutter Studio. if (now - lastPreviewUpdate) < 0.25 then return end lastPreviewUpdate = now -- Visual builds stream JSON — do not mirror it into a Script (user asked: no code dump). if uiState.lastRoute == "visual_build" then pcall(function() livePreviewScript.Source = "-- VibeCoder AI: visual build (JSON hidden in UI; see viewport)\n" end) return end local previewBody = tryExtractLuau(buffer) or buffer local clipped = previewBody -- Keep it bounded so the Script doesn't grow forever on huge outputs. if #clipped > 18000 then clipped = clipped:sub(#clipped - 18000) clipped = "(clipped to last 18k chars)\n" .. clipped end pcall(function() if tryExtractLuau(buffer) then livePreviewScript.Source = "-- Live stream Luau preview (disabled)\n-- Job: " .. tostring(jobId) .. "\n\n" .. clipped else livePreviewScript.Source = "-- Live stream preview (disabled)\n-- Job: " .. tostring(jobId) .. "\n\n--[[\n" .. clipped .. "\n]]" end end) end local function dotWave() local n = math.floor(tick() * 3) % 4 if n == 0 then return "" elseif n == 1 then return "." elseif n == 2 then return ".." end return "..." end local function statusBanner(phase, jobStatus) local p = "" if type(phase) == "string" then p = phase end local s = "" if type(jobStatus) == "string" then s = jobStatus end local line = "⚡ Working on your game" if s == "queued" or p == "queued" then line = "⏳ Starting — job picked up by the worker" elseif p == "enhancing" then line = "🧠 Step 1/2 — Enhancing your prompt" elseif p == "generating" then if uiState.lastRoute == "visual_build" then line = "⚡ Building visuals (summary only — watch the viewport)" elseif uiState.lastRoute == "gameplay" then line = "⚡ Streaming Luau code" else line = "⚡ Working — generating" end elseif p == "complete" or s == "done" then line = "✅ Finished" elseif s == "error" then line = "❌ Error" elseif s == "cancelled" then line = "⛔ Cancelled" end local hint = "\n💡 Text streams below the line." if uiState.lastRoute == "visual_build" then hint = "\n💡 No raw JSON or code here — live preview in the 3D view." elseif uiState.lastRoute == "gameplay" then hint = "\n💡 Luau streams below (gameplay jobs)." end return line .. dotWave() .. hint end local function appendEnhancedSpec(text) local t = tostring(text or ""):gsub("^%s+", ""):gsub("%s+$", "") if t == "" or enhancedShown then return end enhancedShown = true enhancedText = "🧠 Enhanced prompt (server):\n" .. t .. "\n\n────────────────\n\n" end setOutput((headerText or "") .. "\n────────────────\n\n") updateLivePreview(tick()) setTab("code") while not cancelled do local ok2, body2 = httpJson(API_BASE .. "/jobs/" .. jobId .. "?cursor=" .. tostring(cursor), "GET", nil) if cancelled then break end if not ok2 then setOutput(statusBanner(nil, "error") .. "\n────────────────\n\n" .. buffer .. "\n\nError: " .. tostring(body2)) return nil, false end local state = HttpService:JSONDecode(body2) appendEnhancedSpec(state.enhanced) if type(state.route) == "string" and (state.route == "visual_build" or state.route == "gameplay") then uiState.lastRoute = state.route end local chunks = state.chunks or {} for _, raw in ipairs(chunks) do if cancelled then break end local okc, c = pcall(function() return HttpService:JSONDecode(raw) end) if okc and type(c) == "table" then if c.type == "delta" and c.text then buffer ..= c.text elseif c.type == "enhanced" and c.text then appendEnhancedSpec(c.text) elseif c.type == "preview" and c.text then local okp, decodedPreview = pcall(function() return HttpService:JSONDecode(c.text) end) if okp and type(decodedPreview) == "table" then maybeApplyLivePreview(decodedPreview) end end end end if cancelled then break end -- Streaming JSON: opportunistically decode partial buffer and show instances in Workspace. local _blobStream, decodedStream = safeDecodeProjectJson(buffer) if decodedStream then maybeApplyLivePreview(decodedStream) end cursor = state.cursor or cursor local bodyText = buffer if type(state.fullOutput) == "string" and state.fullOutput ~= "" then bodyText = state.fullOutput end local luauOnly = tryExtractLuau(bodyText) local displayText = "" local route = uiState.lastRoute if route == "visual_build" and not luauOnly then local _blob, decodedNow = safeDecodeProjectJson(bodyText) if decodedNow then displayText = formatVisualBuildSummary(decodedNow) else displayText = "⚡ Building visuals…\n\nWatch the 3D viewport — parts appear as they are generated.\nThis panel stays free of raw JSON or code." end elseif luauOnly then displayText = luauOnly elseif route == "gameplay" then local partialLuau = tryExtractLuauStreaming(bodyText) displayText = partialLuau or bodyText if trim(displayText) == "" then displayText = "(streaming Luau…)" end else local _blob, decodedNow = safeDecodeProjectJson(bodyText) if decodedNow then displayText = formatPreview(decodedNow) else local partialLuau = tryExtractLuauStreaming(bodyText) if partialLuau then displayText = partialLuau else displayText = "(streaming… waiting for preview)" end end end if displayText:sub(1, 1) == "{" then displayText = "(streaming… waiting for preview)" end if bodyText == "" then if route == "visual_build" then displayText = "⚡ Starting visual build…" else displayText = "(waiting for first characters…)" end end local prefix = "" if route ~= "visual_build" and enhancedText ~= "" then prefix = enhancedText end setOutput(statusBanner(state.phase, state.status) .. "\n────────────────\n\n" .. prefix .. displayText) updateLivePreview(tick()) if state.status == "done" then lastJobSource = source or "plugin_generate" local finalText = buffer if type(state.fullOutput) == "string" and state.fullOutput ~= "" then finalText = state.fullOutput end -- Save last project JSON (for Improve) only when decode is a real project payload. local blob, decoded = safeDecodeProjectJson(finalText) if blob and decoded then setLastProjectJson(blob) end uiState.lastRawOutput = finalText uiState.lastProjectJson = blob uiState.lastDecoded = decoded local finalLuau = tryExtractLuau(finalText) -- If we have a decoded project JSON, prefer applying that (instances + scripts), -- not the extracted Luau-only view of scripts. uiState.lastLuau = (decoded ~= nil) and nil or finalLuau uiState.canApply = (decoded ~= nil) or (finalLuau ~= nil) refreshActionStates() -- The API can intentionally return instances-only JSON with scripts=[] (e.g. "create a red block"). -- In that case, show a helpful summary instead of "no scripts found", since Apply will still work. local function finalDisplayText() if finalLuau and trim(finalLuau) ~= "" then return finalLuau end if decoded and uiState.lastRoute == "visual_build" then local preview = formatVisualBuildSummary(decoded) local warn = "" local ins = decoded.instances if type(ins) ~= "table" or #ins == 0 then warn = "\n\n⚠️ Expected visuals; none in instances. Regenerate with: 'include visible instances in JSON'." end return preview .. warn end if decoded then local preview = formatPreview(decoded) local tailNote = "\n\n(Instances-only output — no scripts were required for this request.)" return preview .. tailNote end return "(done, but no code was found in the output)" end local tail = "\n\n✅ Ready to apply.\nClick Apply 🚀 to insert into your game." -- Keep Code view Luau-only even when we have full JSON. setOutput(statusBanner("complete", "done") .. "\n────────────────\n\n" .. finalDisplayText() .. tail) setTab("code") -- Leave the live preview script around, but overwrite with final output. updateLivePreview(tick() + 1) return finalText, uiState.canApply end if state.status == "error" then setOutput( statusBanner(nil, "error") .. "\n────────────────\n\n" .. buffer .. "\n\nError: " .. tostring(state.error or "unknown") ) return nil, false end if state.status == "cancelled" then setOutput(statusBanner(nil, "cancelled") .. "\n────────────────\n\n" .. buffer .. "\n\n-- Stopped --") return nil, false end if cancelled then break end task.wait(POLL_INTERVAL) -- Opportunistic auto-fix check while we already have a heartbeat. runAutoFixIfNeeded() end -- If the user clicked Stop, also cancel the server job so credits/time aren't wasted. pcall(function() httpJson(API_BASE .. "/jobs/" .. jobId .. "/cancel", "POST", {}) end) setOutput(statusBanner(nil, "cancelled") .. "\n────────────────\n\n" .. buffer .. "\n\n-- Stopped --") return nil, false end -- Forward declare so Generate can auto-apply after polling completes. local applyToGame local function generate() cancelled = false if uiState.isGenerating then return end local now = tick() if uiState._lastGenClick and (now - uiState._lastGenClick) < 0.6 then return end uiState._lastGenClick = now -- Snapshot before we clear state (improve-job needs previous Luau / context). local snapshotLuau = uiState.lastLuau local trimmedLuauSnapshot = type(snapshotLuau) == "string" and trim(snapshotLuau) or "" local haveLuauPrevious = trimmedLuauSnapshot ~= "" if getToken() == "" then setOutput( "401 = missing token.\n\n" .. "Paste your token in the field above, or run in PowerShell:\n" .. '$b = @{ email="you@mail.com"; password="yourpass" } | ConvertTo-Json\n' .. "Invoke-RestMethod -Method Post -Uri " .. API_BASE .. "/auth -ContentType application/json -Body $b | % token\n" ) return end uiState.isGenerating = true uiState.lastJobId = nil -- until we have a real job id (prevents Stop from cancelling an old job during HTTP). uiState.canApply = false uiState.lastRawOutput = nil uiState.lastProjectJson = nil uiState.lastDecoded = nil uiState.lastLuau = nil uiState.lastRoute = nil refreshActionStates() setOutput("🧠 Understanding your prompt…\n") setPreview("🧠 Understanding…") setTab("code") local rawPrompt = trim(promptBox.Text) if rawPrompt == "" then uiState.isGenerating = false refreshActionStates() setOutput("Type a prompt first.") return end uiState.lastPrompt = rawPrompt -- Follow the user's prompt literally by default. The Enhance button remains available when wanted. local enhancedPrompt = rawPrompt if cancelled then uiState.isGenerating = false refreshActionStates() setOutput("⛔ Stopped by you.") return end local projectJson = getLastProjectJson() local haveProject = projectJson ~= nil and trim(projectJson) ~= "" local haveContext = haveProject or haveLuauPrevious local requestFormat = chooseRequestFormat(rawPrompt) local startFresh = true if wantsExplicitFreshStart(rawPrompt) then startFresh = true elseif not haveContext then startFresh = true else startFresh = shouldStartFresh(rawPrompt, haveContext) end if (not startFresh) and requestFormat == "luau" and not haveLuauPrevious then startFresh = true end -- Improve must use the same shape as what we have on disk (Luau vs JSON), not the small follow-up phrase alone. if not startFresh then if haveLuauPrevious then requestFormat = "luau" elseif haveProject then requestFormat = "json" end end -- Visual props must use JSON generate (preview chunks + partial JSON decode), not Luau improve or JSON improve. -- 1) After Luau gameplay, "add a tree" was wrongly Luau-improve (code only, no 3D preview). -- 2) Stale saved project JSON + "add a …" was JSON-improve (worker has no early preview on improve path). -- 3) Standalone "add a tree" / "also add …" visual prompts always get a fresh instance generate. local routeForPrompt = classifyGenerationRoute(rawPrompt) if routeForPrompt == "visual_build" then local x = trim(rawPrompt):lower() local addLike = (x:match("^add%s") ~= nil) or x:find("add a ", 1, true) or x:find("add an ", 1, true) or x:find("please add", 1, true) or x:find("can you add", 1, true) or (x:match("^also%s") ~= nil) if (haveLuauPrevious and not haveProject) or addLike then startFresh = true requestFormat = "json" end end setPreview("⚡ Starting…") local ok, body if startFresh then setOutput("⚡ Sending to your server…\n") lastJobSource = "plugin_generate" ok, body = httpJson(API_BASE .. "/generate-job", "POST", { prompt = enhancedPrompt, mode = currentMode, format = requestFormat, }) else lastJobSource = "plugin_improve" setOutput( "⚡ Sending to your server…\n\n" .. "📎 Improve mode: only the text in the prompt box is sent as the change request. Your last Applied Luau (or saved JSON project) is the starting point — you do not need to paste the whole game again.\n" ) if requestFormat == "luau" and haveLuauPrevious then ok, body = httpJson(API_BASE .. "/improve-job", "POST", { format = "luau", previousCode = trimmedLuauSnapshot, request = enhancedPrompt, mode = currentMode, }) else ok, body = httpJson(API_BASE .. "/improve-job", "POST", { format = "json", projectJson = projectJson, request = enhancedPrompt, mode = currentMode, }) end end if not ok then uiState.isGenerating = false refreshActionStates() if isPaymentRequired(body) then setOutOfCreditsOutput(body) else showUpgradeBanner(false) setOutput("Error: " .. tostring(body)) end return end showUpgradeBanner(false) local decoded = HttpService:JSONDecode(body) local jobId = decoded.jobId if not jobId then uiState.isGenerating = false refreshActionStates() setOutput("Error: missing jobId\n") return end local r = decoded.route if r == "visual_build" or r == "gameplay" then uiState.lastRoute = r else uiState.lastRoute = classifyGenerationRoute(rawPrompt) end local header = "" if startFresh then header = "✨ Job #" .. tostring(jobId) .. " started (" .. string.upper(currentMode) .. " mode)." else header = "✨ Improve job #" .. tostring(jobId) .. " started (" .. string.upper(currentMode) .. " mode)." end header ..= "\n⏱ Fast: ~15–45s. Full: longer but richer." if uiState.lastRoute == "visual_build" then header ..= "\nMode: VisualBuild (instances)" elseif uiState.lastRoute == "gameplay" then header ..= "\nMode: Gameplay (instances + scripts)" else header ..= "\nMode: Gameplay (instances + scripts)" end uiState.lastJobId = jobId local _finalText, canApply = pollJob(jobId, header, startFresh and "plugin_generate" or "plugin_improve") uiState.isGenerating = false refreshActionStates() -- 1-click: auto-apply when generation succeeded. if canApply and not cancelled then showToast("🚀 Applying…") -- Defer so UI refresh/toasts paint before heavy workspace edits. task.defer(function() local okd, errd = pcall(function() if applyToGame then applyToGame() end end) if not okd then showToast("❌ Apply crashed — see Output") warn("[VibeCoderAI] applyToGame: " .. tostring(errd)) end end) end end local function enhance() cancelled = false if getToken() == "" then setOutput("Paste API token in the field above first (same as Generate).") return end setOutput("Enhancing prompt...\n") local ok, body = httpJson(API_BASE .. "/enhance", "POST", { prompt = promptBox.Text }) if not ok then setOutput("Error: " .. tostring(body)) return end local decoded = HttpService:JSONDecode(body) promptBox.Text = decoded.enhanced or promptBox.Text setOutput("Enhanced prompt applied.\n") end local function clearBuild() cancelled = false uiState.isGenerating = false uiState.canApply = false uiState.lastJobId = nil uiState.lastRawOutput = nil uiState.lastProjectJson = nil uiState.lastDecoded = nil uiState.lastLuau = nil uiState.lastPrompt = nil uiState.lastRoute = nil refreshActionStates() stopLogWatch() local removed = clearGenerated() + deletePreviousGeneratedFolders() setLastProjectJson("") setOutput("") setPreview("") showToast("🧹 Reset complete (new idea)") setOutput("Reset complete. Next Generate starts a new idea.") end local function stop() if not uiState.isGenerating then return end -- IMPORTANT: keep uiState.isGenerating true until pollJob returns — otherwise Generate re-enters, -- clears `cancelled`, and the old poll loop keeps running (Stop feels "broken"). cancelled = true local jobId = uiState.lastJobId if jobId then -- Synchronous cancel so the worker sees it before the next poll finishes. pcall(function() httpJson(API_BASE .. "/jobs/" .. tostring(jobId) .. "/cancel", "POST", {}) end) end showToast("⛔ Stopping…") end -- ServerScripts (InsertService, etc.) do not run in Studio Edit mode — only after Play. local function decodedUsesInsertServiceLoadAsset(decoded) if type(decoded) ~= "table" then return false end local scripts = decoded.scripts if type(scripts) ~= "table" then return false end for _, spec in ipairs(scripts) do if type(spec) == "table" then local code = tostring(spec.code or "") if string.find(code, "InsertService", 1, true) and string.find(code, "LoadAsset", 1, true) then return true end end end return false end applyToGame = function() if uiState.isGenerating then return end if not uiState.lastLuau and not uiState.lastDecoded then showToast("⚠️ Nothing to apply yet") return end local ok, msg = false, "Nothing to apply." -- Prefer applying decoded JSON project (instances + scripts) when present. if uiState.lastDecoded then ok, msg = applyDecodedToGame(uiState.lastDecoded) elseif uiState.lastLuau then if replacePrevious then deletePreviousGeneratedFolders() clearGenerated() end ok, msg = applyLuauToGame(uiState.lastLuau) end if ok then showToast("✅ Build inserted successfully") local currentOut = outBox.Text or "" local nextSteps = "\n\n── Next steps ──\n" .. "Play (▶) to test. Then type a short change → Generate → Apply.\n" if not string.find(currentOut, "── Next steps ──", 1, true) then setOutput(currentOut ~= "" and (currentOut .. nextSteps) or nextSteps) currentOut = outBox.Text or "" end if uiState.lastLuau and uiState.lastRoute == "gameplay" then local tip = "\n\n── Studio tip ──\n" .. "Gameplay Luau lives in **ServerScriptService/VibeCoderAI/VC_Main**. It does **not** run in Edit mode.\n" .. "Press **Play (▶)** to run the server script — then you should see **VC_World**, coins, and ground appear.\n" .. "While editing, the 3D view only shows what was inserted as **instances** (JSON builds), not runtime-created Parts.\n" if not string.find(currentOut, "── Studio tip ──", 1, true) then setOutput(currentOut ~= "" and (currentOut .. tip) or tip) currentOut = outBox.Text or "" end end if uiState.lastDecoded and decodedUsesInsertServiceLoadAsset(uiState.lastDecoded) then local note = "\n\n── Roblox Studio tip ──\n" .. "This build calls **InsertService:LoadAsset** in a ServerScript. That code runs only after you press **Play (▶)** — not while editing.\n" .. "Right after Apply, Studio only shows what is in **instances** in the JSON. If trees or cars are missing here, press Play to run the script, or Generate again with: \"add visible stand-in Parts in instances\".\n" if not string.find(currentOut, "── Roblox Studio tip ──", 1, true) then setOutput(currentOut ~= "" and (currentOut .. note) or note) end end if AUTO_FIX_ENABLED then startLogWatch() task.delay(0.6, function() -- Give scripts a moment to start before evaluating errors. runAutoFixIfNeeded() end) end else showToast("❌ Apply failed") setTab("code") setOutput((outBox.Text ~= "" and (outBox.Text .. "\n\n") or "") .. "Apply error: " .. tostring(msg)) end end genBtn.MouseButton1Click:Connect(generate) clearBtn.MouseButton1Click:Connect(clearBuild) enhBtn.MouseButton1Click:Connect(enhance) stopBtn.MouseButton1Click:Connect(stop) applyBtn.MouseButton1Click:Connect(applyToGame) button.Click:Connect(function() widget.Enabled = not widget.Enabled end)