1use 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 #[serde(default)]
42 pub ontology_domain: Option<String>,
43 #[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
67pub 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
112pub 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}