@open-mercato/channel-discord 0.7.1-develop.7106.1.53fe9dd1b1 → 0.7.1-develop.7112.1.d486e6f1b2

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.
@@ -1,7 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { useParams } from "next/navigation";
5
4
  import { useT } from "@open-mercato/shared/lib/i18n/context";
6
5
  import { Page, PageBody } from "@open-mercato/ui/backend/Page";
7
6
  import { CrudForm } from "@open-mercato/ui/backend/CrudForm";
@@ -11,16 +10,19 @@ import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
11
10
  import { updateCrud } from "@open-mercato/ui/backend/utils/crud";
12
11
  import { createCrudFormError } from "@open-mercato/ui/backend/utils/serverErrors";
13
12
  const CHANNELS_LIST_HREF = "/backend/communication_channels/channels";
14
- function DiscordAiAutoReplyPage() {
13
+ function DiscordAiAutoReplyPage({ params }) {
15
14
  const t = useT();
16
- const params = useParams();
17
15
  const channelId = params?.id ?? "";
18
16
  const [settings, setSettings] = React.useState(null);
19
17
  const [isLoading, setIsLoading] = React.useState(true);
20
18
  const [errorMessage, setErrorMessage] = React.useState(null);
21
19
  const [notFound, setNotFound] = React.useState(false);
22
20
  const load = React.useCallback(async () => {
23
- if (!channelId) return;
21
+ if (!channelId) {
22
+ setIsLoading(false);
23
+ setErrorMessage(t("channel_discord.aiAutoReply.errors.missingChannelId", "No channel selected."));
24
+ return;
25
+ }
24
26
  setIsLoading(true);
25
27
  setErrorMessage(null);
26
28
  setNotFound(false);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../../src/modules/channel_discord/backend/channel_discord/channels/%5Bid%5D/ai-auto-reply/page.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useParams } from 'next/navigation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { ErrorMessage, LoadingMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { updateCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\n\ntype EligibleAgent = {\n id: string\n label: string\n description: string\n requiredFeatures: string[]\n /** Whether the auto-reply principal holds the agent's `requiredFeatures`. */\n invocable: boolean\n missingFeatures: string[]\n}\n\ntype AiAutoReplySettings = {\n channelId: string\n displayName: string\n updatedAt: string | null\n aiAutoReplyEnabled: boolean\n aiAgentId: string | null\n aiAutoReplyLastError: string | null\n aiAutoReplyLastErrorAt: string | null\n defaultAgentId: string\n aiAvailable: boolean\n agents: EligibleAgent[]\n}\n\ntype FormValues = {\n id: string\n updatedAt: string | null\n aiAutoReplyEnabled: boolean\n aiAgentId: string\n}\n\nconst CHANNELS_LIST_HREF = '/backend/communication_channels/channels'\n\n/**\n * Per-channel AI auto-reply settings (issue #4778) \u2014 the surface that writes\n * `aiAutoReplyEnabled` / `aiAgentId`, the two keys the subscriber reads.\n *\n * It is a `CrudForm` rather than a hand-rolled form so the save inherits the\n * platform's contract for free: zod validation with field-level errors, and the\n * optimistic-lock header auto-derived from `initialValues.updatedAt`, so a save\n * from a tab opened before someone else touched the channel surfaces the shared\n * conflict bar instead of overwriting them.\n */\nexport default function DiscordAiAutoReplyPage() {\n const t = useT()\n const params = useParams<{ id: string }>()\n const channelId = (params?.id as string) ?? ''\n\n const [settings, setSettings] = React.useState<AiAutoReplySettings | null>(null)\n const [isLoading, setIsLoading] = React.useState(true)\n const [errorMessage, setErrorMessage] = React.useState<string | null>(null)\n const [notFound, setNotFound] = React.useState(false)\n\n const load = React.useCallback(async () => {\n if (!channelId) return\n setIsLoading(true)\n setErrorMessage(null)\n setNotFound(false)\n const response = await apiCall<AiAutoReplySettings>(\n `/api/channel_discord/channels/${encodeURIComponent(channelId)}/ai-auto-reply`,\n ).catch(() => null)\n if (!response || !response.ok) {\n const status = (response as { status?: number } | null)?.status\n if (status === 404) {\n setNotFound(true)\n } else {\n const body = response?.result as { error?: string } | undefined\n setErrorMessage(\n body?.error ?? t('channel_discord.aiAutoReply.errors.load', 'Failed to load AI auto-reply settings'),\n )\n }\n setSettings(null)\n } else {\n setSettings(response.result ?? null)\n }\n setIsLoading(false)\n }, [channelId, t])\n\n React.useEffect(() => {\n void load()\n }, [load])\n\n const fields = React.useMemo<CrudField[]>(() => {\n // An agent the auto-reply principal cannot invoke stays in the list \u2014 hiding\n // it would leave the operator wondering where an agent they can see elsewhere\n // went \u2014 but it says what it needs, so the requirement is visible before the\n // save rather than only in the 400 that follows.\n const agentOptions = (settings?.agents ?? []).map((agent) => ({\n value: agent.id,\n label: agent.invocable\n ? agent.label\n : t(\n 'channel_discord.aiAutoReply.fields.agentMissingFeatures',\n '{label} \u2014 needs {features}',\n { label: agent.label, features: agent.missingFeatures.join(', ') },\n ),\n }))\n return [\n {\n id: 'aiAutoReplyEnabled',\n type: 'checkbox',\n label: t('channel_discord.aiAutoReply.fields.enabled', 'Answer inbound Discord messages with AI'),\n description: t(\n 'channel_discord.aiAutoReply.fields.enabledHelp',\n 'Off by default. When on, straightforward messages are answered automatically; anything sensitive, low-confidence or suspicious is proposed for a human to approve instead.',\n ),\n disabled: !settings?.aiAvailable,\n },\n {\n id: 'aiAgentId',\n type: 'select',\n label: t('channel_discord.aiAutoReply.fields.agent', 'Agent'),\n description: t(\n 'channel_discord.aiAutoReply.fields.agentHelp',\n 'Only agents that produce structured output can answer a channel. Grant the tenant channel-bot user the agent\u2019s required features before choosing an agent from another module.',\n ),\n options: agentOptions,\n required: true,\n visibleWhen: { field: 'aiAutoReplyEnabled', equals: true },\n },\n ]\n }, [settings, t])\n\n const initialValues = React.useMemo<Partial<FormValues>>(() => {\n if (!settings) return {}\n // Default to an agent the principal can actually invoke, so the form does not\n // pre-select a choice the save will reject.\n const invocable = settings.agents.filter((agent) => agent.invocable)\n const fallbackAgentId = invocable.some((agent) => agent.id === settings.defaultAgentId)\n ? settings.defaultAgentId\n : (invocable[0]?.id ?? settings.agents[0]?.id ?? '')\n return {\n id: settings.channelId,\n updatedAt: settings.updatedAt,\n aiAutoReplyEnabled: settings.aiAutoReplyEnabled,\n aiAgentId: settings.aiAgentId ?? fallbackAgentId,\n }\n }, [settings])\n\n const onSubmit = React.useCallback(\n async (values: FormValues) => {\n const enabled = Boolean(values.aiAutoReplyEnabled)\n const agentId = (values.aiAgentId ?? '').trim()\n if (enabled && !agentId) {\n throw createCrudFormError(\n t('channel_discord.aiAutoReply.errors.agentRequired', 'Choose the agent that should answer this channel'),\n { aiAgentId: t('channel_discord.aiAutoReply.errors.agentRequired', 'Choose the agent that should answer this channel') },\n )\n }\n // The route enforces this too \u2014 it has to, since a role can change between\n // the load and the save. Checking here as well turns the common case into an\n // inline field error instead of a round trip.\n const chosen = settings?.agents.find((agent) => agent.id === agentId)\n if (enabled && chosen && !chosen.invocable) {\n const message = t(\n 'channel_discord.aiAutoReply.errors.agentFeaturesMissing',\n 'The channel-bot user is missing the features this agent needs: {features}. Grant them first, or pick another agent.',\n { features: chosen.missingFeatures.join(', ') },\n )\n throw createCrudFormError(message, { aiAgentId: message })\n }\n // Deliberately unguarded: a 409 from the optimistic-lock check has to reach\n // CrudForm, which renders it on the shared conflict bar. Catching it here\n // would surface the same bar and then let the form believe the save\n // succeeded.\n await updateCrud(\n `channel_discord/channels/${encodeURIComponent(channelId)}/ai-auto-reply`,\n {\n aiAutoReplyEnabled: enabled,\n aiAgentId: enabled ? agentId : undefined,\n },\n {\n errorMessage: t(\n 'channel_discord.aiAutoReply.errors.save',\n 'Failed to save AI auto-reply settings',\n ),\n },\n )\n flash(t('channel_discord.aiAutoReply.saved', 'AI auto-reply settings saved.'), 'success')\n await load()\n },\n [channelId, load, settings, t],\n )\n\n if (isLoading) {\n return (\n <Page>\n <PageBody>\n <LoadingMessage label={t('channel_discord.aiAutoReply.loading', 'Loading AI auto-reply settings...')} />\n </PageBody>\n </Page>\n )\n }\n\n if (notFound) {\n return (\n <Page>\n <PageBody>\n <RecordNotFoundState\n label={t('channel_discord.aiAutoReply.notFound', 'Discord channel not found')}\n backHref={CHANNELS_LIST_HREF}\n />\n </PageBody>\n </Page>\n )\n }\n\n if (errorMessage || !settings) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage\n label={\n errorMessage\n ?? t('channel_discord.aiAutoReply.errors.load', 'Failed to load AI auto-reply settings')\n }\n />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <CrudForm<FormValues>\n title={t('channel_discord.aiAutoReply.page.title', 'Discord AI auto-reply')}\n backHref={CHANNELS_LIST_HREF}\n cancelHref={CHANNELS_LIST_HREF}\n fields={fields}\n initialValues={initialValues}\n onSubmit={onSubmit}\n submitLabel={t('channel_discord.aiAutoReply.save', 'Save')}\n contentHeader={\n <div className=\"mb-4 space-y-2 text-sm text-muted-foreground\">\n <p>\n {t(\n 'channel_discord.aiAutoReply.intro',\n 'Choose whether this Discord channel may answer on its own, and which agent drafts the reply.',\n )}{' '}\n <span className=\"font-medium text-foreground\">{settings.displayName}</span>\n </p>\n {settings.aiAvailable ? null : (\n <p>\n {t(\n 'channel_discord.aiAutoReply.unavailable',\n 'The AI assistant module is not installed in this deployment, so auto-reply cannot be enabled. The channel keeps working as a normal inbox.',\n )}\n </p>\n )}\n {settings.aiAutoReplyLastError ? (\n // An armed channel whose runtime call keeps being refused is the\n // dormancy failure wearing a green tag. It is reported here, on the\n // surface that armed it, rather than only in a subscriber log line.\n <p className=\"rounded-md border border-status-error-border bg-status-error-bg p-3 text-status-error-text\">\n {t(\n 'channel_discord.aiAutoReply.lastError',\n 'The last auto-reply attempt on this channel produced nothing: {reason}',\n { reason: settings.aiAutoReplyLastError },\n )}\n {settings.aiAutoReplyLastErrorAt ? (\n <span className=\"ml-1\">\n {t('channel_discord.aiAutoReply.lastErrorAt', '(first seen {at})', {\n at: new Date(settings.aiAutoReplyLastErrorAt).toLocaleString(),\n })}\n </span>\n ) : null}\n </p>\n ) : null}\n </div>\n }\n />\n </PageBody>\n </Page>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { ErrorMessage, LoadingMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { updateCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\n\ntype EligibleAgent = {\n id: string\n label: string\n description: string\n requiredFeatures: string[]\n /** Whether the auto-reply principal holds the agent's `requiredFeatures`. */\n invocable: boolean\n missingFeatures: string[]\n}\n\ntype AiAutoReplySettings = {\n channelId: string\n displayName: string\n updatedAt: string | null\n aiAutoReplyEnabled: boolean\n aiAgentId: string | null\n aiAutoReplyLastError: string | null\n aiAutoReplyLastErrorAt: string | null\n defaultAgentId: string\n aiAvailable: boolean\n agents: EligibleAgent[]\n}\n\ntype FormValues = {\n id: string\n updatedAt: string | null\n aiAutoReplyEnabled: boolean\n aiAgentId: string\n}\n\nconst CHANNELS_LIST_HREF = '/backend/communication_channels/channels'\n\n/**\n * Per-channel AI auto-reply settings (issue #4778) \u2014 the surface that writes\n * `aiAutoReplyEnabled` / `aiAgentId`, the two keys the subscriber reads.\n *\n * It is a `CrudForm` rather than a hand-rolled form so the save inherits the\n * platform's contract for free: zod validation with field-level errors, and the\n * optimistic-lock header auto-derived from `initialValues.updatedAt`, so a save\n * from a tab opened before someone else touched the channel surfaces the shared\n * conflict bar instead of overwriting them.\n */\nexport default function DiscordAiAutoReplyPage({ params }: { params?: { id?: string } }) {\n const t = useT()\n const channelId = params?.id ?? ''\n\n const [settings, setSettings] = React.useState<AiAutoReplySettings | null>(null)\n const [isLoading, setIsLoading] = React.useState(true)\n const [errorMessage, setErrorMessage] = React.useState<string | null>(null)\n const [notFound, setNotFound] = React.useState(false)\n\n const load = React.useCallback(async () => {\n if (!channelId) {\n setIsLoading(false)\n setErrorMessage(t('channel_discord.aiAutoReply.errors.missingChannelId', 'No channel selected.'))\n return\n }\n setIsLoading(true)\n setErrorMessage(null)\n setNotFound(false)\n const response = await apiCall<AiAutoReplySettings>(\n `/api/channel_discord/channels/${encodeURIComponent(channelId)}/ai-auto-reply`,\n ).catch(() => null)\n if (!response || !response.ok) {\n const status = (response as { status?: number } | null)?.status\n if (status === 404) {\n setNotFound(true)\n } else {\n const body = response?.result as { error?: string } | undefined\n setErrorMessage(\n body?.error ?? t('channel_discord.aiAutoReply.errors.load', 'Failed to load AI auto-reply settings'),\n )\n }\n setSettings(null)\n } else {\n setSettings(response.result ?? null)\n }\n setIsLoading(false)\n }, [channelId, t])\n\n React.useEffect(() => {\n void load()\n }, [load])\n\n const fields = React.useMemo<CrudField[]>(() => {\n // An agent the auto-reply principal cannot invoke stays in the list \u2014 hiding\n // it would leave the operator wondering where an agent they can see elsewhere\n // went \u2014 but it says what it needs, so the requirement is visible before the\n // save rather than only in the 400 that follows.\n const agentOptions = (settings?.agents ?? []).map((agent) => ({\n value: agent.id,\n label: agent.invocable\n ? agent.label\n : t(\n 'channel_discord.aiAutoReply.fields.agentMissingFeatures',\n '{label} \u2014 needs {features}',\n { label: agent.label, features: agent.missingFeatures.join(', ') },\n ),\n }))\n return [\n {\n id: 'aiAutoReplyEnabled',\n type: 'checkbox',\n label: t('channel_discord.aiAutoReply.fields.enabled', 'Answer inbound Discord messages with AI'),\n description: t(\n 'channel_discord.aiAutoReply.fields.enabledHelp',\n 'Off by default. When on, straightforward messages are answered automatically; anything sensitive, low-confidence or suspicious is proposed for a human to approve instead.',\n ),\n disabled: !settings?.aiAvailable,\n },\n {\n id: 'aiAgentId',\n type: 'select',\n label: t('channel_discord.aiAutoReply.fields.agent', 'Agent'),\n description: t(\n 'channel_discord.aiAutoReply.fields.agentHelp',\n 'Only agents that produce structured output can answer a channel. Grant the tenant channel-bot user the agent\u2019s required features before choosing an agent from another module.',\n ),\n options: agentOptions,\n required: true,\n visibleWhen: { field: 'aiAutoReplyEnabled', equals: true },\n },\n ]\n }, [settings, t])\n\n const initialValues = React.useMemo<Partial<FormValues>>(() => {\n if (!settings) return {}\n // Default to an agent the principal can actually invoke, so the form does not\n // pre-select a choice the save will reject.\n const invocable = settings.agents.filter((agent) => agent.invocable)\n const fallbackAgentId = invocable.some((agent) => agent.id === settings.defaultAgentId)\n ? settings.defaultAgentId\n : (invocable[0]?.id ?? settings.agents[0]?.id ?? '')\n return {\n id: settings.channelId,\n updatedAt: settings.updatedAt,\n aiAutoReplyEnabled: settings.aiAutoReplyEnabled,\n aiAgentId: settings.aiAgentId ?? fallbackAgentId,\n }\n }, [settings])\n\n const onSubmit = React.useCallback(\n async (values: FormValues) => {\n const enabled = Boolean(values.aiAutoReplyEnabled)\n const agentId = (values.aiAgentId ?? '').trim()\n if (enabled && !agentId) {\n throw createCrudFormError(\n t('channel_discord.aiAutoReply.errors.agentRequired', 'Choose the agent that should answer this channel'),\n { aiAgentId: t('channel_discord.aiAutoReply.errors.agentRequired', 'Choose the agent that should answer this channel') },\n )\n }\n // The route enforces this too \u2014 it has to, since a role can change between\n // the load and the save. Checking here as well turns the common case into an\n // inline field error instead of a round trip.\n const chosen = settings?.agents.find((agent) => agent.id === agentId)\n if (enabled && chosen && !chosen.invocable) {\n const message = t(\n 'channel_discord.aiAutoReply.errors.agentFeaturesMissing',\n 'The channel-bot user is missing the features this agent needs: {features}. Grant them first, or pick another agent.',\n { features: chosen.missingFeatures.join(', ') },\n )\n throw createCrudFormError(message, { aiAgentId: message })\n }\n // Deliberately unguarded: a 409 from the optimistic-lock check has to reach\n // CrudForm, which renders it on the shared conflict bar. Catching it here\n // would surface the same bar and then let the form believe the save\n // succeeded.\n await updateCrud(\n `channel_discord/channels/${encodeURIComponent(channelId)}/ai-auto-reply`,\n {\n aiAutoReplyEnabled: enabled,\n aiAgentId: enabled ? agentId : undefined,\n },\n {\n errorMessage: t(\n 'channel_discord.aiAutoReply.errors.save',\n 'Failed to save AI auto-reply settings',\n ),\n },\n )\n flash(t('channel_discord.aiAutoReply.saved', 'AI auto-reply settings saved.'), 'success')\n await load()\n },\n [channelId, load, settings, t],\n )\n\n if (isLoading) {\n return (\n <Page>\n <PageBody>\n <LoadingMessage label={t('channel_discord.aiAutoReply.loading', 'Loading AI auto-reply settings...')} />\n </PageBody>\n </Page>\n )\n }\n\n if (notFound) {\n return (\n <Page>\n <PageBody>\n <RecordNotFoundState\n label={t('channel_discord.aiAutoReply.notFound', 'Discord channel not found')}\n backHref={CHANNELS_LIST_HREF}\n />\n </PageBody>\n </Page>\n )\n }\n\n if (errorMessage || !settings) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage\n label={\n errorMessage\n ?? t('channel_discord.aiAutoReply.errors.load', 'Failed to load AI auto-reply settings')\n }\n />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <CrudForm<FormValues>\n title={t('channel_discord.aiAutoReply.page.title', 'Discord AI auto-reply')}\n backHref={CHANNELS_LIST_HREF}\n cancelHref={CHANNELS_LIST_HREF}\n fields={fields}\n initialValues={initialValues}\n onSubmit={onSubmit}\n submitLabel={t('channel_discord.aiAutoReply.save', 'Save')}\n contentHeader={\n <div className=\"mb-4 space-y-2 text-sm text-muted-foreground\">\n <p>\n {t(\n 'channel_discord.aiAutoReply.intro',\n 'Choose whether this Discord channel may answer on its own, and which agent drafts the reply.',\n )}{' '}\n <span className=\"font-medium text-foreground\">{settings.displayName}</span>\n </p>\n {settings.aiAvailable ? null : (\n <p>\n {t(\n 'channel_discord.aiAutoReply.unavailable',\n 'The AI assistant module is not installed in this deployment, so auto-reply cannot be enabled. The channel keeps working as a normal inbox.',\n )}\n </p>\n )}\n {settings.aiAutoReplyLastError ? (\n // An armed channel whose runtime call keeps being refused is the\n // dormancy failure wearing a green tag. It is reported here, on the\n // surface that armed it, rather than only in a subscriber log line.\n <p className=\"rounded-md border border-status-error-border bg-status-error-bg p-3 text-status-error-text\">\n {t(\n 'channel_discord.aiAutoReply.lastError',\n 'The last auto-reply attempt on this channel produced nothing: {reason}',\n { reason: settings.aiAutoReplyLastError },\n )}\n {settings.aiAutoReplyLastErrorAt ? (\n <span className=\"ml-1\">\n {t('channel_discord.aiAutoReply.lastErrorAt', '(first seen {at})', {\n at: new Date(settings.aiAutoReplyLastErrorAt).toLocaleString(),\n })}\n </span>\n ) : null}\n </p>\n ) : null}\n </div>\n }\n />\n </PageBody>\n </Page>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/channel-discord",
3
- "version": "0.7.1-develop.7106.1.53fe9dd1b1",
3
+ "version": "0.7.1-develop.7112.1.d486e6f1b2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -62,14 +62,14 @@
62
62
  }
63
63
  },
64
64
  "dependencies": {
65
- "@open-mercato/core": "0.7.1-develop.7106.1.53fe9dd1b1",
66
- "@open-mercato/queue": "0.7.1-develop.7106.1.53fe9dd1b1",
67
- "@open-mercato/ui": "0.7.1-develop.7106.1.53fe9dd1b1"
65
+ "@open-mercato/core": "0.7.1-develop.7112.1.d486e6f1b2",
66
+ "@open-mercato/queue": "0.7.1-develop.7112.1.d486e6f1b2",
67
+ "@open-mercato/ui": "0.7.1-develop.7112.1.d486e6f1b2"
68
68
  },
69
69
  "peerDependencies": {
70
70
  "@mikro-orm/postgresql": "^7.0.14",
71
- "@open-mercato/ai-assistant": "0.7.1-develop.7106.1.53fe9dd1b1",
72
- "@open-mercato/shared": "0.7.1-develop.7106.1.53fe9dd1b1",
71
+ "@open-mercato/ai-assistant": "0.7.1-develop.7112.1.d486e6f1b2",
72
+ "@open-mercato/shared": "0.7.1-develop.7112.1.d486e6f1b2",
73
73
  "react": "^19.0.0",
74
74
  "react-dom": "^19.0.0"
75
75
  },
@@ -79,8 +79,8 @@
79
79
  }
80
80
  },
81
81
  "devDependencies": {
82
- "@open-mercato/ai-assistant": "0.7.1-develop.7106.1.53fe9dd1b1",
83
- "@open-mercato/shared": "0.7.1-develop.7106.1.53fe9dd1b1",
82
+ "@open-mercato/ai-assistant": "0.7.1-develop.7112.1.d486e6f1b2",
83
+ "@open-mercato/shared": "0.7.1-develop.7112.1.d486e6f1b2",
84
84
  "@types/jest": "^30.0.0",
85
85
  "@types/react": "^19.2.17",
86
86
  "@types/react-dom": "^19.2.3",
@@ -1,7 +1,6 @@
1
1
  'use client'
2
2
 
3
3
  import * as React from 'react'
4
- import { useParams } from 'next/navigation'
5
4
  import { useT } from '@open-mercato/shared/lib/i18n/context'
6
5
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
7
6
  import { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'
@@ -53,10 +52,9 @@ const CHANNELS_LIST_HREF = '/backend/communication_channels/channels'
53
52
  * from a tab opened before someone else touched the channel surfaces the shared
54
53
  * conflict bar instead of overwriting them.
55
54
  */
56
- export default function DiscordAiAutoReplyPage() {
55
+ export default function DiscordAiAutoReplyPage({ params }: { params?: { id?: string } }) {
57
56
  const t = useT()
58
- const params = useParams<{ id: string }>()
59
- const channelId = (params?.id as string) ?? ''
57
+ const channelId = params?.id ?? ''
60
58
 
61
59
  const [settings, setSettings] = React.useState<AiAutoReplySettings | null>(null)
62
60
  const [isLoading, setIsLoading] = React.useState(true)
@@ -64,7 +62,11 @@ export default function DiscordAiAutoReplyPage() {
64
62
  const [notFound, setNotFound] = React.useState(false)
65
63
 
66
64
  const load = React.useCallback(async () => {
67
- if (!channelId) return
65
+ if (!channelId) {
66
+ setIsLoading(false)
67
+ setErrorMessage(t('channel_discord.aiAutoReply.errors.missingChannelId', 'No channel selected.'))
68
+ return
69
+ }
68
70
  setIsLoading(true)
69
71
  setErrorMessage(null)
70
72
  setNotFound(false)
@@ -5,6 +5,7 @@
5
5
  "channel_discord.aiAutoReply.errors.aiUnavailable": "Die Autoantwort benötigt das KI-Assistent-Modul, das in dieser Installation fehlt",
6
6
  "channel_discord.aiAutoReply.errors.load": "KI-Autoantwort-Einstellungen konnten nicht geladen werden",
7
7
  "channel_discord.aiAutoReply.errors.loadChannels": "Discord-Kanäle konnten nicht geladen werden",
8
+ "channel_discord.aiAutoReply.errors.missingChannelId": "Kein Kanal ausgewählt.",
8
9
  "channel_discord.aiAutoReply.errors.save": "KI-Autoantwort-Einstellungen konnten nicht gespeichert werden",
9
10
  "channel_discord.aiAutoReply.fields.agent": "Agent",
10
11
  "channel_discord.aiAutoReply.fields.agentHelp": "Nur Agenten mit strukturierter Ausgabe können auf einem Kanal antworten. Geben Sie dem Kanal-Bot-Benutzer des Mandanten die vom Agenten geforderten Berechtigungen, bevor Sie einen Agenten aus einem anderen Modul wählen.",
@@ -5,6 +5,7 @@
5
5
  "channel_discord.aiAutoReply.errors.aiUnavailable": "Auto-reply needs the AI assistant module, which is not installed in this deployment",
6
6
  "channel_discord.aiAutoReply.errors.load": "Failed to load AI auto-reply settings",
7
7
  "channel_discord.aiAutoReply.errors.loadChannels": "Failed to load Discord channels",
8
+ "channel_discord.aiAutoReply.errors.missingChannelId": "No channel selected.",
8
9
  "channel_discord.aiAutoReply.errors.save": "Failed to save AI auto-reply settings",
9
10
  "channel_discord.aiAutoReply.fields.agent": "Agent",
10
11
  "channel_discord.aiAutoReply.fields.agentHelp": "Only agents that produce structured output can answer a channel. Grant the tenant channel-bot user the agent's required features before choosing an agent from another module.",
@@ -5,6 +5,7 @@
5
5
  "channel_discord.aiAutoReply.errors.aiUnavailable": "La respuesta automática necesita el módulo de asistente de IA, que no está instalado en este despliegue",
6
6
  "channel_discord.aiAutoReply.errors.load": "No se pudo cargar la configuración de respuesta automática de IA",
7
7
  "channel_discord.aiAutoReply.errors.loadChannels": "No se pudieron cargar los canales de Discord",
8
+ "channel_discord.aiAutoReply.errors.missingChannelId": "Ningún canal seleccionado.",
8
9
  "channel_discord.aiAutoReply.errors.save": "No se pudo guardar la configuración de respuesta automática de IA",
9
10
  "channel_discord.aiAutoReply.fields.agent": "Agente",
10
11
  "channel_discord.aiAutoReply.fields.agentHelp": "Solo los agentes que producen salida estructurada pueden responder en un canal. Concede al usuario bot del inquilino los permisos que exige el agente antes de elegir uno de otro módulo.",
@@ -5,6 +5,7 @@
5
5
  "channel_discord.aiAutoReply.errors.aiUnavailable": "자동 응답에는 AI 어시스턴트 모듈이 필요하지만 이 배포에는 설치되어 있지 않습니다",
6
6
  "channel_discord.aiAutoReply.errors.load": "AI 자동 응답 설정을 불러오지 못했습니다",
7
7
  "channel_discord.aiAutoReply.errors.loadChannels": "Discord 채널을 불러오지 못했습니다",
8
+ "channel_discord.aiAutoReply.errors.missingChannelId": "선택된 채널이 없습니다.",
8
9
  "channel_discord.aiAutoReply.errors.save": "AI 자동 응답 설정을 저장하지 못했습니다",
9
10
  "channel_discord.aiAutoReply.fields.agent": "에이전트",
10
11
  "channel_discord.aiAutoReply.fields.agentHelp": "구조화된 출력을 생성하는 에이전트만 채널에서 답장할 수 있습니다. 다른 모듈의 에이전트를 선택하기 전에 테넌트의 채널 봇 사용자에게 해당 에이전트가 요구하는 권한을 부여하세요.",
@@ -5,6 +5,7 @@
5
5
  "channel_discord.aiAutoReply.errors.aiUnavailable": "Automatyczne odpowiedzi wymagają modułu asystenta AI, którego nie ma w tym wdrożeniu",
6
6
  "channel_discord.aiAutoReply.errors.load": "Nie udało się wczytać ustawień automatycznych odpowiedzi AI",
7
7
  "channel_discord.aiAutoReply.errors.loadChannels": "Nie udało się wczytać kanałów Discord",
8
+ "channel_discord.aiAutoReply.errors.missingChannelId": "Nie wybrano kanału.",
8
9
  "channel_discord.aiAutoReply.errors.save": "Nie udało się zapisać ustawień automatycznych odpowiedzi AI",
9
10
  "channel_discord.aiAutoReply.fields.agent": "Agent",
10
11
  "channel_discord.aiAutoReply.fields.agentHelp": "Na kanale mogą odpowiadać tylko agenci zwracający ustrukturyzowany wynik. Zanim wybierzesz agenta z innego modułu, nadaj użytkownikowi-botowi tenanta uprawnienia wymagane przez tego agenta.",