Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions apps/mofa-ui-generator/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use mofa_widgets::{MofaApp, AppInfo, PageId};
use makepad_widgets::*;

pub mod screen;

pub struct MoFaUIGeneratorApp;

impl MofaApp for MoFaUIGeneratorApp {
fn info() -> AppInfo {
AppInfo {
name: "UI Generator",
id: "mofa-ui-generator",
description: "AI-powered Makepad UI Generator",
tab_id: Some(live_id!(ui_generator_tab)),
page_id: Some(live_id!(ui_generator_page)),
show_in_sidebar: true,
}
}

fn live_design(cx: &mut Cx) {
crate::screen::live_design(cx);
}
}
205 changes: 205 additions & 0 deletions apps/mofa-ui-generator/src/screen/design.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
use makepad_widgets::*;
use mofa_ui::widgets::chat_panel::{ChatMessage, ChatPanelWidgetExt};
use mofa_ui::widgets::chat_input::ChatInputWidgetExt;

const SYSTEM_PROMPT: &str = "You are an expert Makepad UI developer. \
Generate high-quality Makepad .ds code based on natural language descriptions. \
Focus on modern aesthetics, responsiveness, and proper use of Makepad widgets.";

live_design! {
use link::theme::*;
use link::shaders::*;
use link::widgets::*;

use mofa_widgets::theme::*;
use mofa_ui::widgets::chat_panel::ChatPanel;
use mofa_ui::widgets::chat_input::ChatInput;

pub MoFaUIGeneratorScreen = {{MoFaUIGeneratorScreen}} {
width: Fill, height: Fill
flow: Right
spacing: 12
padding: 12
show_bg: true
draw_bg: {
color: (DARK_BG)
}

// Chat section on the left
left_column = <View> {
width: 450, height: Fill
flow: Down
spacing: 10

chat_panel = <ChatPanel> {
height: Fill
empty_text: "Tell me what UI you want to build..."
}
chat_input = <ChatInput> {
height: Fit
placeholder: "e.g. Create a profile card with an avatar and stats"
}
}

// Preview section on the right
right_column = <RoundedView> {
width: Fill, height: Fill
show_bg: true
draw_bg: {
instance dark_mode: 1.0
color: (PANEL_BG)
border_radius: 8.0
}
padding: 20
flow: Down
spacing: 0

header = <View> {
width: Fill, height: Fit
flow: Right
align: {y: 0.5}
padding: {bottom: 15}

<Label> {
text: "UI Generator Output"
draw_text: {
text_style: <FONT_BOLD>{ font_size: 16.0 }
color: (TEXT_PRIMARY)
}
}
<Filler> {}

tab_bar = <View> {
width: Fit, height: Fit
flow: Right
spacing: 5

preview_tab = <Button> {
width: Fit, height: Fit
text: "Preview"
}
code_tab = <Button> {
width: Fit, height: Fit
text: "Code"
}
}
}

divider = <View> {
width: Fill, height: 1
show_bg: true
draw_bg: { color: (DIVIDER) }
}

content = <View> {
width: Fill, height: Fill
flow: Overlay
padding: {top: 20}

preview_view = <View> {
width: Fill, height: Fill
flow: Down
align: {x: 0.5, y: 0.5}
visible: true

placeholder_text = <Label> {
text: "Generated UI will be rendered here"
draw_text: {
color: (TEXT_SECONDARY)
text_style: <FONT_REGULAR>{ font_size: 14.0 }
}
}
}

code_view = <View> {
width: Fill, height: Fill
flow: Down
visible: false

code_text = <TextInput> {
width: Fill, height: Fill
empty_message: "// Makepad DSL will appear here"
draw_text: {
text_style: <FONT_MONO>{ font_size: 11.0 }
color: (TEXT_PRIMARY)
}
show_bg: true
draw_bg: { color: (DARK_BG_DARK) }
}
}
}
}
}
}

#[derive(Live, LiveHook, Widget)]
pub struct MoFaUIGeneratorScreen {
#[deref]
view: View,

#[rust]
messages: Vec<ChatMessage>,

#[rust]
generated_code: String,
}

impl Widget for MoFaUIGeneratorScreen {
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
let actions = cx.capture_actions(|cx| self.view.handle_event(cx, event, scope));

self.handle_actions(cx, &actions);
}

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

impl MoFaUIGeneratorScreen {
fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
// Handle Chat Input
let chat_input = self.view.chat_input(id!(left_column.chat_input));
if let Some(text) = chat_input.submitted(actions) {
self.on_submit(cx, text);
}

// Handle Tabs
if self.view.button(id!(right_column.header.tab_bar.preview_tab)).clicked(actions) {
self.switch_tab(cx, true);
}
if self.view.button(id!(right_column.header.tab_bar.code_tab)).clicked(actions) {
self.switch_tab(cx, false);
}
}

fn on_submit(&mut self, cx: &mut Cx, text: String) {
// Add user message
self.messages.push(ChatMessage::new("You", text.clone()));

// Mock generating code
let mock_code = format!(
"// Generated for: {}\n\nlive_design! {{\n MyComponent = <View> {{\n width: Fill, height: 200\n show_bg: true\n draw_bg: {{ color: #f00 }}\n <Label> {{ text: \"Hello from AI!\" }}\n }}\n}}",
text
);
self.generated_code = mock_code.clone();

// Add AI response
self.messages.push(ChatMessage::new("AI", "I've generated the Makepad UI code for you. You can check it in the 'Code' tab. Rendering..."));

// Update components
self.view.chat_panel(id!(left_column.chat_panel)).set_messages(cx, &self.messages);
self.view.text_input(id!(right_column.content.code_view.code_text)).set_text(cx, &mock_code);

// Show code tab by default when generated
self.switch_tab(cx, false);

self.view.redraw(cx);
}

fn switch_tab(&mut self, cx: &mut Cx, show_preview: bool) {
self.view.view(id!(right_column.content.preview_view)).set_visible(show_preview);
self.view.view(id!(right_column.content.code_view)).set_visible(!show_preview);
self.view.redraw(cx);
}
}
5 changes: 5 additions & 0 deletions apps/mofa-ui-generator/src/screen/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use makepad_widgets::*;

pub mod design;

pub use design::*;
14 changes: 14 additions & 0 deletions mofa-studio-shell/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,19 @@ version.workspace = true
edition.workspace = true

[features]
feature/ai-ui-generator
default = ["mofa-fm", "mofa-settings", "mofa-debate", "mofa-asr", "mofa-ui-generator"]
mofa-fm = ["dep:mofa-fm"]
mofa-settings = ["dep:mofa-settings"]
mofa-debate = ["dep:mofa-debate"]
mofa-asr = ["dep:mofa-asr"]
mofa-ui-generator = ["dep:mofa-ui-generator"]
=======
default = ["mofa-fm", "mofa-settings", "mofa-debate"]
mofa-fm = ["dep:mofa-fm"]
mofa-settings = ["dep:mofa-settings"]
mofa-debate = ["dep:mofa-debate"]
main

[dependencies]
makepad-widgets.workspace = true
Expand All @@ -17,6 +26,11 @@ mofa-dora-bridge = { path = "../mofa-dora-bridge" }
mofa-fm = { path = "../apps/mofa-fm", optional = true }
mofa-settings = { path = "../apps/mofa-settings", optional = true }
mofa-debate = { path = "../apps/mofa-debate", optional = true }
feature/ai-ui-generator
mofa-asr = { path = "../apps/mofa-asr", optional = true }
mofa-ui-generator = { path = "../apps/mofa-ui-generator", optional = true }
=======
main

# Audio
cpal.workspace = true
Expand Down
15 changes: 15 additions & 0 deletions mofa-studio-shell/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ pub fn get_cli_args() -> &'static Args {
use mofa_widgets::{MofaApp, AppRegistry, TimerControl, PageRouter, PageId, tab_clicked};
use mofa_fm::{MoFaFMApp, MoFaFMScreenWidgetRefExt};
use mofa_debate::{MoFaDebateApp, MoFaDebateScreenWidgetRefExt};
feature/ai-ui-generator
use mofa_asr::{MoFaASRApp, MoFaASRScreenWidgetRefExt};
use mofa_ui_generator::{MoFaUIGeneratorApp, screen::MoFaUIGeneratorScreenWidgetRefExt};
=======
// use mofa_asr::{MoFaASRApp, MoFaASRScreenWidgetRefExt};
main
use mofa_settings::MoFaSettingsApp;
use mofa_settings::data::Preferences;
use mofa_settings::screen::SettingsScreenWidgetRefExt;
Expand Down Expand Up @@ -391,7 +396,12 @@ impl LiveHook for App {
// Initialize the app registry with all installed apps
self.app_registry.register(MoFaFMApp::info());
self.app_registry.register(MoFaDebateApp::info());
feature/ai-ui-generator
self.app_registry.register(MoFaASRApp::info());
self.app_registry.register(MoFaUIGeneratorApp::info());
=======
// self.app_registry.register(MoFaASRApp::info());
main
self.app_registry.register(MoFaSettingsApp::info());

// Initialize page router (defaults to MoFA FM)
Expand Down Expand Up @@ -462,7 +472,12 @@ impl LiveRegister for App {
// (Makepad constraint), but registration uses the standardized trait interface
<MoFaFMApp as MofaApp>::live_design(cx);
<MoFaDebateApp as MofaApp>::live_design(cx);
feature/ai-ui-generator
<MoFaASRApp as MofaApp>::live_design(cx);
<MoFaUIGeneratorApp as MofaApp>::live_design(cx);
=======
// <MoFaASRApp as MofaApp>::live_design(cx);
main
<MoFaSettingsApp as MofaApp>::live_design(cx);

// Shell widgets (order matters - tabs before dashboard, apps before dashboard)
Expand Down
6 changes: 6 additions & 0 deletions mofa-studio-shell/src/widgets/dashboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ live_design! {
use mofa_settings::screen::SettingsScreen;
use mofa_debate::screen::design::MoFaDebateScreen;
use mofa_asr::screen::design::MoFaASRScreen;
use mofa_ui_generator::screen::design::MoFaUIGeneratorScreen;
use crate::widgets::tabs::TabWidget;
use crate::widgets::tabs::TabBar;

Expand Down Expand Up @@ -341,6 +342,11 @@ live_design! {
visible: false
}

ui_generator_page = <MoFaUIGeneratorScreen> {
width: Fill, height: Fill
visible: false
}

app_page = <View> {
width: Fill, height: Fill
flow: Down
Expand Down
7 changes: 7 additions & 0 deletions mofa-studio-shell/src/widgets/sidebar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,13 @@ live_design! {
}
}

ui_generator_tab = <SidebarMenuButton> {
text: "UI Generator"
draw_icon: {
svg_file: dep("crate://self/resources/icons/apps.svg")
}
}

// Apps container - height Fit so it adapts to content
apps_wrapper = <View> {
width: Fill, height: Fit
Expand Down
8 changes: 8 additions & 0 deletions mofa-widgets/src/app_trait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ pub enum PageId {
Debate,
/// ASR app
Asr,
/// UI Generator app
UIGenerator,
/// Settings page
Settings,
/// Generic app page (for demo apps)
Expand All @@ -113,6 +115,7 @@ impl PageId {
PageId::MofaFM => live_id!(mofa_fm_tab),
PageId::Debate => live_id!(debate_tab),
PageId::Asr => live_id!(mofa_asr_tab),
PageId::UIGenerator => live_id!(ui_generator_tab),
PageId::Settings => live_id!(settings_tab),
PageId::App => live_id!(app_tab),
}
Expand All @@ -124,6 +127,7 @@ impl PageId {
PageId::MofaFM => live_id!(fm_page),
PageId::Debate => live_id!(debate_page),
PageId::Asr => live_id!(asr_page),
PageId::UIGenerator => live_id!(ui_generator_page),
PageId::Settings => live_id!(settings_page),
PageId::App => live_id!(app_page),
}
Expand All @@ -145,13 +149,17 @@ impl PageRouter {
pub fn new() -> Self {
Self {
current_page: Some(PageId::MofaFM), // Default to FM
feature/ai-ui-generator
pages: vec![PageId::MofaFM, PageId::Debate, PageId::Asr, PageId::UIGenerator, PageId::Settings, PageId::App],
=======
pages: vec![
PageId::MofaFM,
PageId::Debate,
PageId::Asr,
PageId::Settings,
PageId::App,
],
main
}
}

Expand Down