diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml new file mode 100644 index 0000000000..2284b93570 --- /dev/null +++ b/.github/workflows/node.js.yml @@ -0,0 +1,31 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Node.js CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x, 22.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npm run build --if-present + - run: npm test diff --git a/app/Http/Controllers/VanillaAiChatController.php b/app/Http/Controllers/VanillaAiChatController.php new file mode 100644 index 0000000000..93709ba3b9 --- /dev/null +++ b/app/Http/Controllers/VanillaAiChatController.php @@ -0,0 +1,82 @@ +validate([ + 'message' => 'required|string|max:2000', + ]); + + $userMessage = $request->input('message'); + + // --- Option 1: Using OpenAI PHP Client (Recommended if set up) --- + // Ensure your OPENAI_API_KEY is in .env and you've run composer require openai-php/client + // Also, make sure you have published the OpenAI config if needed: php artisan vendor:publish --provider="OpenAI\Laravel\OpenAIServiceProvider" + try { + if (!config('openai.api_key') && !env('OPENAI_API_KEY')) { + // Fallback to a canned response if API key is not configured + return response()->json(['reply' => "Hello from Laravel! Your message was: \"{$userMessage}\". (AI not configured)"]); + } + + $response = OpenAI::chat()->create([ + 'model' => 'gpt-3.5-turbo', // Or gpt-4 if you have access + 'messages' => [ + ['role' => 'system', 'content' => 'You are a helpful assistant.'], + ['role' => 'user', 'content' => $userMessage], + ], + ]); + + $reply = $response->choices[0]->message->content; + return response()->json(['reply' => trim($reply)]); + + } catch (\Throwable $th) { + // Log the error for debugging + // Log::error('OpenAI API Error: ' . $th->getMessage()); + // Fallback to a canned response in case of an API error + // In a real app, you might want more sophisticated error handling + if (str_contains($th->getMessage(), 'cURL error 6')) { // Example: DNS resolution error + return response()->json(['reply' => "Sorry, I'm having trouble connecting to the AI service right now. Please check network or API key."]); + } + // Generic error if API key is missing or invalid + if (str_contains($th->getMessage(), 'Incorrect API key provided') || str_contains($th->getMessage(), 'You didn\'t provide an API key')) { + return response()->json(['reply' => "AI service connection error: Please ensure your API key is correctly configured in the .env file."]); + } + return response()->json(['reply' => "Sorry, an error occurred with the AI service. Your message was: \"{$userMessage}\". Error: " . $th->getMessage()]); + } + + // --- Option 2: Canned response (if you don't want to use an API yet) --- + // return response()->json(['reply' => "Hello from Laravel! You said: \"{$userMessage}\". This is a test response."]); + + // --- Option 3: Using Laravel's HTTP Client to call a generic AI API (more manual) --- + /* + $apiKey = env('SOME_OTHER_AI_API_KEY'); + if (!$apiKey) { + return response()->json(['reply' => "AI API key not configured."]); + } + try { + $response = Http::withHeaders([ + 'Authorization' => 'Bearer ' . $apiKey, + 'Content-Type' => 'application/json', + ])->post('https://api.example-ai.com/v1/chat', [ // Replace with actual API endpoint + 'prompt' => $userMessage, + 'max_tokens' => 150, + ]); + + if ($response->successful()) { + return response()->json(['reply' => $response->json()['choices'][0]['text'] ?? 'No reply generated.']); + } else { + return response()->json(['reply' => 'Failed to get response from AI.', 'details' => $response->body()]); + } + } catch (\Exception $e) { + return response()->json(['reply' => 'Error connecting to AI service: ' . $e->getMessage()]); + } + */ + } +} diff --git a/main.py b/main.py new file mode 100644 index 0000000000..f1cd15edb9 --- /dev/null +++ b/main.py @@ -0,0 +1,100 @@ +import tkinter as tk +from tkinter import scrolledtext, Menu, filedialog, messagebox +from patch_suite_integration import KNEAUXPatchSuiteIntegration # Import the integration class + +class AwesomeCodeEditor: + def __init__(self, root): + self.root = root + self.root.title("Awesome Code Editor") + self.root.geometry("800x600") + + self.text_area = scrolledtext.ScrolledText(self.root, wrap=tk.WORD, undo=True) + self.text_area.pack(fill=tk.BOTH, expand=True) + self.current_file_path = None # To keep track of the currently open file + + self.menu_bar = Menu(self.root) + self.root.config(menu=self.menu_bar) + + self.file_menu = Menu(self.menu_bar, tearoff=0) + self.menu_bar.add_cascade(label="File", menu=self.file_menu) + self.file_menu.add_command(label="Open", command=self.open_file) + self.file_menu.add_command(label="Save", command=self.save_file) + self.file_menu.add_command(label="Save As...", command=self.save_file_as) + self.file_menu.add_separator() + self.file_menu.add_command(label="Exit", command=self.root.quit) + + self.tools_menu = Menu(self.menu_bar, tearoff=0) + self.menu_bar.add_cascade(label="Tools", menu=self.tools_menu) + self.tools_menu.add_command(label="Patch with KNEAUX EDU", command=self.open_patch_suite_dialog) + + # Initialize patch suite integration object - created on demand + self.patch_suite_instance = None + + + def open_file(self): + filepath = filedialog.askopenfilename( + filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")] + ) + if not filepath: + return + try: + with open(filepath, "r", encoding="utf-8") as f: + content = f.read() + self.text_area.delete(1.0, tk.END) + self.text_area.insert(tk.END, content) + self.current_file_path = filepath # Store current file path + self.root.title(f"Awesome Code Editor - {filepath}") + except Exception as e: + messagebox.showerror("Error", f"Failed to open file: {e}") + self.current_file_path = None + + def save_file_as(self): + filepath = filedialog.asksaveasfilename( + defaultextension=".txt", + filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")] + ) + if not filepath: + return False # Indicate cancellation + try: + with open(filepath, "w", encoding="utf-8") as f: + content = self.text_area.get(1.0, tk.END) + f.write(content) + self.current_file_path = filepath # Update current file path + self.root.title(f"Awesome Code Editor - {filepath}") + return True # Indicate success + except Exception as e: + messagebox.showerror("Error", f"Failed to save file: {e}") + return False # Indicate failure + + def save_file(self): + if self.current_file_path: + try: + with open(self.current_file_path, "w", encoding="utf-8") as f: + content = self.text_area.get(1.0, tk.END) + f.write(content) + self.root.title(f"Awesome Code Editor - {self.current_file_path}") + except Exception as e: + messagebox.showerror("Error", f"Failed to save file: {e}") + else: + self.save_file_as() # If no current file, use Save As logic + + def open_patch_suite_dialog(self): + if self.patch_suite_instance is None or not self.patch_suite_instance.patch_window or not self.patch_suite_instance.patch_window.winfo_exists(): + self.patch_suite_instance = KNEAUXPatchSuiteIntegration(self.root) + self.patch_suite_instance.show_patch_dialog() + + # Optionally, if a file is open, you could pass its path or directory to the patch suite + # For example: + # current_dir = None + # if self.current_file_path: + # current_dir = Path(self.current_file_path).parent + # self.patch_suite_instance.show_patch_dialog(initial_dir=current_dir) + # This would require KNEAUXPatchSuiteIntegration to accept an initial_dir parameter. + +def main(): + root = tk.Tk() + editor = AwesomeCodeEditor(root) + root.mainloop() + +if __name__ == "__main__": + main() diff --git a/patch_suite_integration.py b/patch_suite_integration.py new file mode 100644 index 0000000000..34cddf075c --- /dev/null +++ b/patch_suite_integration.py @@ -0,0 +1,164 @@ +import tkinter as tk +from tkinter import filedialog, messagebox, Toplevel +import zipfile +import shutil # Added for removing temp directory +import re +from pathlib import Path + +PREMIUM_PATTERNS = [ + (re.compile(r"\b(isPremium|licenseValid|proVersion|hasProAccess)\s*=\s*false\b"), r"\1 = True"), + (re.compile(r"if\s*\(!?\s*(isPremium|licenseValid|proVersion|hasProAccess)\)"), r"if True"), # Simplified for editor context + (re.compile(r"checkLicense\s*\([^)]*\)\s*{[^}]*}"), r"checkLicense() { return True }"), # JS/TS like +] + +TEXT_FILE_EXTENSIONS = {'.js', '.ts', '.json', '.html', '.py', '.txt', '.cfg', '.xml', '.md', '.java', '.cs', '.cpp', '.c', '.php'} # Expanded list + +class KNEAUXPatchSuiteIntegration: + def __init__(self, editor_root): + self.editor_root = editor_root # Main editor window to anchor dialogs + self.patch_window = None + + def show_patch_dialog(self): + if self.patch_window and self.patch_window.winfo_exists(): + self.patch_window.lift() + return + + self.patch_window = Toplevel(self.editor_root) + self.patch_window.title("KNEAUX-COD3 EDU Patch Suite") + self.patch_window.geometry("450x200") + self.patch_window.transient(self.editor_root) # Make it a child of the main window + + tk.Label(self.patch_window, text="🔧 Select an extension (ZIP/CRX) or folder to patch:").pack(pady=15) + tk.Button(self.patch_window, text="📂 Select File or Folder", command=self.select_and_process).pack(pady=5) + tk.Button(self.patch_window, text="âœ–ī¸ Close", command=self.patch_window.destroy).pack(pady=10) + + + def select_and_process(self): + # Determine if it's a file or folder dialog needed + # For simplicity, starting with askopenfilename and then checking type + path_str = filedialog.askopenfilename( + parent=self.patch_window, + title="Select ZIP/CRX file or any file in a target folder", + filetypes=[("Supported Archives", "*.zip *.crx"), ("All files", "*.*")] + ) + + if not path_str: + # User cancelled + # Try asking for a directory if no file was selected, or provide a separate button for directory + path_str = filedialog.askdirectory( + parent=self.patch_window, + title="Select Folder to Patch" + ) + if not path_str: + messagebox.showinfo("Info", "No file or folder selected.", parent=self.patch_window) + return + + file_path = Path(path_str) + + if file_path.is_dir(): + self.scan_and_patch_folder(file_path) + elif file_path.is_file(): + if file_path.suffix in ['.zip', '.crx']: + temp_dir = file_path.parent / (file_path.stem + "_unzipped_temp") + if temp_dir.exists(): + shutil.rmtree(temp_dir) # Clean up previous attempt + temp_dir.mkdir(parents=True, exist_ok=True) + + try: + with zipfile.ZipFile(file_path, 'r') as zip_ref: + zip_ref.extractall(temp_dir) + + patched_files_count = self.scan_and_patch_folder(temp_dir, is_temp=True) + + if patched_files_count > 0: + self.rezip_folder(temp_dir, file_path.parent / (file_path.stem + "_patched" + file_path.suffix)) + elif patched_files_count == 0: + messagebox.showinfo("â„šī¸ No Changes", "No patchable code found in the archive.", parent=self.patch_window) + # If patched_files_count is None, an error occurred during patching + + except zipfile.BadZipFile: + messagebox.showerror("Error", "Invalid or corrupted ZIP/CRX file.", parent=self.patch_window) + except Exception as e: + messagebox.showerror("Error", f"Failed to process archive: {e}", parent=self.patch_window) + finally: + if temp_dir.exists(): + shutil.rmtree(temp_dir) # Clean up + elif file_path.suffix in TEXT_FILE_EXTENSIONS: + # Allow patching a single, currently open, or selected text file + if self.patch_single_file(file_path): + messagebox.showinfo("✅ Patch Complete", f"File '{file_path.name}' patched.", parent=self.patch_window) + else: + messagebox.showinfo("â„šī¸ No Changes", f"No patchable patterns found in '{file_path.name}'.", parent=self.patch_window) + + elif file_path.suffix in ['.exe', '.bin']: + messagebox.showinfo("Note", "Binary patching is not supported. Please select source files, folders, or archives (ZIP/CRX).", parent=self.patch_window) + else: + messagebox.showwarning("Unsupported File", f"File type '{file_path.suffix}' is not directly patchable. Select a folder, ZIP, or CRX.", parent=self.patch_window) + else: + messagebox.showerror("Error", "Selected path is not a valid file or folder.", parent=self.patch_window) + + + def patch_code_content(self, content): + original_content = content + for pattern, repl in PREMIUM_PATTERNS: + content = pattern.sub(repl, content) + return content, content != original_content + + def patch_single_file(self, file_path: Path): + try: + content = file_path.read_text(encoding='utf-8') + patched_content, changed = self.patch_code_content(content) + if changed: + file_path.write_text(patched_content, encoding='utf-8') + print(f"[+] Patched: {file_path}") + return True + except UnicodeDecodeError: + print(f"[!] Skipping binary or non-UTF-8 file: {file_path.name}") + except Exception as e: + messagebox.showerror("File Patch Error", f"Error patching file {file_path.name}:\n{e}", parent=self.patch_window or self.editor_root) + print(f"[!] Error processing {file_path.name}: {e}") + return False + + def scan_and_patch_folder(self, folder_path: Path, is_temp: bool = False): + patched_files_count = 0 + error_occurred = False + for path_object in folder_path.rglob('*'): + if path_object.is_file() and path_object.suffix in TEXT_FILE_EXTENSIONS: + if self.patch_single_file(path_object): + patched_files_count += 1 + # Check if patch_single_file returned None due to an error, if so, set error_occurred flag + # This check is a bit tricky as False means no patches applied, not necessarily an error. + # We rely on messagebox shown in patch_single_file for error reporting. + + if not is_temp: # Only show message for direct folder patching here. Archive patching has its own summary. + if patched_files_count > 0: + messagebox.showinfo("✅ Patch Complete", f"{patched_files_count} file(s) patched in folder '{folder_path.name}'.", parent=self.patch_window) + else: + messagebox.showinfo("â„šī¸ No Changes", f"No patchable code found in folder '{folder_path.name}'.", parent=self.patch_window) + + return patched_files_count # Return count for archive processing logic + + + def rezip_folder(self, folder_to_zip: Path, output_zip_path: Path): + try: + with zipfile.ZipFile(output_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: + for file in folder_to_zip.rglob('*'): + if file.is_file(): + zipf.write(file, file.relative_to(folder_to_zip)) + messagebox.showinfo("đŸ“Ļ Repack Complete", f"Patched archive saved to:\n{output_zip_path}", parent=self.patch_window) + except Exception as e: + messagebox.showerror("Repack Error", f"Failed to repack archive: {e}", parent=self.patch_window) + print(f"[!] Error repacking {folder_to_zip} to {output_zip_path}: {e}") + +# Example of how it might be instantiated from the main editor (for testing purposes) +if __name__ == '__main__': + root = tk.Tk() + root.title("Main Editor Window (Test)") + root.geometry("600x400") + + def open_patcher(): + patch_suite_instance = KNEAUXPatchSuiteIntegration(root) + patch_suite_instance.show_patch_dialog() + + tk.Button(root, text="Open KNEAUX Patcher", command=open_patcher).pack(pady=20) + root.mainloop() diff --git a/public/ai_chat.html b/public/ai_chat.html new file mode 100644 index 0000000000..0248307c22 --- /dev/null +++ b/public/ai_chat.html @@ -0,0 +1,113 @@ + + + + + + Vanilla JS AI Chat + + + +
+
AI Chat (Vanilla JS)
+
+ +
Hello! How can I help you today?
+
+
AI is typing...
+
+ + +
+
+ + + + diff --git a/public/ai_chat.js b/public/ai_chat.js new file mode 100644 index 0000000000..06c4ad8ad7 --- /dev/null +++ b/public/ai_chat.js @@ -0,0 +1,74 @@ +document.addEventListener('DOMContentLoaded', () => { + const messageInput = document.getElementById('messageInput'); + const sendButton = document.getElementById('sendButton'); + const chatMessages = document.getElementById('chatMessages'); + const typingIndicator = document.getElementById('typingIndicator'); + + // Function to add a message to the chat interface + function addMessage(text, sender) { + const messageDiv = document.createElement('div'); + messageDiv.classList.add('message', sender === 'user' ? 'user-message' : 'ai-message'); + messageDiv.textContent = text; + chatMessages.appendChild(messageDiv); + chatMessages.scrollTop = chatMessages.scrollHeight; // Scroll to the bottom + } + + // Function to handle sending a message + async function sendMessage() { + const messageText = messageInput.value.trim(); + if (messageText === '') { + return; + } + + addMessage(messageText, 'user'); + messageInput.value = ''; // Clear input field + typingIndicator.style.display = 'block'; // Show typing indicator + + try { + // Assuming Laravel is served at the root and api.php routes are prefixed with /api + // If your Laravel setup is different (e.g., in a subfolder or different port), adjust the URL. + const response = await fetch('/api/vanilla-ai-chat', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') // Optional: for CSRF if web routes are used + }, + body: JSON.stringify({ message: messageText }) + }); + + typingIndicator.style.display = 'none'; // Hide typing indicator + + if (!response.ok) { + const errorData = await response.json(); + addMessage(`Error: ${errorData.message || response.statusText}`, 'ai'); + if (errorData.reply) { // Show detailed reply from backend if available + addMessage(errorData.reply, 'ai'); + } + return; + } + + const data = await response.json(); + if (data.reply) { + addMessage(data.reply, 'ai'); + } else { + addMessage('Sorry, I could not get a reply.', 'ai'); + } + + } catch (error) { + typingIndicator.style.display = 'none'; // Hide typing indicator + console.error('Send message error:', error); + addMessage(`Network error or server is unreachable. (${error.message})`, 'ai'); + } + } + + // Event listeners + sendButton.addEventListener('click', sendMessage); + messageInput.addEventListener('keypress', (event) => { + if (event.key === 'Enter') { + sendMessage(); + } + }); + + // Initial focus on input + messageInput.focus(); +}); diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000000..234a23e0d0 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,22 @@ +get('/user', function (Request $request) { + return $request->user(); +}); + +Route::post('/vanilla-ai-chat', [VanillaAiChatController::class, 'chat']);