-- Services local HttpService = game:GetService("HttpService") local TweenService = game:GetService("TweenService") local UserInputService = game:GetService("UserInputService") local RunService = game:GetService("RunService") local CoreGui = game:GetService("CoreGui") local Players = game:GetService("Players") -- Singleton Check if getgenv().GeminiChatLoaded then if getgenv().GeminiChatUnload then getgenv().GeminiChatUnload() end end getgenv().GeminiChatLoaded = true -- Generation State local IsGenerating = false local StopGeneration = false -- Configuration local Config = { ApiKey = "", Model = "gemini-3-flash-preview", -- Reverted History = {} } print("[Gemini AI] Loading UI...") -- Theme & Design System (ChatGPT-like) local Theme = { Background = Color3.fromRGB(52, 53, 65), -- Main Chat Background Sidebar = Color3.fromRGB(32, 33, 35), -- Sidebar/Header InputBG = Color3.fromRGB(64, 65, 79), -- Input Field UserBubble = Color3.fromRGB(32, 33, 35), -- User Bubble (Darker) AIBubble = Color3.fromRGB(68, 70, 84), -- AI Bubble (Contrast) TextPrimary = Color3.fromRGB(236, 236, 241), -- Main Text TextSecondary = Color3.fromRGB(142, 142, 160), -- Subtitles/Placeholders Accent = Color3.fromRGB(16, 163, 127), -- ChatGPT Green CodeBlock = Color3.black, -- Code Block BG Scrollbar = Color3.fromRGB(86, 88, 105), Radius = UDim.new(0, 6) } local function MarkdownToRichText(text) -- Escaping text = text:gsub("&", "&"):gsub("<", "<"):gsub(">", ">"):gsub("\"", """):gsub("'", "'") -- Bold **text** or __text__ text = text:gsub("%*%*(.-)%*%*", "%1") text = text:gsub("__(.-)__", "%1") -- Italic *text* or _text_ text = text:gsub("%*(.-)%*", "%1") text = text:gsub("_(.-)_", "%1") -- Headers ### Text (Line aware) text = text:gsub("(\n?)###%s*(.-)(\n?)", "%1%2%3") text = text:gsub("(\n?)##%s*(.-)(\n?)", "%1%2%3") text = text:gsub("(\n?)#%s*(.-)(\n?)", "%1%2%3") -- Bullet points text = text:gsub("\n%* ", "\n • ") text = text:gsub("^%* ", " • ") return text end local function Typewrite(label, text, speed) label.Text = "" local displayed = "" -- UTF-8 safe iteration for _, code in utf8.codes(text) do if StopGeneration then break end displayed = displayed .. utf8.char(code) label.Text = displayed -- Auto-scroll if ChatScroll then ChatScroll.CanvasPosition = Vector2.new(0, ChatScroll.AbsoluteCanvasSize.Y) end -- Speed control (approximate) if speed and speed > 0 then task.wait(speed) else task.wait(0.005) end end label.Text = text -- Ensure it's fully set end -- Utility Functions local function Create(className, properties, children) local instance = Instance.new(className) for k, v in pairs(properties or {}) do instance[k] = v end if children then for _, child in ipairs(children) do child.Parent = instance end end return instance end local function Tween(instance, info, goals) local tween = TweenService:Create(instance, info, goals) tween:Play() return tween end local function GetTextSize(text, font, fontSize, maxWidth) return game:GetService("TextService"):GetTextSize(text, fontSize, font, Vector2.new(maxWidth, 10000)) end -- UI Construction local ScreenGui = Create("ScreenGui", { Name = "GeminiChatUI", Parent = (RunService:IsStudio() and Players.LocalPlayer:WaitForChild("PlayerGui")) or CoreGui, ResetOnSpawn = false, ZIndexBehavior = Enum.ZIndexBehavior.Sibling }) local MainFrame = Create("Frame", { Name = "MainFrame", Parent = ScreenGui, BackgroundColor3 = Theme.Background, Size = UDim2.fromOffset(500, 700), Position = UDim2.new(0.5, 0, 0.5, -350), -- Centered top AnchorPoint = Vector2.new(0.5, 0), -- Anchor to TOP BorderSizePixel = 0, ClipsDescendants = true }, { Create("UICorner", { CornerRadius = Theme.Radius }), Create("UIStroke", { Color = Color3.fromRGB(80, 80, 80), Thickness = 1, Transparency = 0.5 }) }) -- Dragging Logic local Dragging, DragInput, DragStart, StartPos -- Forward Declare for Minimize local ChatScroll, InputContainer MainFrame.InputBegan:Connect(function(input) if input.UserInputType == Enum.UserInputType.MouseButton1 then Dragging = true DragStart = input.Position StartPos = MainFrame.Position input.Changed:Connect(function() if input.UserInputState == Enum.UserInputState.End then Dragging = false end end) end end) MainFrame.InputChanged:Connect(function(input) if input.UserInputType == Enum.UserInputType.MouseMovement then DragInput = input end end) UserInputService.InputChanged:Connect(function(input) if input == DragInput and Dragging then local delta = input.Position - DragStart Tween(MainFrame, TweenInfo.new(0.1), { Position = UDim2.new(StartPos.X.Scale, StartPos.X.Offset + delta.X, StartPos.Y.Scale, StartPos.Y.Offset + delta.Y) }) end end) -- Header local Header = Create("Frame", { Name = "Header", Parent = MainFrame, Size = UDim2.new(1, 0, 0, 50), BackgroundColor3 = Theme.Sidebar, BorderSizePixel = 0 }, { Create("TextLabel", { Text = "Gemini AI", TextColor3 = Theme.TextPrimary, Font = Enum.Font.GothamBold, TextSize = 18, Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, Position = UDim2.fromOffset(15, 0), TextXAlignment = Enum.TextXAlignment.Left }), Create("UICorner", { CornerRadius = UDim.new(0, 6) }), -- Rounded top (visual fix handled by Frame clipping) Create("Frame", { -- Bottom border fix for rounded header Size = UDim2.new(1, 0, 0, 5), Position = UDim2.new(0, 0, 1, -5), BackgroundColor3 = Theme.Sidebar, BorderSizePixel = 0 }) }) -- Close Button local CloseBtn = Create("TextButton", { Parent = Header, Text = "×", TextColor3 = Theme.TextSecondary, Font = Enum.Font.Gotham, TextSize = 24, BackgroundTransparency = 1, Size = UDim2.fromOffset(40, 40), Position = UDim2.new(1, -45, 0.5, -20), AutoButtonColor = false }) CloseBtn.MouseEnter:Connect(function() Tween(CloseBtn, TweenInfo.new(0.2), {TextColor3 = Color3.new(1, 0.3, 0.3)}) end) CloseBtn.MouseLeave:Connect(function() Tween(CloseBtn, TweenInfo.new(0.2), {TextColor3 = Theme.TextSecondary}) end) CloseBtn.MouseButton1Click:Connect(function() getgenv().GeminiChatUnload() end) -- Minimize Button local Minimized = false local MinimizeBtn = Create("TextButton", { Parent = Header, Text = "-", TextColor3 = Theme.TextSecondary, Font = Enum.Font.Gotham, TextSize = 24, BackgroundTransparency = 1, Size = UDim2.fromOffset(40, 40), Position = UDim2.new(1, -85, 0.5, -20), AutoButtonColor = false }) MinimizeBtn.MouseEnter:Connect(function() Tween(MinimizeBtn, TweenInfo.new(0.2), {TextColor3 = Theme.Accent}) end) MinimizeBtn.MouseLeave:Connect(function() Tween(MinimizeBtn, TweenInfo.new(0.2), {TextColor3 = Theme.TextSecondary}) end) MinimizeBtn.MouseButton1Click:Connect(function() Minimized = not Minimized local targetSize = Minimized and UDim2.fromOffset(500, 50) or UDim2.fromOffset(500, 700) Tween(MainFrame, TweenInfo.new(0.3, Enum.EasingStyle.Quart, Enum.EasingDirection.Out), {Size = targetSize}) -- Delay visibility for smooth clipping effect or just toggle if not Minimized then ChatScroll.Visible = true InputContainer.Visible = true MinimizeBtn.Text = "-" else MinimizeBtn.Text = "+" task.delay(0.2, function() if Minimized then ChatScroll.Visible = false InputContainer.Visible = false end end) end end) -- Visibility Toggle (Left Control) local Hidden = false UserInputService.InputBegan:Connect(function(input, gpe) if gpe then return end if input.KeyCode == Enum.KeyCode.LeftControl then Hidden = not Hidden ScreenGui.Enabled = not Hidden end end) -- Scroll Container for content ChatScroll = Create("ScrollingFrame", { Parent = MainFrame, Size = UDim2.new(1, 0, 1, -135), -- Adjusted for 50px header + 80px input + padding Position = UDim2.new(0, 0, 0, 50), BackgroundTransparency = 1, ScrollBarThickness = 6, ScrollBarImageColor3 = Theme.Scrollbar, CanvasSize = UDim2.new(0, 0, 0, 0), AutomaticCanvasSize = Enum.AutomaticSize.Y }, { Create("UIListLayout", { SortOrder = Enum.SortOrder.LayoutOrder, Padding = UDim.new(0, 15), HorizontalAlignment = Enum.HorizontalAlignment.Center }), Create("UIPadding", { PaddingTop = UDim.new(0, 10), PaddingBottom = UDim.new(0, 15), -- Slightly more padding at bottom PaddingLeft = UDim.new(0, 10), PaddingRight = UDim.new(0, 10) }) }) -- Input Area Redesign InputContainer = Create("Frame", { Parent = MainFrame, Size = UDim2.new(1, 0, 0, 80), Position = UDim2.new(0, 0, 1, -80), BackgroundTransparency = 1, BorderSizePixel = 0 }) local Pill = Create("Frame", { Parent = InputContainer, Size = UDim2.new(1, -30, 0, 50), Position = UDim2.new(0.5, 0, 0.5, 0), AnchorPoint = Vector2.new(0.5, 0.5), BackgroundColor3 = Theme.InputBG, BackgroundTransparency = 0.2, BorderSizePixel = 0 }, { Create("UICorner", { CornerRadius = UDim.new(1, 0) }), -- Oval shape Create("UIStroke", { Color = Color3.fromRGB(255, 255, 255), Transparency = 0.9, Thickness = 1 }) }) local InputBox = Create("TextBox", { Parent = Pill, Size = UDim2.new(1, -60, 1, 0), Position = UDim2.new(0, 20, 0, 0), BackgroundTransparency = 1, TextColor3 = Theme.TextPrimary, PlaceholderText = "Ask anything...", -- Updated as per image style PlaceholderColor3 = Theme.TextSecondary, Font = Enum.Font.Gotham, TextSize = 14, TextWrapped = true, ClearTextOnFocus = false, TextXAlignment = Enum.TextXAlignment.Left, TextYAlignment = Enum.TextYAlignment.Center, Text = "" }) local SendBtn = Create("ImageButton", { Parent = Pill, Size = UDim2.fromOffset(32, 32), Position = UDim2.new(1, -9, 0.5, 0), AnchorPoint = Vector2.new(1, 0.5), BackgroundTransparency = 1, -- Removed white background Image = "rbxassetid://123749085081505", -- Your provided Send ID ImageColor3 = Color3.fromRGB(255, 255, 255), ScaleType = Enum.ScaleType.Fit, AutoButtonColor = true }) local function SetGenerating(state) IsGenerating = state if state then SendBtn.Image = "rbxassetid://109356409844353" -- Your provided Stop ID StopGeneration = false else SendBtn.Image = "rbxassetid://123749085081505" -- Your provided Send ID end end -- Helper to make text selectable/copyable if possible (TextBox read-only trick) or just Label -- Roblox TextLabels are not copyable. -- We will implement a special rendering for code blocks. local function ProcessText(text) local parts = {} local pattern = "```(.-)```" local lastPos = 1 -- Use find instead of gmatch to get positions correctly local startPos, endPos, code = text:find(pattern, lastPos) while startPos do -- Add preceding text local preText = text:sub(lastPos, startPos - 1) if #preText > 0 then table.insert(parts, {Type = "Text", Content = preText}) end -- Add code block (strip language identifier and trim) local cleanCode = code:gsub("^%w+\n", ""):gsub("^%s+", ""):gsub("%s+$", "") table.insert(parts, {Type = "Code", Content = cleanCode}) lastPos = endPos + 1 startPos, endPos, code = text:find(pattern, lastPos) end -- Add remaining text local remain = text:sub(lastPos) if #remain > 0 then table.insert(parts, {Type = "Text", Content = remain}) end if #parts == 0 then table.insert(parts, {Type = "Text", Content = text}) end for _, part in ipairs(parts) do if part.Type == "Text" then part.Content = MarkdownToRichText(part.Content) end end return parts end local function RenderMessage(role, text) local isUser = (role == "User") local bubbleColor = isUser and Theme.UserBubble or Theme.AIBubble -- Container for the whole message row local Row = Create("Frame", { Parent = ChatScroll, Size = UDim2.new(1, 0, 0, 0), -- Automatic size BackgroundTransparency = 1, AutomaticSize = Enum.AutomaticSize.Y }) -- Icon / Label Container if isUser then Create("TextLabel", { Parent = Row, Text = "You", TextColor3 = Theme.TextSecondary, Font = Enum.Font.GothamBold, TextSize = 12, Size = UDim2.new(1, -15, 0, 20), Position = UDim2.new(0, 0, 0, 0), BackgroundTransparency = 1, TextXAlignment = Enum.TextXAlignment.Right }) else Create("ImageLabel", { Parent = Row, Size = UDim2.fromOffset(30, 30), BackgroundTransparency = 1, Position = UDim2.new(0, 0, 0, 0), Image = "rbxassetid://119604644446321", -- User provided AI icon }, { Create("UICorner", { CornerRadius = UDim.new(0, 4) }) }) end -- Content Container (The Bubble) local ContentWidth = 380 local Bubble = Create("Frame", { Parent = Row, BackgroundColor3 = bubbleColor, Size = UDim2.new(0, ContentWidth, 0, 0), -- Auto Y Position = isUser and UDim2.new(1, -10, 0, 22) or UDim2.new(0, 40, 0, 0), AnchorPoint = isUser and Vector2.new(1, 0) or Vector2.new(0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 0 }, { Create("UICorner", { CornerRadius = UDim.new(0, 6) }), Create("UIListLayout", { SortOrder = Enum.SortOrder.LayoutOrder, Padding = UDim.new(0, 8) }), Create("UIPadding", { PaddingTop = UDim.new(0, 10), PaddingBottom = UDim.new(0, 10), PaddingLeft = UDim.new(0, 12), PaddingRight = UDim.new(0, 12) }) }) -- Generation Logic task.spawn(function() local parts = ProcessText(text) for _, part in ipairs(parts) do if StopGeneration then break end if part.Type == "Text" then local label = Create("TextLabel", { Parent = Bubble, Text = "", TextColor3 = Theme.TextPrimary, Font = Enum.Font.Gotham, TextSize = 14, TextWrapped = true, BackgroundTransparency = 1, Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, TextXAlignment = Enum.TextXAlignment.Left, RichText = true }) if isUser then label.Text = part.Content else Typewrite(label, part.Content) end elseif part.Type == "Code" then local codeBox local rawContent = part.Content -- Store raw content for copying local codeContainer = Create("Frame", { Parent = Bubble, BackgroundColor3 = Color3.fromRGB(30, 30, 30), BackgroundTransparency = 0.2, Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BorderSizePixel = 0 }, { Create("UICorner", { CornerRadius = UDim.new(0, 4) }), Create("UIPadding", { PaddingTop = UDim.new(0, 8), PaddingBottom = UDim.new(0, 8), PaddingLeft = UDim.new(0, 8), PaddingRight = UDim.new(0, 8) }), Create("UIListLayout", { SortOrder = Enum.SortOrder.LayoutOrder, Padding = UDim.new(0, 4) }) }) local codeHeader = Create("Frame", { Parent = codeContainer, Size = UDim2.new(1, 0, 0, 20), BackgroundTransparency = 1 }) Create("TextLabel", { Parent = codeHeader, Text = "Code", TextColor3 = Theme.TextSecondary, Font = Enum.Font.GothamBold, TextSize = 12, Size = UDim2.new(1, 0, 1, 0), BackgroundTransparency = 1, TextXAlignment = Enum.TextXAlignment.Left }) local copyBtn = Create("TextButton", { Parent = codeHeader, Text = "Copy", TextColor3 = Theme.TextSecondary, Font = Enum.Font.Gotham, TextSize = 12, Size = UDim2.new(0, 40, 1, 0), Position = UDim2.new(1, -45, 0, 0), BackgroundTransparency = 1 }) copyBtn.MouseButton1Click:Connect(function() local clipboardFunc = setclipboard or toclipboard or (Synapse and Synapse.write_clipboard) if clipboardFunc then clipboardFunc(rawContent) -- Use raw content instead of Box.Text to avoid truncation/corruption copyBtn.Text = "Copied!" task.wait(2) copyBtn.Text = "Copy" end end) codeBox = Create("TextBox", { Parent = codeContainer, Text = isUser and rawContent or "", TextColor3 = Color3.fromRGB(200, 200, 200), Font = Enum.Font.Code, TextSize = 13, TextWrapped = true, BackgroundTransparency = 1, Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, TextXAlignment = Enum.TextXAlignment.Left, TextYAlignment = Enum.TextYAlignment.Top, ClearTextOnFocus = false, TextEditable = false, MultiLine = true }) if not isUser then Typewrite(codeBox, rawContent) end end end if ChatScroll then ChatScroll.CanvasPosition = Vector2.new(0, ChatScroll.AbsoluteCanvasSize.Y) end end) end -- Final auto-scroll and animation bypass for sharpness task.spawn(function() task.wait() ChatScroll.CanvasPosition = Vector2.new(0, ChatScroll.AbsoluteCanvasSize.Y) end) -- Since CanvasGroup might crash some executors, we will just tween the Bubble transparency? -- Too complex for recursive tween. Let's just pop it in. local function SendMessage() if IsGenerating then StopGeneration = true SetGenerating(false) return end local text = InputBox.Text if text:gsub("%s+", "") == "" then return end InputBox.Text = "" SetGenerating(true) RenderMessage("User", text) table.insert(Config.History, { role = "user", parts = {{text = text}} }) -- Show Loading Indicator local loadingMsg = Create("TextLabel", { Parent = ChatScroll, Text = "Gemini is thinking...", TextColor3 = Theme.TextSecondary, Font = Enum.Font.Gotham, TextSize = 12, Size = UDim2.new(1, 0, 0, 20), BackgroundTransparency = 1 }) task.spawn(function() local httpRequest = (syn and syn.request) or http_request or request or (Fluxus and Fluxus.request) local url = string.format("https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent?key=%s", Config.Model, Config.ApiKey) local body = { contents = Config.History } local success, res if httpRequest then success, res = pcall(function() return httpRequest({ Url = url, Method = "POST", Headers = { ["Content-Type"] = "application/json" }, Body = HttpService:JSONEncode(body) }) end) else success, res = pcall(function() return { Body = HttpService:PostAsync(url, HttpService:JSONEncode(body), Enum.HttpContentType.ApplicationJson), StatusCode = 200 } end) end loadingMsg:Destroy() if success and res then local data = HttpService:JSONDecode(res.Body or "{}") if data and data.candidates and data.candidates[1] then local responseText = data.candidates[1].content.parts[1].text RenderMessage("Gemini", responseText) table.insert(Config.History, { role = "model", parts = {{text = responseText}} }) else RenderMessage("Gemini", "Error: " .. (data.error and data.error.message or "Unknown response")) end else RenderMessage("Gemini", "Connection Failed.") end SetGenerating(false) end) end SendBtn.MouseButton1Click:Connect(SendMessage) InputBox.FocusLost:Connect(function(enter) if enter then SendMessage() end end) -- Unload Logic getgenv().GeminiChatUnload = function() pcall(function() ScreenGui:Destroy() end) getgenv().GeminiChatLoaded = false end -- Welcome Message RenderMessage("Gemini", "Hello! I am **Gemini AI**. How can I help you today?") print("[Gemini AI] UI Loaded Successfully!")