Skip to main content

qualia_client_core/
studio_pane_generator.rs

1//! Keyword- and domain-driven pane layout planner for the studio prompt bar.
2//!
3//! Shared between the settings portal (`POST /generate_pane`) and any native
4//! callers. The wasm studio fetches this API on desktop; the web demo falls back
5//! to an in-crate copy in `webizen-studio::pane_generator`.
6
7use serde::{Deserialize, Serialize};
8
9#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
10pub enum PresentationMode {
11    #[default]
12    GridBound,
13    NodeRelational,
14    Spatial,
15}
16
17#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
18pub struct PanePlacement {
19    pub component_id: String,
20    pub x: u16,
21    pub y: u16,
22    pub w: u16,
23    pub h: u16,
24    #[serde(default)]
25    pub data_bindings: Vec<String>,
26}
27
28#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
29pub struct PaneGenerationPlan {
30    pub panes: Vec<PanePlacement>,
31    pub presentation: PresentationMode,
32    pub summary: String,
33}
34
35#[derive(Serialize, Deserialize, Clone, Debug, Default)]
36pub struct GeneratePaneRequest {
37    pub prompt: String,
38    #[serde(default)]
39    pub palette_ids: Vec<String>,
40    /// When set, prefer the ontology-domain preset (legal, health, commons, semantics).
41    #[serde(default)]
42    pub ontology_domain: Option<String>,
43    /// When true (default), try local LLM layout generation before keyword routing.
44    #[serde(default)]
45    pub use_llm: Option<bool>,
46}
47
48fn pane(id: &str, x: u16, y: u16, w: u16, h: u16, bindings: &[&str]) -> PanePlacement {
49    PanePlacement {
50        component_id: id.to_string(),
51        x,
52        y,
53        w,
54        h,
55        data_bindings: bindings.iter().map(|s| s.to_string()).collect(),
56    }
57}
58
59fn contains_any(haystack: &str, needles: &[&str]) -> bool {
60    needles.iter().any(|n| haystack.contains(n))
61}
62
63fn has_palette_id(palette_ids: &[String], ids: &[&str]) -> bool {
64    ids.iter().any(|id| palette_ids.iter().any(|p| p == *id))
65}
66
67/// Domain presets aligned with `ontology_import_wizard::builtin_layout_suggestions`.
68pub fn layout_from_ontology_domain(domain: &str) -> Option<PaneGenerationPlan> {
69    let d = domain.to_ascii_lowercase();
70    let plan = match d.as_str() {
71        "legal" => PaneGenerationPlan {
72            panes: vec![
73                pane("contextual-workspace", 0, 0, 56, 62, &["n3:rules"]),
74                pane("n3-logic-studio", 58, 0, 36, 30, &["n3:guardianship"]),
75                pane("shacl-validator", 58, 32, 36, 30, &["shacl:shapes"]),
76            ],
77            presentation: PresentationMode::GridBound,
78            summary: "Legal/guardianship preset: workspace, N3, SHACL.".to_string(),
79        },
80        "health" => PaneGenerationPlan {
81            panes: vec![
82                pane("health-monitor", 0, 0, 48, 40, &["fhir:Patient"]),
83                pane("personal-ontology-builder", 50, 0, 44, 40, &[]),
84                pane("llm-harness", 0, 42, 94, 20, &[]),
85            ],
86            presentation: PresentationMode::NodeRelational,
87            summary: "Health/clinical preset: vitals, ontology builder, inference.".to_string(),
88        },
89        "commons" => PaneGenerationPlan {
90            panes: vec![
91                pane("nexus", 0, 0, 40, 36, &[]),
92                pane("render-preview", 42, 0, 52, 62, &[]),
93                pane("wal-inspector", 0, 38, 40, 24, &[]),
94            ],
95            presentation: PresentationMode::Spatial,
96            summary: "Commons/spatial preset: nexus, render preview, WAL.".to_string(),
97        },
98        "semantics" => PaneGenerationPlan {
99            panes: vec![
100                pane("wordnet-demo", 0, 0, 46, 30, &[]),
101                pane("sparql-explorer", 48, 0, 46, 30, &[]),
102                pane("diffusion-visualizer", 0, 32, 94, 30, &[]),
103            ],
104            presentation: PresentationMode::GridBound,
105            summary: "Research/semantics preset: WordNet, SPARQL, diffusion.".to_string(),
106        },
107        _ => return None,
108    };
109    Some(plan)
110}
111
112/// Map a natural-language prompt (and optional palette/domain hints) to a bounded layout.
113pub fn generate_panes_from_request(req: &GeneratePaneRequest) -> PaneGenerationPlan {
114    if let Some(domain) = req.ontology_domain.as_deref() {
115        if let Some(plan) = layout_from_ontology_domain(domain) {
116            return plan;
117        }
118    }
119
120    let p = req.prompt.to_ascii_lowercase();
121    let palette = &req.palette_ids;
122
123    if contains_any(
124        &p,
125        &["health", "clinical", "vital", "fhir", "dicom", "patient"],
126    ) {
127        return PaneGenerationPlan {
128            panes: vec![
129                pane("health-monitor", 0, 0, 48, 40, &["fhir:Patient"]),
130                pane("sparql-explorer", 50, 0, 44, 40, &["sparql:clinical"]),
131                pane("llm-harness", 0, 42, 94, 20, &[]),
132            ],
133            presentation: PresentationMode::GridBound,
134            summary: "Health/clinical layout: vitals, SPARQL, inference harness.".to_string(),
135        };
136    }
137
138    if contains_any(
139        &p,
140        &[
141            "legal", "guardian", "deontic", "rights", "shacl", "contract",
142        ],
143    ) {
144        return PaneGenerationPlan {
145            panes: vec![
146                pane("contextual-workspace", 0, 0, 56, 62, &["n3:rules"]),
147                pane("n3-logic-studio", 58, 0, 36, 30, &["n3:guardianship"]),
148                pane("shacl-validator", 58, 32, 36, 30, &["shacl:shapes"]),
149            ],
150            presentation: PresentationMode::GridBound,
151            summary: "Legal/guardianship layout: workspace, N3, SHACL.".to_string(),
152        };
153    }
154
155    if contains_any(
156        &p,
157        &[
158            "spatial", "manifold", "10d", "portal", "volume", "render", "commons",
159        ],
160    ) {
161        return PaneGenerationPlan {
162            panes: vec![
163                pane("nexus", 0, 0, 40, 36, &[]),
164                pane("render-preview", 42, 0, 52, 62, &[]),
165                pane("wal-inspector", 0, 38, 40, 24, &[]),
166            ],
167            presentation: PresentationMode::Spatial,
168            summary: "Spatial commons layout: nexus, render preview, WAL.".to_string(),
169        };
170    }
171
172    if contains_any(&p, &["graph", "node", "relation", "binding"]) {
173        return PaneGenerationPlan {
174            panes: vec![
175                pane("provenance-graph", 0, 0, 46, 30, &[]),
176                pane("sparql-explorer", 48, 0, 46, 30, &[]),
177                pane("neuro-symbolic-chat", 0, 32, 94, 30, &[]),
178            ],
179            presentation: PresentationMode::NodeRelational,
180            summary: "Node-relational layout: provenance graph, SPARQL, chat.".to_string(),
181        };
182    }
183
184    if contains_any(&p, &["chat", "llm", "infer", "model", "agent"]) {
185        return PaneGenerationPlan {
186            panes: vec![
187                pane("neuro-symbolic-chat", 0, 0, 62, 62, &[]),
188                pane("inference-monitor", 64, 0, 30, 30, &[]),
189                pane("lora-manager", 64, 32, 30, 30, &[]),
190            ],
191            presentation: PresentationMode::GridBound,
192            summary: "Intelligence layout: chat, inference monitor, LoRA.".to_string(),
193        };
194    }
195
196    if contains_any(
197        &p,
198        &[
199            "sparql",
200            "rdf",
201            "ontology",
202            "triple",
203            "knowledge",
204            "wordnet",
205            "semantics",
206        ],
207    ) {
208        return PaneGenerationPlan {
209            panes: vec![
210                pane("sparql-explorer", 0, 0, 62, 62, &[]),
211                pane("personal-ontology-builder", 64, 0, 30, 30, &[]),
212                pane("n3-logic-studio", 64, 32, 30, 30, &[]),
213            ],
214            presentation: PresentationMode::GridBound,
215            summary: "Knowledge layout: SPARQL, ontology builder, N3.".to_string(),
216        };
217    }
218
219    if contains_any(&p, &["chart", "metric", "dashboard", "monitor", "track"]) {
220        let chart = if has_palette_id(palette, &["time-series-chart"]) {
221            "time-series-chart"
222        } else {
223            "card-view"
224        };
225        return PaneGenerationPlan {
226            panes: vec![
227                pane(chart, 0, 0, 56, 36, &[]),
228                pane("data-ingest-form", 0, 38, 56, 24, &[]),
229                pane("details-view", 58, 0, 36, 62, &[]),
230            ],
231            presentation: PresentationMode::GridBound,
232            summary: "Dashboard layout: chart/metric card, ingest form, details.".to_string(),
233        };
234    }
235
236    let primary = palette.first().map(|s| s.as_str()).unwrap_or("card-view");
237    let secondary = palette.get(1).map(|s| s.as_str()).unwrap_or("details-view");
238    PaneGenerationPlan {
239        panes: vec![
240            pane(primary, 0, 0, 56, 40, &[]),
241            pane(secondary, 58, 0, 36, 40, &[]),
242        ],
243        presentation: PresentationMode::GridBound,
244        summary: format!("Starter layout: {primary} + {secondary}."),
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn health_prompt_selects_clinical_panes() {
254        let plan = generate_panes_from_request(&GeneratePaneRequest {
255            prompt: "Health tracker with vitals chart".to_string(),
256            palette_ids: vec![],
257            ontology_domain: None,
258            use_llm: None,
259        });
260        assert!(plan
261            .panes
262            .iter()
263            .any(|p| p.component_id == "health-monitor"));
264    }
265
266    #[test]
267    fn spatial_prompt_sets_spatial_mode() {
268        let plan = generate_panes_from_request(&GeneratePaneRequest {
269            prompt: "10D manifold spatial portal".to_string(),
270            palette_ids: vec![],
271            ontology_domain: None,
272            use_llm: None,
273        });
274        assert_eq!(plan.presentation, PresentationMode::Spatial);
275    }
276
277    #[test]
278    fn ontology_domain_overrides_prompt() {
279        let plan = generate_panes_from_request(&GeneratePaneRequest {
280            prompt: "anything".to_string(),
281            palette_ids: vec![],
282            ontology_domain: Some("commons".to_string()),
283            use_llm: None,
284        });
285        assert_eq!(plan.presentation, PresentationMode::Spatial);
286        assert!(plan.panes.iter().any(|p| p.component_id == "nexus"));
287    }
288}