diff --git a/apps/docs/docs/management/custom-widgets/authoring.mdx b/apps/docs/docs/management/custom-widgets/authoring.mdx index 2fc8d2c1f..429827c55 100644 --- a/apps/docs/docs/management/custom-widgets/authoring.mdx +++ b/apps/docs/docs/management/custom-widgets/authoring.mdx @@ -27,11 +27,12 @@ Every option has a default. Supported controls include text, textarea, number, s Enter the desired widget and optional API documentation URL, then select **Copy AI prompt**. The single self-contained prompt is below 12,000 characters, starts with your request, includes the lean contract and two polished examples, and never claims that an offline chat has MCP tools. -The model must return exactly: +The model must return one fenced `json` block containing the complete widget, including the JSX in its `template` +field. Copy that single block and select **Paste AI response**. Homarr fills the visual builders and the JSX editor +without any placeholder replacement or second paste. -1. One fenced `json` block with `template: "__HOMARR_TEMPLATE__"`. -2. One fenced `jsx` block with the readable template. - -Select **Paste AI response**. Homarr fills the visual builders and shows all actionable schema or JSX errors inline. Enter credentials separately. The prompt deliberately recommends a focused Mantine component set, visual hierarchy, responsive layout, theme compatibility, and loading, empty, failure, and success states. +Homarr shows all actionable schema or JSX errors inline. Enter credentials separately. The prompt deliberately +recommends a focused Mantine component set, visual hierarchy, responsive layout, theme compatibility, and loading, +empty, failure, and success states. Connected agents should use the skills.sh skill and retrieve the live schema and individual component metadata only as needed. diff --git a/apps/docs/docs/management/custom-widgets/index.mdx b/apps/docs/docs/management/custom-widgets/index.mdx index dfa2a25a5..ec4090084 100644 --- a/apps/docs/docs/management/custom-widgets/index.mdx +++ b/apps/docs/docs/management/custom-widgets/index.mdx @@ -78,8 +78,9 @@ The workbench keeps one continuous layout with visual source, request, and optio Exports, imports, the official Workshop client, and MCP validate the same schema. PocketBase stores community submissions without interpreting their content, and Homarr validates them before installation. Widgets saved in the v1 format remain visible to administrators with a **Migration required** badge after upgrading. Use **Copy migration -prompt**, give the redacted prompt to an LLM, then import its v2 JSON from that widget's action menu. The prompt keeps -safe request metadata but deliberately omits the v1 display configuration because legacy free-text and JSX can contain +prompt**, give the redacted prompt to an LLM, copy its single complete JSON block, then select **Paste migrated widget** +from that widget's action menu. Homarr reads the `template` value into the JSX editor automatically. The prompt keeps safe +request metadata but deliberately omits the v1 display configuration because legacy free-text and JSX can contain hard-coded credentials. Add a credential-free description of the widget's visible layout and behavior when sending the prompt. diff --git a/apps/docs/docs/management/custom-widgets/troubleshooting.mdx b/apps/docs/docs/management/custom-widgets/troubleshooting.mdx index 9a8b11422..d642b2751 100644 --- a/apps/docs/docs/management/custom-widgets/troubleshooting.mdx +++ b/apps/docs/docs/management/custom-widgets/troubleshooting.mdx @@ -6,9 +6,10 @@ The definition does not exist. If it was deleted, remove the unavailable tile fr If the Custom Widgets page shows the definition with a **Migration required** badge, an administrator can open its action menu and select **Copy migration prompt**. Give that redacted prompt to an LLM, then import the resulting v2 JSON -from the same menu. Because arbitrary v1 display text or JSX may contain credentials, the prompt omits that display -configuration; describe the old widget's visible layout and behavior without including secrets. Other viewers see -**Contact an administrator** instead of a migration link. +from the same menu. The LLM returns one complete JSON block; copy it once and select **Paste migrated widget**. Homarr +places its `template` value into the JSX editor automatically. Because arbitrary v1 display text or JSX may contain +credentials, the prompt omits that display configuration; describe the old widget's visible layout and behavior without +including secrets. Other viewers see **Contact an administrator** instead of a migration link. Homarr keeps the v1 definition and encrypted secret after migration. The validated v2 replacement uses the same ID and takes precedence; the archived v1 record reappears if the v2 replacement is deleted. There is no v1 permanent-delete diff --git a/apps/docs/src/components/custom-widget-code-client.tsx b/apps/docs/src/components/custom-widget-code-client.tsx index ce35fc60a..ff4829738 100644 --- a/apps/docs/src/components/custom-widget-code-client.tsx +++ b/apps/docs/src/components/custom-widget-code-client.tsx @@ -84,7 +84,6 @@ export function BundledCustomWidgetGalleryClient() { const [selectedId, setSelectedId] = useState(BUNDLED_CUSTOM_WIDGETS[0]?.id ?? null); const selected = BUNDLED_CUSTOM_WIDGETS.find(({ id }) => id === selectedId) ?? BUNDLED_CUSTOM_WIDGETS[0]; if (!selected) return null; - const manifest = { ...selected.widget, template: "__HOMARR_TEMPLATE__" }; return ( @@ -103,19 +102,11 @@ export function BundledCustomWidgetGalleryClient() { - diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_copy-ai-prompt-button.spec.tsx b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_copy-ai-prompt-button.spec.tsx index 2bb767724..5e50b6127 100644 --- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_copy-ai-prompt-button.spec.tsx +++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_copy-ai-prompt-button.spec.tsx @@ -18,7 +18,7 @@ describe("Copy AI prompt", () => { const prompt = buildCustomWidgetAiPrompt(undefined, null, null, "Create a Pokédex"); expect(prompt.length).toBeLessThanOrEqual(12_000); expect(prompt).toContain("exactly one complete `json` fenced block"); - expect(prompt).toContain("one complete `jsx` fenced block"); + expect(prompt).toContain("Put the complete JSX source directly in the `template` string"); }); it("keeps AI actions and section navigation readable on narrow screens", () => { diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-actions.tsx b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-actions.tsx index 4acb247ba..7aafe51be 100644 --- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-actions.tsx +++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-actions.tsx @@ -5,6 +5,7 @@ import { ActionIcon, Menu } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; import { IconBuildingStore, + IconClipboard, IconCopy, IconDots, IconDownload, @@ -63,22 +64,37 @@ export const CustomWidgetRowActions = ({ widget }: { widget: WidgetRef }) => { } }; + const queueMigratedWidget = (text: string) => { + const result = parseCustomWidgetClipboardDetailed(text); + if (!result.success) { + showErrorNotification({ + title: t("action.migrate"), + message: formatCustomWidgetImportIssues(result.issues), + }); + return; + } + setMigratedWidget(result.widget); + migrationControls.open(); + }; + + const pasteMigratedWidget = async () => { + try { + queueMigratedWidget(await navigator.clipboard.readText()); + } catch { + showErrorNotification({ + title: t("action.migrate"), + message: t("notification.migrationError"), + }); + } + }; + const handleMigrationFile = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.addEventListener("load", (loadEvent) => { try { - const result = parseCustomWidgetClipboardDetailed(loadEvent.target?.result as string); - if (!result.success) { - showErrorNotification({ - title: t("action.migrate"), - message: formatCustomWidgetImportIssues(result.issues), - }); - return; - } - setMigratedWidget(result.widget); - migrationControls.open(); + queueMigratedWidget(loadEvent.target?.result as string); } catch { showErrorNotification({ title: t("action.migrate"), @@ -186,6 +202,9 @@ export const CustomWidgetRowActions = ({ widget }: { widget: WidgetRef }) => { void copyMigrationPrompt()} leftSection={}> {t("action.copyMigrationPrompt")} + void pasteMigratedWidget()} leftSection={}> + {t("action.pasteMigration")} + migrationFileInputRef.current?.click()} leftSection={} diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-ai-card.tsx b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-ai-card.tsx index dd13ce1b4..86fa64951 100644 --- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-ai-card.tsx +++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-ai-card.tsx @@ -26,6 +26,9 @@ export function CustomWidgetAiCard(props: AiCardProps) { {t("title")} + + {t("description")} +