diff --git a/prompts/abstract_glowing_shape.txt b/prompts/abstract_glowing_shape.txt new file mode 100644 index 00000000000..216c1719a5b --- /dev/null +++ b/prompts/abstract_glowing_shape.txt @@ -0,0 +1,11 @@ +# Abstract Glowing Shape Prompt + +## Positive Prompt +abstract symmetrical sigil, radiant neon blue and molten gold ribbons weaving into a luminous geometric mandala, intricate circuitry details, volumetric light, ethereal glow, high-detail digital illustration, 8k, artstation trending + +## Negative Prompt +low detail, blurry, noisy, monochrome, muted colors, text, watermark, artifacts, grainy, distorted anatomy, asymmetry, cropped + +## Notes +- Pair with a high CFG scale to preserve crisp neon edges. +- Works well with Euler a or DPM++ 2M Karras samplers at 25-30 steps. diff --git a/scripts/python_learning_editor.py b/scripts/python_learning_editor.py new file mode 100644 index 00000000000..e8b6815c1dd --- /dev/null +++ b/scripts/python_learning_editor.py @@ -0,0 +1,652 @@ +"""Interactive Python learning editor for Windows users. + +This module implements a lightweight integrated learning environment that +combines a text editor, runnable console, contextual help, curated examples, +and quiz widgets. The user interface is powered by ``tkinter`` so it ships +with the standard CPython distribution on Windows. To launch the editor run:: + + python scripts/python_learning_editor.py + +The application is intentionally self‑contained to make it easy for beginners +to explore Python without installing extra dependencies or IDEs. +""" + +from __future__ import annotations + +import io +import keyword +import textwrap +from contextlib import redirect_stderr, redirect_stdout +from dataclasses import dataclass +from pathlib import Path +from typing import Callable +from tkinter import ( # type: ignore[attr-defined] + BOTH, + END, + LEFT, + RIGHT, + TOP, + VERTICAL, + Y, + Event, + Menu, + Text, + Tk, + Toplevel, +) +from tkinter import filedialog, messagebox, ttk + + +@dataclass +class QuizQuestion: + """Simple representation of a quiz question.""" + + prompt: str + answer: str + explanation: str + + +class PythonLearningEditor: + """Interactive text editor with built-in learning helpers. + + The implementation favours clarity over micro-optimisations so that the + source code itself can serve as a learning resource for beginners who read + through it. + """ + + AUTOCOMPLETE_SUGGESTIONS = sorted( + set(keyword.kwlist) + | { + "print", + "input", + "range", + "len", + "enumerate", + "list", + "dict", + "tuple", + "set", + "int", + "float", + "str", + "open", + "with", + "for", + "while", + "def", + "class", + "import", + "from", + } + ) + + KEYWORD_HELP = { + "for": "Iterate over items in a sequence. Syntax: for item in sequence:", + "while": "Repeat a block while a condition remains True.", + "def": "Define a function. Use parentheses for parameters.", + "class": "Define a class that bundles data and behaviour.", + "with": "Context manager for managing resources, e.g. files.", + "print": "Display text or variables. Useful for debugging.", + "import": "Bring modules or objects into the current namespace.", + "list": "Mutable ordered collection. Use [] to define literals.", + "dict": "Mapping of keys to values. Literal syntax uses {key: value}.", + } + + CHEAT_SHEET = textwrap.dedent( + """ + 🐍 Python Cheat Sheet + -------------------- + • print(value, ...): display output. + • input(prompt): ask the user for data. + • for item in sequence: iterate over items. + • if condition: create decision branches. + • list comprehensions: [expr for item in iterable]. + • with open('file.txt') as handle: manage file resources safely. + • Modules are imported with `import math` or `from math import sqrt`. + • Virtual environments keep project dependencies isolated. + • Use `help(object)` in the console for built-in documentation. + """ + ).strip() + + CODE_EXAMPLES = { + "Hello": "print('Hello, Python adventurer!')", + "FizzBuzz": textwrap.dedent( + """ + for number in range(1, 21): + if number % 15 == 0: + print('FizzBuzz') + elif number % 3 == 0: + print('Fizz') + elif number % 5 == 0: + print('Buzz') + else: + print(number) + """ + ).strip(), + "Guess": textwrap.dedent( + """ + import random + + secret = random.randint(1, 10) + while True: + guess = int(input('Guess between 1 and 10: ')) + if guess == secret: + print('You guessed it!') + break + print('Too high!' if guess > secret else 'Too low!') + """ + ).strip(), + } + + QUIZ_QUESTIONS = ( + QuizQuestion( + prompt="What keyword starts a function definition?", + answer="def", + explanation="Functions begin with the `def` keyword followed by the name.", + ), + QuizQuestion( + prompt="Which built-in converts text to an integer?", + answer="int", + explanation="`int(value)` parses a string or number into an integer.", + ), + QuizQuestion( + prompt="What statement lets you loop while a condition is true?", + answer="while", + explanation="Use `while condition:` to run a block until the condition fails.", + ), + ) + + def __init__(self) -> None: + self.root = Tk() + self.root.title("Python Learning Editor") + self.root.geometry("1200x720") + + self.file_path: Path | None = None + self._build_layout() + self._create_menus() + self._bind_events() + + self._after_id: str | None = None + self._autocomplete_window: Toplevel | None = None + self._autocomplete_list: ttk.Treeview | None = None + + # ------------------------------------------------------------------ GUI -- + def _build_layout(self) -> None: + container = ttk.PanedWindow(self.root, orient=VERTICAL) + container.pack(fill=BOTH, expand=True) + + top_panel = ttk.PanedWindow(container, orient='horizontal') + container.add(top_panel, weight=3) + + editor_frame = ttk.Frame(top_panel) + top_panel.add(editor_frame, weight=3) + + text_scrollbar = ttk.Scrollbar(editor_frame) + text_scrollbar.pack(side=RIGHT, fill=Y) + + self.line_numbers = Text( + editor_frame, + width=4, + padx=4, + takefocus=0, + borderwidth=0, + background="#f0f0f0", + state="disabled", + ) + self.line_numbers.pack(side=LEFT, fill=Y) + + self.text = Text( + editor_frame, + wrap="none", + undo=True, + font=("Consolas", 12), + ) + self.text.pack(fill=BOTH, expand=True) + self.text.config(yscrollcommand=lambda first, last: self._on_text_scroll(text_scrollbar, first, last)) + text_scrollbar.config(command=self._sync_scroll) + + helper_notebook = ttk.Notebook(top_panel, width=320) + top_panel.add(helper_notebook, weight=1) + + self.helper_text = Text(helper_notebook, wrap="word", state="disabled") + helper_notebook.add(self.helper_text, text="Helper") + + cheat_sheet = Text(helper_notebook, wrap="word", state="normal") + cheat_sheet.insert("1.0", self.CHEAT_SHEET) + cheat_sheet.config(state="disabled") + helper_notebook.add(cheat_sheet, text="Cheat Sheet") + + examples_frame = ttk.Frame(helper_notebook) + helper_notebook.add(examples_frame, text="Examples") + self.example_list = ttk.Treeview(examples_frame, show="tree") + self.example_list.pack(fill=BOTH, expand=True) + for label in sorted(self.CODE_EXAMPLES): + self.example_list.insert("", END, iid=label, text=label) + + quiz_frame = ttk.Frame(helper_notebook) + helper_notebook.add(quiz_frame, text="Quiz") + self.quiz_prompt = ttk.Label(quiz_frame, wraplength=260, justify=LEFT) + self.quiz_prompt.pack(padx=8, pady=8, anchor='w') + self.quiz_entry = ttk.Entry(quiz_frame) + self.quiz_entry.pack(fill='x', padx=8) + self.quiz_feedback = ttk.Label(quiz_frame, foreground="blue", wraplength=260) + self.quiz_feedback.pack(padx=8, pady=(4, 8), anchor='w') + self.quiz_button = ttk.Button(quiz_frame, text="Check", command=self._check_quiz) + self.quiz_button.pack(padx=8, pady=(0, 8), anchor='e') + self._quiz_index = 0 + self._show_quiz_question() + + console_frame = ttk.Frame(container) + container.add(console_frame, weight=1) + + ttk.Label(console_frame, text="Console Output").pack(anchor='w') + self.console = Text(console_frame, height=12, background="#1e1e1e", foreground="#d4d4d4") + self.console.pack(fill=BOTH, expand=True) + self.console.config(state="disabled") + + self.status = ttk.Label(self.root, text="Ready", anchor='w') + self.status.pack(fill='x', side=TOP) + + def _create_menus(self) -> None: + menubar = Menu(self.root) + + file_menu = Menu(menubar, tearoff=False) + file_menu.add_command(label="New", accelerator="Ctrl+N", command=self.new_file) + file_menu.add_command(label="Open...", accelerator="Ctrl+O", command=self.open_file) + file_menu.add_command(label="Save", accelerator="Ctrl+S", command=self.save_file) + file_menu.add_command(label="Save As...", command=self.save_file_as) + file_menu.add_separator() + file_menu.add_command(label="Exit", command=self.root.quit) + menubar.add_cascade(label="File", menu=file_menu) + + edit_menu = Menu(menubar, tearoff=False) + edit_menu.add_command(label="Undo", accelerator="Ctrl+Z", command=self._undo) + edit_menu.add_command(label="Redo", accelerator="Ctrl+Y", command=self._redo) + edit_menu.add_separator() + edit_menu.add_command(label="Find", accelerator="Ctrl+F", command=self._open_find_dialog) + edit_menu.add_command(label="Clear Output", command=self.clear_console) + menubar.add_cascade(label="Edit", menu=edit_menu) + + run_menu = Menu(menubar, tearoff=False) + run_menu.add_command(label="Run Script", accelerator="F5", command=self.run_code) + run_menu.add_command(label="Run Selection", accelerator="Shift+F5", command=self.run_selection) + menubar.add_cascade(label="Run", menu=run_menu) + + help_menu = Menu(menubar, tearoff=False) + help_menu.add_command(label="Python Tips", command=self._show_tips) + help_menu.add_command(label="About", command=self._show_about) + menubar.add_cascade(label="Help", menu=help_menu) + + self.root.config(menu=menubar) + + def _bind_events(self) -> None: + self.text.bind("", self._on_text_change) + self.text.bind("", self._update_helper_panel) + self.text.bind("", self._show_autocomplete) + self.text.bind("", self._update_status_bar) + self.text.bind("", self._update_status_bar, add="+") + self.text.bind("<>", self._update_helper_panel) + self.text.bind("", lambda event: self._run_event(self.run_code)) + self.text.bind("", lambda event: self._run_event(self.run_selection)) + self.text.bind("", lambda event: self._run_event(self.new_file)) + self.text.bind("", lambda event: self._run_event(self.open_file)) + self.text.bind("", lambda event: self._run_event(self.save_file)) + self.text.bind("", lambda event: self._run_event(self._open_find_dialog)) + + self.example_list.bind("<>", self._insert_example) + + # ---------------------------------------------------------- Event helpers -- + def _run_event(self, action: Callable[[], None]) -> str: + action() + return "break" + + def _on_text_change(self, event: Event | None = None) -> None: + self._update_line_numbers() + self._update_helper_panel() + if self._after_id: + self.root.after_cancel(self._after_id) + self._after_id = self.root.after(200, self._highlight_syntax) + + def _update_status_bar(self, event: Event | None = None) -> None: + line, column = self._cursor_position() + filename = self.file_path.name if self.file_path else "Untitled" + self.status.config(text=f"{filename} — line {line}, column {column}") + + def _cursor_position(self) -> tuple[int, int]: + index = self.text.index("insert").split(".") + return int(index[0]), int(index[1]) + 1 + + # ----------------------------------------------------------- Line numbers -- + def _update_line_numbers(self) -> None: + lines = int(self.text.index("end-1c").split(".")[0]) + content = "\n".join(str(number) for number in range(1, lines + 1)) + self.line_numbers.config(state="normal") + self.line_numbers.delete("1.0", END) + self.line_numbers.insert("1.0", content) + self.line_numbers.config(state="disabled") + + def _sync_scroll(self, *args: str) -> None: + self.text.yview(*args) + self.line_numbers.yview(*args) + + def _on_text_scroll(self, scrollbar: ttk.Scrollbar, first: str, last: str) -> None: + scrollbar.set(first, last) + self.line_numbers.yview_moveto(first) + + # ----------------------------------------------------------- Syntax colour -- + def _highlight_syntax(self) -> None: + self._after_id = None + keyword_tag = "keyword" + string_tag = "string" + comment_tag = "comment" + for tag in (keyword_tag, string_tag, comment_tag): + self.text.tag_delete(tag) + self.text.tag_configure(keyword_tag, foreground="#005cc5", font=("Consolas", 12, "bold")) + self.text.tag_configure(string_tag, foreground="#a31515") + self.text.tag_configure(comment_tag, foreground="#008000") + + content = self.text.get("1.0", "end-1c") + for kw in keyword.kwlist: + start = "1.0" + while True: + start = self.text.search(rf"\m{kw}\M", start, stopindex=END, regexp=True) + if not start: + break + end = f"{start}+{len(kw)}c" + self.text.tag_add(keyword_tag, start, end) + start = end + + start = "1.0" + while True: + start = self.text.search(r"(#.*)$", start, stopindex=END, regexp=True) + if not start: + break + line_end = f"{start.split('.')[0]}.end" + self.text.tag_add(comment_tag, start, line_end) + start = line_end + + start = "1.0" + while True: + start = self.text.search(r"(['\"])(?:(?=(\\?))\2.)*?\1", start, stopindex=END, regexp=True) + if not start: + break + end_index = self.text.index(f"{start}+1c") + quote = self.text.get(start, end_index) + end = self.text.search(quote, f"{start}+1c", regexp=False, stopindex=END) + if not end: + break + end = self.text.index(f"{end}+1c") + self.text.tag_add(string_tag, start, end) + start = end + + # --------------------------------------------------------- Helper panels -- + def _update_helper_panel(self, event: Event | None = None) -> None: + word = self._current_word() + description = self.KEYWORD_HELP.get(word, "Select a keyword to see a tip.") + self.helper_text.config(state="normal") + self.helper_text.delete("1.0", END) + self.helper_text.insert("1.0", description) + self.helper_text.config(state="disabled") + + def _current_word(self) -> str: + selection = self.text.get("sel.first", "sel.last") if self.text.tag_ranges("sel") else None + if selection: + return selection.strip() + index = self.text.index("insert") + line, column = map(int, index.split(".")) + start = f"{line}.{column} wordstart" + end = f"{line}.{column} wordend" + return self.text.get(start, end).strip() + + def _insert_example(self, event: Event) -> None: + selection = self.example_list.selection() + if not selection: + return + code = self.CODE_EXAMPLES.get(selection[0]) + if not code: + return + self.text.delete("1.0", END) + self.text.insert("1.0", code) + self._update_line_numbers() + self._highlight_syntax() + + # -------------------------------------------------------------- Quiz app -- + def _show_quiz_question(self) -> None: + question = self.QUIZ_QUESTIONS[self._quiz_index] + self.quiz_prompt.config(text=question.prompt) + self.quiz_entry.delete(0, END) + self.quiz_feedback.config(text="") + + def _check_quiz(self) -> None: + question = self.QUIZ_QUESTIONS[self._quiz_index] + answer = self.quiz_entry.get().strip().lower() + if answer == question.answer.lower(): + self.quiz_feedback.config(text=f"✅ Correct! {question.explanation}") + self._quiz_index = (self._quiz_index + 1) % len(self.QUIZ_QUESTIONS) + else: + self.quiz_feedback.config(text=f"❌ Not quite. {question.explanation}") + self.root.after(2000, self._show_quiz_question) + + # -------------------------------------------------------------- Autocomplete -- + def _show_autocomplete(self, event: Event | None = None) -> str: + if self._autocomplete_window: + self._autocomplete_window.destroy() + + word = self._current_word() + matches = [item for item in self.AUTOCOMPLETE_SUGGESTIONS if item.startswith(word)] + if not matches: + return "break" + + bbox = self.text.bbox("insert") + if not bbox: + return "break" + x, y, width, height = bbox + x += self.text.winfo_rootx() + y += self.text.winfo_rooty() + height + + window = Toplevel(self.root) + window.wm_overrideredirect(True) + window.geometry(f"200x200+{x}+{y}") + self._autocomplete_window = window + + tree = ttk.Treeview(window, show="tree") + tree.pack(fill=BOTH, expand=True) + for item in matches: + tree.insert("", END, iid=item, text=item) + tree.focus(matches[0]) + tree.selection_set(matches[0]) + tree.bind("", lambda e: self._insert_autocomplete(tree)) + tree.bind("", lambda e: self._insert_autocomplete(tree)) + tree.bind("", lambda e: self._close_autocomplete()) + tree.focus_set() + self._autocomplete_list = tree + return "break" + + def _insert_autocomplete(self, tree: ttk.Treeview) -> None: + selection = tree.selection() + if not selection: + return + word = selection[0] + self._replace_current_word(word) + self._close_autocomplete() + + def _close_autocomplete(self) -> None: + if self._autocomplete_window: + self._autocomplete_window.destroy() + self._autocomplete_window = None + self._autocomplete_list = None + + def _replace_current_word(self, replacement: str) -> None: + if self.text.tag_ranges("sel"): + self.text.delete("sel.first", "sel.last") + self.text.insert("insert", replacement) + else: + index = self.text.index("insert") + line, column = map(int, index.split(".")) + start = f"{line}.{column} wordstart" + end = f"{line}.{column} wordend" + self.text.delete(start, end) + self.text.insert(start, replacement) + + # --------------------------------------------------------------- File ops -- + def new_file(self) -> None: + if self._confirm_unsaved_changes(): + self.text.delete("1.0", END) + self.file_path = None + self.status.config(text="Untitled — line 1, column 1") + self.text.edit_modified(False) + + def open_file(self) -> None: + if not self._confirm_unsaved_changes(): + return + filename = filedialog.askopenfilename(filetypes=[("Python files", "*.py"), ("All files", "*.*")]) + if filename: + path = Path(filename) + self.text.delete("1.0", END) + self.text.insert("1.0", path.read_text(encoding="utf8")) + self.file_path = path + self._update_line_numbers() + self._highlight_syntax() + self._update_status_bar() + self.text.edit_modified(False) + + def save_file(self) -> None: + if self.file_path is None: + self.save_file_as() + return + self.file_path.write_text(self.text.get("1.0", "end-1c"), encoding="utf8") + messagebox.showinfo("Saved", f"Saved to {self.file_path}") + self.text.edit_modified(False) + + def save_file_as(self) -> None: + filename = filedialog.asksaveasfilename(defaultextension=".py", filetypes=[("Python files", "*.py")]) + if filename: + self.file_path = Path(filename) + self.save_file() + + def _confirm_unsaved_changes(self) -> bool: + if not self.text.edit_modified(): + return True + + answer = messagebox.askyesnocancel( + "Unsaved changes", "Save the current file before proceeding?" + ) + if answer is None: + return False + if answer: + self.save_file() + if self.text.edit_modified(): + return False + else: + self.text.edit_modified(False) + return True + + def _undo(self) -> None: + try: + self.text.edit_undo() + except Exception: + pass + + def _redo(self) -> None: + try: + self.text.edit_redo() + except Exception: + pass + + def _open_find_dialog(self) -> None: + dialog = Toplevel(self.root) + dialog.title("Find text") + dialog.transient(self.root) + ttk.Label(dialog, text="Find:").pack(side=LEFT, padx=4, pady=4) + entry = ttk.Entry(dialog) + entry.pack(side=LEFT, padx=4, pady=4, fill='x', expand=True) + entry.focus_set() + + def find_next() -> None: + needle = entry.get() + if not needle: + return + start = self.text.search(needle, self.text.index("insert"), stopindex=END) + if start: + end = f"{start}+{len(needle)}c" + self.text.tag_remove("sel", "1.0", END) + self.text.tag_add("sel", start, end) + self.text.mark_set("insert", end) + self.text.see(start) + + ttk.Button(dialog, text="Find", command=find_next).pack(side=RIGHT, padx=4, pady=4) + dialog.bind("", lambda event: (find_next(), "break")) + + # ------------------------------------------------------------ Console ops -- + def clear_console(self) -> None: + self.console.config(state="normal") + self.console.delete("1.0", END) + self.console.config(state="disabled") + + def run_code(self) -> None: + code = self.text.get("1.0", "end-1c") + self._execute_code(code) + + def run_selection(self) -> None: + if not self.text.tag_ranges("sel"): + messagebox.showinfo("No selection", "Select some code to run.") + return + code = self.text.get("sel.first", "sel.last") + self._execute_code(code) + + def _execute_code(self, code: str) -> None: + console_output = io.StringIO() + console_error = io.StringIO() + local_env: dict[str, object] = {} + try: + with redirect_stdout(console_output), redirect_stderr(console_error): + exec(code, {"__name__": "__main__"}, local_env) + except Exception as exc: + console_error.write(f"Error: {exc}\n") + finally: + output = console_output.getvalue() + errors = console_error.getvalue() + self.console.config(state="normal") + if output: + self.console.insert(END, output) + if errors: + self.console.insert(END, errors) + self.console.see(END) + self.console.config(state="disabled") + + # --------------------------------------------------------------- Help menu -- + def _show_tips(self) -> None: + tips = textwrap.dedent( + """ + ✨ Productivity Tips + -------------------- + • Press Ctrl+Space for autocomplete suggestions. + • Use Shift+F5 to run only the selected portion of code. + • Explore the Examples tab for curated snippets. + • Keep an eye on the helper panel for keyword hints. + • Experiment freely—the console output does not affect your files. + """ + ).strip() + messagebox.showinfo("Python Tips", tips) + + def _show_about(self) -> None: + messagebox.showinfo( + "About Python Learning Editor", + "A friendly playground for experimenting with Python and learning the basics.", + ) + + # --------------------------------------------------------------- Lifecycle -- + def run(self) -> None: + self._update_line_numbers() + self._highlight_syntax() + self.root.mainloop() + + +def main() -> None: + editor = PythonLearningEditor() + editor.run() + + +if __name__ == "__main__": + main() +