diff --git a/apps/docs/docs/management/assistant.mdx b/apps/docs/docs/management/assistant.mdx index 725a08ce6..25c1497c1 100644 --- a/apps/docs/docs/management/assistant.mdx +++ b/apps/docs/docs/management/assistant.mdx @@ -24,6 +24,10 @@ conversation has a minimize action that returns it to the activity bar without i `Shift + A` opens the compact conversation panel directly. The shortcut is ignored while typing in text fields, selectors, and editable content. +The **Assistant** board widget embeds the same persisted conversation and full interaction surface directly +on a dashboard. By default it follows the current conversation. Widget settings can instead pin a specific +conversation; the conversation picker stays hidden until **Pinned conversation** is selected. + When the assistant is unavailable, its command-menu result remains visible but disabled and explains that an administrator must configure and enable it. diff --git a/apps/docs/docs/widgets/assistant/index.mdx b/apps/docs/docs/widgets/assistant/index.mdx new file mode 100644 index 000000000..9f7d174b7 --- /dev/null +++ b/apps/docs/docs/widgets/assistant/index.mdx @@ -0,0 +1,54 @@ +--- +title: "Assistant" +description: "Have a complete assistant conversation directly on a board." +hide_title: true +--- + +import { AddingWidget } from "@site/src/components/widgets/adding"; +import { WidgetConfig } from "@site/src/components/widgets/configuration"; +import { WidgetHeader } from "@site/src/components/widgets/header"; +import { assistantWidget } from "."; + + + +The Assistant widget brings the same conversation available from the user menu and command menu onto a +board. Messages, conversations, tool calls, approvals, files, mentions, model controls, reasoning controls, +usage details, and response actions remain synchronized with the app-wide assistant. + +The assistant must be enabled by an administrator in **Management → Assistant**. Signed-out users and users +on an instance without an enabled model see an unavailable state instead of a broken composer. + +### Adding the widget + + + +The default widget size is ten columns by four rows and automatically fits boards with fewer columns. Controls +and labels condense as the widget becomes narrower, while the conversation remains scrollable and usable on +touch devices. When a board layout does not provide enough height for conversation controls, the widget shows +a compact **Open assistant** action instead of squeezing an unusable composer into the card. Increase its height +in that layout to keep the complete conversation inline. + +### Configuration + + + +**Current conversation** follows whichever conversation is active in the user-menu panel, `Shift + A`, or +the command menu. This is the default and keeps every assistant entry point synchronized. + +**Pinned conversation** reveals a searchable conversation selector. A pinned widget never changes the +active conversation silently: select **Open conversation** inside the widget before continuing that thread. + +### Conversation features + +- Create, switch, rename, and delete conversations. +- Send text and supported file or image attachments. +- Mention permitted apps, integrations, boards, widgets, and tools with `@`. +- Use `/` shortcuts for common dashboard tasks. +- Review tool calls, approve mutations, and answer structured assistant questions inline. +- Edit user messages and copy, regenerate, branch, quote, export, or rate responses. +- Inspect Markdown, sources, generated files, tool results, reasoning, token use, context-window use, timing, + speed, provider routing, and cost. +- Change the model and reasoning effort for the active conversation. + +The expanded assistant and board widget use the same persisted thread runtime. A response continues in the +background when the widget leaves the viewport or the expanded panel is minimized. diff --git a/apps/docs/docs/widgets/assistant/index.ts b/apps/docs/docs/widgets/assistant/index.ts new file mode 100644 index 000000000..ace552186 --- /dev/null +++ b/apps/docs/docs/widgets/assistant/index.ts @@ -0,0 +1,25 @@ +import type { WidgetDefinition } from "@site/src/types"; +import { IconRobot } from "@tabler/icons-react"; + +export const assistantWidget: WidgetDefinition = { + icon: IconRobot, + name: "Assistant", + description: "Have a complete assistant conversation directly on a board.", + path: "../../widgets/assistant", + configuration: { + items: [ + { + name: "Conversation", + description: "Follow the active conversation or attach the widget to one persisted conversation.", + values: ["Current conversation", "Pinned conversation"], + defaultValue: "Current conversation", + }, + { + name: "Pinned conversation", + description: "Search for a conversation. This field is hidden when Current conversation is selected.", + values: { type: "string" }, + defaultValue: "Hidden", + }, + ], + }, +}; diff --git a/apps/nextjs/src/components/assistant/assistant-panel.module.css b/apps/nextjs/src/components/assistant/assistant-panel.module.css index a0b4cdf9f..2077217c5 100644 --- a/apps/nextjs/src/components/assistant/assistant-panel.module.css +++ b/apps/nextjs/src/components/assistant/assistant-panel.module.css @@ -114,6 +114,38 @@ animation: assistant-panel-in 180ms cubic-bezier(0.22, 1, 0.36, 1); } +.widgetPanel { + container-name: assistant-widget; + container-type: size; + display: flex; + width: 100%; + height: 100%; + min-width: 0; + min-height: 0; + flex-direction: column; + overflow: hidden; + color: var(--mantine-color-text); +} + +.widgetState { + container-type: inline-size; + overflow: auto; +} + +.compactState { + width: 100%; + height: 100%; + padding: 0.75rem; +} + +.compactCopy { + min-width: 0; +} + +.widgetPanel[data-editing] .panelActions { + padding-inline-end: 2.75rem; +} + .panelHeader { min-height: 3.75rem; flex: 0 0 auto; @@ -139,6 +171,120 @@ height: 2.625rem; } +@container assistant-widget (max-width: 30rem) { + .panelHeader { + min-height: 3.25rem; + padding: 0.45rem 0.5rem 0.45rem 0.65rem; + } + + .panelIdentityText > :last-child, + .composerHints { + display: none; + } + + .historyButton { + width: 2.625rem; + max-width: 2.625rem; + height: 2.625rem; + padding-inline: 0.75rem; + } + + .historyButtonSection { + margin-inline-end: 0; + } + + .historyButtonLabel { + display: none; + } + + .messages { + padding: 0.5rem 0.65rem 1rem; + } + + .composerWrap { + padding: 0.45rem; + } + + .composer { + gap: 0.25rem; + padding: 0.25rem 0.3rem; + } + + .composerInput { + min-height: 2.625rem; + padding: 0.6rem 0.5rem; + } + + .runtimeButtonGroup, + .runtimeControls { + width: 100%; + } + + .modelButton { + min-width: 0; + max-width: none; + } + + .reasoningButton { + min-width: 2.5rem; + max-width: 7.75rem; + } + + .suggestions { + grid-template-columns: 1fr; + } +} + +@container assistant-widget (max-width: 22rem) { + .panelIdentityText { + display: none; + } + + .panelActions { + margin-inline-start: auto; + } + + .panelAction, + .historyButton { + width: 2.25rem; + max-width: 2.25rem; + height: 2.25rem; + padding-inline: 0.55rem; + } + + .composerFooter { + overflow: hidden; + } + + .runtimeButton { + padding-inline: 0.5rem; + } +} + +@container assistant-widget (max-width: 18rem) { + .compactDescription { + display: none; + } +} + +@container assistant-widget (max-width: 10rem) { + .compactTitle { + display: none; + } +} + +@container assistant-widget (max-height: 24rem) { + .empty { + min-height: 16rem; + padding-block: 1rem; + } + + .suggestion { + min-height: 2.5rem; + padding-block: 0.45rem; + } +} + .historyMenu { background: var(--mantine-color-body); } diff --git a/apps/nextjs/src/components/assistant/assistant-panel.tsx b/apps/nextjs/src/components/assistant/assistant-panel.tsx index b8e9ebd43..9e28ab3a5 100644 --- a/apps/nextjs/src/components/assistant/assistant-panel.tsx +++ b/apps/nextjs/src/components/assistant/assistant-panel.tsx @@ -59,6 +59,7 @@ import { IconAlertTriangle, IconApps, IconArrowUp, + IconArrowsMaximize, IconAt, IconBrain, IconCheck, @@ -105,7 +106,16 @@ import type { AssistantReasoningMode, AssistantRuntimeModelOption } from "./assi import { getNearestTriggerScrollTop } from "./assistant-trigger-scroll"; import { getToolResultPresentation } from "./assistant-tool-result"; -interface AssistantPanelProps { +export interface AssistantConversationControls { + modelId: string | null; + models: AssistantRuntimeModelOption[]; + modelOptionsLoading: boolean; + reasoning: AssistantReasoningMode; + onModelChange: (modelId: string) => void; + onReasoningChange: (reasoning: AssistantReasoningMode) => void; +} + +interface AssistantPanelProps extends AssistantConversationControls { opened: boolean; onOpen: () => void; onClose: () => void; @@ -117,12 +127,6 @@ interface AssistantPanelProps { latestUserText: string; latestStatus: MessageStatus | undefined; pendingAction: AssistantPendingAction | undefined; - modelId: string | null; - models: AssistantRuntimeModelOption[]; - modelOptionsLoading: boolean; - reasoning: AssistantReasoningMode; - onModelChange: (modelId: string) => void; - onReasoningChange: (reasoning: AssistantReasoningMode) => void; } const markdownRemarkPlugins = [remarkGfm, remarkBreaks]; @@ -1255,10 +1259,7 @@ const ComposerTriggers = () => { ); }; -type ComposerProps = Pick< - AssistantPanelProps, - "modelId" | "models" | "modelOptionsLoading" | "reasoning" | "onModelChange" | "onReasoningChange" -> & { pendingAction: AssistantPendingAction | undefined }; +type ComposerProps = AssistantConversationControls & { pendingAction: AssistantPendingAction | undefined }; const RuntimeControls = ({ modelId, @@ -1622,6 +1623,124 @@ const AssistantActivityBar = ({ ); }; +interface AssistantConversationSurfaceProps extends AssistantConversationControls { + isRunning: boolean; + pendingAction: AssistantPendingAction | undefined; + onExpand?: () => void; + onMinimize?: () => void; +} + +export const AssistantConversationSurface = ({ + isRunning, + pendingAction, + modelId, + models, + modelOptionsLoading, + reasoning, + onModelChange, + onReasoningChange, + onExpand, + onMinimize, +}: AssistantConversationSurfaceProps) => { + const t = useScopedI18n("common.assistant"); + + return ( + <> + + + + + + + + {t("title")} + + + {isRunning ? t("activity.thinking") : t("subtitle")} + + + + + + + + + + + + + {onExpand && ( + + + + + + )} + {onMinimize && ( + + + + + + )} + + + + + + + + + + + }> + {t("quoteSelection")} + + + + + + + + + + + + + > + ); +}; + export const AssistantPanel = ({ opened, onOpen, @@ -1683,82 +1802,17 @@ export const AssistantPanel = ({ {opened && ( - - - - - - - - {t("title")} - - - {isRunning ? t("activity.thinking") : t("subtitle")} - - - - - - - - - - - - - - - - - - - - - - - - - - - - }> - {t("quoteSelection")} - - - - - - - - - - - - + )} diff --git a/apps/nextjs/src/components/assistant/assistant-provider.tsx b/apps/nextjs/src/components/assistant/assistant-provider.tsx index 7b2efeeb6..d8d973456 100644 --- a/apps/nextjs/src/components/assistant/assistant-provider.tsx +++ b/apps/nextjs/src/components/assistant/assistant-provider.tsx @@ -23,6 +23,7 @@ import { hotkeys } from "@homarr/definitions"; import { showErrorNotification, showWarningNotification } from "@homarr/notifications"; import { useScopedI18n } from "@homarr/translation/client"; import { openMediaRequestSearch, openSpotlight, useRegisterSpotlightContextResults } from "@homarr/spotlight"; +import { AssistantWidgetRendererProvider } from "@homarr/widgets"; import { shouldAutomaticallyContinueAssistant } from "./assistant-auto-submit"; import { AssistantAskUserTool, AssistantConfigureAppTool } from "./assistant-human-tools"; @@ -35,9 +36,11 @@ import { createAssistantPromptInteraction } from "./assistant-spotlight"; import { browserToolContracts } from "./assistant-tool-contracts"; import { AssistantConfigureBoardSettingsTool } from "./assistant-board-settings-tool"; import type { AssistantUIMessage } from "./assistant-message-metadata"; +import { AssistantBoardWidget } from "./assistant-widget"; interface AssistantContextValue { enabled: boolean; + unavailableDescription: string | null; opened: boolean; isRunning: boolean; unreadCount: number; @@ -148,7 +151,7 @@ export const useHomarrAssistant = () => { export const useOptionalHomarrAssistant = () => useContext(AssistantContext); -const useAssistantPreferences = () => { +export const useAssistantPreferences = () => { const value = useContext(AssistantPreferencesContext); if (!value) throw new Error("useAssistantPreferences must be used within AssistantPreferencesProvider"); return value; @@ -636,13 +639,23 @@ const EnabledAssistantProvider = ({ children }: PropsWithChildren) => { useRegisterSpotlightContextResults("homarr-assistant", [spotlightItem], [spotlightItem]); const value = useMemo( - () => ({ enabled: true, opened, isRunning: assistantIsRunning, unreadCount, open, close, toggle, sendPrompt }), + () => ({ + enabled: true, + unavailableDescription: null, + opened, + isRunning: assistantIsRunning, + unreadCount, + open, + close, + toggle, + sendPrompt, + }), [assistantIsRunning, close, open, opened, sendPrompt, toggle, unreadCount], ); return ( - {children} + {children} ({ enabled: false, + unavailableDescription: description, opened: false, isRunning: false, unreadCount: 0, @@ -700,10 +714,14 @@ const DisabledAssistantProvider = ({ children, description }: DisabledAssistantP toggle: () => undefined, sendPrompt: () => undefined, }), - [], + [description], ); - return {children}; + return ( + + {children} + + ); }; export const AssistantProvider = ({ children }: PropsWithChildren) => { diff --git a/apps/nextjs/src/components/assistant/assistant-widget.tsx b/apps/nextjs/src/components/assistant/assistant-widget.tsx new file mode 100644 index 000000000..cedd55018 --- /dev/null +++ b/apps/nextjs/src/components/assistant/assistant-widget.tsx @@ -0,0 +1,203 @@ +"use client"; + +import type { ReactNode } from "react"; +import { useCallback, useState } from "react"; +import { useAui, useAuiState } from "@assistant-ui/react"; +import { ActionIcon, Box, Button, Center, Group, Loader, Stack, Text, ThemeIcon, Tooltip } from "@mantine/core"; +import { IconAlertTriangle, IconArrowsMaximize, IconMessage, IconRobot } from "@tabler/icons-react"; + +import { useI18n } from "@homarr/translation/client"; +import type { WidgetComponentProps } from "@homarr/widgets"; + +import classes from "./assistant-panel.module.css"; +import { AssistantConversationSurface } from "./assistant-panel"; +import { getPendingAssistantAction } from "./assistant-pending-action"; +import { useAssistantPreferences, useHomarrAssistant } from "./assistant-provider"; + +export const AssistantBoardWidget = (props: WidgetComponentProps<"assistant">) => { + const assistant = useHomarrAssistant(); + + if (!assistant.enabled) { + return ; + } + + return ; +}; + +const EnabledAssistantBoardWidget = ({ options, width, height, isEditMode }: WidgetComponentProps<"assistant">) => { + const t = useI18n(); + const assistant = useHomarrAssistant(); + const preferences = useAssistantPreferences(); + const aui = useAui(); + const currentThreadId = useAuiState((state) => state.threadListItem.remoteId); + const messages = useAuiState((state) => state.thread.messages); + const isLoading = useAuiState((state) => state.thread.isLoading); + const latestAssistantMessage = messages.findLast((message) => message.role === "assistant"); + const pendingAction = getPendingAssistantAction(latestAssistantMessage); + const pinnedConversation = options.conversationMode === "pinned" ? options.conversation : null; + const pinnedConversationIsActive = pinnedConversation === null || pinnedConversation.value === currentThreadId; + const [switching, setSwitching] = useState(false); + const [switchErrorConversationId, setSwitchErrorConversationId] = useState(null); + const hasSwitchError = switchErrorConversationId === pinnedConversation?.value; + + const selectModel = useCallback( + (modelId: string) => { + preferences.setModelId(modelId); + const threadListItem = aui.threadListItem(); + if (!threadListItem.getState().remoteId) return; + threadListItem.updateCustom({ ...threadListItem.getState().custom, modelId }); + }, + [aui, preferences], + ); + + const openPinnedConversation = async () => { + if (!pinnedConversation) return; + setSwitchErrorConversationId(null); + setSwitching(true); + try { + await aui.threads().switchToThread(pinnedConversation.value); + } catch { + setSwitchErrorConversationId(pinnedConversation.value); + } finally { + setSwitching(false); + } + }; + + if (options.conversationMode === "pinned" && pinnedConversation === null) { + return ( + + ); + } + + if (!pinnedConversationIsActive && pinnedConversation) { + return ( + + void openPinnedConversation()} + loading={switching} + leftSection={switching ? : } + > + {switching ? t("widget.assistant.pinned.opening") : t("widget.assistant.pinned.open")} + + + ); + } + + if (width < 300 || height < 280) { + return ; + } + + return ( + + + + ); +}; + +const CompactAssistantWidget = ({ onOpen }: { onOpen: () => void }) => { + const t = useI18n(); + + return ( + + + + + + + + + {t("widget.assistant.compact.title")} + + + {t("widget.assistant.compact.description")} + + + + + + + + + + + ); +}; + +interface AssistantWidgetStateProps { + icon: typeof IconRobot; + title: string; + description: string; + color?: string; + children?: ReactNode; +} + +const AssistantWidgetState = ({ + icon: Icon, + title, + description, + color = "red", + children, +}: AssistantWidgetStateProps) => ( + + + + + + + {title} + + {description} + + + {children} + + +); + +const UnavailableAssistantWidget = ({ description }: { description: string | null }) => { + const t = useI18n(); + return ( + + ); +}; diff --git a/apps/nextjs/src/components/board/items/actions/test/create-item.spec.ts b/apps/nextjs/src/components/board/items/actions/test/create-item.spec.ts index 7f97054ba..9378a3dea 100644 --- a/apps/nextjs/src/components/board/items/actions/test/create-item.spec.ts +++ b/apps/nextjs/src/components/board/items/actions/test/create-item.spec.ts @@ -129,6 +129,26 @@ describe("item actions create-item", () => { ); }); + test("should fit a wide widget to every board layout", () => { + const layoutId = "compact-layout"; + const layout = new LayoutMockBuilder({ id: layoutId, columnCount: 4 }).build(); + const board = new BoardMockBuilder().addLayout(layout).addEmptySection({ id: "section" }).build(); + const layoutsSpy = vi.spyOn(boardContext, "getBoardLayouts"); + layoutsSpy.mockReturnValue([layoutId]); + const emptyPositionSpy = vi.spyOn(emptyPositionModule, "getFirstEmptyPosition"); + emptyPositionSpy.mockReturnValue({ xOffset: 0, yOffset: 0 }); + + const result = createItemCallback({ kind: "assistant" })(board); + + expect(result.items.at(0)?.layouts.at(0)).toEqual( + expect.objectContaining({ width: layout.columnCount, height: 4 }), + ); + expect(emptyPositionSpy).toHaveBeenCalledWith([], layout.columnCount, undefined, { + width: layout.columnCount, + height: 4, + }); + }); + test("clamps wide defaults and places below a full automatic canvas", () => { const board = new BoardMockBuilder().addEmptySection({ id: "canvas", yOffset: 0 }).build(); const layout = board.layouts[0]; diff --git a/apps/nextjs/src/components/board/items/item-content.tsx b/apps/nextjs/src/components/board/items/item-content.tsx index 614f9017e..84385e5a1 100644 --- a/apps/nextjs/src/components/board/items/item-content.tsx +++ b/apps/nextjs/src/components/board/items/item-content.tsx @@ -47,7 +47,7 @@ interface BoardItemContentProps { const getOverflowFromKind = (kind: SectionItem["kind"], hasCustomCssClasses: boolean) => { if (kind === "systemResources") return { overflowX: "visible", overflowY: "visible" } as const; - if (kind === "iframe") return { overflowX: "hidden", overflowY: "hidden" } as const; + if (kind === "iframe" || kind === "assistant") return { overflowX: "hidden", overflowY: "hidden" } as const; if (hasCustomCssClasses) return {}; return { overflowX: "hidden", overflowY: "auto" } as const; }; diff --git a/packages/definitions/src/docs/widget-doc-slugs.ts b/packages/definitions/src/docs/widget-doc-slugs.ts index cf72dbda4..dd3f02e37 100644 --- a/packages/definitions/src/docs/widget-doc-slugs.ts +++ b/packages/definitions/src/docs/widget-doc-slugs.ts @@ -55,5 +55,6 @@ export const widgetDocSlugs: Record = { beszelSystemStats: "beszel-system-stats", traefik: "traefik", customApi: "custom-api", + assistant: "assistant", wud: "whats-up-docker", }; diff --git a/packages/definitions/src/widget.ts b/packages/definitions/src/widget.ts index e95dbca77..90274f3d8 100644 --- a/packages/definitions/src/widget.ts +++ b/packages/definitions/src/widget.ts @@ -53,6 +53,7 @@ export const widgetKinds = [ "beszelSystemStats", "traefik", "customApi", + "assistant", "wud", ] as const; export type WidgetKind = (typeof widgetKinds)[number]; @@ -64,4 +65,5 @@ export const widgetDefaultSizes: Partial) { + const Renderer = useAssistantWidgetRenderer(); + const t = useI18n(); + + if (Renderer) return ; + + return ( + + + + + + + {t("widget.assistant.unavailable.title")} + + + {t("widget.assistant.unavailable.description")} + + + + ); +} diff --git a/packages/widgets/src/assistant/context.tsx b/packages/widgets/src/assistant/context.tsx new file mode 100644 index 000000000..88b7a8844 --- /dev/null +++ b/packages/widgets/src/assistant/context.tsx @@ -0,0 +1,20 @@ +"use client"; + +import type { ComponentType, PropsWithChildren } from "react"; +import { createContext, useContext } from "react"; + +import type { WidgetComponentProps } from "../definition"; + +type AssistantWidgetRenderer = ComponentType>; + +const AssistantWidgetRendererContext = createContext(null); + +interface AssistantWidgetRendererProviderProps extends PropsWithChildren { + renderer: AssistantWidgetRenderer; +} + +export const AssistantWidgetRendererProvider = ({ children, renderer }: AssistantWidgetRendererProviderProps) => ( + {children} +); + +export const useAssistantWidgetRenderer = () => useContext(AssistantWidgetRendererContext); diff --git a/packages/widgets/src/assistant/definition.spec.ts b/packages/widgets/src/assistant/definition.spec.ts new file mode 100644 index 000000000..d0093e139 --- /dev/null +++ b/packages/widgets/src/assistant/definition.spec.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; + +import { definition } from "."; + +describe("assistant widget definition", () => { + it("follows the current conversation by default", () => { + const options = definition.createOptions(); + + expect(options.conversationMode.defaultValue).toBe("current"); + expect(options.conversation.shouldHide?.({ conversationMode: "current", conversation: null }, [])).toBe(true); + }); + + it("reveals the conversation selector only when a conversation is pinned", () => { + const options = definition.createOptions(); + + expect(options.conversation.shouldHide?.({ conversationMode: "pinned", conversation: null }, [])).toBe(false); + }); +}); diff --git a/packages/widgets/src/assistant/index.ts b/packages/widgets/src/assistant/index.ts new file mode 100644 index 000000000..0d8d99397 --- /dev/null +++ b/packages/widgets/src/assistant/index.ts @@ -0,0 +1,50 @@ +import { IconRobot } from "@tabler/icons-react"; + +import { clientApi } from "@homarr/api/client"; + +import { createWidgetDefinition } from "../definition"; +import { optionsBuilder } from "../options"; + +export const { definition, componentLoader } = createWidgetDefinition("assistant", { + icon: IconRobot, + createOptions() { + return optionsBuilder.from( + (factory) => ({ + conversationMode: factory.select({ + options: (["current", "pinned"] as const).map((value) => ({ + value, + label: (t) => t(`widget.assistant.option.conversationMode.option.${value}`), + })), + defaultValue: "current", + withDescription: true, + }), + conversation: factory.dynamicSelect({ + withDescription: true, + useOptions(query) { + const { data: threads = [], isPending } = clientApi.assistant.listThreads.useQuery(); + const normalizedQuery = query.trim().toLocaleLowerCase(); + return { + isPending, + options: threads + .filter((thread) => { + if (normalizedQuery.length === 0) return true; + return (thread.title ?? "").toLocaleLowerCase().includes(normalizedQuery); + }) + .map((thread) => ({ + value: thread.id, + label: thread.title?.trim() || thread.id, + })), + }; + }, + }), + }), + { + conversation: { + shouldHide: (options) => options.conversationMode !== "pinned", + }, + }, + ); + }, +}).withDynamicImport(() => import("./component")); + +export { AssistantWidgetRendererProvider } from "./context"; diff --git a/packages/widgets/src/index.tsx b/packages/widgets/src/index.tsx index cf131db7b..d6565a702 100644 --- a/packages/widgets/src/index.tsx +++ b/packages/widgets/src/index.tsx @@ -64,6 +64,7 @@ import * as beszelSystemStats from "./beszel-system-stats"; import * as video from "./video"; import * as weather from "./weather"; import * as customApi from "./custom-api"; +import * as assistant from "./assistant"; import * as wud from "./wud"; export type { @@ -88,6 +89,7 @@ export { } from "./definition"; export type { WidgetComponentProps }; export type { WidgetOptionDefinition, WidgetOptionType } from "./options"; +export { AssistantWidgetRendererProvider } from "./assistant/context"; export const widgetImports = { clock, @@ -144,6 +146,7 @@ export const widgetImports = { beszelSystemStats, traefik, customApi, + assistant, wud, } satisfies WidgetImportRecord;