Quickstart

This guide gets you from zero to a working chat in a Makepad app.

Prerequisites

This guide assumes you are familiar with Makepad and have a bare-bones app ready. You should also have read the aitk documentation, as Moly Kit builds on its types and patterns.

Installation

Add Moly Kit to your Cargo.toml:

[dependencies]
makepad-widgets = { git = "https://github.com/makepad/makepad", branch = "main" }
moly-kit = { git = "https://github.com/moly-ai/moly-ai.git", features = ["full"] }

Tip

Pin to a specific version with tag = "x.x.x" or rev = "<commit>" instead of branch = "main" if you want to stay on a stable version.

Register widgets

Define your App and register Moly Kit's widgets in script_mod:

app_main!(App);

#[derive(Script, ScriptHook)]
pub struct App {
    #[live]
    ui: WidgetRef,
}

impl AppMain for App {
    fn script_mod(vm: &mut ScriptVm) -> ScriptValue {
        makepad_widgets::script_mod(vm);
        moly_kit::widgets::script_mod(vm);
        self::script_mod(vm)
    }

    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
        self.ui.handle_event(cx, event, &mut Scope::empty());
    }
}

Place the Chat widget

Use the Chat widget in your DSL layout:

script_mod! {
    use mod.prelude.widgets.*
    use mod.widgets.*

    mod.widgets.MyChatBase = #(MyChat::register_widget(vm))

    load_all_resources() do #(App::script_component(vm)) {
        ui: Root {
            main_window := Window {
                window +: { inner_size: vec2(800 600) }
                pass +: { clear_color: #xfff }
                body +: {
                    my_chat := mod.widgets.MyChatBase {
                        width: Fill
                        height: Fill
                        flow: Down
                        padding: 12
                        chat := Chat {}
                    }
                }
            }
        }
    }
}

Configure the Chat widget

The Chat widget needs a ChatController from aitk to function. Set it up in on_after_new, which runs once when the widget is created:

use std::sync::{Arc, Mutex};
use makepad_widgets::*;
use moly_kit::prelude::*;

#[derive(Script, Widget)]
struct MyChat {
    #[deref]
    view: View,

    #[rust]
    controller: Option<Arc<Mutex<ChatController>>>,
}

impl Widget for MyChat {
    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
        self.view.handle_event(cx, event, scope);
    }

    fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
        self.view.draw_walk(cx, scope, walk)
    }
}

impl ScriptHook for MyChat {
    fn on_after_new(&mut self, vm: &mut ScriptVm) {
        let mut client = OpenAiClient::new("https://api.openai.com/v1".into());
        client.set_key("your-api-key").unwrap();

        let controller = ChatController::builder()
            .with_client(client)
            .with_basic_spawner()
            .build_arc();

        controller
            .lock()
            .unwrap()
            .dispatch_mutation(ChatStateMutation::SetBotId(
                Some(BotId::new("gpt-4.1-nano")),
            ));

        self.controller = Some(controller.clone());
        vm.with_cx_mut(|cx| {
            self.chat(cx, ids!(chat))
                .write()
                .set_chat_controller(cx, Some(controller));
        });
    }
}

That's it. After this setup, the Chat widget handles user input, streaming responses, and message rendering automatically.

What's happening

  1. Create a client: OpenAiClient connects to any OpenAI-compatible endpoint (OpenAI, Ollama, OpenRouter, etc.).
  2. Build the controller: ChatController::builder() creates the state manager that coordinates the conversation. with_basic_spawner() provides cross-platform async task execution.
  3. Set the model: SetBotId is a state mutation that directly sets a specific model on the controller.
  4. Give it to the widget: set_chat_controller gives the controller to Chat so it can drive the conversation.

Note

Moly Kit doesn't duplicate methods from Chat into Makepad's autogenerated ChatRef but provides read() and write() helpers to access the inner widget.

Next steps

This minimal setup uses a single client with a hardcoded model. The next chapter, Multiple Providers and Dynamic Models, shows how to support multiple providers, dynamically load available models, and use plugins to automate model selection.