pgkiln 0.84.0 → 0.86.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/db/migrations/155_mail_available.sql +23 -0
- package/docs/apex-feature-parity.md +2 -2
- package/docs/guide/03-builder.md +14 -5
- package/docs/guide/04-pages-and-regions.md +1 -1
- package/docs/guide/06-processing.md +7 -3
- package/docs/guide/07-dynamic-actions.md +3 -3
- package/docs/guide/09-reference.md +1 -0
- package/docs/guide/12-development.md +10 -1
- package/examples/gallery/README.md +23 -6
- package/examples/gallery/gallery.json +4 -0
- package/examples/gallery/samples/sample-approvals.sql +102 -11
- package/examples/gallery/samples/sample-calendar.sql +40 -5
- package/examples/gallery/samples/sample-dynamic-actions.sql +237 -9
- package/examples/gallery/samples/sample-grids.sql +35 -3
- package/examples/gallery/samples/sample-maps.sql +65 -7
- package/examples/gallery/samples/sample-processes.sql +466 -0
- package/examples/gallery/samples/sample-validations.sql +407 -0
- package/examples/gallery/samples/ux-patterns.sql +154 -14
- package/examples/hr/README.md +2 -0
- package/examples/hr/hr_80_notifications.sql +125 -0
- package/examples/hr/hr_81_vector_search_type.sql +33 -0
- package/package.json +1 -1
- package/public/app.css +8 -1
- package/public/app.js +28 -9
- package/src/app.ts +9 -0
- package/src/builder/ai-agents.ts +86 -85
- package/src/builder/ai-builder.ts +30 -30
- package/src/builder/ai.ts +93 -92
- package/src/builder/api.ts +47 -47
- package/src/builder/app-groups.ts +33 -32
- package/src/builder/appfromfile.ts +57 -58
- package/src/builder/appsheets.ts +33 -32
- package/src/builder/appwizard.ts +65 -65
- package/src/builder/arrange.ts +23 -22
- package/src/builder/assistant.ts +39 -38
- package/src/builder/automations.ts +32 -31
- package/src/builder/availability.ts +24 -24
- package/src/builder/backups.ts +30 -31
- package/src/builder/blueprints.ts +62 -61
- package/src/builder/create-page.ts +52 -50
- package/src/builder/cross-app.ts +37 -35
- package/src/builder/dataload.ts +2 -2
- package/src/builder/datasets.ts +107 -106
- package/src/builder/dependencies.ts +20 -19
- package/src/builder/designer.ts +36 -36
- package/src/builder/diagnostics.ts +58 -57
- package/src/builder/features.ts +39 -35
- package/src/builder/feedback.ts +23 -20
- package/src/builder/find-workspace.ts +28 -24
- package/src/builder/form-designer.ts +36 -35
- package/src/builder/gallery.ts +25 -29
- package/src/builder/globalization.ts +50 -49
- package/src/builder/i18n/de.ts +3453 -0
- package/src/builder/i18n/en.ts +3453 -0
- package/src/builder/i18n/es.ts +3453 -0
- package/src/builder/i18n/fr.ts +3453 -0
- package/src/builder/i18n/it.ts +3453 -0
- package/src/builder/i18n/ja.ts +3453 -0
- package/src/builder/i18n/ko.ts +3453 -0
- package/src/builder/i18n/nl.ts +3453 -0
- package/src/builder/i18n/pt.ts +3453 -0
- package/src/builder/i18n/zh-cn.ts +3453 -0
- package/src/builder/i18n/zh-tw.ts +3453 -0
- package/src/builder/i18n.ts +17 -3
- package/src/builder/json-sources.ts +63 -62
- package/src/builder/layouts.ts +12 -11
- package/src/builder/locks.ts +29 -28
- package/src/builder/login-page.ts +26 -25
- package/src/builder/mail.ts +148 -147
- package/src/builder/newapp.ts +15 -12
- package/src/builder/plugins.ts +47 -44
- package/src/builder/process-jobs.ts +11 -10
- package/src/builder/pwa.ts +22 -21
- package/src/builder/remote-databases.ts +55 -56
- package/src/builder/report-settings.ts +1 -1
- package/src/builder/reporter.ts +43 -42
- package/src/builder/rest-catalogs.ts +87 -86
- package/src/builder/rest.ts +9 -8
- package/src/builder/routes.ts +27 -27
- package/src/builder/search.ts +14 -13
- package/src/builder/static-files.ts +31 -32
- package/src/builder/subscriptions.ts +47 -45
- package/src/builder/supporting.ts +23 -18
- package/src/builder/team.ts +179 -179
- package/src/builder/telemetry.ts +59 -62
- package/src/builder/templates.ts +66 -65
- package/src/builder/themeroller.ts +60 -63
- package/src/builder/ui-defaults.ts +21 -20
- package/src/builder/ui.ts +2 -1
- package/src/builder/upgrade.ts +25 -24
- package/src/builder/utilities.ts +48 -47
- package/src/builder/vector-providers.ts +67 -66
- package/src/builder/websources.ts +43 -42
- package/src/builder/wizards.ts +95 -89
- package/src/builder/workflow-migrate.ts +30 -31
- package/src/builder/workflows.ts +40 -39
- package/src/builder/workingcopies.ts +41 -49
- package/src/builder/workspace-backups.ts +21 -20
- package/src/builder/workspace-mail.ts +17 -16
- package/src/builder/workspace-resources.ts +104 -112
- package/src/builder/workspace-transfer.ts +34 -26
- package/src/runtime/engine.ts +6 -3
- package/src/runtime/lists.ts +2 -2
- package/src/runtime/push.ts +7 -1
- package/src/runtime/regions.ts +9 -4
package/src/builder/ai-agents.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { agentProblems, aiToolProblems, definitionProblems, EFFORTS, type AiAgen
|
|
|
7
7
|
import type { Session } from '../session.ts';
|
|
8
8
|
import { appOr404 } from './forms.ts';
|
|
9
9
|
import { appHeader, back, BASE, csrf, developer, flash, input, region, select, send, shell, type Req } from './ui.ts';
|
|
10
|
+
import { scopedTx as tx } from './i18n.ts';
|
|
10
11
|
|
|
11
12
|
// Shared Components → Generative AI → AI Agents (migration 121): the
|
|
12
13
|
// application's AI tools and AI agents, kept as two JSON lists in meta.app
|
|
@@ -18,13 +19,13 @@ type Body = Record<string, string | string[] | undefined>;
|
|
|
18
19
|
const one = (v: string | string[] | undefined) => (Array.isArray(v) ? v[0] : v) ?? '';
|
|
19
20
|
const all = (v: string | string[] | undefined) => (v === undefined ? [] : [v].flat());
|
|
20
21
|
|
|
21
|
-
const TOOL_TYPES: [string, string][]
|
|
22
|
-
['sql',
|
|
23
|
-
['function', '
|
|
24
|
-
['rest', '
|
|
25
|
-
['javascript', '
|
|
26
|
-
['navigate', '
|
|
27
|
-
['retrieve', '
|
|
22
|
+
const TOOL_TYPES = (): [string, string][] => [
|
|
23
|
+
['sql', tx('agt.type.sql')],
|
|
24
|
+
['function', tx('agt.type.function')],
|
|
25
|
+
['rest', tx('agt.type.rest')],
|
|
26
|
+
['javascript', tx('agt.type.javascript')],
|
|
27
|
+
['navigate', tx('agt.type.navigate')],
|
|
28
|
+
['retrieve', tx('agt.type.retrieve')],
|
|
28
29
|
];
|
|
29
30
|
|
|
30
31
|
const when = (d: Date | string) => new Date(d).toISOString().slice(0, 19).replace('T', ' ');
|
|
@@ -36,12 +37,12 @@ async function schemes(appId: number) {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
const authzSelect = (names: string[], cur: unknown, help: string) =>
|
|
39
|
-
select('authz', '
|
|
40
|
+
select('authz', tx('agt.authorization_scheme'), cur ?? '', [['', tx('agt.none_everyone_who_may_use')], ['MUST_NOT_BE_PUBLIC_USER', tx('agt.must_not_be_the_public')], ...names.map((n): [string, string] => [n, n]),
|
|
40
41
|
...(typeof cur === 'string' && cur && cur !== 'MUST_NOT_BE_PUBLIC_USER' && !names.includes(cur) ? [[cur, `${cur} (does not exist)`] as [string, string]] : [])], help);
|
|
41
42
|
|
|
42
43
|
const crumbs = (a: { id: number; name: string }, last?: string): [string, string?][] => [
|
|
43
|
-
['
|
|
44
|
-
...(last ? [['
|
|
44
|
+
[tx('agt.app_builder'), BASE], [a.name, `${BASE}/apps/${a.id}`], [tx('agt.shared_components'), `${BASE}/apps/${a.id}/shared`],
|
|
45
|
+
...(last ? [[tx('agt.ai_agents'), `${BASE}/apps/${a.id}/ai-agents`], [last]] as [string, string?][] : [[tx('agt.ai_agents')]] as [string, string?][]),
|
|
45
46
|
];
|
|
46
47
|
|
|
47
48
|
// ---------------------------------------------------------------- forms
|
|
@@ -51,7 +52,7 @@ async function agentForm(a: any, s: Session, x: Partial<AiAgent>, original: stri
|
|
|
51
52
|
`select s.name, s.provider, s.model from meta.ai_service s join meta.app_ai_service x on x.service_id = s.id and x.app_id = $1 where s.enabled order by s.name`, [a.id])).rows;
|
|
52
53
|
const tools = (a.ai_tools ?? []) as AiTool[];
|
|
53
54
|
const chosen = new Set(x.tools ?? []);
|
|
54
|
-
const svc: [string, string][] = [['', '
|
|
55
|
+
const svc: [string, string][] = [['', tx('agt.choose')], ...services.map((v): [string, string] => [v.name, `${v.name} (${providerLabel(v.provider)}, ${v.model})`])];
|
|
55
56
|
if (x.service && !services.some((v) => v.name === x.service)) svc.push([x.service, `${x.service} (not available to this application)`]);
|
|
56
57
|
const usedBy = original
|
|
57
58
|
? (await owner.query<{ page_no: number; title: string | null; page_id: number; id: number }>(
|
|
@@ -59,31 +60,31 @@ async function agentForm(a: any, s: Session, x: Partial<AiAgent>, original: stri
|
|
|
59
60
|
where p.app_id = $1 and r.type = 'ai_assistant' and upper(r.config->>'agent') = $2 order by p.page_no, r.seq`, [a.id, original])).rows
|
|
60
61
|
: [];
|
|
61
62
|
return html`${problems.length ? html`<div class="alert alert-error" role="alert">${problems.join(' ')}</div>` : ''}
|
|
62
|
-
${services.length ? '' : html`<p class="muted"
|
|
63
|
+
${services.length ? '' : html`<p class="muted">${tx('agt.this_application_may_not_use')}</p>`}
|
|
63
64
|
<form method="post" action="${BASE}/apps/${a.id}/ai-agents/agent">${csrf(s)}<input type="hidden" name="original" value="${original}">
|
|
64
|
-
<fieldset class="prop-group"><legend
|
|
65
|
-
${input('name', '
|
|
66
|
-
${input('description', '
|
|
67
|
-
${select('service', '
|
|
68
|
-
${select('effort', '
|
|
69
|
-
'
|
|
70
|
-
${input('max_steps', '
|
|
71
|
-
${input('max_tokens', '
|
|
72
|
-
${authzSelect(await schemes(a.id), x.authz, '
|
|
73
|
-
${area('system', '
|
|
65
|
+
<fieldset class="prop-group"><legend>${tx('agt.agent')}</legend><div class="form-grid">
|
|
66
|
+
${input('name', tx('agt.name'), x.name ?? '', { required: true, placeholder: tx('agt.e_g_sales_agent'), help: tx('agt.upper_case_letters_digits_and') })}
|
|
67
|
+
${input('description', tx('agt.description'), x.description ?? '')}
|
|
68
|
+
${select('service', tx('agt.ai_service'), x.service ?? '', svc, tx('agt.the_conversation_the_tools_results'))}
|
|
69
|
+
${select('effort', tx('agt.reasoning_effort'), x.effort ?? '', [['', tx('agt.the_service_s_2')], ...EFFORTS.map((e): [string, string] => [e, e])],
|
|
70
|
+
tx('agt.effort_help'))}
|
|
71
|
+
${input('max_steps', tx('agt.maximum_steps'), x.max_steps ?? '', { type: 'number', placeholder: '5', help: tx('agt.rounds_of_tool_calls_per') })}
|
|
72
|
+
${input('max_tokens', tx('agt.maximum_output_tokens'), x.max_tokens ?? '', { type: 'number', placeholder: tx('agt.the_service_s'), help: tx('agt.per_call_to_the_model') })}
|
|
73
|
+
${authzSelect(await schemes(a.id), x.authz, tx('agt.only_users_who_pass_it'))}
|
|
74
|
+
${area('system', tx('agt.system_prompt'), x.system ?? '', 6, tx('agt.system_help'))}
|
|
74
75
|
</div></fieldset>
|
|
75
|
-
<fieldset class="prop-group"><legend
|
|
76
|
+
<fieldset class="prop-group"><legend>${tx('agt.tools')}</legend>
|
|
76
77
|
${tools.length
|
|
77
|
-
? html`<p class="muted u-mt0"
|
|
78
|
-
: html`<p class="muted u-mt0"
|
|
78
|
+
? html`<p class="muted u-mt0">${tx('agt.the_agent_acts_only_through')}</p>${tools.map((t) => html`<div class="field"><label class="check"><input type="checkbox" name="tools" value="${t.name}"${chosen.has(t.name) ? raw(' checked') : ''}> <b>${t.name}</b> <span class="muted">(${t.type}) ${t.description}</span></label></div>`)}`
|
|
79
|
+
: html`<p class="muted u-mt0">${tx.rich('agt.no_tools', { link: html`<a href="${BASE}/apps/${a.id}/ai-agents/tool">${tx('agt.add_one')}</a>` })}</p>`}
|
|
79
80
|
</fieldset>
|
|
80
|
-
<div class="buttons"><button class="btn btn-hot">${original ? '
|
|
81
|
+
<div class="buttons"><button class="btn btn-hot">${original ? tx('agt.save') : tx('agt.add_agent')}</button></div>
|
|
81
82
|
</form>
|
|
82
|
-
${original ? html`<h3
|
|
83
|
+
${original ? html`<h3>${tx('agt.used_by')}</h3>${usedBy.length
|
|
83
84
|
? html`<ul>${usedBy.map((r) => html`<li><a href="${BASE}/pages/${r.page_id}?c=region-${r.id}">Page ${r.page_no}: ${r.title ?? 'AI assistant'}</a></li>`)}</ul>`
|
|
84
|
-
: html`<p class="muted"
|
|
85
|
+
: html`<p class="muted">${tx('agt.no_ai_assistant_region_uses')}</p>`}
|
|
85
86
|
<form method="post" action="${BASE}/apps/${a.id}/ai-agents/agent/delete" class="danger-zone">${csrf(s)}<input type="hidden" name="name" value="${original}">
|
|
86
|
-
<button class="btn btn-danger" data-confirm="
|
|
87
|
+
<button class="btn btn-danger" data-confirm="${tx('agt.delete_agent_confirm', { name: original })}">${tx('agt.delete_agent')}</button></form>` : ''}`;
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
async function toolForm(a: any, s: Session, x: Partial<AiTool> & { parameters_text?: string }, original: string, problems: string[]): Promise<Raw> {
|
|
@@ -93,28 +94,28 @@ async function toolForm(a: any, s: Session, x: Partial<AiTool> & { parameters_te
|
|
|
93
94
|
const usedBy = original ? ((a.ai_agents ?? []) as AiAgent[]).filter((g) => (g.tools ?? []).includes(original)).map((g) => g.name) : [];
|
|
94
95
|
return html`${problems.length ? html`<div class="alert alert-error" role="alert">${problems.join(' ')}</div>` : ''}
|
|
95
96
|
<form method="post" action="${BASE}/apps/${a.id}/ai-agents/tool">${csrf(s)}<input type="hidden" name="original" value="${original}">
|
|
96
|
-
<fieldset class="prop-group"><legend
|
|
97
|
-
${input('name', '
|
|
98
|
-
${select('type', '
|
|
99
|
-
${area('description', '
|
|
100
|
-
${area('sql', '
|
|
101
|
-
${input('function', '
|
|
102
|
-
${select('source', '
|
|
103
|
-
${input('action', '
|
|
104
|
-
${input('pages', '
|
|
105
|
-
${select('search_config', '
|
|
97
|
+
<fieldset class="prop-group"><legend>${tx('agt.tool')}</legend><div class="form-grid">
|
|
98
|
+
${input('name', tx('agt.name'), x.name ?? '', { required: true, placeholder: tx('agt.e_g_top_earners'), help: tx('agt.letters_digits_and_the_model') })}
|
|
99
|
+
${select('type', tx('agt.type'), x.type ?? 'sql', TOOL_TYPES(), tx('agt.what_the_tool_does_each'))}
|
|
100
|
+
${area('description', tx('agt.description'), x.description ?? '', 2, tx('agt.tells_the_model_what_the'))}
|
|
101
|
+
${area('sql', tx('agt.sql_query_sql'), x.sql ?? '', 6, tx('agt.sql_help'), true)}
|
|
102
|
+
${input('function', tx('agt.function_pl_pgsql'), x.function ?? '', { placeholder: tx('agt.e_g_sales_give_discount'), help: tx('agt.schema_name_called_as_the') })}
|
|
103
|
+
${select('source', tx('agt.rest_data_source_rest'), x.source ?? '', [['', '-'], ...sources.map((n): [string, string] => [n, n])], tx('agt.the_parameters_become_the_source'))}
|
|
104
|
+
${input('action', tx('agt.action_javascript'), x.action ?? '', { placeholder: tx('agt.e_g_highlightorder'), help: tx('agt.after_the_answer_the_browser') })}
|
|
105
|
+
${input('pages', tx('agt.pages_navigate'), (x.pages ?? []).join(', '), { placeholder: 'e.g. 3, 7', help: tx('agt.the_page_numbers_the_agent') })}
|
|
106
|
+
${select('search_config', tx('agt.search_configuration_retrieve'), x.search_config ?? '', [['', '-'], ...configs.map((c): [string, string] => [c.name, `${c.name} (${c.search_type})`]),
|
|
106
107
|
...(x.search_config && !configs.some((c) => c.name === x.search_config) ? [[x.search_config, `${x.search_config} (does not exist)`] as [string, string]] : [])],
|
|
107
108
|
'The model gives a text; pgkiln runs this search configuration (best: vector or hybrid) as the application\'s role, so row level security applies, and returns the best rows\' title, text and score.')}
|
|
108
|
-
${area('parameters', '
|
|
109
|
-
${input('max_rows', '
|
|
110
|
-
${input('timeout_s', '
|
|
111
|
-
${authzSelect(await schemes(a.id), x.authz, '
|
|
109
|
+
${area('parameters', tx('agt.parameters_json_not_for_navigate'), params, 6, '{"DNAME": {"type": "string", "description": "A department name"}, "LIMIT": {"type": "integer", "optional": true}}. Types: string, integer, number, boolean, date; "enum" for a list of texts.', true)}
|
|
110
|
+
${input('max_rows', tx('agt.maximum_rows'), x.max_rows ?? '', { type: 'number', placeholder: '50', help: tx('agt.rows_the_model_gets_back') })}
|
|
111
|
+
${input('timeout_s', tx('agt.time_limit_seconds'), x.timeout_s ?? '', { type: 'number', placeholder: '15', help: tx('agt.sql_and_function_tools_1') })}
|
|
112
|
+
${authzSelect(await schemes(a.id), x.authz, tx('agt.only_users_who_pass_it_2'))}
|
|
112
113
|
</div></fieldset>
|
|
113
|
-
<div class="buttons"><button class="btn btn-hot">${original ? '
|
|
114
|
+
<div class="buttons"><button class="btn btn-hot">${original ? tx('agt.save') : tx('agt.add_tool')}</button></div>
|
|
114
115
|
</form>
|
|
115
|
-
${original ? html`<h3
|
|
116
|
+
${original ? html`<h3>${tx('agt.used_by')}</h3>${usedBy.length ? html`<p>${usedBy.join(', ')}</p>` : html`<p class="muted">${tx('agt.no_agent_uses_this_tool')}</p>`}
|
|
116
117
|
<form method="post" action="${BASE}/apps/${a.id}/ai-agents/tool/delete" class="danger-zone">${csrf(s)}<input type="hidden" name="name" value="${original}">
|
|
117
|
-
<button class="btn btn-danger" data-confirm="
|
|
118
|
+
<button class="btn btn-danger" data-confirm="${tx('agt.delete_tool_confirm', { name: original })}">${tx('agt.delete_tool')}</button></form>` : ''}`;
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
// ---------------------------------------------------------------- reading the forms
|
|
@@ -123,7 +124,7 @@ const intOrUndef = (v: string, what: string, errors: string[]) => {
|
|
|
123
124
|
const t = v.trim();
|
|
124
125
|
if (!t) return undefined;
|
|
125
126
|
const n = Number(t);
|
|
126
|
-
if (!Number.isInteger(n)) errors.push(
|
|
127
|
+
if (!Number.isInteger(n)) errors.push(tx('agt.whole_number', { what }));
|
|
127
128
|
return n;
|
|
128
129
|
};
|
|
129
130
|
|
|
@@ -139,9 +140,9 @@ function agentFrom(b: Body, errors: string[]): AiAgent {
|
|
|
139
140
|
if (effort) x.effort = effort as AiAgent['effort'];
|
|
140
141
|
const tools = all(b.tools).map((t) => t.trim()).filter(Boolean);
|
|
141
142
|
if (tools.length) x.tools = [...new Set(tools)];
|
|
142
|
-
const steps = intOrUndef(one(b.max_steps), '
|
|
143
|
+
const steps = intOrUndef(one(b.max_steps), tx('agt.f_steps'), errors);
|
|
143
144
|
if (steps !== undefined) x.max_steps = steps;
|
|
144
|
-
const tokens = intOrUndef(one(b.max_tokens), '
|
|
145
|
+
const tokens = intOrUndef(one(b.max_tokens), tx('agt.f_tokens'), errors);
|
|
145
146
|
if (tokens !== undefined) x.max_tokens = tokens;
|
|
146
147
|
const authz = one(b.authz).trim();
|
|
147
148
|
if (authz) x.authz = authz;
|
|
@@ -162,16 +163,16 @@ function toolFrom(b: Body, errors: string[]): AiTool & { parameters_text?: strin
|
|
|
162
163
|
try {
|
|
163
164
|
x.parameters = JSON.parse(params);
|
|
164
165
|
} catch {
|
|
165
|
-
errors.push('
|
|
166
|
+
errors.push(tx('agt.parameters_not_valid_json'));
|
|
166
167
|
x.parameters_text = params;
|
|
167
168
|
}
|
|
168
169
|
}
|
|
169
170
|
if (type === 'sql' || type === 'function' || type === 'rest' || type === 'retrieve') {
|
|
170
|
-
const rows = intOrUndef(one(b.max_rows), '
|
|
171
|
+
const rows = intOrUndef(one(b.max_rows), tx('agt.f_rows'), errors);
|
|
171
172
|
if (rows !== undefined) x.max_rows = rows;
|
|
172
173
|
}
|
|
173
174
|
if (type === 'sql' || type === 'function' || type === 'retrieve') {
|
|
174
|
-
const t = intOrUndef(one(b.timeout_s), '
|
|
175
|
+
const t = intOrUndef(one(b.timeout_s), tx('agt.f_timeout'), errors);
|
|
175
176
|
if (t !== undefined) x.timeout_s = t;
|
|
176
177
|
}
|
|
177
178
|
const authz = one(b.authz).trim();
|
|
@@ -197,15 +198,15 @@ async function update(appId: number, change: (agents: AiAgent[], tools: AiTool[]
|
|
|
197
198
|
/** The "Tool calls" region of Activity → AI usage: the latest calls with their arguments. */
|
|
198
199
|
export async function toolCallTable(appId: number): Promise<Raw> {
|
|
199
200
|
const rows = (await owner.query('select * from meta.ai_tool_call where app_id = $1 order by id desc limit 100', [appId])).rows;
|
|
200
|
-
return region('
|
|
201
|
+
return region(tx('agt.tool_calls'), html`<p class="muted u-mt0">${tx('agt.every_tool_call_of_the')}</p>
|
|
201
202
|
<div class="table-wrap"><table class="report report-reflow">
|
|
202
|
-
<thead><tr><th
|
|
203
|
+
<thead><tr><th>${tx('agt.when_utc')}</th><th class="num">${tx('agt.page')}</th><th>${tx('agt.user')}</th><th>${tx('agt.agent')}</th><th>${tx('agt.tool')}</th><th>${tx('agt.arguments')}</th><th class="num">ms</th><th>${tx('agt.status')}</th></tr></thead>
|
|
203
204
|
<tbody>${rows.length ? rows.map((r) => html`<tr>
|
|
204
|
-
<td data-label="
|
|
205
|
-
<td data-label="
|
|
206
|
-
<td data-label="
|
|
207
|
-
<td data-label="
|
|
208
|
-
: html`<tr><td colspan="8" class="empty"
|
|
205
|
+
<td data-label="${tx('agt.when')}">${when(r.at)}</td><td class="num" data-label="${tx('agt.page')}">${r.page_no ?? ''}</td><td data-label="${tx('agt.user')}">${r.username ?? ''}</td>
|
|
206
|
+
<td data-label="${tx('agt.agent')}">${r.agent ?? html`<span class="muted">region</span>`}</td><td data-label="${tx('agt.tool')}">${r.tool} <span class="muted">${r.type}</span></td>
|
|
207
|
+
<td data-label="${tx('agt.arguments')}"><code class="ai-args">${JSON.stringify(r.arguments)}</code></td><td class="num" data-label="ms">${r.duration_ms}</td>
|
|
208
|
+
<td data-label="${tx('agt.status')}"><span class="tag${r.status === 'ok' ? ' tag-ok' : ' tag-error'}">${r.status}</span>${r.message ? html` <span class="muted">${r.message}</span>` : ''}</td></tr>`)
|
|
209
|
+
: html`<tr><td colspan="8" class="empty">${tx('agt.no_tool_calls_yet')}</td></tr>`}</tbody></table></div>`);
|
|
209
210
|
}
|
|
210
211
|
|
|
211
212
|
// ---------------------------------------------------------------- routes
|
|
@@ -230,22 +231,22 @@ export async function aiAgentRoutes(app: FastifyInstance) {
|
|
|
230
231
|
const tools = (a.ai_tools ?? []) as AiTool[];
|
|
231
232
|
const problems = definitionProblems(agents, tools);
|
|
232
233
|
const main = html`${appHeader(a, 'shared', s)}
|
|
233
|
-
<div class="title-row"><h1
|
|
234
|
-
<p class="muted u-mt0"
|
|
234
|
+
<div class="title-row"><h1>${tx('agt.ai_agents')}</h1></div>
|
|
235
|
+
<p class="muted u-mt0">${tx.rich('agt.intro', { usage: html`<a href="${BASE}/apps/${a.id}/ai">${tx('agt.ai_usage')}</a>` })}</p>
|
|
235
236
|
${problems.length ? html`<div class="alert alert-error" role="alert">${problems.join(' ')}</div>` : ''}
|
|
236
237
|
<div class="columns">
|
|
237
|
-
${region('
|
|
238
|
-
<tbody>${agents.length ? agents.map((g) => html`<tr><td data-label="
|
|
239
|
-
<td data-label="
|
|
240
|
-
: html`<tr><td colspan="4" class="empty"
|
|
241
|
-
<p><a class="btn" href="${BASE}/apps/${a.id}/ai-agents/agent">${icon('plus')}
|
|
242
|
-
${region('
|
|
243
|
-
<tbody>${tools.length ? tools.map((t) => html`<tr><td data-label="
|
|
244
|
-
<td data-label="
|
|
245
|
-
: html`<tr><td colspan="3" class="empty"
|
|
246
|
-
<p><a class="btn" href="${BASE}/apps/${a.id}/ai-agents/tool">${icon('plus')}
|
|
238
|
+
${region(tx('agt.agents'), html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th>${tx('agt.agent')}</th><th>${tx('agt.ai_service')}</th><th>${tx('agt.tools')}</th><th>${tx('agt.authorization')}</th></tr></thead>
|
|
239
|
+
<tbody>${agents.length ? agents.map((g) => html`<tr><td data-label="${tx('agt.agent')}"><a href="${BASE}/apps/${a.id}/ai-agents/agent?name=${encodeURIComponent(g.name)}">${g.name}</a>${g.description ? html` <span class="muted">${g.description}</span>` : ''}</td>
|
|
240
|
+
<td data-label="${tx('agt.ai_service')}">${g.service}${g.effort ? html` <span class="muted">(${g.effort})</span>` : ''}</td><td data-label="${tx('agt.tools')}">${(g.tools ?? []).join(', ')}</td><td data-label="${tx('agt.authorization')}">${g.authz ?? ''}</td></tr>`)
|
|
241
|
+
: html`<tr><td colspan="4" class="empty">${tx('agt.no_ai_agents_yet')}</td></tr>`}</tbody></table></div>
|
|
242
|
+
<p><a class="btn" href="${BASE}/apps/${a.id}/ai-agents/agent">${icon('plus')} ${tx('agt.add_agent')}</a></p>`)}
|
|
243
|
+
${region(tx('agt.tools'), html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th>${tx('agt.tool')}</th><th>${tx('agt.type')}</th><th>${tx('agt.authorization')}</th></tr></thead>
|
|
244
|
+
<tbody>${tools.length ? tools.map((t) => html`<tr><td data-label="${tx('agt.tool')}"><a href="${BASE}/apps/${a.id}/ai-agents/tool?name=${encodeURIComponent(t.name)}">${t.name}</a> <span class="muted">${t.description}</span></td>
|
|
245
|
+
<td data-label="${tx('agt.type')}">${t.type}</td><td data-label="${tx('agt.authorization')}">${t.authz ?? ''}</td></tr>`)
|
|
246
|
+
: html`<tr><td colspan="3" class="empty">${tx('agt.no_ai_tools_yet')}</td></tr>`}</tbody></table></div>
|
|
247
|
+
<p><a class="btn" href="${BASE}/apps/${a.id}/ai-agents/tool">${icon('plus')} ${tx('agt.add_tool')}</a></p>`)}
|
|
247
248
|
</div>`;
|
|
248
|
-
return send(reply, s, shell(s, '
|
|
249
|
+
return send(reply, s, shell(s, tx('agt.ai_agents'), crumbs(a), main));
|
|
249
250
|
});
|
|
250
251
|
|
|
251
252
|
app.get(`${BASE}/apps/:id(^\\d+$)/ai-agents/agent`, async (req: Req, reply) => {
|
|
@@ -256,7 +257,7 @@ export async function aiAgentRoutes(app: FastifyInstance) {
|
|
|
256
257
|
const x = name ? ((a.ai_agents ?? []) as AiAgent[]).find((g) => g.name === name) : {};
|
|
257
258
|
if (!x) return reply.code(404).send('Not found');
|
|
258
259
|
const title = name || 'New agent';
|
|
259
|
-
const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${title}</h1></div>${region('
|
|
260
|
+
const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${title}</h1></div>${region(tx('agt.ai_agent'), await agentForm(a, s, x, name, []))}`;
|
|
260
261
|
return send(reply, s, shell(s, title, crumbs(a, title), main));
|
|
261
262
|
});
|
|
262
263
|
|
|
@@ -271,21 +272,21 @@ export async function aiAgentRoutes(app: FastifyInstance) {
|
|
|
271
272
|
const problems = errors.length ? errors : await update(a.id, (agents, tools) => {
|
|
272
273
|
const out = agentProblems(x, new Set(tools.map((t) => t.name)));
|
|
273
274
|
const at = original ? agents.findIndex((g) => g.name === original) : -1;
|
|
274
|
-
if (original && at < 0) out.push(
|
|
275
|
-
if (agents.some((g, i) => g.name === x.name && i !== at)) out.push(
|
|
275
|
+
if (original && at < 0) out.push(tx('agt.agent_gone', { name: original }));
|
|
276
|
+
if (agents.some((g, i) => g.name === x.name && i !== at)) out.push(tx('agt.agent_exists', { name: x.name }));
|
|
276
277
|
if (out.length) return out;
|
|
277
278
|
if (at >= 0) agents[at] = x;
|
|
278
279
|
else agents.push(x);
|
|
279
280
|
return [];
|
|
280
281
|
});
|
|
281
282
|
if (problems.length) {
|
|
282
|
-
const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${original || 'New agent'}</h1></div>${region('
|
|
283
|
+
const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${original || 'New agent'}</h1></div>${region(tx('agt.ai_agent'), await agentForm(a, s, x, original, problems))}`;
|
|
283
284
|
return send(reply.code(422), s, shell(s, original || 'New agent', crumbs(a, original || 'New agent'), main));
|
|
284
285
|
}
|
|
285
286
|
if (original && original !== x.name)
|
|
286
287
|
await owner.query(`update meta.region r set config = jsonb_set(r.config, '{agent}', to_jsonb($3::text)) from meta.page p
|
|
287
288
|
where p.id = r.page_id and p.app_id = $1 and r.type = 'ai_assistant' and upper(r.config->>'agent') = $2`, [a.id, original, x.name]);
|
|
288
|
-
flash(s, '
|
|
289
|
+
flash(s, tx('agt.ai_agent_saved'));
|
|
289
290
|
return back(reply, s, `${BASE}/apps/${a.id}/ai-agents/agent?name=${encodeURIComponent(x.name)}`);
|
|
290
291
|
});
|
|
291
292
|
|
|
@@ -311,7 +312,7 @@ export async function aiAgentRoutes(app: FastifyInstance) {
|
|
|
311
312
|
const x = name ? ((a.ai_tools ?? []) as AiTool[]).find((t) => t.name === name) : {};
|
|
312
313
|
if (!x) return reply.code(404).send('Not found');
|
|
313
314
|
const title = name || 'New tool';
|
|
314
|
-
const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${title}</h1></div>${region('
|
|
315
|
+
const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${title}</h1></div>${region(tx('agt.ai_tool'), await toolForm(a, s, x, name, []))}`;
|
|
315
316
|
return send(reply, s, shell(s, title, crumbs(a, title), main));
|
|
316
317
|
});
|
|
317
318
|
|
|
@@ -327,15 +328,15 @@ export async function aiAgentRoutes(app: FastifyInstance) {
|
|
|
327
328
|
const problems = errors.length ? errors : await update(a.id, async (agents, tools) => {
|
|
328
329
|
const out = aiToolProblems(tool);
|
|
329
330
|
if (tool.type === 'rest' && tool.source && !(await owner.one('select 1 from meta.rest_source where app_id = $1 and name = $2', [a.id, tool.source])))
|
|
330
|
-
out.push(
|
|
331
|
+
out.push(tx('agt.no_rest_source', { name: tool.source }));
|
|
331
332
|
if (tool.type === 'retrieve' && tool.search_config && !(await owner.one(`select 1 from meta.search_config where app_id = $1 and name = $2 and source_type <> 'list'`, [a.id, tool.search_config])))
|
|
332
|
-
out.push(
|
|
333
|
+
out.push(tx('agt.no_search_config', { name: tool.search_config }));
|
|
333
334
|
if (tool.type === 'navigate') {
|
|
334
335
|
const known = new Set((await owner.query<{ page_no: number }>('select page_no from meta.page where app_id = $1', [a.id])).rows.map((p) => p.page_no));
|
|
335
|
-
for (const p of tool.pages ?? []) if (!known.has(p)) out.push(
|
|
336
|
+
for (const p of tool.pages ?? []) if (!known.has(p)) out.push(tx('agt.no_page', { n: p }));
|
|
336
337
|
}
|
|
337
338
|
const at = original ? tools.findIndex((t) => t.name === original) : -1;
|
|
338
|
-
if (original && at < 0) out.push(
|
|
339
|
+
if (original && at < 0) out.push(tx('agt.tool_gone', { name: original }));
|
|
339
340
|
if (tools.some((t, i) => t.name === tool.name && i !== at)) out.push(`A tool ${tool.name} exists already.`);
|
|
340
341
|
if (out.length) return out;
|
|
341
342
|
if (at >= 0) tools[at] = tool;
|
|
@@ -345,10 +346,10 @@ export async function aiAgentRoutes(app: FastifyInstance) {
|
|
|
345
346
|
return [];
|
|
346
347
|
});
|
|
347
348
|
if (problems.length) {
|
|
348
|
-
const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${original || 'New tool'}</h1></div>${region('
|
|
349
|
+
const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${original || 'New tool'}</h1></div>${region(tx('agt.ai_tool'), await toolForm(a, s, x, original, problems))}`;
|
|
349
350
|
return send(reply.code(422), s, shell(s, original || 'New tool', crumbs(a, original || 'New tool'), main));
|
|
350
351
|
}
|
|
351
|
-
flash(s, '
|
|
352
|
+
flash(s, tx('agt.ai_tool_saved'));
|
|
352
353
|
return back(reply, s, `${BASE}/apps/${a.id}/ai-agents/tool?name=${encodeURIComponent(tool.name)}`);
|
|
353
354
|
});
|
|
354
355
|
|
|
@@ -359,7 +360,7 @@ export async function aiAgentRoutes(app: FastifyInstance) {
|
|
|
359
360
|
const name = one((req.body as Body)?.name);
|
|
360
361
|
const problems = await update(a.id, (agents, tools) => {
|
|
361
362
|
const users = agents.filter((g) => (g.tools ?? []).includes(name)).map((g) => g.name);
|
|
362
|
-
if (users.length) return [
|
|
363
|
+
if (users.length) return [tx('agt.tool_used', { name, agents: users.join(', ') })];
|
|
363
364
|
const at = tools.findIndex((t) => t.name === name);
|
|
364
365
|
if (at >= 0) tools.splice(at, 1);
|
|
365
366
|
return [];
|
|
@@ -10,8 +10,8 @@ import { appOr404 } from './forms.ts';
|
|
|
10
10
|
import { isAdmin } from './locks.ts';
|
|
11
11
|
import { appHeader, back, BASE, csrf, developer, flash, region, select, send, shell, workshopTabs, type Req } from './ui.ts';
|
|
12
12
|
import { workshopDb } from './workspaces.ts';
|
|
13
|
-
import { WIZARD_KINDS, wizardTables } from './wizards.ts';
|
|
14
|
-
import { tr } from './i18n.ts';
|
|
13
|
+
import { kindLabel, WIZARD_KINDS, wizardTables } from './wizards.ts';
|
|
14
|
+
import { scopedTx as tx, tr } from './i18n.ts';
|
|
15
15
|
|
|
16
16
|
// App Builder AI (migration 062), with the AI service an administrator
|
|
17
17
|
// chooses for the builder (meta.builder_ai):
|
|
@@ -41,7 +41,7 @@ export async function builderService() {
|
|
|
41
41
|
|
|
42
42
|
async function ask(s: Session, req: Parameters<typeof generate>[1]) {
|
|
43
43
|
const svc = await builderService();
|
|
44
|
-
if (!svc) throw new AiError('config', '
|
|
44
|
+
if (!svc) throw new AiError('config', tx('aib.the_app_builder_has_no'));
|
|
45
45
|
return generate(svc.name, req, { appId: null, user: s.username, source: 'builder' });
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -128,7 +128,7 @@ async function serviceBox(s: Session) {
|
|
|
128
128
|
<div class="buttons"><button class="btn">${tx('scr.save')}</button></div></form>`;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
const errorBox = (e: unknown) => html`<div class="alert alert-error" role="alert">${e instanceof AiError || e instanceof Error ? e.message : '
|
|
131
|
+
const errorBox = (e: unknown) => html`<div class="alert alert-error" role="alert">${e instanceof AiError || e instanceof Error ? e.message : tx('aib.the_ai_request_failed')}</div>`;
|
|
132
132
|
|
|
133
133
|
// ---------------------------------------------------------------- SQL Workshop → AI
|
|
134
134
|
|
|
@@ -228,33 +228,33 @@ async function freePages(appId: number) {
|
|
|
228
228
|
async function pagesPage(s: Session, reply: FastifyReply, a: any, description: string, proposals: Proposal[] | null, message?: Raw) {
|
|
229
229
|
const tables = (await wizardTables(a.db_role)).filter((t) => t.access).map((t) => t.t);
|
|
230
230
|
const svc = await builderService();
|
|
231
|
-
const kindOptions = WIZARD_KINDS.map(([k, label]): [string, string] => [k, label]);
|
|
231
|
+
const kindOptions = WIZARD_KINDS.map(([k, label]): [string, string] => [k, kindLabel(k, label)]);
|
|
232
232
|
const main = html`${appHeader(a, 'pages', s)}
|
|
233
233
|
${message ?? ''}
|
|
234
|
-
${region('
|
|
235
|
-
<p class="muted u-mt0"
|
|
236
|
-
${svc ? '' : html`<div class="alert alert-info"
|
|
234
|
+
${region(tx('aib.create_pages_with_ai'), html`
|
|
235
|
+
<p class="muted u-mt0">${tx.rich('aib.intro', { role: a.db_role ?? tx('aib.the_owner'), descriptions: html`<a href="${BASE}/sql/ai/describe">${tx('aib.descriptions')}</a>` })}</p>
|
|
236
|
+
${svc ? '' : html`<div class="alert alert-info">${tx.rich('aib.no_service', { link: html`<a href="${BASE}/sql/ai">${tx('aib.sql_workshop_ai')}</a>` })}</div>`}
|
|
237
237
|
<form method="post" action="${BASE}/apps/${a.id}/ai-pages">${csrf(s)}
|
|
238
|
-
<div class="field"><label class="label" for="f_description"
|
|
239
|
-
<textarea id="f_description" name="description" rows="4" maxlength="4000" required placeholder="
|
|
240
|
-
<div class="buttons"><button class="btn btn-hot" data-busy="
|
|
238
|
+
<div class="field"><label class="label" for="f_description">${tx('aib.description')}</label>
|
|
239
|
+
<textarea id="f_description" name="description" rows="4" maxlength="4000" required placeholder="${tx('aib.e_g_a_page_to')}">${description}</textarea></div>
|
|
240
|
+
<div class="buttons"><button class="btn btn-hot" data-busy="${tx('aib.thinking')}"${svc ? '' : raw(' disabled')}>${tx('aib.propose_pages')}</button></div>
|
|
241
241
|
</form>`)}
|
|
242
|
-
${proposals ? region('
|
|
242
|
+
${proposals ? region(tx('aib.proposed_pages'), proposals.length ? html`
|
|
243
243
|
<form method="post" action="${BASE}/apps/${a.id}/ai-pages/create">${csrf(s)}
|
|
244
244
|
<input type="hidden" name="count" value="${proposals.length}">
|
|
245
|
-
<div class="table-wrap"><table class="report report-reflow"><thead><tr><th scope="col"
|
|
245
|
+
<div class="table-wrap"><table class="report report-reflow"><thead><tr><th scope="col">${tx('aib.create')}</th><th scope="col">${tx('aib.page_type')}</th><th scope="col">${tx('aib.table_or_view')}</th><th scope="col">${tx('aib.page')}</th><th scope="col">${tx('aib.form_page')}</th><th scope="col">${tx('aib.label')}</th><th scope="col">${tx('aib.why')}</th></tr></thead><tbody>
|
|
246
246
|
${proposals.map((p, i) => html`<tr>
|
|
247
|
-
<td data-label="
|
|
248
|
-
<td data-label="
|
|
249
|
-
<td data-label="
|
|
250
|
-
<td data-label="
|
|
251
|
-
<td data-label="
|
|
252
|
-
<td data-label="
|
|
253
|
-
<td data-label="
|
|
247
|
+
<td data-label="${tx('aib.create')}"><input type="checkbox" name="on_${i}" value="true" checked aria-label="${tx('aib.p_create', { n: i + 1 })}"></td>
|
|
248
|
+
<td data-label="${tx('aib.page_type')}"><select name="kind_${i}" aria-label="${tx('aib.p_kind', { n: i + 1 })}">${kindOptions.map(([k, l]) => opt(k, l, p.kind))}</select></td>
|
|
249
|
+
<td data-label="${tx('aib.table_or_view')}"><select name="table_${i}" aria-label="${tx('aib.p_table', { n: i + 1 })}">${tables.map((t) => opt(t, t, p.table))}</select></td>
|
|
250
|
+
<td data-label="${tx('aib.page')}"><input name="page_${i}" type="number" min="1" max="99999" value="${p.page}" aria-label="${tx('aib.p_page', { n: i + 1 })}"></td>
|
|
251
|
+
<td data-label="${tx('aib.form_page')}"><input name="form_${i}" type="number" min="1" max="99999" value="${p.form_page ?? ''}" aria-label="${tx('aib.p_form', { n: i + 1 })}"></td>
|
|
252
|
+
<td data-label="${tx('aib.label')}"><input name="label_${i}" maxlength="80" value="${p.label}" aria-label="${tx('aib.p_label', { n: i + 1 })}"></td>
|
|
253
|
+
<td data-label="${tx('aib.why')}" class="muted">${p.reason ?? ''}</td></tr>`)}
|
|
254
254
|
</tbody></table></div>
|
|
255
|
-
<div class="buttons"><button class="btn btn-hot"
|
|
256
|
-
</form>` : html`<p class="muted"
|
|
257
|
-
return send(reply, s, shell(s, a.name, [['
|
|
255
|
+
<div class="buttons"><button class="btn btn-hot">${tx('aib.create_the_ticked_pages')}</button></div>
|
|
256
|
+
</form>` : html`<p class="muted">${tx('aib.no_pages_were_proposed_describe')}</p>`) : ''}`;
|
|
257
|
+
return send(reply, s, shell(s, a.name, [[tx('aib.app_builder'), BASE], [a.name, `${BASE}/apps/${a.id}`], [tx('aib.create_pages_with_ai')]], main));
|
|
258
258
|
}
|
|
259
259
|
|
|
260
260
|
/** The model's proposals checked: known page types and tables, free page numbers (others moved to the next free ones). */
|
|
@@ -293,7 +293,7 @@ export async function aiBuilderRoutes(app: FastifyInstance) {
|
|
|
293
293
|
app.post(`${BASE}/sql/ai/service`, async (req: Req, reply) => {
|
|
294
294
|
const s = await developer(req, reply);
|
|
295
295
|
if (!s) return;
|
|
296
|
-
if (!(await isAdmin(s.username))) return reply.code(403).send('
|
|
296
|
+
if (!(await isAdmin(s.username))) return reply.code(403).send(tx('aib.admins_only'));
|
|
297
297
|
const name = String(req.body?.service ?? '').toUpperCase();
|
|
298
298
|
const svc = name ? await owner.one<{ id: number }>('select id from meta.ai_service where name = $1', [name]) : null;
|
|
299
299
|
if (name && !svc) {
|
|
@@ -452,9 +452,9 @@ export async function aiBuilderRoutes(app: FastifyInstance) {
|
|
|
452
452
|
if (!a) return reply.code(404).send('Not found');
|
|
453
453
|
const description = String(req.body?.description ?? '').trim().slice(0, 4000);
|
|
454
454
|
try {
|
|
455
|
-
if (!description) throw new AiError('config', '
|
|
455
|
+
if (!description) throw new AiError('config', tx('aib.describe_the_pages_you_want'));
|
|
456
456
|
const tables = (await wizardTables(a.db_role)).filter((t) => t.access).map((t) => t.t).slice(0, 300);
|
|
457
|
-
if (!tables.length) throw new AiError('config',
|
|
457
|
+
if (!tables.length) throw new AiError('config', tx('aib.no_tables', { role: a.db_role }));
|
|
458
458
|
const used = await freePages(a.id);
|
|
459
459
|
const firstFree = (() => {
|
|
460
460
|
let n = 1;
|
|
@@ -517,13 +517,13 @@ export async function aiBuilderRoutes(app: FastifyInstance) {
|
|
|
517
517
|
const form = num(b[`form_${i}`]);
|
|
518
518
|
const label = String(b[`label_${i}`] ?? '').trim().slice(0, 80);
|
|
519
519
|
if (!KINDS.has(kind) || !tables.has(table) || page === null || (kind === 'report_form' && form === null)) {
|
|
520
|
-
errors.push(
|
|
520
|
+
errors.push(tx('aib.p_invalid', { n: i + 1 }));
|
|
521
521
|
continue;
|
|
522
522
|
}
|
|
523
523
|
chosen.push({ kind, table, page, form_page: kind === 'report_form' ? form : null, label: label || table.split('.').pop()! });
|
|
524
524
|
}
|
|
525
525
|
if (errors.length || !chosen.length) {
|
|
526
|
-
flash(s, errors.length ? errors.join(' ') : '
|
|
526
|
+
flash(s, errors.length ? errors.join(' ') : tx('aib.tick_the_pages_to_create'), 'error');
|
|
527
527
|
return back(reply, s, `${BASE}/apps/${a.id}/ai-pages`);
|
|
528
528
|
}
|
|
529
529
|
try {
|
|
@@ -538,10 +538,10 @@ export async function aiBuilderRoutes(app: FastifyInstance) {
|
|
|
538
538
|
}
|
|
539
539
|
return out;
|
|
540
540
|
});
|
|
541
|
-
flash(s,
|
|
541
|
+
flash(s, tx('aib.created', { list: created.join(', ') }));
|
|
542
542
|
return back(reply, s, `${BASE}/apps/${a.id}`);
|
|
543
543
|
} catch (e) {
|
|
544
|
-
flash(s,
|
|
544
|
+
flash(s, tx('aib.not_created', { error: (e as Error).message }), 'error');
|
|
545
545
|
return back(reply, s, `${BASE}/apps/${a.id}/ai-pages`);
|
|
546
546
|
}
|
|
547
547
|
});
|