aimeat 1.23.3 → 1.24.0
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/dist/locales/en.json +44 -0
- package/dist/locales/fi.json +44 -0
- package/dist/public/css/views/workflows.css +111 -0
- package/dist/public/js/services/workflows.js +57 -0
- package/dist/public/llms-template.txt +7 -0
- package/dist/public/spa.html +3 -1
- package/dist/public/views/profile/landing-page.js +1 -0
- package/dist/public/views/profile/workflows-form.js +220 -0
- package/dist/public/views/profile/workflows-tab.js +244 -0
- package/dist/public/views/profile.js +2 -0
- package/dist/src/cli/connect/mcp/tools/index.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/index.js +2 -0
- package/dist/src/cli/connect/mcp/tools/index.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/workflows.d.ts +12 -0
- package/dist/src/cli/connect/mcp/tools/workflows.d.ts.map +1 -0
- package/dist/src/cli/connect/mcp/tools/workflows.js +37 -0
- package/dist/src/cli/connect/mcp/tools/workflows.js.map +1 -0
- package/dist/src/cli/connect/tool-call.d.ts.map +1 -1
- package/dist/src/cli/connect/tool-call.js +37 -0
- package/dist/src/cli/connect/tool-call.js.map +1 -1
- package/dist/src/generated/api-types.d.ts +569 -0
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/mcp/annotations.d.ts.map +1 -1
- package/dist/src/mcp/annotations.js +3 -0
- package/dist/src/mcp/annotations.js.map +1 -1
- package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
- package/dist/src/mcp/catalog/definitions.js +27 -0
- package/dist/src/mcp/catalog/definitions.js.map +1 -1
- package/dist/src/mcp/catalog/scopes.d.ts.map +1 -1
- package/dist/src/mcp/catalog/scopes.js +5 -1
- package/dist/src/mcp/catalog/scopes.js.map +1 -1
- package/dist/src/mcp/catalog/surfaces.d.ts.map +1 -1
- package/dist/src/mcp/catalog/surfaces.js +1 -0
- package/dist/src/mcp/catalog/surfaces.js.map +1 -1
- package/dist/src/mcp/index.d.ts.map +1 -1
- package/dist/src/mcp/index.js +2 -0
- package/dist/src/mcp/index.js.map +1 -1
- package/dist/src/mcp/workflows.d.ts +18 -0
- package/dist/src/mcp/workflows.d.ts.map +1 -0
- package/dist/src/mcp/workflows.js +68 -0
- package/dist/src/mcp/workflows.js.map +1 -0
- package/dist/src/models/offer-schemas.d.ts +7 -0
- package/dist/src/models/offer-schemas.d.ts.map +1 -1
- package/dist/src/models/offer-schemas.js +12 -0
- package/dist/src/models/offer-schemas.js.map +1 -1
- package/dist/src/models/workflow-schemas.d.ts +220 -0
- package/dist/src/models/workflow-schemas.d.ts.map +1 -0
- package/dist/src/models/workflow-schemas.js +102 -0
- package/dist/src/models/workflow-schemas.js.map +1 -0
- package/dist/src/routes/agent-tasks.d.ts.map +1 -1
- package/dist/src/routes/agent-tasks.js +16 -0
- package/dist/src/routes/agent-tasks.js.map +1 -1
- package/dist/src/routes/memory.d.ts.map +1 -1
- package/dist/src/routes/memory.js +5 -0
- package/dist/src/routes/memory.js.map +1 -1
- package/dist/src/routes/stats.d.ts.map +1 -1
- package/dist/src/routes/stats.js +13 -0
- package/dist/src/routes/stats.js.map +1 -1
- package/dist/src/routes/workflows.d.ts +30 -0
- package/dist/src/routes/workflows.d.ts.map +1 -0
- package/dist/src/routes/workflows.js +178 -0
- package/dist/src/routes/workflows.js.map +1 -0
- package/dist/src/server-bootstrap/routes-loader.d.ts +2 -0
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +7 -1
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/server-bootstrap/service-init.d.ts +2 -0
- package/dist/src/server-bootstrap/service-init.d.ts.map +1 -1
- package/dist/src/server-bootstrap/service-init.js +6 -0
- package/dist/src/server-bootstrap/service-init.js.map +1 -1
- package/dist/src/server.d.ts.map +1 -1
- package/dist/src/server.js +1 -0
- package/dist/src/server.js.map +1 -1
- package/dist/src/services/handbooks/agent.d.ts +1 -1
- package/dist/src/services/handbooks/agent.d.ts.map +1 -1
- package/dist/src/services/handbooks/agent.js +10 -0
- package/dist/src/services/handbooks/agent.js.map +1 -1
- package/dist/src/services/scheduler.d.ts +6 -0
- package/dist/src/services/scheduler.d.ts.map +1 -1
- package/dist/src/services/scheduler.js +22 -0
- package/dist/src/services/scheduler.js.map +1 -1
- package/dist/src/services/schema-validator.d.ts +9 -0
- package/dist/src/services/schema-validator.d.ts.map +1 -1
- package/dist/src/services/schema-validator.js +16 -0
- package/dist/src/services/schema-validator.js.map +1 -1
- package/dist/src/services/workflow/engine.d.ts +84 -0
- package/dist/src/services/workflow/engine.d.ts.map +1 -0
- package/dist/src/services/workflow/engine.js +545 -0
- package/dist/src/services/workflow/engine.js.map +1 -0
- package/dist/src/services/workflow/eval-context.d.ts +23 -0
- package/dist/src/services/workflow/eval-context.d.ts.map +1 -0
- package/dist/src/services/workflow/eval-context.js +52 -0
- package/dist/src/services/workflow/eval-context.js.map +1 -0
- package/dist/src/services/workflow/lifecycle.d.ts +38 -0
- package/dist/src/services/workflow/lifecycle.d.ts.map +1 -0
- package/dist/src/services/workflow/lifecycle.js +99 -0
- package/dist/src/services/workflow/lifecycle.js.map +1 -0
- package/dist/src/services/workflow/signal-eval.d.ts +70 -0
- package/dist/src/services/workflow/signal-eval.d.ts.map +1 -0
- package/dist/src/services/workflow/signal-eval.js +174 -0
- package/dist/src/services/workflow/signal-eval.js.map +1 -0
- package/dist/src/services/workflow/store.d.ts +89 -0
- package/dist/src/services/workflow/store.d.ts.map +1 -0
- package/dist/src/services/workflow/store.js +284 -0
- package/dist/src/services/workflow/store.js.map +1 -0
- package/dist/src/storage/interface.d.ts +2 -2
- package/dist/src/storage/interface.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/locales/en.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"profile.landing.home": "Home",
|
|
3
3
|
"profile.landing.menu": "Menu",
|
|
4
|
+
"profile.tabs.workflows": "Workflows",
|
|
5
|
+
"profile.workflows.title": "Workflows",
|
|
6
|
+
"profile.workflows.desc": "Declared, ordered agent pipelines with per-step health checks — they show whether each step actually produced, not just that it fired.",
|
|
7
|
+
"profile.workflows.loading": "Loading workflows…",
|
|
8
|
+
"profile.workflows.empty": "No workflows yet. An agent or the API can define one; it will appear here.",
|
|
9
|
+
"profile.workflows.trigger.schedule": "schedule",
|
|
10
|
+
"profile.workflows.trigger.event": "event",
|
|
11
|
+
"profile.workflows.trigger.manual": "manual",
|
|
12
|
+
"profile.workflows.stepCount": "{count} steps",
|
|
13
|
+
"profile.workflows.lastRun": "last run",
|
|
14
|
+
"profile.workflows.check": "Check",
|
|
15
|
+
"profile.workflows.run": "Run",
|
|
16
|
+
"profile.workflows.runStarted": "Run started",
|
|
17
|
+
"profile.workflows.back": "Back",
|
|
18
|
+
"profile.workflows.blueprint": "Blueprint",
|
|
19
|
+
"profile.workflows.after": "after",
|
|
20
|
+
"profile.workflows.recentRuns": "Recent runs",
|
|
21
|
+
"profile.workflows.noRuns": "No runs yet.",
|
|
22
|
+
"profile.workflows.attempt": "attempt",
|
|
23
|
+
"profile.workflows.health.title": "Per-step health over recent runs (green = produced, red = failed)",
|
|
24
|
+
"profile.workflows.inspectorDispatched": "{count} inspector task(s) dispatched",
|
|
25
|
+
"profile.workflows.new": "New",
|
|
26
|
+
"profile.workflows.edit": "Edit",
|
|
27
|
+
"profile.workflows.form.newTitle": "New workflow",
|
|
28
|
+
"profile.workflows.form.editTitle": "Edit workflow",
|
|
29
|
+
"profile.workflows.form.id": "Id (slug)",
|
|
30
|
+
"profile.workflows.form.idHint": "Id must be a lowercase slug (a-z, 0-9, hyphens).",
|
|
31
|
+
"profile.workflows.form.titleField": "Title",
|
|
32
|
+
"profile.workflows.form.description": "Description",
|
|
33
|
+
"profile.workflows.form.trigger": "Trigger",
|
|
34
|
+
"profile.workflows.form.matchKey": "match key (glob)",
|
|
35
|
+
"profile.workflows.form.matchOffer": "match offer id",
|
|
36
|
+
"profile.workflows.form.vars": "Variables",
|
|
37
|
+
"profile.workflows.form.addVar": "var",
|
|
38
|
+
"profile.workflows.form.steps": "Steps",
|
|
39
|
+
"profile.workflows.form.addStep": "step",
|
|
40
|
+
"profile.workflows.form.pickAgent": "agent…",
|
|
41
|
+
"profile.workflows.form.pickOffer": "offer…",
|
|
42
|
+
"profile.workflows.form.noCompatibleOffers": "This agent has no workflow-compatible offers (must publish success_signal + required_to_function + deliverable.location).",
|
|
43
|
+
"profile.workflows.form.noInput": "no input gate",
|
|
44
|
+
"profile.workflows.form.llmApproved": "Allow the node LLM to evaluate llm signal leaves in this workflow",
|
|
45
|
+
"profile.workflows.form.cancel": "Cancel",
|
|
46
|
+
"profile.workflows.form.save": "Save",
|
|
47
|
+
"profile.workflows.form.saved": "Workflow saved",
|
|
4
48
|
"profile.tabs.scheduler": "Scheduler",
|
|
5
49
|
"profile.agents.detail.tabs.schedules": "Schedules",
|
|
6
50
|
"profile.scheduler.title": "Scheduler",
|
package/dist/locales/fi.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"profile.landing.home": "Etusivu",
|
|
3
3
|
"profile.landing.menu": "Valikko",
|
|
4
|
+
"profile.tabs.workflows": "Työnkulut",
|
|
5
|
+
"profile.workflows.title": "Työnkulut",
|
|
6
|
+
"profile.workflows.desc": "Määritellyt, järjestetyt agenttiketjut askelkohtaisilla terveystarkistuksilla — ne näyttävät tuottiko jokainen askel oikeasti tuloksen, ei vain että se käynnistyi.",
|
|
7
|
+
"profile.workflows.loading": "Ladataan työnkulkuja…",
|
|
8
|
+
"profile.workflows.empty": "Ei työnkulkuja vielä. Agentti tai API voi määritellä sellaisen; se ilmestyy tähän.",
|
|
9
|
+
"profile.workflows.trigger.schedule": "ajastus",
|
|
10
|
+
"profile.workflows.trigger.event": "tapahtuma",
|
|
11
|
+
"profile.workflows.trigger.manual": "manuaalinen",
|
|
12
|
+
"profile.workflows.stepCount": "{count} askelta",
|
|
13
|
+
"profile.workflows.lastRun": "viimeisin ajo",
|
|
14
|
+
"profile.workflows.check": "Tarkista",
|
|
15
|
+
"profile.workflows.run": "Aja",
|
|
16
|
+
"profile.workflows.runStarted": "Ajo käynnistetty",
|
|
17
|
+
"profile.workflows.back": "Takaisin",
|
|
18
|
+
"profile.workflows.blueprint": "Rakennekaavio",
|
|
19
|
+
"profile.workflows.after": "jälkeen",
|
|
20
|
+
"profile.workflows.recentRuns": "Viimeisimmät ajot",
|
|
21
|
+
"profile.workflows.noRuns": "Ei ajoja vielä.",
|
|
22
|
+
"profile.workflows.attempt": "yritys",
|
|
23
|
+
"profile.workflows.health.title": "Askelkohtainen terveys viime ajoista (vihreä = tuotti, punainen = epäonnistui)",
|
|
24
|
+
"profile.workflows.inspectorDispatched": "{count} tarkastustaskia lähetetty",
|
|
25
|
+
"profile.workflows.new": "Uusi",
|
|
26
|
+
"profile.workflows.edit": "Muokkaa",
|
|
27
|
+
"profile.workflows.form.newTitle": "Uusi työnkulku",
|
|
28
|
+
"profile.workflows.form.editTitle": "Muokkaa työnkulkua",
|
|
29
|
+
"profile.workflows.form.id": "Tunnus (slug)",
|
|
30
|
+
"profile.workflows.form.idHint": "Tunnuksen on oltava pienaakkos-slug (a-z, 0-9, väliviivat).",
|
|
31
|
+
"profile.workflows.form.titleField": "Otsikko",
|
|
32
|
+
"profile.workflows.form.description": "Kuvaus",
|
|
33
|
+
"profile.workflows.form.trigger": "Liipaisin",
|
|
34
|
+
"profile.workflows.form.matchKey": "osuma-avain (glob)",
|
|
35
|
+
"profile.workflows.form.matchOffer": "osuma-offer-id",
|
|
36
|
+
"profile.workflows.form.vars": "Muuttujat",
|
|
37
|
+
"profile.workflows.form.addVar": "muuttuja",
|
|
38
|
+
"profile.workflows.form.steps": "Askeleet",
|
|
39
|
+
"profile.workflows.form.addStep": "askel",
|
|
40
|
+
"profile.workflows.form.pickAgent": "agentti…",
|
|
41
|
+
"profile.workflows.form.pickOffer": "offer…",
|
|
42
|
+
"profile.workflows.form.noCompatibleOffers": "Tällä agentilla ei ole työnkulku-yhteensopivia offereita (täytyy julkaista success_signal + required_to_function + deliverable.location).",
|
|
43
|
+
"profile.workflows.form.noInput": "ei syöteporttia",
|
|
44
|
+
"profile.workflows.form.llmApproved": "Salli node-LLM:n arvioida llm-signaalilehdet tässä työnkulussa",
|
|
45
|
+
"profile.workflows.form.cancel": "Peruuta",
|
|
46
|
+
"profile.workflows.form.save": "Tallenna",
|
|
47
|
+
"profile.workflows.form.saved": "Työnkulku tallennettu",
|
|
4
48
|
"profile.tabs.scheduler": "Ajastin",
|
|
5
49
|
"profile.agents.detail.tabs.schedules": "Ajastukset",
|
|
6
50
|
"profile.scheduler.title": "Ajastin",
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file workflows.css
|
|
3
|
+
* @description Styles for Profile › Workflows (workflows-tab.js): the list of workflows with a
|
|
4
|
+
* health sparkline, the blueprint graph + recent runs detail, and the single-run per-step
|
|
5
|
+
* timeline. Prefix: wf-. Theme-token driven (no hardcoded colors) so it flips with dark/light.
|
|
6
|
+
* @version-history
|
|
7
|
+
* v1.0.0 -- 2026-06-13 -- Phase 9: initial workflows tab styles.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
.wf-tab { display: flex; flex-direction: column; gap: 1.1rem; }
|
|
11
|
+
.wf-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }
|
|
12
|
+
.wf-loading, .wf-empty { color: var(--text-dim); padding: 0.75rem 0; font-size: 0.92rem; }
|
|
13
|
+
.wf-error {
|
|
14
|
+
background: var(--danger-bg); color: var(--danger-fg);
|
|
15
|
+
border: 1px solid var(--danger-border, var(--border)); border-radius: var(--radius-sm); padding: 0.6rem 0.85rem;
|
|
16
|
+
}
|
|
17
|
+
.wf-muted { color: var(--text-dim); font-size: 0.82rem; }
|
|
18
|
+
.wf-section-title { font-size: 1rem; font-weight: 700; color: var(--text); margin: 0.5rem 0 0.1rem; }
|
|
19
|
+
|
|
20
|
+
/* ── badges ── */
|
|
21
|
+
.wf-badge {
|
|
22
|
+
display: inline-block; padding: 0.1rem 0.5rem; border-radius: var(--radius-full, 999px);
|
|
23
|
+
font-size: 0.72rem; font-weight: 700; line-height: 1.5;
|
|
24
|
+
}
|
|
25
|
+
.wf-badge--ok { background: var(--success-bg); color: var(--success-fg); }
|
|
26
|
+
.wf-badge--err { background: var(--danger-bg); color: var(--danger-fg); }
|
|
27
|
+
.wf-badge--run { background: var(--info-bg); color: var(--info-fg); }
|
|
28
|
+
.wf-badge--muted { background: var(--bg-dim); color: var(--text-dim); }
|
|
29
|
+
|
|
30
|
+
/* ── list ── */
|
|
31
|
+
.wf-card-list { display: flex; flex-direction: column; gap: 0.75rem; }
|
|
32
|
+
.wf-card {
|
|
33
|
+
display: flex; align-items: center; justify-content: space-between; gap: 1rem;
|
|
34
|
+
background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 0.85rem 1rem;
|
|
35
|
+
}
|
|
36
|
+
.wf-card-main { flex: 1; cursor: pointer; display: flex; flex-direction: column; gap: 0.25rem; }
|
|
37
|
+
.wf-card-title { font-weight: 700; color: var(--text); display: flex; align-items: center; gap: 0.5rem; }
|
|
38
|
+
.wf-card-meta { font-size: 0.82rem; color: var(--text-dim); }
|
|
39
|
+
.wf-card-actions { display: flex; gap: 0.4rem; white-space: nowrap; }
|
|
40
|
+
|
|
41
|
+
/* ── sparkline ── */
|
|
42
|
+
.wf-spark { display: inline-flex; gap: 2px; margin-top: 0.2rem; align-items: flex-end; }
|
|
43
|
+
.wf-spark-bar { width: 7px; height: 14px; border-radius: 2px; display: inline-block; }
|
|
44
|
+
.wf-spark-bar--ok { background: var(--success-fg); }
|
|
45
|
+
.wf-spark-bar--err { background: var(--danger-fg); }
|
|
46
|
+
.wf-spark-bar--none { background: var(--bg-dim); }
|
|
47
|
+
|
|
48
|
+
/* ── detail ── */
|
|
49
|
+
.wf-detail-head { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
|
|
50
|
+
.wf-detail-title { font-size: 1.1rem; font-weight: 700; color: var(--text); flex: 1; }
|
|
51
|
+
.wf-detail-actions { display: flex; gap: 0.4rem; }
|
|
52
|
+
|
|
53
|
+
.wf-graph { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
54
|
+
.wf-node {
|
|
55
|
+
background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-sm);
|
|
56
|
+
padding: 0.6rem 0.8rem; display: flex; flex-direction: column; gap: 0.15rem;
|
|
57
|
+
}
|
|
58
|
+
.wf-node-id { font-weight: 700; color: var(--text); }
|
|
59
|
+
.wf-node-agent { font-size: 0.82rem; color: var(--text-dim); }
|
|
60
|
+
.wf-node-io { font-size: 0.78rem; color: var(--text-dim); font-family: var(--font-mono, monospace); }
|
|
61
|
+
.wf-node-dep { font-size: 0.74rem; color: var(--accent); }
|
|
62
|
+
|
|
63
|
+
.wf-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
|
|
64
|
+
.wf-table td { padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--border); color: var(--text); }
|
|
65
|
+
.wf-run-row { cursor: pointer; }
|
|
66
|
+
.wf-run-row:hover { background: var(--bg-dim); }
|
|
67
|
+
|
|
68
|
+
/* ── run timeline ── */
|
|
69
|
+
.wf-steps { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
70
|
+
.wf-step {
|
|
71
|
+
background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 0.55rem 0.8rem;
|
|
72
|
+
}
|
|
73
|
+
.wf-step-row { display: flex; align-items: center; gap: 0.6rem; }
|
|
74
|
+
.wf-step-id { font-weight: 600; color: var(--text); }
|
|
75
|
+
.wf-observed {
|
|
76
|
+
margin: 0.4rem 0 0; padding: 0.5rem 0.65rem; background: var(--bg-dim); border-radius: var(--radius-sm);
|
|
77
|
+
font-size: 0.76rem; color: var(--text-dim); overflow-x: auto; white-space: pre-wrap;
|
|
78
|
+
}
|
|
79
|
+
.wf-inspections {
|
|
80
|
+
margin-top: 0.5rem; padding: 0.5rem 0.7rem; background: var(--warn-bg); color: var(--warn-fg);
|
|
81
|
+
border-radius: var(--radius-sm); font-size: 0.82rem;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* ── create/edit form ── */
|
|
85
|
+
.wf-form { display: flex; flex-direction: column; gap: 0.8rem; }
|
|
86
|
+
.wf-field { display: flex; flex-direction: column; gap: 0.25rem; }
|
|
87
|
+
.wf-field > span { font-size: 0.82rem; font-weight: 600; color: var(--text-dim); }
|
|
88
|
+
.wf-field input[type="text"], .wf-field input[type="number"], .wf-field select,
|
|
89
|
+
.wf-field-row input, .wf-field-row select {
|
|
90
|
+
background: var(--bg-input, var(--bg-dim)); color: var(--text);
|
|
91
|
+
border: 1px solid var(--border); border-radius: var(--radius-sm);
|
|
92
|
+
padding: 0.4rem 0.55rem; font: inherit;
|
|
93
|
+
}
|
|
94
|
+
.wf-field-row { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
|
|
95
|
+
.wf-field-row > input, .wf-field-row > select { flex: 1; min-width: 8rem; }
|
|
96
|
+
.wf-form-section {
|
|
97
|
+
border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 0.7rem 0.85rem;
|
|
98
|
+
display: flex; flex-direction: column; gap: 0.55rem;
|
|
99
|
+
}
|
|
100
|
+
.wf-form-section-head { display: flex; justify-content: space-between; align-items: center; font-weight: 700; color: var(--text); }
|
|
101
|
+
.wf-step-edit {
|
|
102
|
+
border-left: 2px solid var(--accent-soft, var(--border)); padding-left: 0.7rem;
|
|
103
|
+
display: flex; flex-direction: column; gap: 0.4rem;
|
|
104
|
+
}
|
|
105
|
+
.wf-step-opts { font-size: 0.82rem; color: var(--text-dim); }
|
|
106
|
+
.wf-step-after { display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: center; font-size: 0.82rem; }
|
|
107
|
+
.wf-inline { display: inline-flex; align-items: center; gap: 0.3rem; color: var(--text); }
|
|
108
|
+
.wf-inline input[type="number"] { width: 4.5rem; background: var(--bg-input, var(--bg-dim)); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 0.2rem 0.4rem; }
|
|
109
|
+
.wf-llm-toggle { margin-top: 0.2rem; font-size: 0.9rem; }
|
|
110
|
+
.wf-form-actions { display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 0.4rem; }
|
|
111
|
+
.wf-err-list { margin: 0; padding-left: 1.1rem; }
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file workflows.js
|
|
3
|
+
* @description Frontend API service for Agent Workflows (Profile › Workflows tab). Wraps the
|
|
4
|
+
* /v1/workflows endpoints (list / get / blueprint / runs / run / health). All functions return
|
|
5
|
+
* the AIMEAT envelope; callers read `res.data`.
|
|
6
|
+
* @usage import { listWorkflows, getBlueprint, runWorkflow } from '/js/services/workflows.js';
|
|
7
|
+
* @version-history
|
|
8
|
+
* v1.0.0 -- 2026-06-13 -- Phase 9: initial workflows service for the profile tab.
|
|
9
|
+
*/
|
|
10
|
+
import { apiGet, apiPost, apiPut, apiDelete } from '/js/api.js';
|
|
11
|
+
|
|
12
|
+
/** Create/update a workflow definition (PUT /v1/workflows/:id). Returns the envelope. */
|
|
13
|
+
export async function putWorkflow(id, def) {
|
|
14
|
+
return apiPut(`/v1/workflows/${encodeURIComponent(id)}`, def);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function deleteWorkflow(id) {
|
|
18
|
+
return apiDelete(`/v1/workflows/${encodeURIComponent(id)}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Read one agent's published offers (to pick a workflow-compatible offer in the form). */
|
|
22
|
+
export async function getAgentOffers(agentName) {
|
|
23
|
+
return apiGet(`/v1/agents/${encodeURIComponent(agentName)}/offers`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** List the owner's workflow definitions: { workflows, count }. */
|
|
27
|
+
export async function listWorkflows() {
|
|
28
|
+
return apiGet('/v1/workflows');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function getWorkflow(id) {
|
|
32
|
+
return apiGet(`/v1/workflows/${encodeURIComponent(id)}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Derived structural graph: { workflowId, nodes, edges }. */
|
|
36
|
+
export async function getBlueprint(id) {
|
|
37
|
+
return apiGet(`/v1/workflows/${encodeURIComponent(id)}/blueprint`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Run-health trend over recent runs: { sample, lastStatus, steps:[{stepId,green,red}] , … }. */
|
|
41
|
+
export async function getHealth(id) {
|
|
42
|
+
return apiGet(`/v1/workflows/${encodeURIComponent(id)}/health`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** List runs (newest first): { runs, count }. */
|
|
46
|
+
export async function listRuns(id) {
|
|
47
|
+
return apiGet(`/v1/workflows/${encodeURIComponent(id)}/runs`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function getRun(id, runId) {
|
|
51
|
+
return apiGet(`/v1/workflows/${encodeURIComponent(id)}/runs/${encodeURIComponent(runId)}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Start a run. mode: 'signals-only' | 'full'. Returns { runId, mode }. */
|
|
55
|
+
export async function runWorkflow(id, mode, vars) {
|
|
56
|
+
return apiPost(`/v1/workflows/${encodeURIComponent(id)}/run`, { mode, ...(vars ? { vars } : {}) });
|
|
57
|
+
}
|
|
@@ -385,6 +385,13 @@ for completeness but are NOT needed for typical app building:
|
|
|
385
385
|
- Packages (versioned bundles), Knowledge packages
|
|
386
386
|
- Federation (node peering, cross-node routing)
|
|
387
387
|
- Agent collaboration (shared memory, organisms)
|
|
388
|
+
- Agent Workflows: declared, ordered agent pipelines with per-step input/output signals checked
|
|
389
|
+
after each step, so the owner sees whether each step PRODUCED (not just fired). One trigger
|
|
390
|
+
(schedule / manual / event) drives the chain; each step names an agent + an offer and inherits
|
|
391
|
+
that offer's signals + deliverable location. Connected agents use aimeat_workflow_save / _get /
|
|
392
|
+
_run (signals-only = check vs memory, no dispatch; full = execute). Stored in owner memory
|
|
393
|
+
(workflows.def.* / workflows.run.*); API under /v1/workflows. Plan:
|
|
394
|
+
docs/plans/2026-06-13-agent-workflows-node-plan.md.
|
|
388
395
|
- Organism workspaces: an organism can hold manifest-driven workspaces of markdown documents
|
|
389
396
|
(a wiki) and schema-locked record lists, with a draft -> publish -> version flow. Connected
|
|
390
397
|
agents use the aimeat_workspace_* MCP tools (list / read / write / publish / object_delete /
|
package/dist/public/spa.html
CHANGED
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
<link rel="stylesheet" href="/css/views/capabilities.css">
|
|
85
85
|
<link rel="stylesheet" href="/css/views/agents-detail.css">
|
|
86
86
|
<link rel="stylesheet" href="/css/views/scheduler.css">
|
|
87
|
+
<link rel="stylesheet" href="/css/views/workflows.css">
|
|
87
88
|
<link rel="stylesheet" href="/css/views/admin.css">
|
|
88
89
|
<link rel="stylesheet" href="/css/views/admin-agent-integration.css">
|
|
89
90
|
<link rel="stylesheet" href="/css/views/public-knowledge-viewer.css">
|
|
@@ -199,7 +200,8 @@
|
|
|
199
200
|
"/js/services/agent-messages.js": "/js/services/agent-messages.js",
|
|
200
201
|
"/js/services/agent-integration.js": "/js/services/agent-integration.js",
|
|
201
202
|
"/js/services/admin-agent-integration.js": "/js/services/admin-agent-integration.js",
|
|
202
|
-
"/js/services/schedules.js": "/js/services/schedules.js"
|
|
203
|
+
"/js/services/schedules.js": "/js/services/schedules.js",
|
|
204
|
+
"/js/services/workflows.js": "/js/services/workflows.js"
|
|
203
205
|
}
|
|
204
206
|
}
|
|
205
207
|
</script>
|
|
@@ -755,6 +755,7 @@ const SIDEBAR_GROUPS = [
|
|
|
755
755
|
{ id: 'agents', icon: '\u{1F916}', labelKey: 'profile.tabs.agents' },
|
|
756
756
|
{ id: 'offers', icon: '\u{1F9F0}', labelKey: 'profile.tabs.offers' },
|
|
757
757
|
{ id: 'scheduler', icon: '⏰', labelKey: 'profile.tabs.scheduler' },
|
|
758
|
+
{ id: 'workflows', icon: '\u{1F500}', labelKey: 'profile.tabs.workflows' },
|
|
758
759
|
{ id: 'actions', icon: '\u{1F6E0}️', labelKey: 'profile.tabs.services' },
|
|
759
760
|
{ id: 'mcp', icon: '\u{1F517}', labelKey: 'profile.tabs.mcp' },
|
|
760
761
|
] },
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file workflows-form.js
|
|
3
|
+
* @description Create/Edit form for an Agent Workflow (Profile › Workflows). Authors the descriptor
|
|
4
|
+
* the API validates: trigger (manual/schedule/event) + typed vars + steps (each picks an agent +
|
|
5
|
+
* a workflow-compatible offer; signals are inherited from that offer, so the form needs no signal
|
|
6
|
+
* editor) + the llm-consent toggle. Save → PUT /v1/workflows/:id; save-time validation errors
|
|
7
|
+
* (DAG / offer-compatibility / undeclared var) render inline. See plan §12.
|
|
8
|
+
* @structure WorkflowForm({ existing, onSaved, onCancel, showToast })
|
|
9
|
+
* @version-history
|
|
10
|
+
* v1.0.0 -- 2026-06-13 -- Phase 9b: create/edit form (closes the §12 form gap).
|
|
11
|
+
*/
|
|
12
|
+
import { h } from 'preact';
|
|
13
|
+
import { useState, useEffect, useCallback } from 'preact/hooks';
|
|
14
|
+
import htm from 'htm';
|
|
15
|
+
import { t } from '/js/i18n.js';
|
|
16
|
+
import { listAgents } from '/js/services/agents.js';
|
|
17
|
+
import { putWorkflow, getAgentOffers } from '/js/services/workflows.js';
|
|
18
|
+
|
|
19
|
+
const html = htm.bind(h);
|
|
20
|
+
|
|
21
|
+
function loc(s) {
|
|
22
|
+
if (!s) return '';
|
|
23
|
+
if (typeof s === 'string') return s;
|
|
24
|
+
return s.en_US || s.fi_FI || Object.values(s)[0] || '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const blankStep = () => ({ id: '', agent: '', offer: '', after: [], noInput: false, timeout_min: 60, retryMax: 0, backoffMin: 5, description: '' });
|
|
28
|
+
|
|
29
|
+
export default function WorkflowForm({ existing, onSaved, onCancel, showToast }) {
|
|
30
|
+
const editing = !!existing;
|
|
31
|
+
const [id, setId] = useState(existing?.id || '');
|
|
32
|
+
const [title, setTitle] = useState(loc(existing?.title));
|
|
33
|
+
const [description, setDescription] = useState(loc(existing?.description));
|
|
34
|
+
const [triggerKind, setTriggerKind] = useState(existing?.trigger?.kind || 'manual');
|
|
35
|
+
const [cron, setCron] = useState(existing?.trigger?.cron || '0 17 * * *');
|
|
36
|
+
const [timezone, setTimezone] = useState(existing?.trigger?.timezone || 'Europe/Helsinki');
|
|
37
|
+
const [eventOn, setEventOn] = useState(existing?.trigger?.on || 'memory.write');
|
|
38
|
+
const [eventMatch, setEventMatch] = useState(existing?.trigger?.match?.key || existing?.trigger?.match?.offer || '');
|
|
39
|
+
const [vars, setVars] = useState(existing?.vars?.map(v => ({ ...v, description: loc(v.description) })) || []);
|
|
40
|
+
const [steps, setSteps] = useState(
|
|
41
|
+
existing?.steps?.map(s => ({
|
|
42
|
+
id: s.id, agent: Array.isArray(s.agent) ? s.agent[0] : s.agent, offer: s.offer,
|
|
43
|
+
after: s.after || [], noInput: s.required_to_function === 'none',
|
|
44
|
+
timeout_min: s.timeout_min ?? 60, retryMax: s.retry?.max || 0, backoffMin: s.retry?.backoff_min || 5,
|
|
45
|
+
description: loc(s.description),
|
|
46
|
+
})) || [blankStep()],
|
|
47
|
+
);
|
|
48
|
+
const [llmApproved, setLlmApproved] = useState(!!existing?.llm?.approved);
|
|
49
|
+
const [agents, setAgents] = useState([]);
|
|
50
|
+
const [offersByAgent, setOffersByAgent] = useState({});
|
|
51
|
+
const [errors, setErrors] = useState([]);
|
|
52
|
+
const [saving, setSaving] = useState(false);
|
|
53
|
+
|
|
54
|
+
// listAgents() returns the agent array directly (not an envelope); stay tolerant of both shapes.
|
|
55
|
+
useEffect(() => { listAgents().then(r => setAgents(Array.isArray(r) ? r : (r?.data?.agents ?? r?.data ?? []))).catch(() => {}); }, []);
|
|
56
|
+
|
|
57
|
+
const loadOffers = useCallback(async (agentName) => {
|
|
58
|
+
if (!agentName) return;
|
|
59
|
+
setOffersByAgent(prev => {
|
|
60
|
+
if (prev[agentName]) return prev;
|
|
61
|
+
getAgentOffers(agentName).then(r => {
|
|
62
|
+
const offers = (r?.data?.offers || []).filter(o => o.success_signal && o.required_to_function && o.deliverable?.location);
|
|
63
|
+
setOffersByAgent(p => ({ ...p, [agentName]: offers }));
|
|
64
|
+
}).catch(() => setOffersByAgent(p => ({ ...p, [agentName]: [] })));
|
|
65
|
+
return prev;
|
|
66
|
+
});
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
// Pre-load offers for steps that already name an agent (edit mode).
|
|
70
|
+
useEffect(() => { steps.forEach(s => s.agent && loadOffers(s.agent)); }, []); // eslint-disable-line
|
|
71
|
+
|
|
72
|
+
const setStep = (i, patch) => setSteps(prev => prev.map((s, j) => j === i ? { ...s, ...patch } : s));
|
|
73
|
+
const addStep = () => setSteps(prev => [...prev, blankStep()]);
|
|
74
|
+
const removeStep = (i) => setSteps(prev => prev.filter((_, j) => j !== i));
|
|
75
|
+
const setVar = (i, patch) => setVars(prev => prev.map((v, j) => j === i ? { ...v, ...patch } : v));
|
|
76
|
+
const addVar = () => setVars(prev => [...prev, { name: '', type: 'string', description: '', default: '' }]);
|
|
77
|
+
const removeVar = (i) => setVars(prev => prev.filter((_, j) => j !== i));
|
|
78
|
+
|
|
79
|
+
const save = async () => {
|
|
80
|
+
setErrors([]);
|
|
81
|
+
if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(id)) { setErrors([t('profile.workflows.form.idHint')]); return; }
|
|
82
|
+
const trigger = triggerKind === 'schedule' ? { kind: 'schedule', cron, ...(timezone ? { timezone } : {}) }
|
|
83
|
+
: triggerKind === 'event' ? { kind: 'event', on: eventOn, match: eventOn === 'memory.write' ? { key: eventMatch } : { offer: eventMatch } }
|
|
84
|
+
: { kind: 'manual' };
|
|
85
|
+
const def = {
|
|
86
|
+
title: title || id,
|
|
87
|
+
description: description || '-',
|
|
88
|
+
trigger,
|
|
89
|
+
vars: vars.filter(v => v.name).map(v => ({ name: v.name, type: v.type || 'string', description: v.description || v.name, ...(v.default ? { default: v.default } : {}) })),
|
|
90
|
+
steps: steps.map(s => ({
|
|
91
|
+
id: s.id, agent: s.agent, offer: s.offer,
|
|
92
|
+
...(s.after?.length ? { after: s.after } : {}),
|
|
93
|
+
description: s.description || s.id,
|
|
94
|
+
...(s.noInput ? { required_to_function: 'none' } : {}),
|
|
95
|
+
...(Number(s.retryMax) > 0 ? { retry: { max: Number(s.retryMax), backoff_min: Number(s.backoffMin) || 5 } } : {}),
|
|
96
|
+
timeout_min: Number(s.timeout_min) || 60,
|
|
97
|
+
})),
|
|
98
|
+
on_step_fail: 'inspect',
|
|
99
|
+
...(llmApproved ? { llm: { approved: true } } : {}),
|
|
100
|
+
};
|
|
101
|
+
setSaving(true);
|
|
102
|
+
try {
|
|
103
|
+
await putWorkflow(id, def);
|
|
104
|
+
showToast?.(t('profile.workflows.form.saved'), false);
|
|
105
|
+
onSaved?.(id);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
const list = e?.response?.error?.details?.errors;
|
|
108
|
+
setErrors(Array.isArray(list) && list.length ? list : [e?.message || 'Save failed']);
|
|
109
|
+
} finally {
|
|
110
|
+
setSaving(false);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const stepIds = steps.map(s => s.id).filter(Boolean);
|
|
115
|
+
|
|
116
|
+
return html`
|
|
117
|
+
<div class="wf-tab">
|
|
118
|
+
<div class="wf-detail-head">
|
|
119
|
+
<button class="btn-ghost" onClick=${onCancel}>← ${t('profile.workflows.back')}</button>
|
|
120
|
+
<div class="wf-detail-title">${editing ? t('profile.workflows.form.editTitle') : t('profile.workflows.form.newTitle')}</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
${errors.length > 0 && html`<div class="wf-error"><ul class="wf-err-list">${errors.map(e => html`<li>${e}</li>`)}</ul></div>`}
|
|
124
|
+
|
|
125
|
+
<div class="wf-form">
|
|
126
|
+
<label class="wf-field">
|
|
127
|
+
<span>${t('profile.workflows.form.id')}</span>
|
|
128
|
+
<input type="text" value=${id} disabled=${editing} placeholder="laimeat-sanomat-evening"
|
|
129
|
+
onInput=${e => setId(e.target.value)} />
|
|
130
|
+
</label>
|
|
131
|
+
<label class="wf-field">
|
|
132
|
+
<span>${t('profile.workflows.form.titleField')}</span>
|
|
133
|
+
<input type="text" value=${title} onInput=${e => setTitle(e.target.value)} />
|
|
134
|
+
</label>
|
|
135
|
+
<label class="wf-field">
|
|
136
|
+
<span>${t('profile.workflows.form.description')}</span>
|
|
137
|
+
<input type="text" value=${description} onInput=${e => setDescription(e.target.value)} />
|
|
138
|
+
</label>
|
|
139
|
+
|
|
140
|
+
<!-- Trigger -->
|
|
141
|
+
<label class="wf-field">
|
|
142
|
+
<span>${t('profile.workflows.form.trigger')}</span>
|
|
143
|
+
<select value=${triggerKind} onChange=${e => setTriggerKind(e.target.value)}>
|
|
144
|
+
<option value="manual">${t('profile.workflows.trigger.manual')}</option>
|
|
145
|
+
<option value="schedule">${t('profile.workflows.trigger.schedule')}</option>
|
|
146
|
+
<option value="event">${t('profile.workflows.trigger.event')}</option>
|
|
147
|
+
</select>
|
|
148
|
+
</label>
|
|
149
|
+
${triggerKind === 'schedule' && html`<div class="wf-field-row">
|
|
150
|
+
<label class="wf-field"><span>cron</span><input type="text" value=${cron} onInput=${e => setCron(e.target.value)} /></label>
|
|
151
|
+
<label class="wf-field"><span>timezone</span><input type="text" value=${timezone} onInput=${e => setTimezone(e.target.value)} /></label>
|
|
152
|
+
</div>`}
|
|
153
|
+
${triggerKind === 'event' && html`<div class="wf-field-row">
|
|
154
|
+
<label class="wf-field"><span>on</span>
|
|
155
|
+
<select value=${eventOn} onChange=${e => setEventOn(e.target.value)}>
|
|
156
|
+
<option value="memory.write">memory.write</option>
|
|
157
|
+
<option value="offer.ordered">offer.ordered</option>
|
|
158
|
+
</select>
|
|
159
|
+
</label>
|
|
160
|
+
<label class="wf-field"><span>${eventOn === 'memory.write' ? t('profile.workflows.form.matchKey') : t('profile.workflows.form.matchOffer')}</span>
|
|
161
|
+
<input type="text" value=${eventMatch} onInput=${e => setEventMatch(e.target.value)} placeholder=${eventOn === 'memory.write' ? 'news.*' : 'fetch'} /></label>
|
|
162
|
+
</div>`}
|
|
163
|
+
|
|
164
|
+
<!-- Vars -->
|
|
165
|
+
<div class="wf-form-section">
|
|
166
|
+
<div class="wf-form-section-head"><span>${t('profile.workflows.form.vars')}</span>
|
|
167
|
+
<button class="btn-ghost" onClick=${addVar}>+ ${t('profile.workflows.form.addVar')}</button></div>
|
|
168
|
+
${vars.map((v, i) => html`<div class="wf-field-row" key=${i}>
|
|
169
|
+
<input type="text" placeholder="name" value=${v.name} onInput=${e => setVar(i, { name: e.target.value })} />
|
|
170
|
+
<input type="text" placeholder="type" value=${v.type} onInput=${e => setVar(i, { type: e.target.value })} />
|
|
171
|
+
<input type="text" placeholder="default (<run-date>)" value=${v.default || ''} onInput=${e => setVar(i, { default: e.target.value })} />
|
|
172
|
+
<button class="btn-ghost" onClick=${() => removeVar(i)}>✕</button>
|
|
173
|
+
</div>`)}
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<!-- Steps -->
|
|
177
|
+
<div class="wf-form-section">
|
|
178
|
+
<div class="wf-form-section-head"><span>${t('profile.workflows.form.steps')}</span>
|
|
179
|
+
<button class="btn-ghost" onClick=${addStep}>+ ${t('profile.workflows.form.addStep')}</button></div>
|
|
180
|
+
${steps.map((s, i) => html`<div class="wf-step-edit" key=${i}>
|
|
181
|
+
<div class="wf-field-row">
|
|
182
|
+
<input type="text" placeholder="step id" value=${s.id} onInput=${e => setStep(i, { id: e.target.value })} />
|
|
183
|
+
<select value=${s.agent} onChange=${e => { setStep(i, { agent: e.target.value, offer: '' }); loadOffers(e.target.value); }}>
|
|
184
|
+
<option value="">${t('profile.workflows.form.pickAgent')}</option>
|
|
185
|
+
${agents.map(a => html`<option value=${a.name} key=${a.name}>${a.name}</option>`)}
|
|
186
|
+
</select>
|
|
187
|
+
<select value=${s.offer} onChange=${e => setStep(i, { offer: e.target.value })}>
|
|
188
|
+
<option value="">${t('profile.workflows.form.pickOffer')}</option>
|
|
189
|
+
${(offersByAgent[s.agent] || []).map(o => html`<option value=${o.id} key=${o.id}>${o.id}</option>`)}
|
|
190
|
+
</select>
|
|
191
|
+
<button class="btn-ghost" onClick=${() => removeStep(i)}>✕</button>
|
|
192
|
+
</div>
|
|
193
|
+
${s.agent && (offersByAgent[s.agent]?.length === 0) && html`<div class="wf-muted">${t('profile.workflows.form.noCompatibleOffers')}</div>`}
|
|
194
|
+
<div class="wf-field-row wf-step-opts">
|
|
195
|
+
<label class="wf-inline"><input type="checkbox" checked=${s.noInput} onChange=${e => setStep(i, { noInput: e.target.checked })} /> ${t('profile.workflows.form.noInput')}</label>
|
|
196
|
+
<label class="wf-inline">timeout_min <input type="number" min="1" value=${s.timeout_min} onInput=${e => setStep(i, { timeout_min: e.target.value })} /></label>
|
|
197
|
+
<label class="wf-inline">retry <input type="number" min="0" value=${s.retryMax} onInput=${e => setStep(i, { retryMax: e.target.value })} /></label>
|
|
198
|
+
${Number(s.retryMax) > 0 && html`<label class="wf-inline">backoff_min <input type="number" min="0" value=${s.backoffMin} onInput=${e => setStep(i, { backoffMin: e.target.value })} /></label>`}
|
|
199
|
+
</div>
|
|
200
|
+
${stepIds.filter(x => x && x !== s.id).length > 0 && html`<div class="wf-step-after">
|
|
201
|
+
<span class="wf-muted">${t('profile.workflows.after')}:</span>
|
|
202
|
+
${stepIds.filter(x => x && x !== s.id).map(other => html`<label class="wf-inline" key=${other}>
|
|
203
|
+
<input type="checkbox" checked=${s.after?.includes(other)} onChange=${e => setStep(i, { after: e.target.checked ? [...(s.after || []), other] : (s.after || []).filter(a => a !== other) })} /> ${other}
|
|
204
|
+
</label>`)}
|
|
205
|
+
</div>`}
|
|
206
|
+
</div>`)}
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<label class="wf-inline wf-llm-toggle">
|
|
210
|
+
<input type="checkbox" checked=${llmApproved} onChange=${e => setLlmApproved(e.target.checked)} />
|
|
211
|
+
${t('profile.workflows.form.llmApproved')}
|
|
212
|
+
</label>
|
|
213
|
+
|
|
214
|
+
<div class="wf-form-actions">
|
|
215
|
+
<button class="btn-ghost" onClick=${onCancel}>${t('profile.workflows.form.cancel')}</button>
|
|
216
|
+
<button class="btn-primary" disabled=${saving} onClick=${save}>${saving ? '…' : t('profile.workflows.form.save')}</button>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
</div>`;
|
|
220
|
+
}
|