====== Lab 02: Flet GUI basics ====== [[https://flet.dev/|Flet]] is an opensource Python framework for building real, reactive user interfaces—web, desktop, and mobile—using simple Python code. It wraps Google’s Flutter engine under the hood, so you get native-feeling UI, smooth animations, and a rich set of ready-made controls without touching Dart. You write components, manage state, and handle events in Python, then run the same app in a browser, as a desktop app, or packaged for phones. It’s great for quickly turning scripts and data tools into polished, shareable apps. ===== Installation ===== * **Windows/macOS:** Download from [[https://www.python.org/downloads/|python.org downloads]] (on Windows, tick “Add Python to PATH”). * **Linux (Debian/Ubuntu):** sudo apt update && sudo apt install -y python3 python3-pip python3-venv * **Create a Virtual Environment:** (recommended) # macOS/Linux python3 -m venv .venv source .venv/bin/activate # Windows (PowerShell) python -m venv .venv .\.venv\Scripts\Activate.ps1 * **Install Flet:** pip install --upgrade pip pip install flet==0.28.3 Note: Flet is in very active development - we will be using the 0.28.3 release specifically.\\ Tip: In VS Code, pick the right interpreter: //Cmd/Ctrl+Shift+P → Python: Select Interpreter// and choose ''.venv''. Your system may need a restart before ''flet'' becomes a globally recognized command. ===== Hello, Flet! ===== Create a new Flet project by running the following command in your workspace directory: flet create --project-name MLE-flet-demo --description "becm33mle app demo" You may encounter an error if you do not have [[https://git-scm.com/downloads|GIT installed]]. Directory structure created by ''flet create'' (with a local Python virtual environment ''.venv'' at the project root). ├── README.md ├── pyproject.toml ├── .venv ├── src │ ├── assets │ │ └── icon.png │ └── main.py └── storage ├── data └── temp Run the template ''main.py'' app: flet run ''main.py'' is the default entry-point of any Flet app. Different files can be launched using ''flet run ''. Flet apps can also be launched using ''python3 '', though this approach is not recommended! You should see the following interactive window: {{ courses:becm33mle:flet_template_app.png?nolink&400 |}} ===== Running the app... Everywhere! ===== Flet can be run as a desktop app: flet run Or a web app flet run --web Or on a mobile phone using the official companion app ([[https://apps.apple.com/app/flet/id1624979699|iOS]], [[https://play.google.com/store/apps/details?id=com.appveyor.flet|Android]]): # Android flet run --android # iOS flet run --ios Note: devices must be on the same local network. You can use the hot-reload feature for fast iterative GUI development by running ''flet run -d -r''. This will watch for any changes in all subdirectories. ====== Routing in Flet ====== Depending on the target platform Flet can be a multi-user app (web). For this purpose, Flet spawns a new ''Page'' for each connected user (on desktop and mobile usually just a single page). Each Page can have multiple ''Views'', which are stacked in a list, acting as sort of navigation history. ''Views'' can be appended (opening a new page) or poped (going back). Each ''View'' has an assortment of ''Controls'', building the GUI of that particular ''View''. {{ courses:becm33mle:fletarchitecture.svg |}} The ''Page'' exposes current ''route'' and an ''on_route_change'' event handler. This can be used to handle View poping/appending and unknown route handling (404 page not found). Route is the part of the URL after the ''/'' symbol (inclusive) - i.e. mydomain.cz/route/to/somewhere Each page below is a class inheriting from ''View''. The app builds the view stack in ''on_route_change'' and handles back navigation in ''on_view_pop''. === Routing demo === import flet as ft class MainView(ft.View): def __init__(self, page: ft.Page): super().__init__( route="/", appbar=ft.AppBar(title=ft.Text("Main")), controls=[ ft.Text("This is the main page"), ft.Row([ ft.ElevatedButton("Go to 1", on_click=lambda _:page.go("/1")), ft.ElevatedButton("Go to 2", on_click=lambda _:page.go("/2")), ]) ], ) class PageOne(ft.View): def __init__(self, page: ft.Page): super().__init__( route="/1", appbar=ft.AppBar(title=ft.Text("Page 1")), controls=[ ft.Text("Hello from page 1"), ft.ElevatedButton("Home", on_click=lambda _:page.go("/")), ], ) class PageTwo(ft.View): def __init__(self, page: ft.Page): super().__init__( route="/2", appbar=ft.AppBar(title=ft.Text("Page 2")), controls=[ ft.Text("Hello from page 2"), ft.ElevatedButton("Home", on_click=lambda _:page.go("/")), ], ) def main(page: ft.Page): page.title = "Routing demo" def route_change(e: ft.RouteChangeEvent): page.views.clear() page.views.append(MainView(page)) if page.route == "/1": page.views.append(PageOne(page)) if page.route == "/2": page.views.append(PageTwo(page)) page.update() def view_pop(e: ft.ViewPopEvent): page.views.pop() page.go(page.views[-1].route) page.on_route_change = route_change page.on_view_pop = view_pop page.go(page.route) ft.app(target=main) * ''ft.app(target=main)'' starts the app and creates a ''Page'' (one per user) * ''Page'', ''View'', and anything placed in the ''Views'' are so-called ''controls'' (i.e. ''ft.Text'', ''ft.ElevatedButton'', ...) * For changes made to a ''control'' to take effect, we must call it's ''.update()'' method or any of it's parent's ''.update()'' methods. * It can be handy to pass the reference to ''Page'' to our ''Views''. * All ''controls'' have a ''.page'' property, which is only accessible if the control is currently "visible" on the page (usually not accessible in init). ====== Custom controls ====== Flet ships with many built-ins, but you can create **reusable components** by **styling** a built-in control or **composing** several controls into one. Pick the closest existing control (e.g., ''Container'', ''Row'', ''Text'', ''ElevatedButton'', or even ''View'') and inherit from it. You can mirror the **Routing demo** where each page inherited from ''ft.View'' — here you inherit from the most appropriate control and specialize it. ==== Patterns ==== * **Styled control (inherit & set defaults):** Ideal for consistent buttons, texts, inputs used across the app. * **Composite control (inherit a container):** Build a mini-widget by composing children and exposing a tiny API. * **State & updates:** Keep state in the class and call ``self.update()`` (or update child props then ``self.update()``). ==== Example 1 — Styled control ==== A primary button with consistent look & behavior used app-wide. import flet as ft class PrimaryButton(ft.ElevatedButton): def __init__(self, **kwargs): super().__init__( bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE, style=ft.ButtonStyle(shape=ft.RoundedRectangleBorder(radius=8)), **kwargs ) ==== Example 2 — Composite control (counter) ==== A small component composed of a label + value + two buttons. We inherit from a ''Row'' and manage internal state. import flet as ft class Counter(ft.Row): def __init__(self, value: int = 0): super().__init__(alignment=ft.MainAxisAlignment.START, spacing=20) self._value = value self.value_text = ft.Text(str(self._value), weight=ft.FontWeight.BOLD) dec_btn = ft.IconButton(ft.Icons.REMOVE, on_click=lambda _:self._value_change(delta=-1)) inc_btn = ft.IconButton(ft.Icons.ADD, on_click=lambda _:self._value_change(delta=1)) self.controls = [ft.Text("Count:"), self.value_text, dec_btn, inc_btn] def _value_change(self, delta=0): self._value += delta self.value_text.value = str(self._value) self.update() ==== Minimal demo of custom controls ==== A tiny app demo using both custom controls. import flet as ft def main(page: ft.Page): page.title = "Custom controls (modern API)" out = ft.Text("Click the button or use the counter.") page.add( PrimaryButton(text="Primary action", on_click=lambda e: (setattr(out, "value", "Clicked!"), page.update())), Counter(3), out ) ft.app(target=main) === When to pick the base class === * **Looks like text:** inherit ''Text'' (e.g., timer, counter, clock,...). * **Horizontal/vertical layout:** inherit ''Row'' / ''Column''. * **Enclosed container with controls:** inherit ''Container'' (most common). * **Whole screen:** inherit ''View'' (as in the routing example). ====== Local Ollama chatbot demo ====== Two ways to run the demo: * **Local machine:** install Ollama on your laptop/desktop and run it there. * **CTU GPU servers:** SSH to ''gpu.fel.cvut.cz'' and run Ollama remotely, then **forward the API port** to your machine. Ollama serves an HTTP API on port **11434** bound to **localhost** by default. When running remotely, use SSH **local port forwarding** so your laptop can call the remote API safely. ===== Option A — Local install (short) ===== You need a dedicated GPU with ~8GB of VRAM to run LLM localy! - Install [[https://ollama.com/|Ollama]] for your OS (Linux/macOS/Windows). - Start server: ''ollama serve'' - Pull a small model (e.g., Llama 3 8B): ''ollama pull llama3:8b'' ===== Option B — CTU GPU servers ===== ==== 1) SSH with local port forwarding ==== From your laptop, open the tunnel **first**: ssh -L 11434:localhost:11434 @gpu.fel.cvut.cz **Explanation:** ''-L local_port:remote_host:remote_port'' maps your **local** ''11434'' to **remote** ''localhost:11434''. After connecting, any request you send to ''http://localhost:11434'' on your laptop goes through the SSH tunnel to the server’s Ollama. Keep this SSH session open while you work. If you close it, the tunnel (and access to the API) closes. You must be to the CTU network or connected via the [[https://ist.cvut.cz/en/our-services/vpn/|CTU VPN]]. More info about the CTU GPU cluster can be found [[https://cyber.felk.cvut.cz/study/gpu-servers/|here]]. ==== 2) Install Ollama (Linux x86_64 tarball) ==== Download the latest release (as of writing v0.12.4): wget https://github.com/ollama/ollama/releases/download/v0.12.4-rc6/ollama-linux-amd64.tgz Unpack to a folder in your home: mkdir ollama tar -xvzf ollama-linux-amd64.tgz -C ollama ==== 3) Start the Ollama server ==== Run the server (keeps the process in the foreground): ./ollama/bin/ollama serve ==== 4) Download a small Llama 3 model ==== In a **new** SSH tab, pull the model: ./ollama/bin/ollama pull llama3:8b (Adjust the tag if you prefer a different small variant.) ==== 5) Test from your laptop via the tunnel ==== With the SSH tunnel still open, you can hit the remote API at **your** localhost: # list models curl http://localhost:11434/api/tags # quick generate call curl http://localhost:11434/api/generate -d '{"model":"llama3:8b","prompt":"Say hello from CTU GPU."}' Do **not** change Ollama’s bind address to ``0.0.0.0`` on the server. Keep it on ''localhost'' + SSH tunneling. ===== Flet app demo ===== A Flet UI using the ''ollama'' python package (''pip install ollama'') API on ''http://localhost:11434''. import threading from dataclasses import dataclass from typing import List, Dict, Optional import flet as ft try: import ollama except ImportError: ollama = None # ----------------------------- # Data structures # ----------------------------- @dataclass class ChatMsg: is_user: str content: str # ----------------------------- # UI Message Bubble # ----------------------------- class MessageBubble(ft.Container): def __init__(self, is_user: bool, text: str): super().__init__() self.padding = 12 self.margin = ft.margin.only( left=40 if is_user else 0, right=0 if is_user else 40, top=6, bottom=6, ) self.bgcolor = ft.Colors.BLUE_800 if is_user else ft.Colors.GREY_800 self.border_radius = ft.border_radius.all(14) self.content = ft.Text(text, selectable=True, color=ft.Colors.WHITE, size=14) def set_text(self, text: str): if isinstance(self.content, ft.Text): self.content.value = text # ----------------------------- # Main app # ----------------------------- class FletOllamaDemo: def __init__(self, page: ft.Page): self.page = page self.page.title = "Ollama Demo" self.page.theme_mode = ft.ThemeMode.DARK self.page.padding = 0 self.page.window.width = 480 self.page.window.height = 640 # chat state self.messages: List[ChatMsg] = [] self.streaming_thread: Optional[threading.Thread] = None self.stop_event = threading.Event() self.model = "llama3:8b" # controls self.chat_list = ft.ListView(expand=True, spacing=0, padding=16) self.input_field = ft.TextField( hint_text="Type and press Enter…", autofocus=True, shift_enter=True, min_lines=1, max_lines=5, expand=True, on_submit=self._on_send_clicked, ) self.send_btn = ft.FilledButton("Send", icon=ft.Icons.SEND_ROUNDED, on_click=self._on_send_clicked) self.stop_btn = ft.OutlinedButton("Stop", icon=ft.Icons.STOP, on_click=self._on_stop_clicked, disabled=True) bottombar = ft.Container( padding=12, content=ft.Row([self.input_field, self.send_btn, self.stop_btn], vertical_alignment=ft.CrossAxisAlignment.CENTER), ) body = ft.Column([ ft.Container(content=self.chat_list, expand=True), bottombar, ], expand=True) self.page.add(body) def _ensure_model(self) -> bool: if ollama is None: self.page.open(ft.SnackBar(ft.Text("Ollama not available: pip install ollama and run the server."), open=True)) self.page.update() return False try: have = {m.get("model") for m in (ollama.list() or {}).get("models", [])} if self.model not in have: ollama.pull(self.model) return True except Exception as ex: try: ollama.pull(self.model) return True except Exception: self.page.open(ft.SnackBar(ft.Text(f"Model setup error: {ex}"), open=True)) self.page.update() return False def _on_send_clicked(self, e: Optional[ft.ControlEvent] = None): text = (self.input_field.value or "").strip() if not text: return self.input_field.value = "" self._append_user_message(text) self._start_streaming() def _on_stop_clicked(self, e: Optional[ft.ControlEvent] = None): self.stop_event.set() def _append_user_message(self, text: str): self.messages.append(ChatMsg(is_user=True, content=text)) self.chat_list.controls.append(MessageBubble(is_user=True, text=text)) self.chat_list.controls.append(MessageBubble(is_user=False, text="")) # AI reply self.page.update() def _start_streaming(self): if self.streaming_thread and self.streaming_thread.is_alive(): return self.stop_event.clear() self.send_btn.disabled = True self.stop_btn.disabled = False self.page.update() def run(): err = None try: self._stream_from_ollama() except Exception as ex: err = ex finally: self.send_btn.disabled = False self.stop_btn.disabled = True if err: self.page.open(ft.SnackBar(ft.Text(f"Error: {err}"), open=True)) self.page.update() self.streaming_thread = threading.Thread(target=run, daemon=True) self.streaming_thread.start() def _collect_messages(self) -> List[Dict[str, str]]: msgs: List[Dict[str, str]] = [] for m in self.messages: msgs.append({"role": "user" if m.is_user else "assistant", "content": m.content}) return msgs def _stream_from_ollama(self): # find the last bubble to stream to if not self.chat_list.controls or not isinstance(self.chat_list.controls[-1], MessageBubble): return assistant_bubble: MessageBubble = self.chat_list.controls[-1] if not self._ensure_model(): assistant_bubble.set_text( "Model unavailable. Ensure Ollama client/server are installed and running.\n" f"- pip install ollama\n- ollama serve\n- ollama pull {self.model}" ) self.page.update() return msgs = self._collect_messages() full_text = "" try: stream = ollama.chat(model=self.model, messages=msgs, stream=True) for part in stream: if self.stop_event.is_set(): break delta = part.get("message", {}).get("content", "") if not delta: continue full_text += delta assistant_bubble.set_text(full_text) self.chat_list.scroll_to(offset=-1, duration=100) self.page.update() except Exception as ex: assistant_bubble.set_text(f"Error contacting model: {ex}") self.page.update() return if full_text.strip(): self.messages.append(ChatMsg(is_user=False, content=full_text)) else: self.chat_list.controls.pop() self.page.update() def main(page: ft.Page): FletOllamaDemo(page) if __name__ == "__main__": ft.app(target=main) Run with: flet run Try to run the code on your mobile device and/or in the browser. {{ courses:becm33mle:flet_ollama_app.png?nolink&400 |}} If you are not using the default port, you can ''export OLLAMA_HOST=http://127.0.0.1:12345'' to change the expected API port. ====== Useful links ====== * Documentation: https://flet.dev/docs/ * Example programs: https://flet.dev/gallery * Controls gallery: https://flet-controls-gallery.fly.dev/ * Icon browser: https://gallery.flet.dev/icons-browser/ * Chat demo: https://flet-chat.fly.dev/ * Github: https://github.com/flet-dev/flet ====== HW02: Gitlab Readme.md and project setup ====== Create project repo structure on FEE gitlab + README.md with required fields (up to 5p) The fields do not have to be finalized, as many features of your project are yet to be added. Focus on drafting out the general structure of your project. Requirements for README.md: * Short project description * Features * Architecture overview (diagram + data flow) * Project timeline (1st, 2nd and final milestones highlighted) * Install & run (HW/SW reqs, requirements.txt/pyproject.toml, how to run client/server, CLI args/kwargs.) * Models used (name/version/source/license/intended use) * Datasets used (name/version/source/license/intended use/structure summary) * Reproducibility: how to retrain/re-export/regenerate datasets * Troubleshooting (optional) * Contributing (optional) * License Note about milestones: - Milestone: 5th week - Milestone: 9th week - Milestone: 13th (submission) week