diff --git a/src/lib/helpers/enums.js b/src/lib/helpers/enums.js index 7117bc2c..fef2a980 100644 --- a/src/lib/helpers/enums.js +++ b/src/lib/helpers/enums.js @@ -90,6 +90,12 @@ const routingMode = { }; export const RoutingMode = Object.freeze(routingMode); +const ruleCriteriaMode = { + Llm: "llm", + PythonScript: "python_script" +}; +export const RuleCriteriaMode = Object.freeze(ruleCriteriaMode); + const functionVisMode = { Manual: "manual", Auto: "auto" diff --git a/src/lib/helpers/store.js b/src/lib/helpers/store.js index 23071f54..cc739b21 100644 --- a/src/lib/helpers/store.js +++ b/src/lib/helpers/store.js @@ -37,6 +37,42 @@ export const globalMenuStore = createGlobalMenuStore(); /** @type {Writable} */ export const userStore = writable({ id: "", full_name: "", expires: 0, token: null }); +/** @type {boolean} */ +let openerSessionRestored = false; + +/** + * The signed-in session lives in sessionStorage, which is per-tab: a tab opened + * from an in-app link starts without it (browsers only clone session storage in + * some cases, and never when the link severs the opener), which would bounce the + * user to the login page. When this tab was opened by another tab of the same + * app, copy the session across once instead. + * + * Reading the opener is only permitted — and only attempted — same-origin, and + * existing keys in this tab always win. + */ +export function restoreSessionFromOpener() { + if (!browser || openerSessionRestored) return; + openerSessionRestored = true; + + try { + const opener = window.opener; + if (!opener || opener.closed) return; + // Touching location.origin on a cross-origin opener throws, hence the try. + if (opener.location.origin !== window.location.origin) return; + + [userKey, tenantKey, tenantNameKey].forEach(key => { + if (sessionStorage.getItem(key)) return; + + const value = opener.sessionStorage.getItem(key); + if (value) { + sessionStorage.setItem(key, value); + } + }); + } catch (e) { + // No accessible opener (closed, cross-origin, or blocked) — nothing to restore. + } +} + /** * @returns {Writable} */ @@ -48,6 +84,11 @@ export function getUserStore() { } let json = sessionStorage.getItem(userKey); + if (!json) { + restoreSessionFromOpener(); + json = sessionStorage.getItem(userKey); + } + if (json) { return JSON.parse(json); } @@ -60,6 +101,7 @@ export function getUserStore() { /** @returns {string} */ export function getTenantId() { if (!browser) return ''; + restoreSessionFromOpener(); return sessionStorage.getItem(tenantKey) || ''; } @@ -81,6 +123,7 @@ export function clearTenantId() { /** @returns {string} */ export function getTenantName() { if (!browser) return ''; + restoreSessionFromOpener(); return sessionStorage.getItem(tenantNameKey) || ''; } diff --git a/src/lib/helpers/types/agentTypes.js b/src/lib/helpers/types/agentTypes.js index cc8cfb62..61454ba2 100644 --- a/src/lib/helpers/types/agentTypes.js +++ b/src/lib/helpers/types/agentTypes.js @@ -244,15 +244,34 @@ * @property {boolean} [expanded] */ +/** + * Trigger option returned by the rule-options endpoints. Note this is the + * option catalog, not a rule configured on an agent. + * @typedef {Object} AgentRuleOption + * @property {string} trigger_name + * @property {any?} [output_args] + * @property {string?} [json_args] + * @property {string?} [statement] + * @property {string?} [mode] - Default criteria mode for this trigger + */ + +/** + * @typedef {Object} RuleCriteria + * @property {string?} [mode] - Criteria mode: llm, code, etc. Takes precedence over the mode carried on the trigger options. + * @property {string?} [criteria] - Criteria text + */ + /** * @typedef {Object} AgentRule - * @property {string} trigger_name + * @property {string} trigger_name * @property {string?} [displayName] * @property {boolean} disabled - * @property {any?} [config] + * @property {string?} [message] - Message sent to agent + * @property {RuleCriteria?} [criteria] * @property {any?} [output_args] * @property {string?} [json_args] * @property {string?} [statement] + * @property {string?} [default_mode] - Criteria mode carried on the trigger options, used as fallback * @property {boolean} [expanded] */ diff --git a/src/lib/services/agent-service.js b/src/lib/services/agent-service.js index a88bcfbc..12652c65 100644 --- a/src/lib/services/agent-service.js +++ b/src/lib/services/agent-service.js @@ -108,7 +108,7 @@ export async function getAgentUtilityOptions() { /** * Get agent rule options - * @returns {Promise} + * @returns {Promise} */ export async function getAgentRuleOptions() { const url = endpoints.agentRuleOptionsUrl; @@ -119,7 +119,7 @@ export async function getAgentRuleOptions() { /** * Get agent rule options by agent id * @param {string} agentId - * @returns {Promise} + * @returns {Promise} */ export async function getAgentRuleOptionsById(agentId) { const url = endpoints.agentRuleOptionsByIdUrl.replace("{agentId}", agentId); diff --git a/src/lib/styles/pages/_agent.scss b/src/lib/styles/pages/_agent.scss index 0ba09aff..e58df430 100644 --- a/src/lib/styles/pages/_agent.scss +++ b/src/lib/styles/pages/_agent.scss @@ -2108,6 +2108,33 @@ $panel-radius: 0.5rem; } +/* "Criteria Text" doubles as a link to the agent's code scripts page. */ +.ari-label-link { + display: inline-flex; + align-items: center; + gap: 0.25rem; + color: var(--color-primary); + text-decoration: none; + transition: filter 0.15s ease; + + &:hover { + color: var(--color-primary); + text-decoration: underline; + filter: brightness(1.15); + } + + &:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; + border-radius: 0.25rem; + } + + i { + font-size: 0.875rem; + } +} + + .ari-textarea { width: 100%; padding: 0.375rem 0.5rem; diff --git a/src/routes/page/agent/[agentId]/agent-components/rules/agent-rule-item.svelte b/src/routes/page/agent/[agentId]/agent-components/rules/agent-rule-item.svelte index e168d16d..bec880e9 100644 --- a/src/routes/page/agent/[agentId]/agent-components/rules/agent-rule-item.svelte +++ b/src/routes/page/agent/[agentId]/agent-components/rules/agent-rule-item.svelte @@ -3,13 +3,20 @@ import Markdown from '$lib/common/markdown/Markdown.svelte'; import BotsharpTooltip from '$lib/common/tooltip/BotsharpTooltip.svelte'; import Select from '$lib/common/dropdowns/Select.svelte'; + import { RuleCriteriaMode } from '$lib/helpers/enums'; const textLimit = 1024; + const criteriaModeOptions = [ + { label: 'LLM', value: RuleCriteriaMode.Llm }, + { label: 'Python Script', value: RuleCriteriaMode.PythonScript } + ]; + /** * @type {{ * rule: import('$agentTypes').AgentRule, * ruleIndex: number, + * agentId?: string, * collapsed?: boolean, * ruleOptions?: any[], * windowWidth: number, @@ -23,6 +30,7 @@ let { rule, ruleIndex, + agentId = '', collapsed = true, ruleOptions = [], windowWidth, @@ -35,7 +43,17 @@ // Code script can only be generated by admins, once a trigger is picked and criteria text exists. let canCompile = $derived( - !!rule.trigger_name && !!rule.config?.criteria?.trim() + !!rule.trigger_name && !!rule.criteria?.criteria?.trim() + ); + + // The rule's own mode wins; otherwise the trigger option's mode applies. + let modePlaceholder = $derived( + rule.default_mode ? `Trigger default (${rule.default_mode})` : 'Trigger default' + ); + + // Deep link to the code scripts page, preselected on this agent. + let codeScriptUrl = $derived( + agentId ? `/page/agent/code-scripts?agentId=${encodeURIComponent(agentId)}` : '' ); /** @@ -82,15 +100,32 @@ /** * @param {any} e + * @param {string} field */ - function changeCriteria(e) { + function changeText(e, field) { onchange?.({ ruleIdx: ruleIndex, - field: 'criteria', + field: field, value: e?.target?.value || '' }); } + /** + * The session token lives in sessionStorage, which a new tab does not get + * when the link drops the opener (`rel="noopener"`, which `target="_blank"` + * also implies on its own). Opening through window.open keeps this tab as + * the opener, so the new tab can pull the session across and skip the login + * page. Also covers ctrl/cmd-click, which would otherwise take the default + * opener-less path. + * @param {any} e + */ + function openCodeScripts(e) { + if (!codeScriptUrl) return; + + e.preventDefault(); + window.open(codeScriptUrl, '_blank'); + } + function compile() { oncompile?.({ ruleIdx: ruleIndex, @@ -159,6 +194,8 @@ tag={`rule-trigger-${ruleIndex}`} containerStyles={'width: 100%;'} placeholder={'Select a trigger'} + searchMode + searchPlaceholder={'Search triggers'} disabled={rule.disabled} selectedValues={rule.trigger_name ? [rule.trigger_name] : []} options={ruleOptions.filter(o => !!o.name).map(o => ({ label: o.displayName || o.name, value: o.name }))} @@ -181,7 +218,65 @@
- {'Criteria'} + {'Message'} +
+
+
+
+ +
+
+
+
+ +
+
+
+ {'Criteria Mode'} +
+
+
+
+