create-aiaui-module 0.0.0-stage → 0.1.1-TEST
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -2
- package/bin/create-aiaui-module.js +137 -0
- package/package.json +30 -4
- package/runtime/__init__.py +82 -0
- package/runtime/_runtime/__init__.py +40 -0
- package/runtime/_runtime/host.py +88 -0
- package/runtime/_runtime/loader.py +39 -0
- package/runtime/_runtime/registry.py +93 -0
- package/src/errors.js +6 -0
- package/src/generator.js +194 -0
- package/src/protocol.js +98 -0
- package/templates/HelloWorld/README.md +13 -0
- package/templates/HelloWorld/contractVersion +1 -0
- package/templates/HelloWorld/resources/build.mjs +9 -0
- package/templates/HelloWorld/resources/dist/manifest.json +6 -0
- package/templates/HelloWorld/resources/dist/modules.js +3753 -0
- package/templates/HelloWorld/resources/package.json +12 -0
- package/templates/HelloWorld/resources/src/Hello.ce.vue +25 -0
- package/templates/HelloWorld/resources/src/index.ts +32 -0
- package/templates/HelloWorld/resources/vite.config.mjs +13 -0
- package/templates/JobInformation/CUSTOMIZATION.md +173 -0
- package/templates/JobInformation/README.md +114 -0
- package/templates/JobInformation/api.py +53 -0
- package/templates/JobInformation/contractVersion +1 -0
- package/templates/JobInformation/resources/build.mjs +9 -0
- package/templates/JobInformation/resources/dist/manifest.json +6 -0
- package/templates/JobInformation/resources/dist/modules.js +2919 -0
- package/templates/JobInformation/resources/package-lock.json +1032 -0
- package/templates/JobInformation/resources/package.json +15 -0
- package/templates/JobInformation/resources/src/bus.ts +38 -0
- package/templates/JobInformation/resources/src/contract.ts +74 -0
- package/templates/JobInformation/resources/src/customization.ts +25 -0
- package/templates/JobInformation/resources/src/defaults/DefaultPanel.vue +22 -0
- package/templates/JobInformation/resources/src/defaults/Header.vue +12 -0
- package/templates/JobInformation/resources/src/defaults/InputCarriers.vue +15 -0
- package/templates/JobInformation/resources/src/defaults/OutputCarriers.vue +14 -0
- package/templates/JobInformation/resources/src/defaults/Recipe.vue +18 -0
- package/templates/JobInformation/resources/src/defaults/Timeline.vue +22 -0
- package/templates/JobInformation/resources/src/defaults/slots.ts +16 -0
- package/templates/JobInformation/resources/src/defaults/styles.css +90 -0
- package/templates/JobInformation/resources/src/index.ts +45 -0
- package/templates/JobInformation/resources/src/vite-env.d.ts +1 -0
- package/templates/JobInformation/resources/vite.config.mjs +27 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aiaui-hello-world-template",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": { "build": "node build.mjs" },
|
|
6
|
+
"dependencies": { "vue": "^3.5.0" },
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@vitejs/plugin-vue": "^6.0.0",
|
|
9
|
+
"typescript": "^5.9.0",
|
|
10
|
+
"vite": "^8.0.0"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{ instanceId: string }>();
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<section>
|
|
7
|
+
<h2>Hello from this Instance</h2>
|
|
8
|
+
<p>Instance: {{ instanceId }}</p>
|
|
9
|
+
</section>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<style>
|
|
13
|
+
:host {
|
|
14
|
+
display: block;
|
|
15
|
+
color: var(--aia-text, #222);
|
|
16
|
+
font: inherit;
|
|
17
|
+
}
|
|
18
|
+
section {
|
|
19
|
+
padding: 16px;
|
|
20
|
+
}
|
|
21
|
+
h2 {
|
|
22
|
+
margin: 0 0 12px;
|
|
23
|
+
font-size: 1.25rem;
|
|
24
|
+
}
|
|
25
|
+
</style>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineCustomElement } from "vue";
|
|
2
|
+
import Hello from "./Hello.ce.vue";
|
|
3
|
+
|
|
4
|
+
type ModuleContext = {
|
|
5
|
+
instanceId: string;
|
|
6
|
+
place(spec: {
|
|
7
|
+
kind: "route";
|
|
8
|
+
path: string;
|
|
9
|
+
label: string;
|
|
10
|
+
order: number;
|
|
11
|
+
}): void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const tagName = "aiaui-hello-world-panel";
|
|
15
|
+
let mountedContainer: HTMLElement | null = null;
|
|
16
|
+
|
|
17
|
+
export function mount(container: HTMLElement, context: ModuleContext) {
|
|
18
|
+
context.place({ kind: "route", path: "/hello", label: "Hello", order: 30 });
|
|
19
|
+
if (!customElements.get(tagName))
|
|
20
|
+
customElements.define(tagName, defineCustomElement(Hello));
|
|
21
|
+
const element = document.createElement(tagName) as HTMLElement & {
|
|
22
|
+
instanceId: string;
|
|
23
|
+
};
|
|
24
|
+
element.instanceId = context.instanceId;
|
|
25
|
+
container.replaceChildren(element);
|
|
26
|
+
mountedContainer = container;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function unmount() {
|
|
30
|
+
mountedContainer?.replaceChildren();
|
|
31
|
+
mountedContainer = null;
|
|
32
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import vue from "@vitejs/plugin-vue";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [vue()],
|
|
6
|
+
// Browser builds must never reference Node's `process` global.
|
|
7
|
+
// Collapse Vue's process.env.NODE_ENV guards at build time.
|
|
8
|
+
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
|
9
|
+
build: {
|
|
10
|
+
emptyOutDir: true,
|
|
11
|
+
lib: { entry: "src/index.ts", formats: ["es"], fileName: "modules" },
|
|
12
|
+
},
|
|
13
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# JobInformation customization
|
|
2
|
+
|
|
3
|
+
This module ships with a built-in panel and exposes three customization
|
|
4
|
+
levels. All of them keep the host-facing `mount(container, context)` /
|
|
5
|
+
`unmount()` contract stable, so the Instance does not need to know which
|
|
6
|
+
level you chose.
|
|
7
|
+
|
|
8
|
+
## Customization entry
|
|
9
|
+
|
|
10
|
+
Edit `resources/src/customization.ts`. It exports a single `customization`
|
|
11
|
+
object:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import type { Customization } from "./contract";
|
|
15
|
+
|
|
16
|
+
export const customization: Customization = { mode: "slots", slots: {} };
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Level 1 — default
|
|
20
|
+
|
|
21
|
+
Leave `customization.ts` untouched. The module renders the built-in panel
|
|
22
|
+
from `resources/src/defaults/`.
|
|
23
|
+
|
|
24
|
+
## Level 2 — partial (replace boxes)
|
|
25
|
+
|
|
26
|
+
Set `mode: "slots"` and provide a `slots` map. Keys are the built-in slot
|
|
27
|
+
names; values are Vue components that receive `context` as a prop.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import MyHeader from "./custom/MyHeader.vue";
|
|
31
|
+
import type { Customization } from "./contract";
|
|
32
|
+
|
|
33
|
+
export const customization: Customization = {
|
|
34
|
+
mode: "slots",
|
|
35
|
+
slots: { header: MyHeader },
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Built-in slots
|
|
40
|
+
|
|
41
|
+
- `header` — job/lot identity and current state
|
|
42
|
+
- `inputCarriers` — input carriers
|
|
43
|
+
- `outputCarriers` — output carriers
|
|
44
|
+
- `timeline` — progress steps
|
|
45
|
+
- `recipe` — recipe/tool summary and actions
|
|
46
|
+
|
|
47
|
+
### Slot component contract
|
|
48
|
+
|
|
49
|
+
```vue
|
|
50
|
+
<script setup lang="ts">
|
|
51
|
+
import type { ModuleContext } from "../contract";
|
|
52
|
+
|
|
53
|
+
defineProps<{ context: ModuleContext }>();
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<template>
|
|
57
|
+
<section class="aiaji-slot">…</section>
|
|
58
|
+
</template>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`context.api.call(method, path, options)` reaches this module's backend
|
|
62
|
+
router. `context.signal` is aborted when the host unmounts the module, so
|
|
63
|
+
long-running requests should observe it.
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
## Communicating between slots
|
|
67
|
+
|
|
68
|
+
Every slot component in one mount — built-in or custom — receives the same
|
|
69
|
+
`context` object. On top of `api` and `signal`, `context` carries a **bus**
|
|
70
|
+
created fresh on each `mount` and dead on `unmount` (so switching Instance
|
|
71
|
+
gets you a clean bus automatically). It never crosses module or Instance
|
|
72
|
+
boundaries.
|
|
73
|
+
|
|
74
|
+
### 1. Events (recommended for most cases)
|
|
75
|
+
|
|
76
|
+
Decoupled one-to-many communication:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// MyInput.vue — sender
|
|
80
|
+
const props = defineProps<{ context: ModuleContext }>();
|
|
81
|
+
|
|
82
|
+
function onSelect(id: string) {
|
|
83
|
+
props.context.bus.emit("lot:selected", { id });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
onBeforeUnmount(() => props.context.bus.off("lot:selected", onSelect));
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// MyOutput.vue — receiver
|
|
91
|
+
const props = defineProps<{ context: ModuleContext }>();
|
|
92
|
+
|
|
93
|
+
onMounted(() =>
|
|
94
|
+
props.context.bus.on("lot:selected", (payload) => {
|
|
95
|
+
// `payload` is whatever the emitter passed; type it locally if you like
|
|
96
|
+
console.log("lot picked:", payload?.id);
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
onBeforeUnmount(() => props.context.bus.off("lot:selected", someHandler));
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- `emit` with no listeners is a no-op (safe).
|
|
103
|
+
- Always `off` what you `on` in `onBeforeUnmount`, or old listeners fire on remount.
|
|
104
|
+
- Type the payload where you need it; the bus signature is loose by design.
|
|
105
|
+
|
|
106
|
+
### 2. Imperative handles (when you must call into a sibling)
|
|
107
|
+
|
|
108
|
+
For `expose/peer` — e.g. OutputCarriers telling InputCarriers to focus a lot:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// MyInput.vue — publisher
|
|
112
|
+
const handle = {
|
|
113
|
+
focusLot(id: string) {
|
|
114
|
+
/* imperative action inside InputCarriers */
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
onMounted(() => props.context.bus.expose("inputCarriers", handle));
|
|
118
|
+
onUnmounted(() => props.context.bus.release("inputCarriers"));
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
// MyOutput.vue — caller
|
|
123
|
+
props.context.bus
|
|
124
|
+
.peer<{ focusLot(id: string): void }>("inputCarriers")
|
|
125
|
+
?.focusLot("CR-01"); // always `?.` — peer returns undefined if the slot
|
|
126
|
+
// was replaced away or hasn't mounted yet
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 3. Module-scope shared state (no bus needed)
|
|
130
|
+
|
|
131
|
+
For pure data sync between two custom slots, a `reactive()` in a
|
|
132
|
+
module-scope file also works with zero coupling:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
// resources/src/custom/shared.ts
|
|
136
|
+
import { reactive } from "vue";
|
|
137
|
+
export const shared = reactive({ selectedLot: null as string | null });
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
// both MyInput.vue and MyOutput.vue
|
|
142
|
+
import { shared } from "./shared";
|
|
143
|
+
shared.selectedLot = "CR-01"; // set in one, read in the other
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Rule of thumb: defaults → events; only reach for `expose/peer` when the
|
|
147
|
+
caller genuinely needs the peer's imperative API and accepts a
|
|
148
|
+
not-yet-mounted/null risk.
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
## Level 3 — full override
|
|
152
|
+
|
|
153
|
+
Set `mode: "full"` and implement both entry points. The shell skips the
|
|
154
|
+
built-in panel entirely.
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
export const customization: Customization = {
|
|
158
|
+
mode: "full",
|
|
159
|
+
mount(container, context) {
|
|
160
|
+
// render anything you want into `container`
|
|
161
|
+
},
|
|
162
|
+
unmount() {
|
|
163
|
+
// tear down your renderer
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Styling
|
|
169
|
+
|
|
170
|
+
The built-in styles are bundled into `modules.js`, so no separate CSS asset
|
|
171
|
+
is required. Slot components can reuse the built-in `aiaji-*` classes and the
|
|
172
|
+
`--aia-*` CSS variables (`--aia-text`, `--aia-border`, `--aia-accent`), or
|
|
173
|
+
bring their own inline styles.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# JobInformation module
|
|
2
|
+
|
|
3
|
+
This template scaffolds an editable "job information" UI module. The
|
|
4
|
+
generator only produces scaffolding plus an inlined runtime
|
|
5
|
+
(`PyAIAUI/_runtime`); **you do not need to install an `aiaui` package.**
|
|
6
|
+
|
|
7
|
+
## Backend contract (what you implement)
|
|
8
|
+
|
|
9
|
+
Implement one entry point in `api.py`:
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from fastapi import APIRouter
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_api(context) -> APIRouter:
|
|
16
|
+
router = APIRouter()
|
|
17
|
+
# reach Instance objects through context.services
|
|
18
|
+
return router
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The generated host calls `create_api(context)` once and mounts the returned
|
|
22
|
+
router at `/modules/JobInformation`. You receive:
|
|
23
|
+
|
|
24
|
+
- `context.instance_root` — the Instance root directory.
|
|
25
|
+
- `context.services` — a dict of the live objects your Instance passes in at
|
|
26
|
+
startup (see below).
|
|
27
|
+
- `context.logger` — an optional logger, or `None`.
|
|
28
|
+
|
|
29
|
+
Everything else about how to fetch or present data is yours to implement.
|
|
30
|
+
|
|
31
|
+
## Frontend
|
|
32
|
+
|
|
33
|
+
The frontend is organized around a small customization contract:
|
|
34
|
+
|
|
35
|
+
- `resources/src/index.ts` — the stable `mount` / `unmount` bridge the host loads.
|
|
36
|
+
- `resources/src/contract.ts` — the shared types for the module context, bus, and customization.
|
|
37
|
+
- `resources/src/customization.ts` — the only file you normally edit to customize.
|
|
38
|
+
- `resources/src/bus.ts` — a per-mount communication bus shared by all slot components.
|
|
39
|
+
- `resources/src/defaults/` — the built-in panel and its replaceable boxes.
|
|
40
|
+
|
|
41
|
+
Slot components also communicate through `context.bus` (events or imperative handles); see CUSTOMIZATION.md for the patterns.
|
|
42
|
+
|
|
43
|
+
Three levels are supported without changing `index.ts`:
|
|
44
|
+
|
|
45
|
+
1. **Default** — do nothing; the built-in panel renders.
|
|
46
|
+
2. **Partial** — replace one or more named boxes:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// resources/src/customization.ts
|
|
50
|
+
import MyHeader from "./custom/MyHeader.vue";
|
|
51
|
+
import type { Customization } from "./contract";
|
|
52
|
+
|
|
53
|
+
export const customization: Customization = {
|
|
54
|
+
mode: "slots",
|
|
55
|
+
slots: { header: MyHeader },
|
|
56
|
+
};
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
3. **Full** — replace the whole module, including its page and data flow:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
export const customization: Customization = {
|
|
63
|
+
mode: "full",
|
|
64
|
+
mount(container, context) { /* your own renderer */ },
|
|
65
|
+
unmount() { /* your own teardown */ },
|
|
66
|
+
};
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
See `CUSTOMIZATION.md` for the slot names and component contract.
|
|
70
|
+
|
|
71
|
+
Rebuild after editing:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
cd resources
|
|
75
|
+
npm install
|
|
76
|
+
npm run build
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
For quick iteration, `npm run dev` runs `vite build --watch` and keeps
|
|
80
|
+
`dist/manifest.json` valid on every rebuild.
|
|
81
|
+
|
|
82
|
+
## Instance startup (wiring)
|
|
83
|
+
|
|
84
|
+
Call the generated entry point from your Instance's own startup method:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from PyAIAUI import AIAUI
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
aiaui = AIAUI.start(services={
|
|
91
|
+
"secs_server": secs_server,
|
|
92
|
+
"job_repository": job_repository,
|
|
93
|
+
})
|
|
94
|
+
# aiaui is None when no modules are enabled
|
|
95
|
+
|
|
96
|
+
# ... later, during shutdown ...
|
|
97
|
+
aiaui.stop()
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`start()` returns `None` when `JobInformation` is disabled in
|
|
101
|
+
`Config/AIAUIProtocol.json`; otherwise it returns the running host.
|
|
102
|
+
|
|
103
|
+
## Dependencies (reminder, not installed automatically)
|
|
104
|
+
|
|
105
|
+
The backend example uses FastAPI and the generated host runs on Uvicorn.
|
|
106
|
+
Install them into the Instance's own Python environment:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
pip install fastapi uvicorn
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Generation does not install dependencies or start any process. Set a free
|
|
113
|
+
loopback port and enable the module in `Config/AIAUIProtocol.json` only after
|
|
114
|
+
you have integrated `start()` / `stop()` into the Instance lifecycle.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Editable Job Information example, mounted under /modules/JobInformation."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_api(context) -> APIRouter:
|
|
9
|
+
"""Entry point invoked by the generated PyAIAUI host.
|
|
10
|
+
|
|
11
|
+
The Instance injects its live objects via ``PyAIAUI.AIAUI.start(services={...})``
|
|
12
|
+
and they arrive here through ``context.services``.
|
|
13
|
+
"""
|
|
14
|
+
router = APIRouter()
|
|
15
|
+
|
|
16
|
+
@router.get("/server")
|
|
17
|
+
async def server():
|
|
18
|
+
secs_server = context.services.get("secs_server")
|
|
19
|
+
if secs_server is None:
|
|
20
|
+
return {"available": False, "name": None, "connected": None}
|
|
21
|
+
driver = getattr(secs_server, "driver", None)
|
|
22
|
+
check = getattr(driver, "is_connected", None)
|
|
23
|
+
connected = check() if callable(check) else None
|
|
24
|
+
if inspect.isawaitable(connected):
|
|
25
|
+
connected = await connected
|
|
26
|
+
return {
|
|
27
|
+
"available": True,
|
|
28
|
+
"name": getattr(secs_server, "name", None),
|
|
29
|
+
"connected": connected,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@router.get("/jobs")
|
|
33
|
+
async def jobs(name: str = "", page: int = 1, pageSize: int = 20):
|
|
34
|
+
repository = context.services.get("job_repository")
|
|
35
|
+
if repository is not None:
|
|
36
|
+
result = repository.list_jobs(name=name, page=page, page_size=pageSize)
|
|
37
|
+
return await result if inspect.isawaitable(result) else result
|
|
38
|
+
|
|
39
|
+
# Sample data is used only until the Instance injects a repository.
|
|
40
|
+
items = [
|
|
41
|
+
{"id": "demo-1", "name": "DEMO-JOB-001", "state": "Running"},
|
|
42
|
+
{"id": "demo-2", "name": "DEMO-JOB-002", "state": "Queued"},
|
|
43
|
+
]
|
|
44
|
+
matched = [item for item in items if name.casefold() in item["name"].casefold()]
|
|
45
|
+
start = (page - 1) * pageSize
|
|
46
|
+
return {
|
|
47
|
+
"items": matched[start : start + pageSize],
|
|
48
|
+
"total": len(matched),
|
|
49
|
+
"page": page,
|
|
50
|
+
"pageSize": pageSize,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return router
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1
|