@open-mercato/core 0.6.6-develop.6184.1.b7e55f8d61 → 0.6.6-develop.6198.1.0822f97cc6
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/auth/frontend/login.js +5 -5
- package/dist/modules/auth/frontend/login.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js +3 -2
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js.map +2 -2
- package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js +26 -14
- package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/route.js +169 -149
- package/dist/modules/customers/api/deals/[id]/route.js.map +3 -3
- package/dist/modules/customers/components/detail/InlineEditors.js +9 -4
- package/dist/modules/customers/components/detail/InlineEditors.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +13 -9
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/detail-page-refresh.js +17 -0
- package/dist/modules/integrations/backend/integrations/detail-page-refresh.js.map +7 -0
- package/dist/modules/sales/components/documents/SalesDocumentForm.js +299 -297
- package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
- package/dist/modules/workflows/components/EdgeEditDialog.js +120 -141
- package/dist/modules/workflows/components/EdgeEditDialog.js.map +2 -2
- package/dist/modules/workflows/components/NodeEditDialog.js +114 -118
- package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
- package/dist/modules/workflows/frontend/checkout-demo/page.js +155 -260
- package/dist/modules/workflows/frontend/checkout-demo/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/frontend/login.tsx +5 -5
- package/src/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.tsx +4 -3
- package/src/modules/catalog/widgets/injection/product-seo/widget.client.tsx +27 -17
- package/src/modules/customers/api/deals/[id]/route.ts +208 -174
- package/src/modules/customers/components/detail/InlineEditors.tsx +6 -10
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +13 -9
- package/src/modules/integrations/backend/integrations/detail-page-refresh.ts +36 -0
- package/src/modules/sales/components/documents/SalesDocumentForm.tsx +351 -327
- package/src/modules/workflows/components/EdgeEditDialog.tsx +132 -157
- package/src/modules/workflows/components/NodeEditDialog.tsx +116 -121
- package/src/modules/workflows/frontend/checkout-demo/page.tsx +172 -239
- package/src/modules/workflows/i18n/de.json +2 -0
- package/src/modules/workflows/i18n/en.json +2 -0
- package/src/modules/workflows/i18n/es.json +2 -0
- package/src/modules/workflows/i18n/pl.json +2 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -312,22 +312,22 @@ function LoginPage() {
|
|
|
312
312
|
feature: translatedFeatures.join(", ")
|
|
313
313
|
}) }) }),
|
|
314
314
|
activeAuthenticatedUser && (translatedRoles.length || translatedFeatures.length) ? /* @__PURE__ */ jsx("div", { className: "flex justify-center", "data-testid": "login-return-dashboard", children: /* @__PURE__ */ jsx(Button, { asChild: true, type: "button", variant: "outline", size: "sm", children: /* @__PURE__ */ jsx(Link, { href: "/backend", children: translate("auth.accessDenied.dashboard", "Go to Dashboard") }) }) }) : null,
|
|
315
|
-
showTenantInvalid ? /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-
|
|
315
|
+
showTenantInvalid ? /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-center text-xs text-status-error-text", children: [
|
|
316
316
|
/* @__PURE__ */ jsx("div", { className: "font-medium", children: translate("auth.login.errors.tenantInvalid", "Tenant not found. Clear the tenant selection and try again.") }),
|
|
317
|
-
/* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "mt-2 border-
|
|
317
|
+
/* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "mt-2 border-status-error-border text-status-error-text hover:text-status-error-text", onClick: handleClearTenant, children: [
|
|
318
318
|
/* @__PURE__ */ jsx(X, { className: "mr-2 size-4", "aria-hidden": "true" }),
|
|
319
319
|
translate("auth.login.tenantClear", "Clear")
|
|
320
320
|
] })
|
|
321
|
-
] }) : tenantId ? /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-
|
|
321
|
+
] }) : tenantId ? /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-status-success-border bg-status-success-bg px-3 py-2 text-center text-xs text-status-success-text", children: [
|
|
322
322
|
/* @__PURE__ */ jsx("div", { className: "font-medium", children: tenantLoading ? translate("auth.login.tenantLoading", "Loading tenant details...") : translate("auth.login.tenantBanner", "You're logging in to {tenant}.", {
|
|
323
323
|
tenant: tenantName || tenantId
|
|
324
324
|
}) }),
|
|
325
|
-
/* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "mt-2 border-
|
|
325
|
+
/* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "mt-2 border-status-success-border text-status-success-text hover:text-status-success-text", onClick: handleClearTenant, children: [
|
|
326
326
|
/* @__PURE__ */ jsx(X, { className: "mr-2 size-4", "aria-hidden": "true" }),
|
|
327
327
|
translate("auth.login.tenantClear", "Clear")
|
|
328
328
|
] })
|
|
329
329
|
] }) : null,
|
|
330
|
-
error && !showTenantInvalid && /* @__PURE__ */ jsx("div", { className: "rounded-md border border-
|
|
330
|
+
error && !showTenantInvalid && /* @__PURE__ */ jsx("div", { className: "rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-center text-sm text-status-error-text", role: "alert", "aria-live": "polite", children: error }),
|
|
331
331
|
/* @__PURE__ */ jsxs("div", { className: "grid gap-1", children: [
|
|
332
332
|
/* @__PURE__ */ jsx(Label, { htmlFor: "email", children: t("auth.email") }),
|
|
333
333
|
/* @__PURE__ */ jsx(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/auth/frontend/login.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport Image from 'next/image'\nimport Link from 'next/link'\nimport { useRouter, useSearchParams } from 'next/navigation'\nimport { Card, CardContent, CardHeader, CardDescription } from '@open-mercato/ui/primitives/card'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { EmailInput } from '@open-mercato/ui/primitives/email-input'\nimport { PasswordInput } from '@open-mercato/ui/primitives/password-input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\nimport { clearAllOperations } from '@open-mercato/ui/backend/operations/store'\nimport { notifyAuthIdentityChange } from '@open-mercato/ui/backend/AuthSessionGuard'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { X } from 'lucide-react'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { useRegisteredComponent } from '@open-mercato/ui/backend/injection/useRegisteredComponent'\nimport type { AuthOverride, LoginFormWidgetContext } from './login-injection'\n\nconst loginTenantKey = 'om_login_tenant'\nconst loginTenantCookieMaxAge = 60 * 60 * 24 * 14\n\nfunction readTenantCookie() {\n if (typeof document === 'undefined') return null\n const entries = document.cookie.split(';')\n for (const entry of entries) {\n const [name, ...rest] = entry.trim().split('=')\n if (name === loginTenantKey) return decodeURIComponent(rest.join('='))\n }\n return null\n}\n\nfunction setTenantCookie(value: string) {\n if (typeof document === 'undefined') return\n document.cookie = `${loginTenantKey}=${encodeURIComponent(value)}; path=/; max-age=${loginTenantCookieMaxAge}; samesite=lax`\n}\n\nfunction clearTenantCookie() {\n if (typeof document === 'undefined') return\n document.cookie = `${loginTenantKey}=; path=/; max-age=0; samesite=lax`\n}\n\nfunction extractErrorMessage(payload: unknown): string | null {\n if (!payload) return null\n if (typeof payload === 'string') return payload\n if (Array.isArray(payload)) {\n for (const entry of payload) {\n const resolved = extractErrorMessage(entry)\n if (resolved) return resolved\n }\n return null\n }\n if (typeof payload === 'object') {\n const record = payload as Record<string, unknown>\n const candidates: unknown[] = [\n record.error,\n record.message,\n record.detail,\n record.details,\n record.description,\n ]\n for (const candidate of candidates) {\n const resolved = extractErrorMessage(candidate)\n if (resolved) return resolved\n }\n }\n return null\n}\n\nfunction looksLikeJsonString(value: string): boolean {\n const trimmed = value.trim()\n return trimmed.startsWith('{') || trimmed.startsWith('[')\n}\n\ntype LoginResponseEventDetail = Record<string, unknown> | null\n\ntype LoginFormSectionProps = {\n children: ReactNode\n}\n\nfunction LoginFormSectionDefault({ children }: LoginFormSectionProps) {\n return <>{children}</>\n}\n\nfunction emitLoginResponseEvent(detail: LoginResponseEventDetail) {\n if (typeof window === 'undefined') return\n window.dispatchEvent(new CustomEvent('om:auth:login-response', { detail }))\n}\n\nexport default function LoginPage() {\n const t = useT()\n const translate = useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>) =>\n translateWithFallback(t, key, fallback, params),\n [t],\n )\n const router = useRouter()\n const searchParams = useSearchParams()\n const requireRole = (searchParams.get('requireRole') || searchParams.get('role') || '').trim()\n const requireFeature = (searchParams.get('requireFeature') || '').trim()\n const requiredRoles = requireRole ? requireRole.split(',').map((value) => value.trim()).filter(Boolean) : []\n const requiredFeatures = requireFeature ? requireFeature.split(',').map((value) => value.trim()).filter(Boolean) : []\n const translatedRoles = requiredRoles.map((role) => translate(`auth.roles.${role}`, role))\n const translatedFeatures = requiredFeatures.map((feature) => translate(`features.${feature}`, feature))\n const [error, setError] = useState<string | null>(null)\n const [submitting, setSubmitting] = useState(false)\n const [authOverride, setAuthOverride] = useState<AuthOverride | null>(null)\n const [authOverridePending, setAuthOverridePending] = useState(false)\n const [clientReady, setClientReady] = useState(false)\n const [activeAuthenticatedUser, setActiveAuthenticatedUser] = useState(false)\n const [email, setEmail] = useState('')\n const [tenantId, setTenantId] = useState<string | null>(null)\n const [tenantName, setTenantName] = useState<string | null>(null)\n const [tenantLoading, setTenantLoading] = useState(false)\n const [tenantInvalid, setTenantInvalid] = useState<string | null>(null)\n const showTenantInvalid = tenantId != null && tenantInvalid === tenantId\n const LoginFormSection = useRegisteredComponent<LoginFormSectionProps>(\n 'section:auth.login.form',\n LoginFormSectionDefault,\n )\n\n useEffect(() => {\n setClientReady(true)\n }, [])\n\n useEffect(() => {\n let cancelled = false\n const hasAclChallenge = requiredFeatures.length > 0 || requiredRoles.length > 0\n void (async () => {\n try {\n const res = await apiCall<{ userId?: string }>('/api/auth/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: [] }),\n cache: 'no-store',\n })\n if (cancelled) return\n const activeUserId = typeof res.result?.userId === 'string' ? res.result.userId : ''\n if (!activeUserId) return\n setActiveAuthenticatedUser(true)\n // When a feature/role challenge is present in the URL, the user already\n // failed an ACL check while authenticated. Auto-redirecting back to\n // `redirect` would re-trigger the same 403 and re-bounce here,\n // producing an infinite loop (see GH #2070). Stay on the login page so\n // the access-denied banner is visible.\n if (hasAclChallenge) return\n const rawRedirect = searchParams.get('redirect') || ''\n let destination = '/backend'\n if (rawRedirect) {\n try {\n const resolved = new URL(rawRedirect, window.location.origin)\n if (\n resolved.origin === window.location.origin &&\n resolved.pathname.startsWith('/') &&\n !resolved.pathname.includes('//')\n ) {\n destination = resolved.pathname + resolved.search + resolved.hash\n }\n } catch {\n // fall back to /backend\n }\n }\n router.replace(destination)\n } catch {\n // ignore \u2014 leave login form usable on network failure\n }\n })()\n return () => { cancelled = true }\n }, [router, searchParams, requiredFeatures.length, requiredRoles.length])\n\n useEffect(() => {\n const tenantParam = (searchParams.get('tenant') || '').trim()\n if (tenantParam) {\n setTenantId(tenantParam)\n window.localStorage.setItem(loginTenantKey, tenantParam)\n setTenantCookie(tenantParam)\n return\n }\n const storedTenant = window.localStorage.getItem(loginTenantKey) || readTenantCookie()\n if (storedTenant) {\n setTenantId(storedTenant)\n }\n }, [searchParams])\n\n useEffect(() => {\n if (!tenantId) {\n setTenantName(null)\n setTenantInvalid(null)\n return\n }\n if (tenantInvalid === tenantId) {\n setTenantName(null)\n setTenantLoading(false)\n return\n }\n let active = true\n setTenantLoading(true)\n setTenantInvalid(null)\n apiCall<{ ok: boolean; tenant?: { id: string; name: string }; error?: string }>(\n `/api/directory/tenants/lookup?tenantId=${encodeURIComponent(tenantId)}`,\n )\n .then(({ result }) => {\n if (!active) return\n if (result?.ok && result.tenant) {\n setTenantName(result.tenant.name)\n return\n }\n setTenantName(null)\n setTenantInvalid(tenantId)\n setError(null)\n })\n .catch(() => {\n if (!active) return\n setTenantName(null)\n setTenantInvalid(tenantId)\n setError(null)\n })\n .finally(() => {\n if (active) setTenantLoading(false)\n })\n return () => {\n active = false\n }\n }, [tenantId, translate])\n\n function handleClearTenant() {\n window.localStorage.removeItem(loginTenantKey)\n clearTenantCookie()\n setTenantId(null)\n setTenantName(null)\n setTenantInvalid(null)\n const params = new URLSearchParams(searchParams)\n params.delete('tenant')\n setError(null)\n const query = params.toString()\n router.replace(query ? `/login?${query}` : '/login')\n }\n\n async function onSubmit(e: React.FormEvent<HTMLFormElement>) {\n e.preventDefault()\n if (!clientReady || authOverridePending) {\n return\n }\n setError(null)\n if (authOverride) {\n authOverride.onSubmit()\n return\n }\n setSubmitting(true)\n try {\n const form = new FormData(e.currentTarget)\n if (requiredRoles.length) form.set('requireRole', requiredRoles.join(','))\n const redirectParam = searchParams.get('redirect')\n if (redirectParam) form.set('redirect', redirectParam)\n const res = await fetch('/api/auth/login', { method: 'POST', body: form })\n if (res.redirected) {\n clearAllOperations()\n notifyAuthIdentityChange()\n // NextResponse.redirect from API\n router.replace(res.url)\n return\n }\n if (!res.ok) {\n const fallback = (() => {\n if (res.status === 403) {\n return translate(\n 'auth.login.errors.permissionDenied',\n 'You do not have permission to access this area. Please contact your administrator.',\n )\n }\n if (res.status === 401 || res.status === 400) {\n return translate('auth.login.errors.invalidCredentials', 'Invalid email or password')\n }\n return translate('auth.login.errors.generic', 'An error occurred. Please try again.')\n })()\n const cloned = res.clone()\n let errorMessage = ''\n const contentType = res.headers.get('content-type') || ''\n if (contentType.includes('application/json')) {\n try {\n const data = await res.json()\n errorMessage = extractErrorMessage(data) || ''\n } catch {\n try {\n const text = await cloned.text()\n const trimmed = text.trim()\n if (trimmed && !looksLikeJsonString(trimmed)) {\n errorMessage = trimmed\n }\n } catch {\n errorMessage = ''\n }\n }\n } else {\n try {\n const text = await res.text()\n const trimmed = text.trim()\n if (trimmed && !looksLikeJsonString(trimmed)) {\n errorMessage = trimmed\n }\n } catch {\n errorMessage = ''\n }\n }\n setError(errorMessage || fallback)\n return\n }\n // In case API returns 200 with JSON\n const data = await res.json().catch(() => null) as LoginResponseEventDetail\n emitLoginResponseEvent(data)\n clearAllOperations()\n notifyAuthIdentityChange()\n if (data && typeof data.redirect === 'string' && data.redirect.length > 0) {\n router.replace(data.redirect)\n }\n } catch (err: unknown) {\n // Handle any errors thrown (e.g., network errors or thrown exceptions)\n const message = err instanceof Error ? err.message : ''\n setError(message || translate('auth.login.errors.generic', 'An error occurred. Please try again.'))\n } finally {\n setSubmitting(false)\n }\n }\n\n const loginFormContext = useMemo<LoginFormWidgetContext>(() => ({\n email,\n tenantId,\n searchParams,\n setAuthOverride,\n setAuthOverridePending,\n setError,\n }), [email, tenantId, searchParams])\n\n const formReady = clientReady && !authOverridePending\n\n return (\n <div className=\"min-h-svh flex items-center justify-center p-4\">\n <Card className=\"w-full max-w-sm\">\n <CardHeader className=\"flex flex-col items-center gap-4 text-center p-10\">\n <Image alt={translate('auth.login.logoAlt', 'Open Mercato logo')} src=\"/open-mercato.svg\" width={150} height={150} priority />\n <h1 className=\"text-2xl font-semibold\">{translate('auth.login.brandName', 'Open Mercato')}</h1>\n <CardDescription>{translate('auth.login.subtitle', 'Access your workspace')}</CardDescription>\n </CardHeader>\n <CardContent>\n <LoginFormSection>\n <form className=\"grid gap-3\" onSubmit={onSubmit} noValidate data-auth-ready={formReady ? '1' : '0'}>\n {tenantId ? (\n <input type=\"hidden\" name=\"tenantId\" value={tenantId} />\n ) : null}\n {!!translatedRoles.length && (\n <Alert variant=\"info\" className=\"text-center\">\n <AlertDescription>\n {translate(\n translatedRoles.length > 1 ? 'auth.login.requireRolesMessage' : 'auth.login.requireRoleMessage',\n translatedRoles.length > 1\n ? 'Access requires one of the following roles: {roles}'\n : 'Access requires role: {roles}',\n { roles: translatedRoles.join(', ') },\n )}\n </AlertDescription>\n </Alert>\n )}\n {!!translatedFeatures.length && (\n <Alert variant=\"info\" className=\"text-center\">\n <AlertDescription>\n {translate('auth.login.featureDenied', \"You don't have access to this feature ({feature}). Please contact your administrator.\", {\n feature: translatedFeatures.join(', '),\n })}\n </AlertDescription>\n </Alert>\n )}\n {activeAuthenticatedUser && (translatedRoles.length || translatedFeatures.length) ? (\n <div className=\"flex justify-center\" data-testid=\"login-return-dashboard\">\n <Button asChild type=\"button\" variant=\"outline\" size=\"sm\">\n <Link href=\"/backend\">\n {translate('auth.accessDenied.dashboard', 'Go to Dashboard')}\n </Link>\n </Button>\n </div>\n ) : null}\n {showTenantInvalid ? (\n <div className=\"rounded-md border border-red-200 bg-red-50 px-3 py-2 text-center text-xs text-red-700\">\n <div className=\"font-medium\">{translate('auth.login.errors.tenantInvalid', 'Tenant not found. Clear the tenant selection and try again.')}</div>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"mt-2 border-red-300 text-red-700\" onClick={handleClearTenant}>\n <X className=\"mr-2 size-4\" aria-hidden=\"true\" />\n {translate('auth.login.tenantClear', 'Clear')}\n </Button>\n </div>\n ) : tenantId ? (\n <div className=\"rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2 text-center text-xs text-emerald-900\">\n <div className=\"font-medium\">\n {tenantLoading\n ? translate('auth.login.tenantLoading', 'Loading tenant details...')\n : translate('auth.login.tenantBanner', \"You're logging in to {tenant}.\", {\n tenant: tenantName || tenantId,\n })}\n </div>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"mt-2 border-emerald-300 text-emerald-900\" onClick={handleClearTenant}>\n <X className=\"mr-2 size-4\" aria-hidden=\"true\" />\n {translate('auth.login.tenantClear', 'Clear')}\n </Button>\n </div>\n ) : null}\n {error && !showTenantInvalid && (\n <div className=\"rounded-md border border-red-200 bg-red-50 px-3 py-2 text-center text-sm text-red-700\" role=\"alert\" aria-live=\"polite\">\n {error}\n </div>\n )}\n <div className=\"grid gap-1\">\n <Label htmlFor=\"email\">{t('auth.email')}</Label>\n <EmailInput\n id=\"email\"\n name=\"email\"\n required\n aria-invalid={!!error}\n onChange={(e) => setEmail(e.target.value)}\n onBlur={(e) => setEmail(e.target.value)}\n />\n </div>\n <InjectionSpot<LoginFormWidgetContext>\n spotId=\"auth.login:form\"\n context={loginFormContext}\n />\n {authOverride?.hidePassword ? null : (\n <div className=\"grid gap-1\">\n <Label htmlFor=\"password\">{t('auth.password')}</Label>\n <PasswordInput id=\"password\" name=\"password\" required={!authOverride} aria-invalid={!!error} autoComplete=\"current-password\" />\n </div>\n )}\n {!authOverride?.hideRememberMe && !authOverride?.hidePassword && (\n <label className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <input type=\"checkbox\" name=\"remember\" className=\"accent-foreground\" />\n <span>{translate('auth.login.rememberMe', 'Remember me')}</span>\n </label>\n )}\n <Button type=\"submit\" disabled={submitting || !formReady} className=\"h-10 mt-2\">\n {submitting\n ? translate('auth.login.loading', 'Loading...')\n : authOverride\n ? authOverride.providerLabel\n : translate('auth.signIn', 'Sign in')}\n </Button>\n {!authOverride?.hideForgotPassword && (\n <div className=\"text-xs text-muted-foreground mt-2\">\n <Link className=\"underline\" href=\"/reset\">\n {translate('auth.login.forgotPassword', 'Forgot password?')}\n </Link>\n </div>\n )}\n </form>\n </LoginFormSection>\n </CardContent>\n </Card>\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport Image from 'next/image'\nimport Link from 'next/link'\nimport { useRouter, useSearchParams } from 'next/navigation'\nimport { Card, CardContent, CardHeader, CardDescription } from '@open-mercato/ui/primitives/card'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { EmailInput } from '@open-mercato/ui/primitives/email-input'\nimport { PasswordInput } from '@open-mercato/ui/primitives/password-input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\nimport { clearAllOperations } from '@open-mercato/ui/backend/operations/store'\nimport { notifyAuthIdentityChange } from '@open-mercato/ui/backend/AuthSessionGuard'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { X } from 'lucide-react'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { useRegisteredComponent } from '@open-mercato/ui/backend/injection/useRegisteredComponent'\nimport type { AuthOverride, LoginFormWidgetContext } from './login-injection'\n\nconst loginTenantKey = 'om_login_tenant'\nconst loginTenantCookieMaxAge = 60 * 60 * 24 * 14\n\nfunction readTenantCookie() {\n if (typeof document === 'undefined') return null\n const entries = document.cookie.split(';')\n for (const entry of entries) {\n const [name, ...rest] = entry.trim().split('=')\n if (name === loginTenantKey) return decodeURIComponent(rest.join('='))\n }\n return null\n}\n\nfunction setTenantCookie(value: string) {\n if (typeof document === 'undefined') return\n document.cookie = `${loginTenantKey}=${encodeURIComponent(value)}; path=/; max-age=${loginTenantCookieMaxAge}; samesite=lax`\n}\n\nfunction clearTenantCookie() {\n if (typeof document === 'undefined') return\n document.cookie = `${loginTenantKey}=; path=/; max-age=0; samesite=lax`\n}\n\nfunction extractErrorMessage(payload: unknown): string | null {\n if (!payload) return null\n if (typeof payload === 'string') return payload\n if (Array.isArray(payload)) {\n for (const entry of payload) {\n const resolved = extractErrorMessage(entry)\n if (resolved) return resolved\n }\n return null\n }\n if (typeof payload === 'object') {\n const record = payload as Record<string, unknown>\n const candidates: unknown[] = [\n record.error,\n record.message,\n record.detail,\n record.details,\n record.description,\n ]\n for (const candidate of candidates) {\n const resolved = extractErrorMessage(candidate)\n if (resolved) return resolved\n }\n }\n return null\n}\n\nfunction looksLikeJsonString(value: string): boolean {\n const trimmed = value.trim()\n return trimmed.startsWith('{') || trimmed.startsWith('[')\n}\n\ntype LoginResponseEventDetail = Record<string, unknown> | null\n\ntype LoginFormSectionProps = {\n children: ReactNode\n}\n\nfunction LoginFormSectionDefault({ children }: LoginFormSectionProps) {\n return <>{children}</>\n}\n\nfunction emitLoginResponseEvent(detail: LoginResponseEventDetail) {\n if (typeof window === 'undefined') return\n window.dispatchEvent(new CustomEvent('om:auth:login-response', { detail }))\n}\n\nexport default function LoginPage() {\n const t = useT()\n const translate = useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>) =>\n translateWithFallback(t, key, fallback, params),\n [t],\n )\n const router = useRouter()\n const searchParams = useSearchParams()\n const requireRole = (searchParams.get('requireRole') || searchParams.get('role') || '').trim()\n const requireFeature = (searchParams.get('requireFeature') || '').trim()\n const requiredRoles = requireRole ? requireRole.split(',').map((value) => value.trim()).filter(Boolean) : []\n const requiredFeatures = requireFeature ? requireFeature.split(',').map((value) => value.trim()).filter(Boolean) : []\n const translatedRoles = requiredRoles.map((role) => translate(`auth.roles.${role}`, role))\n const translatedFeatures = requiredFeatures.map((feature) => translate(`features.${feature}`, feature))\n const [error, setError] = useState<string | null>(null)\n const [submitting, setSubmitting] = useState(false)\n const [authOverride, setAuthOverride] = useState<AuthOverride | null>(null)\n const [authOverridePending, setAuthOverridePending] = useState(false)\n const [clientReady, setClientReady] = useState(false)\n const [activeAuthenticatedUser, setActiveAuthenticatedUser] = useState(false)\n const [email, setEmail] = useState('')\n const [tenantId, setTenantId] = useState<string | null>(null)\n const [tenantName, setTenantName] = useState<string | null>(null)\n const [tenantLoading, setTenantLoading] = useState(false)\n const [tenantInvalid, setTenantInvalid] = useState<string | null>(null)\n const showTenantInvalid = tenantId != null && tenantInvalid === tenantId\n const LoginFormSection = useRegisteredComponent<LoginFormSectionProps>(\n 'section:auth.login.form',\n LoginFormSectionDefault,\n )\n\n useEffect(() => {\n setClientReady(true)\n }, [])\n\n useEffect(() => {\n let cancelled = false\n const hasAclChallenge = requiredFeatures.length > 0 || requiredRoles.length > 0\n void (async () => {\n try {\n const res = await apiCall<{ userId?: string }>('/api/auth/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: [] }),\n cache: 'no-store',\n })\n if (cancelled) return\n const activeUserId = typeof res.result?.userId === 'string' ? res.result.userId : ''\n if (!activeUserId) return\n setActiveAuthenticatedUser(true)\n // When a feature/role challenge is present in the URL, the user already\n // failed an ACL check while authenticated. Auto-redirecting back to\n // `redirect` would re-trigger the same 403 and re-bounce here,\n // producing an infinite loop (see GH #2070). Stay on the login page so\n // the access-denied banner is visible.\n if (hasAclChallenge) return\n const rawRedirect = searchParams.get('redirect') || ''\n let destination = '/backend'\n if (rawRedirect) {\n try {\n const resolved = new URL(rawRedirect, window.location.origin)\n if (\n resolved.origin === window.location.origin &&\n resolved.pathname.startsWith('/') &&\n !resolved.pathname.includes('//')\n ) {\n destination = resolved.pathname + resolved.search + resolved.hash\n }\n } catch {\n // fall back to /backend\n }\n }\n router.replace(destination)\n } catch {\n // ignore \u2014 leave login form usable on network failure\n }\n })()\n return () => { cancelled = true }\n }, [router, searchParams, requiredFeatures.length, requiredRoles.length])\n\n useEffect(() => {\n const tenantParam = (searchParams.get('tenant') || '').trim()\n if (tenantParam) {\n setTenantId(tenantParam)\n window.localStorage.setItem(loginTenantKey, tenantParam)\n setTenantCookie(tenantParam)\n return\n }\n const storedTenant = window.localStorage.getItem(loginTenantKey) || readTenantCookie()\n if (storedTenant) {\n setTenantId(storedTenant)\n }\n }, [searchParams])\n\n useEffect(() => {\n if (!tenantId) {\n setTenantName(null)\n setTenantInvalid(null)\n return\n }\n if (tenantInvalid === tenantId) {\n setTenantName(null)\n setTenantLoading(false)\n return\n }\n let active = true\n setTenantLoading(true)\n setTenantInvalid(null)\n apiCall<{ ok: boolean; tenant?: { id: string; name: string }; error?: string }>(\n `/api/directory/tenants/lookup?tenantId=${encodeURIComponent(tenantId)}`,\n )\n .then(({ result }) => {\n if (!active) return\n if (result?.ok && result.tenant) {\n setTenantName(result.tenant.name)\n return\n }\n setTenantName(null)\n setTenantInvalid(tenantId)\n setError(null)\n })\n .catch(() => {\n if (!active) return\n setTenantName(null)\n setTenantInvalid(tenantId)\n setError(null)\n })\n .finally(() => {\n if (active) setTenantLoading(false)\n })\n return () => {\n active = false\n }\n }, [tenantId, translate])\n\n function handleClearTenant() {\n window.localStorage.removeItem(loginTenantKey)\n clearTenantCookie()\n setTenantId(null)\n setTenantName(null)\n setTenantInvalid(null)\n const params = new URLSearchParams(searchParams)\n params.delete('tenant')\n setError(null)\n const query = params.toString()\n router.replace(query ? `/login?${query}` : '/login')\n }\n\n async function onSubmit(e: React.FormEvent<HTMLFormElement>) {\n e.preventDefault()\n if (!clientReady || authOverridePending) {\n return\n }\n setError(null)\n if (authOverride) {\n authOverride.onSubmit()\n return\n }\n setSubmitting(true)\n try {\n const form = new FormData(e.currentTarget)\n if (requiredRoles.length) form.set('requireRole', requiredRoles.join(','))\n const redirectParam = searchParams.get('redirect')\n if (redirectParam) form.set('redirect', redirectParam)\n const res = await fetch('/api/auth/login', { method: 'POST', body: form })\n if (res.redirected) {\n clearAllOperations()\n notifyAuthIdentityChange()\n // NextResponse.redirect from API\n router.replace(res.url)\n return\n }\n if (!res.ok) {\n const fallback = (() => {\n if (res.status === 403) {\n return translate(\n 'auth.login.errors.permissionDenied',\n 'You do not have permission to access this area. Please contact your administrator.',\n )\n }\n if (res.status === 401 || res.status === 400) {\n return translate('auth.login.errors.invalidCredentials', 'Invalid email or password')\n }\n return translate('auth.login.errors.generic', 'An error occurred. Please try again.')\n })()\n const cloned = res.clone()\n let errorMessage = ''\n const contentType = res.headers.get('content-type') || ''\n if (contentType.includes('application/json')) {\n try {\n const data = await res.json()\n errorMessage = extractErrorMessage(data) || ''\n } catch {\n try {\n const text = await cloned.text()\n const trimmed = text.trim()\n if (trimmed && !looksLikeJsonString(trimmed)) {\n errorMessage = trimmed\n }\n } catch {\n errorMessage = ''\n }\n }\n } else {\n try {\n const text = await res.text()\n const trimmed = text.trim()\n if (trimmed && !looksLikeJsonString(trimmed)) {\n errorMessage = trimmed\n }\n } catch {\n errorMessage = ''\n }\n }\n setError(errorMessage || fallback)\n return\n }\n // In case API returns 200 with JSON\n const data = await res.json().catch(() => null) as LoginResponseEventDetail\n emitLoginResponseEvent(data)\n clearAllOperations()\n notifyAuthIdentityChange()\n if (data && typeof data.redirect === 'string' && data.redirect.length > 0) {\n router.replace(data.redirect)\n }\n } catch (err: unknown) {\n // Handle any errors thrown (e.g., network errors or thrown exceptions)\n const message = err instanceof Error ? err.message : ''\n setError(message || translate('auth.login.errors.generic', 'An error occurred. Please try again.'))\n } finally {\n setSubmitting(false)\n }\n }\n\n const loginFormContext = useMemo<LoginFormWidgetContext>(() => ({\n email,\n tenantId,\n searchParams,\n setAuthOverride,\n setAuthOverridePending,\n setError,\n }), [email, tenantId, searchParams])\n\n const formReady = clientReady && !authOverridePending\n\n return (\n <div className=\"min-h-svh flex items-center justify-center p-4\">\n <Card className=\"w-full max-w-sm\">\n <CardHeader className=\"flex flex-col items-center gap-4 text-center p-10\">\n <Image alt={translate('auth.login.logoAlt', 'Open Mercato logo')} src=\"/open-mercato.svg\" width={150} height={150} priority />\n <h1 className=\"text-2xl font-semibold\">{translate('auth.login.brandName', 'Open Mercato')}</h1>\n <CardDescription>{translate('auth.login.subtitle', 'Access your workspace')}</CardDescription>\n </CardHeader>\n <CardContent>\n <LoginFormSection>\n <form className=\"grid gap-3\" onSubmit={onSubmit} noValidate data-auth-ready={formReady ? '1' : '0'}>\n {tenantId ? (\n <input type=\"hidden\" name=\"tenantId\" value={tenantId} />\n ) : null}\n {!!translatedRoles.length && (\n <Alert variant=\"info\" className=\"text-center\">\n <AlertDescription>\n {translate(\n translatedRoles.length > 1 ? 'auth.login.requireRolesMessage' : 'auth.login.requireRoleMessage',\n translatedRoles.length > 1\n ? 'Access requires one of the following roles: {roles}'\n : 'Access requires role: {roles}',\n { roles: translatedRoles.join(', ') },\n )}\n </AlertDescription>\n </Alert>\n )}\n {!!translatedFeatures.length && (\n <Alert variant=\"info\" className=\"text-center\">\n <AlertDescription>\n {translate('auth.login.featureDenied', \"You don't have access to this feature ({feature}). Please contact your administrator.\", {\n feature: translatedFeatures.join(', '),\n })}\n </AlertDescription>\n </Alert>\n )}\n {activeAuthenticatedUser && (translatedRoles.length || translatedFeatures.length) ? (\n <div className=\"flex justify-center\" data-testid=\"login-return-dashboard\">\n <Button asChild type=\"button\" variant=\"outline\" size=\"sm\">\n <Link href=\"/backend\">\n {translate('auth.accessDenied.dashboard', 'Go to Dashboard')}\n </Link>\n </Button>\n </div>\n ) : null}\n {showTenantInvalid ? (\n <div className=\"rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-center text-xs text-status-error-text\">\n <div className=\"font-medium\">{translate('auth.login.errors.tenantInvalid', 'Tenant not found. Clear the tenant selection and try again.')}</div>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"mt-2 border-status-error-border text-status-error-text hover:text-status-error-text\" onClick={handleClearTenant}>\n <X className=\"mr-2 size-4\" aria-hidden=\"true\" />\n {translate('auth.login.tenantClear', 'Clear')}\n </Button>\n </div>\n ) : tenantId ? (\n <div className=\"rounded-md border border-status-success-border bg-status-success-bg px-3 py-2 text-center text-xs text-status-success-text\">\n <div className=\"font-medium\">\n {tenantLoading\n ? translate('auth.login.tenantLoading', 'Loading tenant details...')\n : translate('auth.login.tenantBanner', \"You're logging in to {tenant}.\", {\n tenant: tenantName || tenantId,\n })}\n </div>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"mt-2 border-status-success-border text-status-success-text hover:text-status-success-text\" onClick={handleClearTenant}>\n <X className=\"mr-2 size-4\" aria-hidden=\"true\" />\n {translate('auth.login.tenantClear', 'Clear')}\n </Button>\n </div>\n ) : null}\n {error && !showTenantInvalid && (\n <div className=\"rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-center text-sm text-status-error-text\" role=\"alert\" aria-live=\"polite\">\n {error}\n </div>\n )}\n <div className=\"grid gap-1\">\n <Label htmlFor=\"email\">{t('auth.email')}</Label>\n <EmailInput\n id=\"email\"\n name=\"email\"\n required\n aria-invalid={!!error}\n onChange={(e) => setEmail(e.target.value)}\n onBlur={(e) => setEmail(e.target.value)}\n />\n </div>\n <InjectionSpot<LoginFormWidgetContext>\n spotId=\"auth.login:form\"\n context={loginFormContext}\n />\n {authOverride?.hidePassword ? null : (\n <div className=\"grid gap-1\">\n <Label htmlFor=\"password\">{t('auth.password')}</Label>\n <PasswordInput id=\"password\" name=\"password\" required={!authOverride} aria-invalid={!!error} autoComplete=\"current-password\" />\n </div>\n )}\n {!authOverride?.hideRememberMe && !authOverride?.hidePassword && (\n <label className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <input type=\"checkbox\" name=\"remember\" className=\"accent-foreground\" />\n <span>{translate('auth.login.rememberMe', 'Remember me')}</span>\n </label>\n )}\n <Button type=\"submit\" disabled={submitting || !formReady} className=\"h-10 mt-2\">\n {submitting\n ? translate('auth.login.loading', 'Loading...')\n : authOverride\n ? authOverride.providerLabel\n : translate('auth.signIn', 'Sign in')}\n </Button>\n {!authOverride?.hideForgotPassword && (\n <div className=\"text-xs text-muted-foreground mt-2\">\n <Link className=\"underline\" href=\"/reset\">\n {translate('auth.login.forgotPassword', 'Forgot password?')}\n </Link>\n </div>\n )}\n </form>\n </LoginFormSection>\n </CardContent>\n </Card>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["data"]
|
|
7
7
|
}
|
|
@@ -320,12 +320,13 @@ function MerchandisingAssistantSheet({
|
|
|
320
320
|
/* @__PURE__ */ jsxs(PopoverContent, { align: "end", className: "w-72 p-1", children: [
|
|
321
321
|
/* @__PURE__ */ jsx("div", { className: "px-3 pt-2 pb-1 text-xs font-medium uppercase tracking-wide text-muted-foreground", children: t("catalog.merchandising_assistant.popover.heading", "AI assistants") }),
|
|
322
322
|
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-0.5", children: agents.map((agent) => /* @__PURE__ */ jsxs(
|
|
323
|
-
|
|
323
|
+
Button,
|
|
324
324
|
{
|
|
325
325
|
type: "button",
|
|
326
|
+
variant: "ghost",
|
|
326
327
|
onClick: () => handleSelectAgent(agent.id),
|
|
327
328
|
"data-ai-merchandising-agent-option": agent.id,
|
|
328
|
-
className: "
|
|
329
|
+
className: "h-auto w-full items-start justify-start gap-2 whitespace-normal rounded-sm px-2 py-2 text-left text-sm font-normal",
|
|
329
330
|
children: [
|
|
330
331
|
/* @__PURE__ */ jsx("span", { className: "mt-0.5 inline-flex size-6 items-center justify-center rounded-full bg-secondary text-secondary-foreground", children: agent.icon }),
|
|
331
332
|
/* @__PURE__ */ jsxs("span", { className: "flex-1 min-w-0", children: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\n/**\n * MerchandisingAssistantSheet \u2014 Step 4.9 (Spec \u00A710 D18).\n *\n * Embeds `<AiChat agent=\"catalog.merchandising_assistant\" pageContext={...} />`\n * in a right-side sheet (built on the shared Dialog primitive because\n * `packages/ui` does not ship a dedicated Sheet/Drawer primitive in\n * Phase 2). The trigger is a button rendered in the products-list page\n * header.\n *\n * Phase 2 is strictly read-only: the sheet shows proposals (structured\n * output), but the mutation tools (`catalog.update_product`,\n * `catalog.bulk_update_products`, `catalog.apply_attribute_extraction`,\n * `catalog.update_product_media_descriptions`) are intentionally NOT in\n * the agent whitelist. Phase 5.14 introduces those via the pending-action\n * contract.\n *\n * pageContext follows spec \u00A710.1 exactly:\n *\n * {\n * view: 'catalog.products.list',\n * recordType: null,\n * recordId: string, // \"\" or comma-separated UUIDs\n * extra: {\n * filter: { categoryId, priceRange, tags, status },\n * totalMatching: number,\n * selectedCount: number,\n * }\n * }\n */\n\nimport * as React from 'react'\nimport { Boxes, ChevronDown, FileText, Package, PanelRightOpen, PenLine, Tags, TrendingUp } from 'lucide-react'\nimport { AiChat, type AiChatSuggestion, type AiChatContextItem } from '@open-mercato/ui/ai/AiChat'\nimport { AiIcon } from '@open-mercato/ui/ai/AiIcon'\nimport { useAiDock } from '@open-mercato/ui/ai/AiDock'\nimport { useAiChatSessions } from '@open-mercato/ui/ai/AiChatSessions'\nimport { ChatPaneTabs } from '@open-mercato/ui/ai/ChatPaneTabs'\n// Side-effect import: registers the `catalog.stats-card` UI part on the\n// global registry the first time this client bundle loads. Tools that\n// emit `{ uiPart: { componentId: 'catalog.stats-card' } }` envelopes\n// (catalog.show_stats today; user-defined tools tomorrow) automatically\n// resolve to the card without dispatcher changes.\nimport '../../../components/CatalogStatsCard'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { ButtonGroup } from '@open-mercato/ui/primitives/button-group'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nexport interface MerchandisingPageContextFilter {\n categoryId: string | null\n priceRange: { min?: number; max?: number } | null\n tags: string[]\n status: string | null\n}\n\nexport interface MerchandisingPageContext {\n view: 'catalog.products.list'\n entityType?: 'catalog.products.list'\n recordType: null\n recordId: string\n extra: {\n filter: MerchandisingPageContextFilter\n totalMatching: number\n selectedCount: number\n }\n}\n\nexport interface MerchandisingAssistantSheetProps {\n /** Selection-aware page context, built by the products list host. */\n pageContext: MerchandisingPageContext\n /** When false (feature-gated by the host), the sheet renders nothing. */\n enabled?: boolean\n className?: string\n}\n\nexport const MERCHANDISING_AGENT_ID = 'catalog.merchandising_assistant'\n\nfunction useMerchandisingSuggestions(\n hasSelection: boolean,\n selectedCount: number,\n): AiChatSuggestion[] {\n const t = useT()\n return React.useMemo(() => {\n if (hasSelection) {\n return [\n {\n label: t(\n 'catalog.merchandising_assistant.suggestions.draftDescriptions',\n 'Draft product descriptions for selected items',\n ),\n prompt: `Draft compelling product descriptions for my ${selectedCount} selected products`,\n icon: <PenLine className=\"size-4\" />,\n },\n {\n label: t(\n 'catalog.merchandising_assistant.suggestions.extractAttributes',\n 'Extract attributes from descriptions',\n ),\n prompt: `Extract structured attributes from the descriptions of my ${selectedCount} selected products`,\n icon: <Tags className=\"size-4\" />,\n },\n {\n label: t(\n 'catalog.merchandising_assistant.suggestions.titleVariants',\n 'Generate title variants for SEO',\n ),\n prompt: `Generate SEO-optimized title variants for my ${selectedCount} selected products`,\n icon: <FileText className=\"size-4\" />,\n },\n {\n label: t(\n 'catalog.merchandising_assistant.suggestions.priceAdjustments',\n 'Suggest price adjustments',\n ),\n prompt: `Analyze and suggest price adjustments for my ${selectedCount} selected products`,\n icon: <TrendingUp className=\"size-4\" />,\n },\n ]\n }\n return [\n {\n label: t(\n 'catalog.merchandising_assistant.suggestions.showStats',\n 'Show catalog overview',\n ),\n // Triggers the `catalog.show_stats` tool, which returns the inline\n // catalog-stats UI part (live counts of products, active products,\n // categories, tags). Demo entry-point for the dynamic UI-part path.\n prompt: 'Show me a quick catalog overview using the stats card.',\n icon: <Boxes className=\"size-4\" />,\n },\n {\n label: t(\n 'catalog.merchandising_assistant.suggestions.browseProducts',\n 'Show me an overview of my product catalog',\n ),\n prompt: 'Give me an overview of my product catalog \u2014 categories, total products, and pricing ranges',\n icon: <Package className=\"size-4\" />,\n },\n {\n label: t(\n 'catalog.merchandising_assistant.suggestions.findMissingDescriptions',\n 'Find products with missing descriptions',\n ),\n prompt: 'Find products that are missing descriptions or have very short descriptions',\n icon: <PenLine className=\"size-4\" />,\n },\n {\n label: t(\n 'catalog.merchandising_assistant.suggestions.analyzeAttributes',\n 'Analyze attribute coverage',\n ),\n prompt: 'Analyze which products have incomplete attribute data',\n icon: <Tags className=\"size-4\" />,\n },\n {\n label: t(\n 'catalog.merchandising_assistant.suggestions.pricingOverview',\n 'Show pricing distribution',\n ),\n prompt: 'Show me the pricing distribution across categories',\n icon: <TrendingUp className=\"size-4\" />,\n },\n ]\n }, [hasSelection, selectedCount, t])\n}\n\nfunction useContextItems(pageContext: MerchandisingPageContext): AiChatContextItem[] {\n const t = useT()\n return React.useMemo(() => {\n const items: AiChatContextItem[] = []\n const { selectedCount, totalMatching, filter } = pageContext.extra\n if (selectedCount > 0) {\n items.push({\n label: t(\n 'catalog.merchandising_assistant.context.selectedProducts',\n '{count} products selected',\n ).replace('{count}', String(selectedCount)),\n })\n } else if (totalMatching > 0) {\n items.push({\n label: t(\n 'catalog.merchandising_assistant.context.matchingProducts',\n '{count} products in view',\n ).replace('{count}', String(totalMatching)),\n })\n }\n if (filter.categoryId) {\n items.push({\n label: t('catalog.merchandising_assistant.context.filteredByCategory', 'Filtered by category'),\n detail: filter.categoryId,\n })\n }\n if (filter.status) {\n items.push({ label: filter.status })\n }\n if (filter.tags.length > 0) {\n items.push({\n label: t('catalog.merchandising_assistant.context.tags', '{count} tags').replace(\n '{count}',\n String(filter.tags.length),\n ),\n })\n }\n return items\n }, [pageContext, t])\n}\n\ninterface MerchandisingAgentDescriptor {\n id: string\n label: string\n description: string\n icon: React.ReactNode\n}\n\ninterface AgentsResponse {\n agents?: Array<{\n id?: string | null\n }>\n}\n\ninterface MerchandisingAgentsState {\n agents: MerchandisingAgentDescriptor[]\n loaded: boolean\n}\n\nfunction useMerchandisingAgents(): MerchandisingAgentsState {\n const t = useT()\n const [accessibleAgentIds, setAccessibleAgentIds] = React.useState<Set<string> | null>(null)\n const declaredAgents = React.useMemo(\n () => [\n {\n id: MERCHANDISING_AGENT_ID,\n label: t(\n 'catalog.merchandising_assistant.agents.merchandising.label',\n 'Merchandising Assistant',\n ),\n description: t(\n 'catalog.merchandising_assistant.agents.merchandising.description',\n 'Draft copy, normalize attributes, and propose price changes for the current selection.',\n ),\n icon: <AiIcon className=\"size-4\" />,\n },\n ],\n [t],\n )\n\n React.useEffect(() => {\n let cancelled = false\n apiCall<AgentsResponse>('/api/ai_assistant/ai/agents', {\n credentials: 'same-origin',\n headers: { 'x-om-forbidden-redirect': '0', 'x-om-unauthorized-redirect': '0' },\n })\n .then((call) => {\n if (cancelled) return\n if (!call.ok || !call.result || !Array.isArray(call.result.agents)) {\n setAccessibleAgentIds(new Set())\n return\n }\n setAccessibleAgentIds(\n new Set(\n call.result.agents\n .map((agent) => agent?.id)\n .filter((id): id is string => typeof id === 'string' && id.length > 0),\n ),\n )\n })\n .catch(() => {\n if (!cancelled) setAccessibleAgentIds(new Set())\n })\n return () => {\n cancelled = true\n }\n }, [])\n\n return React.useMemo(\n () => ({\n agents:\n accessibleAgentIds === null\n ? []\n : declaredAgents.filter((agent) => accessibleAgentIds.has(agent.id)),\n loaded: accessibleAgentIds !== null,\n }),\n [accessibleAgentIds, declaredAgents],\n )\n}\n\nexport function MerchandisingAssistantSheet({\n pageContext,\n enabled = true,\n className,\n}: MerchandisingAssistantSheetProps): React.ReactElement | null {\n const t = useT()\n const dock = useAiDock()\n const [open, setOpen] = React.useState(false)\n const [popoverOpen, setPopoverOpen] = React.useState(false)\n const [activeAgent, setActiveAgent] = React.useState<string>(MERCHANDISING_AGENT_ID)\n const [lastAgent, setLastAgent] = React.useState<string | null>(null)\n\n const selectedCount = pageContext.extra.selectedCount\n const hasSelection = selectedCount > 0\n const suggestions = useMerchandisingSuggestions(hasSelection, selectedCount)\n const contextItems = useContextItems(pageContext)\n const { agents, loaded: agentsLoaded } = useMerchandisingAgents()\n\n if (!enabled || !agentsLoaded || agents.length === 0) return null\n\n const openAgent = (agentId: string) => {\n setActiveAgent(agentId)\n setLastAgent(agentId)\n setPopoverOpen(false)\n if (dock.state.assistant?.agent === agentId) {\n dock.dock(dock.state.assistant)\n setOpen(false)\n return\n }\n setOpen(true)\n }\n\n const handleSelectAgent = (agentId: string) => {\n openAgent(agentId)\n }\n\n const handleMainTriggerClick = () => {\n if (agents.length === 1) {\n openAgent(agents[0].id)\n return\n }\n if (lastAgent && agents.some((a) => a.id === lastAgent)) {\n openAgent(lastAgent)\n return\n }\n setPopoverOpen(true)\n }\n\n const handleDock = () => {\n const agent = agents.find((a) => a.id === activeAgent) ?? agents[0]\n if (!agent) return\n dock.dock({\n agent: agent.id,\n label: agent.label,\n description: t('catalog.merchandising_assistant.dock.subtitle', 'Catalog'),\n pageContext: pageContext as unknown as Record<string, unknown>,\n placeholder: t(\n 'catalog.merchandising_assistant.sheet.composerPlaceholder',\n 'Ask for descriptions, attributes, titles, or price ideas...',\n ),\n suggestions,\n contextItems,\n welcomeTitle: t(\n 'catalog.merchandising_assistant.sheet.welcomeTitle',\n 'Merchandising Assistant',\n ),\n welcomeDescription: hasSelection\n ? t(\n 'catalog.merchandising_assistant.sheet.welcomeDescriptionSelection',\n 'Ready to work with your {count} selected products. Try one of these:',\n ).replace('{count}', String(selectedCount))\n : t(\n 'catalog.merchandising_assistant.sheet.welcomeDescriptionAll',\n 'Select products for targeted actions, or explore your catalog:',\n ),\n })\n setOpen(false)\n }\n\n const triggerLabel = t(\n 'catalog.merchandising_assistant.trigger.ariaLabel',\n 'Open AI merchandising assistant',\n )\n const labelText = t('catalog.merchandising_assistant.trigger.label', 'AI')\n const moreAgentsLabel = t(\n 'catalog.merchandising_assistant.trigger.moreAgentsAriaLabel',\n 'Choose an AI assistant',\n )\n\n return (\n <>\n <ButtonGroup className={className}>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={handleMainTriggerClick}\n data-ai-merchandising-trigger=\"\"\n aria-label={triggerLabel}\n title={triggerLabel}\n className=\"relative\"\n >\n <AiIcon className=\"size-4\" />\n <span>{labelText}</span>\n {hasSelection ? (\n <span\n className=\"absolute -top-1 -right-1 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-medium leading-none text-primary-foreground\"\n data-ai-merchandising-selected-count={selectedCount}\n >\n {selectedCount}\n </span>\n ) : null}\n </Button>\n {agents.length > 1 ? (\n <Popover open={popoverOpen} onOpenChange={setPopoverOpen}>\n <PopoverTrigger asChild>\n <IconButton\n type=\"button\"\n variant=\"outline\"\n aria-label={moreAgentsLabel}\n title={moreAgentsLabel}\n data-ai-merchandising-picker=\"\"\n >\n <ChevronDown className=\"size-4\" aria-hidden />\n </IconButton>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"w-72 p-1\">\n <div className=\"px-3 pt-2 pb-1 text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n {t('catalog.merchandising_assistant.popover.heading', 'AI assistants')}\n </div>\n <div className=\"flex flex-col gap-0.5\">\n {agents.map((agent) => (\n <button\n key={agent.id}\n type=\"button\"\n onClick={() => handleSelectAgent(agent.id)}\n data-ai-merchandising-agent-option={agent.id}\n className=\"flex items-start gap-2 rounded-sm px-2 py-2 text-left text-sm hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground focus-visible:outline-none\"\n >\n <span className=\"mt-0.5 inline-flex size-6 items-center justify-center rounded-full bg-secondary text-secondary-foreground\">\n {agent.icon}\n </span>\n <span className=\"flex-1 min-w-0\">\n <span className=\"block font-medium leading-tight\">{agent.label}</span>\n <span className=\"block text-xs text-muted-foreground leading-snug\">\n {agent.description}\n </span>\n </span>\n </button>\n ))}\n </div>\n </PopoverContent>\n </Popover>\n ) : null}\n </ButtonGroup>\n <Dialog open={open} onOpenChange={setOpen}>\n <DialogContent\n className={cn(\n // Mobile: full-screen sheet. Desktop (\u2265sm): right-anchored side sheet.\n // The Dialog primitive applies a centering transform at the\n // sm breakpoint; each piece (`top`, `left`, transform, inset)\n // must be overridden at the same breakpoint or the panel\n // renders half off the viewport.\n 'top-0 left-0 right-0 bottom-0 translate-x-0 translate-y-0 max-w-none w-screen h-svh max-h-svh rounded-none',\n 'sm:top-0 sm:bottom-0 sm:right-0 sm:left-auto sm:translate-x-0 sm:translate-y-0',\n 'sm:max-w-xl sm:w-[36rem] sm:rounded-l-2xl sm:h-screen sm:max-h-screen',\n 'flex flex-col gap-3 p-4 z-[70]',\n )}\n data-ai-merchandising-sheet=\"\"\n >\n <DialogHeader>\n <div className=\"flex items-center gap-3 pr-8\">\n {/* Dock button lives on the LEFT \u2014 the Dialog primitive\n auto-renders an X close button absolutely positioned in\n the top-right corner, so anything we drop in the header's\n right side visually collides with it. Mobile hides the\n dock entirely (the side panel is desktop-only). */}\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('catalog.merchandising_assistant.sheet.dock', 'Dock to side')}\n title={t('catalog.merchandising_assistant.sheet.dock', 'Dock to side')}\n onClick={handleDock}\n data-ai-merchandising-dock=\"\"\n className=\"hidden lg:inline-flex shrink-0\"\n >\n <PanelRightOpen className=\"size-4\" aria-hidden />\n </IconButton>\n <DialogTitle className=\"flex-1 min-w-0 truncate\">\n {t('catalog.merchandising_assistant.sheet.title', 'Catalog merchandising assistant')}\n </DialogTitle>\n {hasSelection ? (\n <span\n className=\"shrink-0 inline-flex items-center rounded-full border border-border bg-secondary px-2 py-0.5 text-xs text-secondary-foreground\"\n data-ai-merchandising-selection-pill=\"\"\n data-ai-merchandising-selected-count={selectedCount}\n >\n {t(\n 'catalog.merchandising_assistant.sheet.selectionPill',\n 'Acting on {count} products',\n ).replace('{count}', String(selectedCount))}\n </span>\n ) : null}\n </div>\n <DialogDescription>\n {hasSelection\n ? t(\n 'catalog.merchandising_assistant.sheet.descriptionWithSelection',\n 'Working with {count} selected products. Ask for descriptions, attribute extraction, title suggestions, or pricing analysis.',\n ).replace('{count}', String(selectedCount))\n : t(\n 'catalog.merchandising_assistant.sheet.description',\n 'Your AI merchandising copilot. Select products from the list for targeted actions, or explore your full catalog.',\n )}\n </DialogDescription>\n </DialogHeader>\n <MerchandisingChatBody\n activeAgent={activeAgent}\n pageContext={pageContext}\n suggestions={suggestions}\n contextItems={contextItems}\n hasSelection={hasSelection}\n selectedCount={selectedCount}\n />\n </DialogContent>\n </Dialog>\n </>\n )\n}\n\ninterface MerchandisingChatBodyProps {\n activeAgent: string\n pageContext: MerchandisingPageContext\n suggestions: AiChatSuggestion[]\n contextItems: AiChatContextItem[]\n hasSelection: boolean\n selectedCount: number\n}\n\nfunction MerchandisingChatBody({\n activeAgent,\n pageContext,\n suggestions,\n contextItems,\n hasSelection,\n selectedCount,\n}: MerchandisingChatBodyProps) {\n const t = useT()\n const sessions = useAiChatSessions()\n const session = sessions.getActiveSession(activeAgent)\n\n React.useEffect(() => {\n if (!session) sessions.ensureSession(activeAgent)\n }, [activeAgent, session, sessions])\n\n return (\n <>\n <ChatPaneTabs agentId={activeAgent} className=\"border-b\" />\n <div className=\"min-h-0 flex-1\" data-ai-merchandising-chat-container=\"\">\n {session ? (\n <AiChat\n key={session.id}\n agent={activeAgent}\n conversationId={session.conversationId}\n pageContext={pageContext as unknown as Record<string, unknown>}\n className=\"h-full\"\n placeholder={t(\n 'catalog.merchandising_assistant.sheet.composerPlaceholder',\n 'Ask for descriptions, attributes, titles, or price ideas...',\n )}\n suggestions={suggestions}\n contextItems={contextItems}\n welcomeTitle={t(\n 'catalog.merchandising_assistant.sheet.welcomeTitle',\n 'Merchandising Assistant',\n )}\n welcomeDescription={\n hasSelection\n ? t(\n 'catalog.merchandising_assistant.sheet.welcomeDescriptionSelection',\n 'Ready to work with your {count} selected products. Try one of these:',\n ).replace('{count}', String(selectedCount))\n : t(\n 'catalog.merchandising_assistant.sheet.welcomeDescriptionAll',\n 'Select products for targeted actions, or explore your catalog:',\n )\n }\n />\n ) : null}\n </div>\n </>\n )\n}\n\nexport default MerchandisingAssistantSheet\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\n/**\n * MerchandisingAssistantSheet \u2014 Step 4.9 (Spec \u00A710 D18).\n *\n * Embeds `<AiChat agent=\"catalog.merchandising_assistant\" pageContext={...} />`\n * in a right-side sheet (built on the shared Dialog primitive because\n * `packages/ui` does not ship a dedicated Sheet/Drawer primitive in\n * Phase 2). The trigger is a button rendered in the products-list page\n * header.\n *\n * Phase 2 is strictly read-only: the sheet shows proposals (structured\n * output), but the mutation tools (`catalog.update_product`,\n * `catalog.bulk_update_products`, `catalog.apply_attribute_extraction`,\n * `catalog.update_product_media_descriptions`) are intentionally NOT in\n * the agent whitelist. Phase 5.14 introduces those via the pending-action\n * contract.\n *\n * pageContext follows spec \u00A710.1 exactly:\n *\n * {\n * view: 'catalog.products.list',\n * recordType: null,\n * recordId: string, // \"\" or comma-separated UUIDs\n * extra: {\n * filter: { categoryId, priceRange, tags, status },\n * totalMatching: number,\n * selectedCount: number,\n * }\n * }\n */\n\nimport * as React from 'react'\nimport { Boxes, ChevronDown, FileText, Package, PanelRightOpen, PenLine, Tags, TrendingUp } from 'lucide-react'\nimport { AiChat, type AiChatSuggestion, type AiChatContextItem } from '@open-mercato/ui/ai/AiChat'\nimport { AiIcon } from '@open-mercato/ui/ai/AiIcon'\nimport { useAiDock } from '@open-mercato/ui/ai/AiDock'\nimport { useAiChatSessions } from '@open-mercato/ui/ai/AiChatSessions'\nimport { ChatPaneTabs } from '@open-mercato/ui/ai/ChatPaneTabs'\n// Side-effect import: registers the `catalog.stats-card` UI part on the\n// global registry the first time this client bundle loads. Tools that\n// emit `{ uiPart: { componentId: 'catalog.stats-card' } }` envelopes\n// (catalog.show_stats today; user-defined tools tomorrow) automatically\n// resolve to the card without dispatcher changes.\nimport '../../../components/CatalogStatsCard'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { ButtonGroup } from '@open-mercato/ui/primitives/button-group'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nexport interface MerchandisingPageContextFilter {\n categoryId: string | null\n priceRange: { min?: number; max?: number } | null\n tags: string[]\n status: string | null\n}\n\nexport interface MerchandisingPageContext {\n view: 'catalog.products.list'\n entityType?: 'catalog.products.list'\n recordType: null\n recordId: string\n extra: {\n filter: MerchandisingPageContextFilter\n totalMatching: number\n selectedCount: number\n }\n}\n\nexport interface MerchandisingAssistantSheetProps {\n /** Selection-aware page context, built by the products list host. */\n pageContext: MerchandisingPageContext\n /** When false (feature-gated by the host), the sheet renders nothing. */\n enabled?: boolean\n className?: string\n}\n\nexport const MERCHANDISING_AGENT_ID = 'catalog.merchandising_assistant'\n\nfunction useMerchandisingSuggestions(\n hasSelection: boolean,\n selectedCount: number,\n): AiChatSuggestion[] {\n const t = useT()\n return React.useMemo(() => {\n if (hasSelection) {\n return [\n {\n label: t(\n 'catalog.merchandising_assistant.suggestions.draftDescriptions',\n 'Draft product descriptions for selected items',\n ),\n prompt: `Draft compelling product descriptions for my ${selectedCount} selected products`,\n icon: <PenLine className=\"size-4\" />,\n },\n {\n label: t(\n 'catalog.merchandising_assistant.suggestions.extractAttributes',\n 'Extract attributes from descriptions',\n ),\n prompt: `Extract structured attributes from the descriptions of my ${selectedCount} selected products`,\n icon: <Tags className=\"size-4\" />,\n },\n {\n label: t(\n 'catalog.merchandising_assistant.suggestions.titleVariants',\n 'Generate title variants for SEO',\n ),\n prompt: `Generate SEO-optimized title variants for my ${selectedCount} selected products`,\n icon: <FileText className=\"size-4\" />,\n },\n {\n label: t(\n 'catalog.merchandising_assistant.suggestions.priceAdjustments',\n 'Suggest price adjustments',\n ),\n prompt: `Analyze and suggest price adjustments for my ${selectedCount} selected products`,\n icon: <TrendingUp className=\"size-4\" />,\n },\n ]\n }\n return [\n {\n label: t(\n 'catalog.merchandising_assistant.suggestions.showStats',\n 'Show catalog overview',\n ),\n // Triggers the `catalog.show_stats` tool, which returns the inline\n // catalog-stats UI part (live counts of products, active products,\n // categories, tags). Demo entry-point for the dynamic UI-part path.\n prompt: 'Show me a quick catalog overview using the stats card.',\n icon: <Boxes className=\"size-4\" />,\n },\n {\n label: t(\n 'catalog.merchandising_assistant.suggestions.browseProducts',\n 'Show me an overview of my product catalog',\n ),\n prompt: 'Give me an overview of my product catalog \u2014 categories, total products, and pricing ranges',\n icon: <Package className=\"size-4\" />,\n },\n {\n label: t(\n 'catalog.merchandising_assistant.suggestions.findMissingDescriptions',\n 'Find products with missing descriptions',\n ),\n prompt: 'Find products that are missing descriptions or have very short descriptions',\n icon: <PenLine className=\"size-4\" />,\n },\n {\n label: t(\n 'catalog.merchandising_assistant.suggestions.analyzeAttributes',\n 'Analyze attribute coverage',\n ),\n prompt: 'Analyze which products have incomplete attribute data',\n icon: <Tags className=\"size-4\" />,\n },\n {\n label: t(\n 'catalog.merchandising_assistant.suggestions.pricingOverview',\n 'Show pricing distribution',\n ),\n prompt: 'Show me the pricing distribution across categories',\n icon: <TrendingUp className=\"size-4\" />,\n },\n ]\n }, [hasSelection, selectedCount, t])\n}\n\nfunction useContextItems(pageContext: MerchandisingPageContext): AiChatContextItem[] {\n const t = useT()\n return React.useMemo(() => {\n const items: AiChatContextItem[] = []\n const { selectedCount, totalMatching, filter } = pageContext.extra\n if (selectedCount > 0) {\n items.push({\n label: t(\n 'catalog.merchandising_assistant.context.selectedProducts',\n '{count} products selected',\n ).replace('{count}', String(selectedCount)),\n })\n } else if (totalMatching > 0) {\n items.push({\n label: t(\n 'catalog.merchandising_assistant.context.matchingProducts',\n '{count} products in view',\n ).replace('{count}', String(totalMatching)),\n })\n }\n if (filter.categoryId) {\n items.push({\n label: t('catalog.merchandising_assistant.context.filteredByCategory', 'Filtered by category'),\n detail: filter.categoryId,\n })\n }\n if (filter.status) {\n items.push({ label: filter.status })\n }\n if (filter.tags.length > 0) {\n items.push({\n label: t('catalog.merchandising_assistant.context.tags', '{count} tags').replace(\n '{count}',\n String(filter.tags.length),\n ),\n })\n }\n return items\n }, [pageContext, t])\n}\n\ninterface MerchandisingAgentDescriptor {\n id: string\n label: string\n description: string\n icon: React.ReactNode\n}\n\ninterface AgentsResponse {\n agents?: Array<{\n id?: string | null\n }>\n}\n\ninterface MerchandisingAgentsState {\n agents: MerchandisingAgentDescriptor[]\n loaded: boolean\n}\n\nfunction useMerchandisingAgents(): MerchandisingAgentsState {\n const t = useT()\n const [accessibleAgentIds, setAccessibleAgentIds] = React.useState<Set<string> | null>(null)\n const declaredAgents = React.useMemo(\n () => [\n {\n id: MERCHANDISING_AGENT_ID,\n label: t(\n 'catalog.merchandising_assistant.agents.merchandising.label',\n 'Merchandising Assistant',\n ),\n description: t(\n 'catalog.merchandising_assistant.agents.merchandising.description',\n 'Draft copy, normalize attributes, and propose price changes for the current selection.',\n ),\n icon: <AiIcon className=\"size-4\" />,\n },\n ],\n [t],\n )\n\n React.useEffect(() => {\n let cancelled = false\n apiCall<AgentsResponse>('/api/ai_assistant/ai/agents', {\n credentials: 'same-origin',\n headers: { 'x-om-forbidden-redirect': '0', 'x-om-unauthorized-redirect': '0' },\n })\n .then((call) => {\n if (cancelled) return\n if (!call.ok || !call.result || !Array.isArray(call.result.agents)) {\n setAccessibleAgentIds(new Set())\n return\n }\n setAccessibleAgentIds(\n new Set(\n call.result.agents\n .map((agent) => agent?.id)\n .filter((id): id is string => typeof id === 'string' && id.length > 0),\n ),\n )\n })\n .catch(() => {\n if (!cancelled) setAccessibleAgentIds(new Set())\n })\n return () => {\n cancelled = true\n }\n }, [])\n\n return React.useMemo(\n () => ({\n agents:\n accessibleAgentIds === null\n ? []\n : declaredAgents.filter((agent) => accessibleAgentIds.has(agent.id)),\n loaded: accessibleAgentIds !== null,\n }),\n [accessibleAgentIds, declaredAgents],\n )\n}\n\nexport function MerchandisingAssistantSheet({\n pageContext,\n enabled = true,\n className,\n}: MerchandisingAssistantSheetProps): React.ReactElement | null {\n const t = useT()\n const dock = useAiDock()\n const [open, setOpen] = React.useState(false)\n const [popoverOpen, setPopoverOpen] = React.useState(false)\n const [activeAgent, setActiveAgent] = React.useState<string>(MERCHANDISING_AGENT_ID)\n const [lastAgent, setLastAgent] = React.useState<string | null>(null)\n\n const selectedCount = pageContext.extra.selectedCount\n const hasSelection = selectedCount > 0\n const suggestions = useMerchandisingSuggestions(hasSelection, selectedCount)\n const contextItems = useContextItems(pageContext)\n const { agents, loaded: agentsLoaded } = useMerchandisingAgents()\n\n if (!enabled || !agentsLoaded || agents.length === 0) return null\n\n const openAgent = (agentId: string) => {\n setActiveAgent(agentId)\n setLastAgent(agentId)\n setPopoverOpen(false)\n if (dock.state.assistant?.agent === agentId) {\n dock.dock(dock.state.assistant)\n setOpen(false)\n return\n }\n setOpen(true)\n }\n\n const handleSelectAgent = (agentId: string) => {\n openAgent(agentId)\n }\n\n const handleMainTriggerClick = () => {\n if (agents.length === 1) {\n openAgent(agents[0].id)\n return\n }\n if (lastAgent && agents.some((a) => a.id === lastAgent)) {\n openAgent(lastAgent)\n return\n }\n setPopoverOpen(true)\n }\n\n const handleDock = () => {\n const agent = agents.find((a) => a.id === activeAgent) ?? agents[0]\n if (!agent) return\n dock.dock({\n agent: agent.id,\n label: agent.label,\n description: t('catalog.merchandising_assistant.dock.subtitle', 'Catalog'),\n pageContext: pageContext as unknown as Record<string, unknown>,\n placeholder: t(\n 'catalog.merchandising_assistant.sheet.composerPlaceholder',\n 'Ask for descriptions, attributes, titles, or price ideas...',\n ),\n suggestions,\n contextItems,\n welcomeTitle: t(\n 'catalog.merchandising_assistant.sheet.welcomeTitle',\n 'Merchandising Assistant',\n ),\n welcomeDescription: hasSelection\n ? t(\n 'catalog.merchandising_assistant.sheet.welcomeDescriptionSelection',\n 'Ready to work with your {count} selected products. Try one of these:',\n ).replace('{count}', String(selectedCount))\n : t(\n 'catalog.merchandising_assistant.sheet.welcomeDescriptionAll',\n 'Select products for targeted actions, or explore your catalog:',\n ),\n })\n setOpen(false)\n }\n\n const triggerLabel = t(\n 'catalog.merchandising_assistant.trigger.ariaLabel',\n 'Open AI merchandising assistant',\n )\n const labelText = t('catalog.merchandising_assistant.trigger.label', 'AI')\n const moreAgentsLabel = t(\n 'catalog.merchandising_assistant.trigger.moreAgentsAriaLabel',\n 'Choose an AI assistant',\n )\n\n return (\n <>\n <ButtonGroup className={className}>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={handleMainTriggerClick}\n data-ai-merchandising-trigger=\"\"\n aria-label={triggerLabel}\n title={triggerLabel}\n className=\"relative\"\n >\n <AiIcon className=\"size-4\" />\n <span>{labelText}</span>\n {hasSelection ? (\n <span\n className=\"absolute -top-1 -right-1 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-medium leading-none text-primary-foreground\"\n data-ai-merchandising-selected-count={selectedCount}\n >\n {selectedCount}\n </span>\n ) : null}\n </Button>\n {agents.length > 1 ? (\n <Popover open={popoverOpen} onOpenChange={setPopoverOpen}>\n <PopoverTrigger asChild>\n <IconButton\n type=\"button\"\n variant=\"outline\"\n aria-label={moreAgentsLabel}\n title={moreAgentsLabel}\n data-ai-merchandising-picker=\"\"\n >\n <ChevronDown className=\"size-4\" aria-hidden />\n </IconButton>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"w-72 p-1\">\n <div className=\"px-3 pt-2 pb-1 text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n {t('catalog.merchandising_assistant.popover.heading', 'AI assistants')}\n </div>\n <div className=\"flex flex-col gap-0.5\">\n {agents.map((agent) => (\n <Button\n key={agent.id}\n type=\"button\"\n variant=\"ghost\"\n onClick={() => handleSelectAgent(agent.id)}\n data-ai-merchandising-agent-option={agent.id}\n className=\"h-auto w-full items-start justify-start gap-2 whitespace-normal rounded-sm px-2 py-2 text-left text-sm font-normal\"\n >\n <span className=\"mt-0.5 inline-flex size-6 items-center justify-center rounded-full bg-secondary text-secondary-foreground\">\n {agent.icon}\n </span>\n <span className=\"flex-1 min-w-0\">\n <span className=\"block font-medium leading-tight\">{agent.label}</span>\n <span className=\"block text-xs text-muted-foreground leading-snug\">\n {agent.description}\n </span>\n </span>\n </Button>\n ))}\n </div>\n </PopoverContent>\n </Popover>\n ) : null}\n </ButtonGroup>\n <Dialog open={open} onOpenChange={setOpen}>\n <DialogContent\n className={cn(\n // Mobile: full-screen sheet. Desktop (\u2265sm): right-anchored side sheet.\n // The Dialog primitive applies a centering transform at the\n // sm breakpoint; each piece (`top`, `left`, transform, inset)\n // must be overridden at the same breakpoint or the panel\n // renders half off the viewport.\n 'top-0 left-0 right-0 bottom-0 translate-x-0 translate-y-0 max-w-none w-screen h-svh max-h-svh rounded-none',\n 'sm:top-0 sm:bottom-0 sm:right-0 sm:left-auto sm:translate-x-0 sm:translate-y-0',\n 'sm:max-w-xl sm:w-[36rem] sm:rounded-l-2xl sm:h-screen sm:max-h-screen',\n 'flex flex-col gap-3 p-4 z-[70]',\n )}\n data-ai-merchandising-sheet=\"\"\n >\n <DialogHeader>\n <div className=\"flex items-center gap-3 pr-8\">\n {/* Dock button lives on the LEFT \u2014 the Dialog primitive\n auto-renders an X close button absolutely positioned in\n the top-right corner, so anything we drop in the header's\n right side visually collides with it. Mobile hides the\n dock entirely (the side panel is desktop-only). */}\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('catalog.merchandising_assistant.sheet.dock', 'Dock to side')}\n title={t('catalog.merchandising_assistant.sheet.dock', 'Dock to side')}\n onClick={handleDock}\n data-ai-merchandising-dock=\"\"\n className=\"hidden lg:inline-flex shrink-0\"\n >\n <PanelRightOpen className=\"size-4\" aria-hidden />\n </IconButton>\n <DialogTitle className=\"flex-1 min-w-0 truncate\">\n {t('catalog.merchandising_assistant.sheet.title', 'Catalog merchandising assistant')}\n </DialogTitle>\n {hasSelection ? (\n <span\n className=\"shrink-0 inline-flex items-center rounded-full border border-border bg-secondary px-2 py-0.5 text-xs text-secondary-foreground\"\n data-ai-merchandising-selection-pill=\"\"\n data-ai-merchandising-selected-count={selectedCount}\n >\n {t(\n 'catalog.merchandising_assistant.sheet.selectionPill',\n 'Acting on {count} products',\n ).replace('{count}', String(selectedCount))}\n </span>\n ) : null}\n </div>\n <DialogDescription>\n {hasSelection\n ? t(\n 'catalog.merchandising_assistant.sheet.descriptionWithSelection',\n 'Working with {count} selected products. Ask for descriptions, attribute extraction, title suggestions, or pricing analysis.',\n ).replace('{count}', String(selectedCount))\n : t(\n 'catalog.merchandising_assistant.sheet.description',\n 'Your AI merchandising copilot. Select products from the list for targeted actions, or explore your full catalog.',\n )}\n </DialogDescription>\n </DialogHeader>\n <MerchandisingChatBody\n activeAgent={activeAgent}\n pageContext={pageContext}\n suggestions={suggestions}\n contextItems={contextItems}\n hasSelection={hasSelection}\n selectedCount={selectedCount}\n />\n </DialogContent>\n </Dialog>\n </>\n )\n}\n\ninterface MerchandisingChatBodyProps {\n activeAgent: string\n pageContext: MerchandisingPageContext\n suggestions: AiChatSuggestion[]\n contextItems: AiChatContextItem[]\n hasSelection: boolean\n selectedCount: number\n}\n\nfunction MerchandisingChatBody({\n activeAgent,\n pageContext,\n suggestions,\n contextItems,\n hasSelection,\n selectedCount,\n}: MerchandisingChatBodyProps) {\n const t = useT()\n const sessions = useAiChatSessions()\n const session = sessions.getActiveSession(activeAgent)\n\n React.useEffect(() => {\n if (!session) sessions.ensureSession(activeAgent)\n }, [activeAgent, session, sessions])\n\n return (\n <>\n <ChatPaneTabs agentId={activeAgent} className=\"border-b\" />\n <div className=\"min-h-0 flex-1\" data-ai-merchandising-chat-container=\"\">\n {session ? (\n <AiChat\n key={session.id}\n agent={activeAgent}\n conversationId={session.conversationId}\n pageContext={pageContext as unknown as Record<string, unknown>}\n className=\"h-full\"\n placeholder={t(\n 'catalog.merchandising_assistant.sheet.composerPlaceholder',\n 'Ask for descriptions, attributes, titles, or price ideas...',\n )}\n suggestions={suggestions}\n contextItems={contextItems}\n welcomeTitle={t(\n 'catalog.merchandising_assistant.sheet.welcomeTitle',\n 'Merchandising Assistant',\n )}\n welcomeDescription={\n hasSelection\n ? t(\n 'catalog.merchandising_assistant.sheet.welcomeDescriptionSelection',\n 'Ready to work with your {count} selected products. Try one of these:',\n ).replace('{count}', String(selectedCount))\n : t(\n 'catalog.merchandising_assistant.sheet.welcomeDescriptionAll',\n 'Select products for targeted actions, or explore your catalog:',\n )\n }\n />\n ) : null}\n </div>\n </>\n )\n}\n\nexport default MerchandisingAssistantSheet\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -3,6 +3,8 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { subscribeProductSeoValidation } from "./state.js";
|
|
5
5
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
6
|
+
import { StatusBadge } from "@open-mercato/ui/primitives/status-badge";
|
|
7
|
+
import { Alert } from "@open-mercato/ui/primitives/alert";
|
|
6
8
|
function computeIssueKeys(title, description) {
|
|
7
9
|
const issues = [];
|
|
8
10
|
if (!title) {
|
|
@@ -37,17 +39,17 @@ function ProductSeoWidget({ data }) {
|
|
|
37
39
|
});
|
|
38
40
|
}, []);
|
|
39
41
|
const titleScore = React.useMemo(() => {
|
|
40
|
-
if (!title) return { text: t("catalog.products.create.seoWidget.missing", "Missing"),
|
|
41
|
-
if (title.length < 10) return { text: t("catalog.products.create.seoWidget.tooShort", "Too short"),
|
|
42
|
-
if (title.length > 60) return { text: t("catalog.products.create.seoWidget.tooLong", "Too long"),
|
|
43
|
-
return { text: t("catalog.products.create.seoWidget.good", "Good"),
|
|
42
|
+
if (!title) return { text: t("catalog.products.create.seoWidget.missing", "Missing"), variant: "error" };
|
|
43
|
+
if (title.length < 10) return { text: t("catalog.products.create.seoWidget.tooShort", "Too short"), variant: "warning" };
|
|
44
|
+
if (title.length > 60) return { text: t("catalog.products.create.seoWidget.tooLong", "Too long"), variant: "warning" };
|
|
45
|
+
return { text: t("catalog.products.create.seoWidget.good", "Good"), variant: "success" };
|
|
44
46
|
}, [title, t]);
|
|
45
47
|
const descScore = React.useMemo(() => {
|
|
46
|
-
if (!description) return { text: t("catalog.products.create.seoWidget.missing", "Missing"),
|
|
47
|
-
if (description.length < 50) return { text: t("catalog.products.create.seoWidget.tooShort", "Too short"),
|
|
48
|
-
return { text: t("catalog.products.create.seoWidget.good", "Good"),
|
|
48
|
+
if (!description) return { text: t("catalog.products.create.seoWidget.missing", "Missing"), variant: "error" };
|
|
49
|
+
if (description.length < 50) return { text: t("catalog.products.create.seoWidget.tooShort", "Too short"), variant: "warning" };
|
|
50
|
+
return { text: t("catalog.products.create.seoWidget.good", "Good"), variant: "success" };
|
|
49
51
|
}, [description, t]);
|
|
50
|
-
const statusBadge = validation.ok ? /* @__PURE__ */ jsx(
|
|
52
|
+
const statusBadge = validation.ok ? /* @__PURE__ */ jsx(StatusBadge, { variant: "success", children: t("catalog.products.create.seoWidget.ready", "Ready") }) : /* @__PURE__ */ jsx(StatusBadge, { variant: "warning", children: t("catalog.products.create.seoWidget.needsAttention", "Needs attention") });
|
|
51
53
|
const translateIssue = (issueKey) => {
|
|
52
54
|
return t(`catalog.products.create.seoWidget.issues.${issueKey}`, issueKey);
|
|
53
55
|
};
|
|
@@ -59,18 +61,28 @@ function ProductSeoWidget({ data }) {
|
|
|
59
61
|
] }),
|
|
60
62
|
statusBadge
|
|
61
63
|
] }),
|
|
62
|
-
validation.message || validation.issues.length ? /* @__PURE__ */ jsxs(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
validation.message || validation.issues.length ? /* @__PURE__ */ jsxs(
|
|
65
|
+
Alert,
|
|
66
|
+
{
|
|
67
|
+
status: validation.ok ? "success" : "warning",
|
|
68
|
+
style: "lighter",
|
|
69
|
+
size: "sm",
|
|
70
|
+
showIcon: false,
|
|
71
|
+
className: "text-xs",
|
|
72
|
+
children: [
|
|
73
|
+
validation.message ? /* @__PURE__ */ jsx("div", { className: "font-medium", children: validation.message }) : null,
|
|
74
|
+
validation.issues.length ? /* @__PURE__ */ jsx("ul", { className: "ml-4 list-disc space-y-1 pt-1", children: validation.issues.map((issue) => /* @__PURE__ */ jsx("li", { children: translateIssue(issue) }, issue)) }) : null
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
) : null,
|
|
66
78
|
/* @__PURE__ */ jsxs("div", { className: "rounded border bg-muted/30 p-3 text-sm", children: [
|
|
67
79
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
68
80
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("catalog.products.create.seoWidget.titleLabel", "Title ({{count}} chars)", { count: title.length }) }),
|
|
69
|
-
/* @__PURE__ */ jsx(
|
|
81
|
+
/* @__PURE__ */ jsx(StatusBadge, { variant: titleScore.variant, children: titleScore.text })
|
|
70
82
|
] }),
|
|
71
83
|
/* @__PURE__ */ jsxs("div", { className: "mt-2 flex items-center justify-between", children: [
|
|
72
84
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("catalog.products.create.seoWidget.descriptionLabel", "Description ({{count}} chars)", { count: description.length }) }),
|
|
73
|
-
/* @__PURE__ */ jsx(
|
|
85
|
+
/* @__PURE__ */ jsx(StatusBadge, { variant: descScore.variant, children: descScore.text })
|
|
74
86
|
] })
|
|
75
87
|
] }),
|
|
76
88
|
/* @__PURE__ */ jsxs("p", { className: "text-overline text-muted-foreground", children: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/catalog/widgets/injection/product-seo/widget.client.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport { subscribeProductSeoValidation } from './state'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ntype SeoData = {\n title?: string | null\n name?: string | null\n description?: string | null\n}\n\ntype ValidationState = { ok: boolean; issues: string[]; message?: string }\n\ntype IssueKey = 'addTitle' | 'titleTooShort' | 'titleTooLong' | 'addDescription' | 'descriptionTooShort'\n\nfunction computeIssueKeys(title: string, description: string): IssueKey[] {\n const issues: IssueKey[] = []\n if (!title) {\n issues.push('addTitle')\n } else {\n if (title.length < 10) issues.push('titleTooShort')\n if (title.length > 60) issues.push('titleTooLong')\n }\n if (!description) {\n issues.push('addDescription')\n } else if (description.length < 50) {\n issues.push('descriptionTooShort')\n }\n return issues\n}\n\nexport default function ProductSeoWidget({ data }: InjectionWidgetComponentProps<unknown, SeoData>) {\n const t = useT()\n const title = (data?.title || data?.name || '') ?? ''\n const description = data?.description ?? ''\n const baselineIssueKeys = React.useMemo(() => computeIssueKeys(title, description), [title, description])\n const [validation, setValidation] = React.useState<ValidationState>({ ok: baselineIssueKeys.length === 0, issues: baselineIssueKeys })\n\n React.useEffect(() => {\n setValidation({ ok: baselineIssueKeys.length === 0, issues: baselineIssueKeys })\n }, [baselineIssueKeys])\n\n React.useEffect(() => {\n return subscribeProductSeoValidation((payload) => {\n setValidation({\n ok: payload.ok,\n issues: payload.issues,\n message: payload.message,\n })\n })\n }, [])\n\n const titleScore = React.useMemo(() => {\n if (!title) return { text: t('catalog.products.create.seoWidget.missing', 'Missing'),
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport { subscribeProductSeoValidation } from './state'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { StatusBadge, type StatusBadgeVariant } from '@open-mercato/ui/primitives/status-badge'\nimport { Alert } from '@open-mercato/ui/primitives/alert'\n\ntype SeoData = {\n title?: string | null\n name?: string | null\n description?: string | null\n}\n\ntype ValidationState = { ok: boolean; issues: string[]; message?: string }\n\ntype IssueKey = 'addTitle' | 'titleTooShort' | 'titleTooLong' | 'addDescription' | 'descriptionTooShort'\n\ntype SeoScore = { text: string; variant: StatusBadgeVariant }\n\nfunction computeIssueKeys(title: string, description: string): IssueKey[] {\n const issues: IssueKey[] = []\n if (!title) {\n issues.push('addTitle')\n } else {\n if (title.length < 10) issues.push('titleTooShort')\n if (title.length > 60) issues.push('titleTooLong')\n }\n if (!description) {\n issues.push('addDescription')\n } else if (description.length < 50) {\n issues.push('descriptionTooShort')\n }\n return issues\n}\n\nexport default function ProductSeoWidget({ data }: InjectionWidgetComponentProps<unknown, SeoData>) {\n const t = useT()\n const title = (data?.title || data?.name || '') ?? ''\n const description = data?.description ?? ''\n const baselineIssueKeys = React.useMemo(() => computeIssueKeys(title, description), [title, description])\n const [validation, setValidation] = React.useState<ValidationState>({ ok: baselineIssueKeys.length === 0, issues: baselineIssueKeys })\n\n React.useEffect(() => {\n setValidation({ ok: baselineIssueKeys.length === 0, issues: baselineIssueKeys })\n }, [baselineIssueKeys])\n\n React.useEffect(() => {\n return subscribeProductSeoValidation((payload) => {\n setValidation({\n ok: payload.ok,\n issues: payload.issues,\n message: payload.message,\n })\n })\n }, [])\n\n const titleScore = React.useMemo<SeoScore>(() => {\n if (!title) return { text: t('catalog.products.create.seoWidget.missing', 'Missing'), variant: 'error' }\n if (title.length < 10) return { text: t('catalog.products.create.seoWidget.tooShort', 'Too short'), variant: 'warning' }\n if (title.length > 60) return { text: t('catalog.products.create.seoWidget.tooLong', 'Too long'), variant: 'warning' }\n return { text: t('catalog.products.create.seoWidget.good', 'Good'), variant: 'success' }\n }, [title, t])\n\n const descScore = React.useMemo<SeoScore>(() => {\n if (!description) return { text: t('catalog.products.create.seoWidget.missing', 'Missing'), variant: 'error' }\n if (description.length < 50) return { text: t('catalog.products.create.seoWidget.tooShort', 'Too short'), variant: 'warning' }\n return { text: t('catalog.products.create.seoWidget.good', 'Good'), variant: 'success' }\n }, [description, t])\n\n const statusBadge = validation.ok ? (\n <StatusBadge variant=\"success\">\n {t('catalog.products.create.seoWidget.ready', 'Ready')}\n </StatusBadge>\n ) : (\n <StatusBadge variant=\"warning\">\n {t('catalog.products.create.seoWidget.needsAttention', 'Needs attention')}\n </StatusBadge>\n )\n\n const translateIssue = (issueKey: string): string => {\n return t(`catalog.products.create.seoWidget.issues.${issueKey}`, issueKey)\n }\n\n return (\n <div className=\"mt-4 w-full space-y-3 rounded-lg border bg-card p-4 shadow-sm\">\n <div className=\"flex items-start justify-between gap-3\">\n <div>\n <div className=\"text-sm font-semibold text-foreground\">{t('catalog.products.create.seoWidget.title', 'SEO Optimization')}</div>\n <p className=\"text-xs text-muted-foreground\">{t('catalog.products.create.seoWidget.hint', 'Keep titles 10\u201360 chars and descriptions 50+ chars.')}</p>\n </div>\n {statusBadge}\n </div>\n\n {validation.message || validation.issues.length ? (\n <Alert\n status={validation.ok ? 'success' : 'warning'}\n style=\"lighter\"\n size=\"sm\"\n showIcon={false}\n className=\"text-xs\"\n >\n {validation.message ? <div className=\"font-medium\">{validation.message}</div> : null}\n {validation.issues.length ? (\n <ul className=\"ml-4 list-disc space-y-1 pt-1\">\n {validation.issues.map((issue) => (\n <li key={issue}>{translateIssue(issue)}</li>\n ))}\n </ul>\n ) : null}\n </Alert>\n ) : null}\n\n <div className=\"rounded border bg-muted/30 p-3 text-sm\">\n <div className=\"flex items-center justify-between\">\n <span className=\"text-muted-foreground\">{t('catalog.products.create.seoWidget.titleLabel', 'Title ({{count}} chars)', { count: title.length })}</span>\n <StatusBadge variant={titleScore.variant}>{titleScore.text}</StatusBadge>\n </div>\n <div className=\"mt-2 flex items-center justify-between\">\n <span className=\"text-muted-foreground\">{t('catalog.products.create.seoWidget.descriptionLabel', 'Description ({{count}} chars)', { count: description.length })}</span>\n <StatusBadge variant={descScore.variant}>{descScore.text}</StatusBadge>\n </div>\n </div>\n\n <p className=\"text-overline text-muted-foreground\">\n {t('catalog.products.create.seoWidget.footer', 'Example widget powered by the injection system.')}{' '}\n <a className=\"text-primary underline\" href=\"/docs/framework/admin-ui/widget-injection\" target=\"_blank\" rel=\"noreferrer\">\n {t('catalog.products.create.seoWidget.learnMore', 'Learn how to build your own')}\n </a>\n .\n </p>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAuEI,cAgBI,YAhBJ;AAtEJ,YAAY,WAAW;AAEvB,SAAS,qCAAqC;AAC9C,SAAS,YAAY;AACrB,SAAS,mBAA4C;AACrD,SAAS,aAAa;AActB,SAAS,iBAAiB,OAAe,aAAiC;AACxE,QAAM,SAAqB,CAAC;AAC5B,MAAI,CAAC,OAAO;AACV,WAAO,KAAK,UAAU;AAAA,EACxB,OAAO;AACL,QAAI,MAAM,SAAS,GAAI,QAAO,KAAK,eAAe;AAClD,QAAI,MAAM,SAAS,GAAI,QAAO,KAAK,cAAc;AAAA,EACnD;AACA,MAAI,CAAC,aAAa;AAChB,WAAO,KAAK,gBAAgB;AAAA,EAC9B,WAAW,YAAY,SAAS,IAAI;AAClC,WAAO,KAAK,qBAAqB;AAAA,EACnC;AACA,SAAO;AACT;AAEe,SAAR,iBAAkC,EAAE,KAAK,GAAoD;AAClG,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,MAAM,SAAS,MAAM,QAAQ,OAAO;AACnD,QAAM,cAAc,MAAM,eAAe;AACzC,QAAM,oBAAoB,MAAM,QAAQ,MAAM,iBAAiB,OAAO,WAAW,GAAG,CAAC,OAAO,WAAW,CAAC;AACxG,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAA0B,EAAE,IAAI,kBAAkB,WAAW,GAAG,QAAQ,kBAAkB,CAAC;AAErI,QAAM,UAAU,MAAM;AACpB,kBAAc,EAAE,IAAI,kBAAkB,WAAW,GAAG,QAAQ,kBAAkB,CAAC;AAAA,EACjF,GAAG,CAAC,iBAAiB,CAAC;AAEtB,QAAM,UAAU,MAAM;AACpB,WAAO,8BAA8B,CAAC,YAAY;AAChD,oBAAc;AAAA,QACZ,IAAI,QAAQ;AAAA,QACZ,QAAQ,QAAQ;AAAA,QAChB,SAAS,QAAQ;AAAA,MACnB,CAAC;AAAA,IACH,CAAC;AAAA,EACH,GAAG,CAAC,CAAC;AAEL,QAAM,aAAa,MAAM,QAAkB,MAAM;AAC/C,QAAI,CAAC,MAAO,QAAO,EAAE,MAAM,EAAE,6CAA6C,SAAS,GAAG,SAAS,QAAQ;AACvG,QAAI,MAAM,SAAS,GAAI,QAAO,EAAE,MAAM,EAAE,8CAA8C,WAAW,GAAG,SAAS,UAAU;AACvH,QAAI,MAAM,SAAS,GAAI,QAAO,EAAE,MAAM,EAAE,6CAA6C,UAAU,GAAG,SAAS,UAAU;AACrH,WAAO,EAAE,MAAM,EAAE,0CAA0C,MAAM,GAAG,SAAS,UAAU;AAAA,EACzF,GAAG,CAAC,OAAO,CAAC,CAAC;AAEb,QAAM,YAAY,MAAM,QAAkB,MAAM;AAC9C,QAAI,CAAC,YAAa,QAAO,EAAE,MAAM,EAAE,6CAA6C,SAAS,GAAG,SAAS,QAAQ;AAC7G,QAAI,YAAY,SAAS,GAAI,QAAO,EAAE,MAAM,EAAE,8CAA8C,WAAW,GAAG,SAAS,UAAU;AAC7H,WAAO,EAAE,MAAM,EAAE,0CAA0C,MAAM,GAAG,SAAS,UAAU;AAAA,EACzF,GAAG,CAAC,aAAa,CAAC,CAAC;AAEnB,QAAM,cAAc,WAAW,KAC7B,oBAAC,eAAY,SAAQ,WAClB,YAAE,2CAA2C,OAAO,GACvD,IAEA,oBAAC,eAAY,SAAQ,WAClB,YAAE,oDAAoD,iBAAiB,GAC1E;AAGF,QAAM,iBAAiB,CAAC,aAA6B;AACnD,WAAO,EAAE,4CAA4C,QAAQ,IAAI,QAAQ;AAAA,EAC3E;AAEA,SACE,qBAAC,SAAI,WAAU,iEACb;AAAA,yBAAC,SAAI,WAAU,0CACb;AAAA,2BAAC,SACC;AAAA,4BAAC,SAAI,WAAU,yCAAyC,YAAE,2CAA2C,kBAAkB,GAAE;AAAA,QACzH,oBAAC,OAAE,WAAU,iCAAiC,YAAE,0CAA0C,0DAAqD,GAAE;AAAA,SACnJ;AAAA,MACC;AAAA,OACH;AAAA,IAEC,WAAW,WAAW,WAAW,OAAO,SACvC;AAAA,MAAC;AAAA;AAAA,QACC,QAAQ,WAAW,KAAK,YAAY;AAAA,QACpC,OAAM;AAAA,QACN,MAAK;AAAA,QACL,UAAU;AAAA,QACV,WAAU;AAAA,QAET;AAAA,qBAAW,UAAU,oBAAC,SAAI,WAAU,eAAe,qBAAW,SAAQ,IAAS;AAAA,UAC/E,WAAW,OAAO,SACjB,oBAAC,QAAG,WAAU,iCACX,qBAAW,OAAO,IAAI,CAAC,UACtB,oBAAC,QAAgB,yBAAe,KAAK,KAA5B,KAA8B,CACxC,GACH,IACE;AAAA;AAAA;AAAA,IACN,IACE;AAAA,IAEJ,qBAAC,SAAI,WAAU,0CACb;AAAA,2BAAC,SAAI,WAAU,qCACb;AAAA,4BAAC,UAAK,WAAU,yBAAyB,YAAE,gDAAgD,2BAA2B,EAAE,OAAO,MAAM,OAAO,CAAC,GAAE;AAAA,QAC/I,oBAAC,eAAY,SAAS,WAAW,SAAU,qBAAW,MAAK;AAAA,SAC7D;AAAA,MACA,qBAAC,SAAI,WAAU,0CACb;AAAA,4BAAC,UAAK,WAAU,yBAAyB,YAAE,sDAAsD,iCAAiC,EAAE,OAAO,YAAY,OAAO,CAAC,GAAE;AAAA,QACjK,oBAAC,eAAY,SAAS,UAAU,SAAU,oBAAU,MAAK;AAAA,SAC3D;AAAA,OACF;AAAA,IAEA,qBAAC,OAAE,WAAU,uCACV;AAAA,QAAE,4CAA4C,iDAAiD;AAAA,MAAG;AAAA,MACnG,oBAAC,OAAE,WAAU,0BAAyB,MAAK,6CAA4C,QAAO,UAAS,KAAI,cACxG,YAAE,+CAA+C,6BAA6B,GACjF;AAAA,MAAI;AAAA,OAEN;AAAA,KACF;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|