diff --git a/packages/ariakit/src/input/TextInput.tsx b/packages/ariakit/src/input/TextInput.tsx index aa21323481..d4b9817f4a 100644 --- a/packages/ariakit/src/input/TextInput.tsx +++ b/packages/ariakit/src/input/TextInput.tsx @@ -25,7 +25,7 @@ export const TextInput = forwardRef< onChange, onSubmit, autoComplete, - rightSection, // TODO: add rightSection + rightSection, ...rest } = props; @@ -40,7 +40,7 @@ export const TextInput = forwardRef< className={mergeCSSClasses( "bn-ak-input", className || "", - variant === "large" ? "bn-ak-input-large" : "" + variant === "large" ? "bn-ak-input-large" : "", )} ref={ref} name={name} @@ -53,6 +53,7 @@ export const TextInput = forwardRef< onSubmit={onSubmit} autoComplete={autoComplete} /> + {rightSection} ); diff --git a/packages/ariakit/src/style.css b/packages/ariakit/src/style.css index feeeffe7ea..23ab0d04fa 100644 --- a/packages/ariakit/src/style.css +++ b/packages/ariakit/src/style.css @@ -11,6 +11,10 @@ gap: 0.5rem; } +.bn-ak-input-wrapper svg { + width: 24px; +} + .bn-ak-toolbar { height: fit-content; overflow: scroll; @@ -23,11 +27,15 @@ .bn-toolbar .bn-ak-button[data-selected] { padding-top: 0.125rem; - box-shadow: inset 0 0 0 1px var(--border), inset 0 2px 0 var(--border); + box-shadow: + inset 0 0 0 1px var(--border), + inset 0 2px 0 var(--border); } .bn-toolbar .bn-ak-button[data-selected]:where(.dark, .dark *) { - box-shadow: inset 0 0 0 1px var(--border), inset 0 1px 1px 1px var(--shadow); + box-shadow: + inset 0 0 0 1px var(--border), + inset 0 1px 1px 1px var(--shadow); } .bn-toolbar .bn-ak-popover { @@ -64,9 +72,11 @@ overflow: visible; } -.bn-ariakit .bn-suggestion-menu { +.bn-ariakit .bn-suggestion-menu, +.bn-ariakit .ai-suggestion-menu { height: fit-content; max-height: inherit; + overflow: auto; } .bn-ariakit .bn-color-picker-dropdown { @@ -106,13 +116,34 @@ --border: rgb(0 0 0/13%); --highlight: rgb(255 255 255/20%); --shadow: rgb(0 0 0/10%); - box-shadow: inset 0 0 0 1px var(--border), inset 0 2px 0 var(--highlight), - inset 0 -1px 0 var(--shadow), 0 1px 1px var(--shadow); + box-shadow: + inset 0 0 0 1px var(--border), + inset 0 2px 0 var(--highlight), + inset 0 -1px 0 var(--shadow), + 0 1px 1px var(--shadow); font-size: 0.7rem; border-radius: 4px; padding-inline: 4px; } +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.bn-ariakit .bn-suggestion-menu-loader { + align-items: center; + animation: spin 1s linear infinite; + display: flex; + height: 16px; + justify-content: center; + width: 16px; +} + .bn-ariakit .bn-grid-suggestion-menu { background: var(--bn-colors-menu-background); border-radius: var(--bn-border-radius-large); @@ -263,7 +294,7 @@ .bn-ak-author-info { align-items: center; display: flex; - gap: 16px + gap: 16px; } .bn-ariakit .bn-comment-editor .bn-editor { @@ -319,6 +350,48 @@ padding: 0; } +.bn-ariakit .bn-combobox .bn-ak-input-wrapper { + display: flex; + border-radius: 0.5rem; + border-width: 1px; + border-style: solid; + border-color: hsl(204 20% 88%); + background-color: hsl(204 20% 100%); + padding: 0.5rem; + color: hsl(204 4% 0%); + box-shadow: + 0 10px 15px -3px rgb(0 0 0 / 0.1), + 0 4px 6px -4px rgb(0 0 0 / 0.1); +} + +.bn-ariakit .bn-combobox .bn-ak-input-wrapper:where(.dark, .dark *) { + border-color: hsl(204 4% 24%); + background-color: hsl(204 4% 16%); + color: hsl(204 20% 100%); + box-shadow: + 0 10px 15px -3px rgb(0 0 0 / 0.25), + 0 4px 6px -4px rgb(0 0 0 / 0.1); +} + +.bn-ariakit .bn-combobox .bn-ak-input { + background: transparent; + border: none; + box-shadow: none; + outline: none; +} + +.bn-ariakit .bn-combobox .bn-combobox-icon, +.bn-ariakit .bn-combobox .bn-combobox-right-section { + align-items: start; + display: flex; + justify-content: center; + width: 24px; +} + +.bn-ariakit .bn-combobox .bn-combobox-error { + color: var(--bn-colors-highlights-red-background); +} + .bn-ariakit .bn-comment-actions-wrapper { align-items: start; display: flex; @@ -355,4 +428,4 @@ .bn-ariakit .bn-thread.selected .bn-ak-author-info, .bn-ariakit .bn-thread.selected .bn-ak-expand-sections-prompt { color: var(--bn-colors-selected-text); -} \ No newline at end of file +} diff --git a/packages/ariakit/src/suggestionMenu/SuggestionMenuLoader.tsx b/packages/ariakit/src/suggestionMenu/SuggestionMenuLoader.tsx index ff43ed5ea8..987142824b 100644 --- a/packages/ariakit/src/suggestionMenu/SuggestionMenuLoader.tsx +++ b/packages/ariakit/src/suggestionMenu/SuggestionMenuLoader.tsx @@ -6,13 +6,22 @@ export const SuggestionMenuLoader = forwardRef< HTMLDivElement, ComponentProps["SuggestionMenu"]["Loader"] >((props, ref) => { - const { className, children, ...rest } = props; + const { className, ...rest } = props; assertEmpty(rest); return (
- {children} + {/* Taken from Google Material Icons */} + {/* https://fonts.google.com/icons?selected=Material+Symbols+Rounded:progress_activity:FILL@0;wght@400;GRAD@0;opsz@24&icon.query=load&icon.size=24&icon.color=%23e8eaed&icon.set=Material+Symbols&icon.style=Rounded&icon.platform=web */} + + +
); }); diff --git a/packages/mantine/src/style.css b/packages/mantine/src/style.css index 23149a68e0..05fed84909 100644 --- a/packages/mantine/src/style.css +++ b/packages/mantine/src/style.css @@ -262,7 +262,8 @@ max-height: 100%; position: relative; box-shadow: var(--mantine-shadow-md); - border: calc(0.0625rem * var(--mantine-scale)) solid var(--mantine-color-gray-2); + border: calc(0.0625rem * var(--mantine-scale)) solid + var(--mantine-color-gray-2); border-radius: var(--mantine-radius-default); padding: 4px; } @@ -350,12 +351,15 @@ padding: 8px; } -.bn-suggestion-menu-item-small .bn-mt-suggestion-menu-item-section[data-position="left"] { +.bn-suggestion-menu-item-small + .bn-mt-suggestion-menu-item-section[data-position="left"] { background-color: transparent; padding: 0; } -.bn-suggestion-menu-item-small .bn-mt-suggestion-menu-item-section[data-position="left"] svg { +.bn-suggestion-menu-item-small + .bn-mt-suggestion-menu-item-section[data-position="left"] + svg { height: 14px; width: 14px; } @@ -607,8 +611,8 @@ } .bn-mt-sub-menu-item -> .mantine-Menu-itemLabel -> div:not(.mantine-Menu-dropdown) { + > .mantine-Menu-itemLabel + > div:not(.mantine-Menu-dropdown) { align-items: center; display: flex; justify-content: space-between; @@ -745,23 +749,47 @@ } .bn-mantine -.bn-badge -.mantine-Chip-label -> span:not(.mantine-Chip-iconWrapper) { + .bn-badge + .mantine-Chip-label + > span:not(.mantine-Chip-iconWrapper) { display: inline-flex; gap: 4px; } .bn-mantine -.bn-badge -.mantine-Chip-label -> span:not(.mantine-Chip-iconWrapper) -> span { + .bn-badge + .mantine-Chip-label + > span:not(.mantine-Chip-iconWrapper) + > span { align-items: center; display: inline-flex; justify-content: center; } +/* Combobox styling */ +.bn-mantine .bn-combobox-input, +.bn-mantine .bn-combobox-items:not(:empty) { + background-color: var(--bn-colors-menu-background); + border: var(--bn-border); + border-radius: var(--bn-border-radius-medium); + box-shadow: var(--bn-shadow-medium); + color: var(--bn-colors-menu-text); + gap: 4px; + min-width: 145px; + padding: 2px; +} + +.bn-mantine .bn-combobox-input .bn-combobox-icon, +.bn-mantine .bn-combobox-input .bn-combobox-right-section { + align-items: center; + display: flex; + justify-content: center; +} + +.bn-mantine .bn-combobox-input .bn-combobox-error { + color: var(--bn-colors-highlights-red-background); +} + /* We need to get rid of the checked icon - you can set the icon prop to an empty element (<>), but even so Mantine leaves extra space for the icon, so we just don't display it in CSS instead. */ diff --git a/packages/mantine/src/suggestionMenu/SuggestionMenuLoader.tsx b/packages/mantine/src/suggestionMenu/SuggestionMenuLoader.tsx index 3f1596bfe5..93255e0bd2 100644 --- a/packages/mantine/src/suggestionMenu/SuggestionMenuLoader.tsx +++ b/packages/mantine/src/suggestionMenu/SuggestionMenuLoader.tsx @@ -8,13 +8,11 @@ export const SuggestionMenuLoader = forwardRef< HTMLDivElement, ComponentProps["SuggestionMenu"]["Loader"] >((props, ref) => { - const { - className, - children, // unused, using "dots" instead - ...rest - } = props; + const { className, ...rest } = props; assertEmpty(rest); - return ; + return ( + + ); }); diff --git a/packages/react/src/editor/ComponentsContext.tsx b/packages/react/src/editor/ComponentsContext.tsx index c72c267f1a..2eb161d828 100644 --- a/packages/react/src/editor/ComponentsContext.tsx +++ b/packages/react/src/editor/ComponentsContext.tsx @@ -150,7 +150,6 @@ export type ComponentProps = { }; Loader: { className?: string; - children?: ReactNode; }; }; GridSuggestionMenu: { @@ -360,7 +359,7 @@ export type Components = { }; export const ComponentsContext = createContext( - undefined + undefined, ); export function useComponentsContext(): Components | undefined { diff --git a/packages/shadcn/src/form/TextInput.tsx b/packages/shadcn/src/form/TextInput.tsx index 71a3baeeb2..9a3bd12cca 100644 --- a/packages/shadcn/src/form/TextInput.tsx +++ b/packages/shadcn/src/form/TextInput.tsx @@ -3,6 +3,7 @@ import { ComponentProps } from "@blocknote/react"; import { forwardRef } from "react"; import { useShadCNComponentsContext } from "../ShadCNComponentsContext.js"; +import { cn } from "../lib/utils.js"; export const TextInput = forwardRef< HTMLInputElement, @@ -30,41 +31,34 @@ export const TextInput = forwardRef< const ShadCNComponents = useShadCNComponentsContext()!; - if (!label) { - return ( - - ); - } - return ( -
- - {label} - - +
+ {icon} +
+ {label && ( + + {label} + + )} + +
+ {rightSection}
); }); diff --git a/packages/shadcn/src/style.css b/packages/shadcn/src/style.css index 00c52b6180..00707adb6b 100644 --- a/packages/shadcn/src/style.css +++ b/packages/shadcn/src/style.css @@ -189,3 +189,8 @@ font-size: 14px; font-style: italic; } + +.bn-shadcn .bn-combobox-error { + color: var(--bn-colors-highlights-red-background); + font-weight: bold; +} diff --git a/packages/shadcn/src/suggestionMenu/SuggestionMenuLoader.tsx b/packages/shadcn/src/suggestionMenu/SuggestionMenuLoader.tsx index ff43ed5ea8..b8ef2013b2 100644 --- a/packages/shadcn/src/suggestionMenu/SuggestionMenuLoader.tsx +++ b/packages/shadcn/src/suggestionMenu/SuggestionMenuLoader.tsx @@ -2,17 +2,28 @@ import { assertEmpty } from "@blocknote/core"; import { ComponentProps } from "@blocknote/react"; import { forwardRef } from "react"; +import { cn } from "../lib/utils.js"; + export const SuggestionMenuLoader = forwardRef< HTMLDivElement, ComponentProps["SuggestionMenu"]["Loader"] >((props, ref) => { - const { className, children, ...rest } = props; + const { className, ...rest } = props; assertEmpty(rest); return ( -
- {children} +
+ {/* Taken from Google Material Icons */} + {/* https://fonts.google.com/icons?selected=Material+Symbols+Rounded:progress_activity:FILL@0;wght@400;GRAD@0;opsz@24&icon.query=load&icon.size=24&icon.color=%23e8eaed&icon.set=Material+Symbols&icon.style=Rounded&icon.platform=web */} + + +
); }); diff --git a/packages/xl-ai-server/README.md b/packages/xl-ai-server/README.md index 852eea310b..ca6c275232 100644 --- a/packages/xl-ai-server/README.md +++ b/packages/xl-ai-server/README.md @@ -17,7 +17,7 @@ Configure your environment variables according to `.env.example`. ## Running (dev mode): mkcert localhost - npm run dev + pnpm run dev ## Client Usage diff --git a/packages/xl-ai/src/components/AIMenu/AIMenu.tsx b/packages/xl-ai/src/components/AIMenu/AIMenu.tsx index 544fa4dd74..2a071faa2a 100644 --- a/packages/xl-ai/src/components/AIMenu/AIMenu.tsx +++ b/packages/xl-ai/src/components/AIMenu/AIMenu.tsx @@ -1,8 +1,9 @@ -import { useBlockNoteEditor } from "@blocknote/react"; -import { useCallback, useEffect, useMemo, useState } from "react"; -// import { useAIDictionary } from "../../i18n/useAIDictionary"; import { BlockNoteEditor } from "@blocknote/core"; +import { useBlockNoteEditor, useComponentsContext } from "@blocknote/react"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { RiSparkling2Fill } from "react-icons/ri"; import { useStore } from "zustand"; + import { getAIExtension } from "../../AIExtension.js"; import { useAIDictionary } from "../../i18n/useAIDictionary.js"; import { PromptSuggestionMenu } from "./PromptSuggestionMenu.js"; @@ -22,7 +23,7 @@ export const AIMenu = (props: { | "ai-writing" | "error" | "user-reviewing" - | "closed" + | "closed", ) => AIMenuSuggestionItem[]; onManualPromptSubmit?: (userPrompt: string) => void; }) => { @@ -30,10 +31,12 @@ export const AIMenu = (props: { const [prompt, setPrompt] = useState(""); const dict = useAIDictionary(); + const Components = useComponentsContext()!; + const ai = getAIExtension(editor); const aiResponseStatus = useStore(ai.store, (state) => - state.aiMenuState !== "closed" ? state.aiMenuState.status : "closed" + state.aiMenuState !== "closed" ? state.aiMenuState.status : "closed", ); const { items: externalItems } = props; @@ -72,7 +75,7 @@ export const AIMenu = (props: { useSelection: editor.getSelection() !== undefined, }); }, - [ai, editor] + [ai, editor], ); useEffect(() => { @@ -82,6 +85,45 @@ export const AIMenu = (props: { } }, [aiResponseStatus]); + const placeholder = useMemo(() => { + if (aiResponseStatus === "thinking") { + return dict.formatting_toolbar.ai.thinking; + } else if (aiResponseStatus === "ai-writing") { + return dict.formatting_toolbar.ai.editing; + } else if (aiResponseStatus === "error") { + return dict.formatting_toolbar.ai.error; + } + + return dict.formatting_toolbar.ai.input_placeholder; + }, [aiResponseStatus, dict]); + + const rightSection = useMemo(() => { + if (aiResponseStatus === "thinking" || aiResponseStatus === "ai-writing") { + return ( + + ); + } else if (aiResponseStatus === "error") { + return ( +
+ {/* Taken from Google Material Icons */} + {/* https://fonts.google.com/icons?selected=Material+Symbols+Rounded:error:FILL@0;wght@400;GRAD@0;opsz@24&icon.query=error&icon.size=24&icon.color=%23e8eaed&icon.set=Material+Symbols&icon.style=Rounded&icon.platform=web */} + + + +
+ ); + } + + return undefined; + }, [Components, aiResponseStatus]); + return ( + +
} + rightSection={rightSection} /> ); }; diff --git a/packages/xl-ai/src/components/AIMenu/PromptSuggestionMenu.tsx b/packages/xl-ai/src/components/AIMenu/PromptSuggestionMenu.tsx index 605de5e77a..b18e423a8a 100644 --- a/packages/xl-ai/src/components/AIMenu/PromptSuggestionMenu.tsx +++ b/packages/xl-ai/src/components/AIMenu/PromptSuggestionMenu.tsx @@ -7,23 +7,22 @@ import { import { ChangeEvent, KeyboardEvent, + ReactNode, useCallback, useEffect, useMemo, useState, } from "react"; -import { RiSparkling2Fill } from "react-icons/ri"; - export type PromptSuggestionMenuProps = { items: DefaultReactSuggestionItem[]; onManualPromptSubmit: (userPrompt: string) => void; promptText?: string; onPromptTextChange?: (userPrompt: string) => void; + icon?: ReactNode; + rightSection?: ReactNode; placeholder?: string; disabled?: boolean; - loading?: boolean; - error?: string; }; export const PromptSuggestionMenu = (props: PromptSuggestionMenuProps) => { @@ -99,7 +98,7 @@ export const PromptSuggestionMenu = (props: PromptSuggestionMenuProps) => { className={"bn-combobox-input"} name={"ai-prompt"} variant={"large"} - icon={} + icon={props.icon} value={promptTextToUse || ""} autoFocus={true} placeholder={props.placeholder} @@ -107,8 +106,7 @@ export const PromptSuggestionMenu = (props: PromptSuggestionMenuProps) => { onKeyDown={handleKeyDown} onChange={handleChange} autoComplete={"off"} - // TODO: loader or error - // rightSection={props.loading ? : props.error} + rightSection={props.rightSection} /> .bn-combobox-input, -div:not(.bn-popover-content) > .bn-combobox-items:not(:empty) { - background-color: var(--bn-colors-menu-background); - border: var(--bn-border); - border-radius: var(--bn-border-radius-medium); - box-shadow: var(--bn-shadow-medium); - color: var(--bn-colors-menu-text); - gap: 4px; - min-width: 145px; - padding: 2px; -} - .bn-combobox-items { max-width: 50%; } +.bn-combobox-items:empty { + display: none; +} + div[data-type="modification"] { display: inline; }