Skip to content

Commit

Permalink
enhance: wysiwyg introductions (#1797)
Browse files Browse the repository at this point in the history
* enhance: add introductions wysiwyg

* fix: textarea starts small, expands on focus fix

* styling

* markdown.css

* Update markdown.css

* Update markdown.css

* Update markdown.tsx
  • Loading branch information
ivyjeong13 authored Feb 20, 2025
1 parent 4ce2b43 commit 51a74f6
Show file tree
Hide file tree
Showing 7 changed files with 1,752 additions and 39 deletions.
19 changes: 9 additions & 10 deletions ui/admin/app/components/agent/AgentIntroForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";

import {
ControlledAutosizeTextarea,
ControlledInput,
} from "~/components/form/controlledInputs";
import { ControlledInput } from "~/components/form/controlledInputs";
import { Button } from "~/components/ui/button";
import { CardDescription } from "~/components/ui/card";
import { Form } from "~/components/ui/form";
import { MarkdownEditor } from "~/components/ui/markdown";

export { MDXEditor } from "@mdxeditor/editor";

const formSchema = z.object({
introductionMessage: z.string().optional(),
Expand Down Expand Up @@ -77,12 +77,11 @@ export function AgentIntroForm({
The introduction is <b>Markdown</b> syntax supported.
</CardDescription>

<ControlledAutosizeTextarea
control={form.control}
autoComplete="off"
name="introductionMessage"
maxHeight={300}
placeholder="Give the agent a friendly introduction message."
<MarkdownEditor
markdown={form.watch("introductionMessage") ?? ""}
onChange={(markdown) =>
form.setValue("introductionMessage", markdown)
}
/>

<p className="flex items-end justify-between pt-2 font-normal">
Expand Down
1 change: 0 additions & 1 deletion ui/admin/app/components/agent/__tests__/Agent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ describe(Agent, () => {
"placeholder",
],
["prompt", "Instructions", "textbox", 2],
["introductionMessage", "Introductions", "textbox"],
])("Updating %s triggers save", async (field, searchFor, as, index = 0) => {
const putSpy = setupServer(mockedAgent);
render(
Expand Down
20 changes: 20 additions & 0 deletions ui/admin/app/components/ui/markdown.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[class*="_selectTrigger"] {
@apply bg-transparent text-foreground;
}
[class*="_linkDialogPopoverContent"],
[class*="_dialogContent"],
.mdxeditor-select-content {
@apply bg-background;
}
.mdxeditor-toolbar {
@apply bg-background-secondary;
}
.mdxeditor-popup-container input {
@apply bg-background;
}
[class*="_primaryButton"] {
@apply relative flex h-9 flex-row items-center justify-center gap-2 whitespace-nowrap rounded-full border-0 bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/80 hover:shadow-inner focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-foreground disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0;
}
[class*="_secondaryButton"] {
@apply relative inline-flex h-9 items-center justify-center gap-2 whitespace-nowrap rounded-full border-0 bg-secondary px-4 py-2 text-sm font-medium text-secondary-foreground shadow-sm transition-colors hover:bg-secondary/80 hover:shadow-inner focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0;
}
99 changes: 99 additions & 0 deletions ui/admin/app/components/ui/markdown.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
import {
BlockTypeSelect,
BoldItalicUnderlineToggles,
CodeToggle,
CreateLink,
InsertImage,
ListsToggle,
MDXEditor,
MDXEditorMethods,
Separator,
StrikeThroughSupSubToggles,
UndoRedo,
codeBlockPlugin,
headingsPlugin,
imagePlugin,
linkDialogPlugin,
linkPlugin,
listsPlugin,
markdownShortcutPlugin,
quotePlugin,
tablePlugin,
thematicBreakPlugin,
toolbarPlugin,
} from "@mdxeditor/editor";
import "@mdxeditor/editor/style.css";
import { useEffect, useRef, useState } from "react";
import ReactMarkdown, { defaultUrlTransform } from "react-markdown";
import rehypeExternalLinks from "rehype-external-links";
import rehypeRaw from "rehype-raw";
Expand All @@ -7,6 +33,8 @@ import remarkGfm from "remark-gfm";
import { cn } from "~/lib/utils/cn";

import { CustomMarkdownComponents } from "~/components/react-markdown";
import { useTheme } from "~/components/theme";
import "~/components/ui/markdown.css";

// Allow links for file references in messages if it starts with file://, otherwise this will cause an empty href and cause app to reload when clicking on it
export const urlTransformAllowFiles = (u: string) => {
Expand Down Expand Up @@ -51,3 +79,74 @@ export function Markdown({
</ReactMarkdown>
);
}

export function MarkdownEditor({
className,
markdown,
onChange,
}: {
className?: string;
markdown: string;
onChange: (markdown: string) => void;
}) {
const { isDark } = useTheme();
const ref = useRef<MDXEditorMethods>(null);
const [isExpanded, setIsExpanded] = useState(false);

useEffect(() => {
if (ref.current) {
ref.current.setMarkdown(markdown);
}
}, [markdown]);

return (
<div onFocusCapture={() => setIsExpanded(true)}>
<MDXEditor
ref={ref}
className={cn(
{
"dark-theme": isDark,
},
"flex flex-col rounded-md p-0.5 ring-1 ring-inset ring-input has-[:focus-visible]:outline has-[:focus-visible]:outline-1 has-[:focus-visible]:outline-ring",
className
)}
contentEditableClassName={cn(
isExpanded ? "h-[300px] overflow-y-auto" : "h-[54px] overflow-hidden"
)}
markdown={markdown}
onChange={onChange}
plugins={[
toolbarPlugin({
toolbarContents: () => (
<>
<UndoRedo />
<Separator />
<BoldItalicUnderlineToggles />
<CodeToggle />
<Separator />
<StrikeThroughSupSubToggles />
<Separator />
<ListsToggle />
<Separator />
<BlockTypeSelect />
<Separator />
<CreateLink />
<InsertImage />
</>
),
}),
headingsPlugin(),
imagePlugin(),
linkPlugin(),
linkDialogPlugin(),
tablePlugin(),
listsPlugin(),
thematicBreakPlugin(),
markdownShortcutPlugin(),
codeBlockPlugin({ defaultCodeBlockLanguage: "txt" }),
quotePlugin(),
]}
/>
</div>
);
}
36 changes: 25 additions & 11 deletions ui/admin/app/components/ui/textarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ const useAutosizeTextArea = ({
maxHeight = Number.MAX_SAFE_INTEGER,
minHeight = 0,
}: UseAutosizeTextAreaProps) => {
const [init, setInit] = React.useState(true);
const initRef = React.useRef(true);

const resize = React.useCallback(
(node: HTMLTextAreaElement) => {
Expand All @@ -121,13 +121,14 @@ const useAutosizeTextArea = ({

const offsetBorder = 2;

if (init) {
if (initRef.current) {
node.style.minHeight = `${minHeight + offsetBorder}px`;
if (maxHeight > minHeight) {
node.style.maxHeight = `${maxHeight}px`;
}
node.style.height = `${minHeight + offsetBorder}px`;
setInit(false);
initRef.current = false;
return;
}

const newHeight = Math.min(
Expand All @@ -137,25 +138,38 @@ const useAutosizeTextArea = ({

node.style.height = `${newHeight}px`;
},
[maxHeight, minHeight, setInit, init]
[maxHeight, minHeight]
);

const initResizer = React.useCallback(
(node: HTMLTextAreaElement) => {
node.onkeyup = () => resize(node);
node.onfocus = () => resize(node);
node.oninput = () => resize(node);
node.onresize = () => resize(node);
node.onchange = () => resize(node);
const handleResize = () => resize(node);

node.addEventListener("input", handleResize);
node.addEventListener("focus", handleResize);
node.addEventListener("change", handleResize);
node.addEventListener("keyup", handleResize);
node.addEventListener("resize", handleResize);
if (initRef.current) {
resize(node);
}

resize(node);
// Cleanup function to remove event listeners
return () => {
node.removeEventListener("input", handleResize);
node.removeEventListener("focus", handleResize);
node.removeEventListener("change", handleResize);
node.removeEventListener("keyup", handleResize);
node.removeEventListener("resize", handleResize);
};
},
[resize]
);

React.useEffect(() => {
if (textAreaRef) {
initResizer(textAreaRef);
const cleanup = initResizer(textAreaRef);
return cleanup;
}
}, [initResizer, textAreaRef]);

Expand Down
1 change: 1 addition & 0 deletions ui/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@dnd-kit/utilities": "^3.2.2",
"@gptscript-ai/gptscript": "^0.9.5-rc5",
"@hookform/resolvers": "^3.9.0",
"@mdxeditor/editor": "^3.23.2",
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.2",
Expand Down
Loading

0 comments on commit 51a74f6

Please sign in to comment.