Skip to content

Commit 51a74f6

Browse files
authored
enhance: wysiwyg introductions (#1797)
* enhance: add introductions wysiwyg * fix: textarea starts small, expands on focus fix * styling * markdown.css * Update markdown.css * Update markdown.css * Update markdown.tsx
1 parent 4ce2b43 commit 51a74f6

File tree

7 files changed

+1752
-39
lines changed

7 files changed

+1752
-39
lines changed

ui/admin/app/components/agent/AgentIntroForm.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import { useEffect } from "react";
44
import { useForm } from "react-hook-form";
55
import { z } from "zod";
66

7-
import {
8-
ControlledAutosizeTextarea,
9-
ControlledInput,
10-
} from "~/components/form/controlledInputs";
7+
import { ControlledInput } from "~/components/form/controlledInputs";
118
import { Button } from "~/components/ui/button";
129
import { CardDescription } from "~/components/ui/card";
1310
import { Form } from "~/components/ui/form";
11+
import { MarkdownEditor } from "~/components/ui/markdown";
12+
13+
export { MDXEditor } from "@mdxeditor/editor";
1414

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

80-
<ControlledAutosizeTextarea
81-
control={form.control}
82-
autoComplete="off"
83-
name="introductionMessage"
84-
maxHeight={300}
85-
placeholder="Give the agent a friendly introduction message."
80+
<MarkdownEditor
81+
markdown={form.watch("introductionMessage") ?? ""}
82+
onChange={(markdown) =>
83+
form.setValue("introductionMessage", markdown)
84+
}
8685
/>
8786

8887
<p className="flex items-end justify-between pt-2 font-normal">

ui/admin/app/components/agent/__tests__/Agent.test.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ describe(Agent, () => {
9090
"placeholder",
9191
],
9292
["prompt", "Instructions", "textbox", 2],
93-
["introductionMessage", "Introductions", "textbox"],
9493
])("Updating %s triggers save", async (field, searchFor, as, index = 0) => {
9594
const putSpy = setupServer(mockedAgent);
9695
render(
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[class*="_selectTrigger"] {
2+
@apply bg-transparent text-foreground;
3+
}
4+
[class*="_linkDialogPopoverContent"],
5+
[class*="_dialogContent"],
6+
.mdxeditor-select-content {
7+
@apply bg-background;
8+
}
9+
.mdxeditor-toolbar {
10+
@apply bg-background-secondary;
11+
}
12+
.mdxeditor-popup-container input {
13+
@apply bg-background;
14+
}
15+
[class*="_primaryButton"] {
16+
@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;
17+
}
18+
[class*="_secondaryButton"] {
19+
@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;
20+
}

ui/admin/app/components/ui/markdown.tsx

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,29 @@
1+
import {
2+
BlockTypeSelect,
3+
BoldItalicUnderlineToggles,
4+
CodeToggle,
5+
CreateLink,
6+
InsertImage,
7+
ListsToggle,
8+
MDXEditor,
9+
MDXEditorMethods,
10+
Separator,
11+
StrikeThroughSupSubToggles,
12+
UndoRedo,
13+
codeBlockPlugin,
14+
headingsPlugin,
15+
imagePlugin,
16+
linkDialogPlugin,
17+
linkPlugin,
18+
listsPlugin,
19+
markdownShortcutPlugin,
20+
quotePlugin,
21+
tablePlugin,
22+
thematicBreakPlugin,
23+
toolbarPlugin,
24+
} from "@mdxeditor/editor";
25+
import "@mdxeditor/editor/style.css";
26+
import { useEffect, useRef, useState } from "react";
127
import ReactMarkdown, { defaultUrlTransform } from "react-markdown";
228
import rehypeExternalLinks from "rehype-external-links";
329
import rehypeRaw from "rehype-raw";
@@ -7,6 +33,8 @@ import remarkGfm from "remark-gfm";
733
import { cn } from "~/lib/utils/cn";
834

935
import { CustomMarkdownComponents } from "~/components/react-markdown";
36+
import { useTheme } from "~/components/theme";
37+
import "~/components/ui/markdown.css";
1038

1139
// 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
1240
export const urlTransformAllowFiles = (u: string) => {
@@ -51,3 +79,74 @@ export function Markdown({
5179
</ReactMarkdown>
5280
);
5381
}
82+
83+
export function MarkdownEditor({
84+
className,
85+
markdown,
86+
onChange,
87+
}: {
88+
className?: string;
89+
markdown: string;
90+
onChange: (markdown: string) => void;
91+
}) {
92+
const { isDark } = useTheme();
93+
const ref = useRef<MDXEditorMethods>(null);
94+
const [isExpanded, setIsExpanded] = useState(false);
95+
96+
useEffect(() => {
97+
if (ref.current) {
98+
ref.current.setMarkdown(markdown);
99+
}
100+
}, [markdown]);
101+
102+
return (
103+
<div onFocusCapture={() => setIsExpanded(true)}>
104+
<MDXEditor
105+
ref={ref}
106+
className={cn(
107+
{
108+
"dark-theme": isDark,
109+
},
110+
"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",
111+
className
112+
)}
113+
contentEditableClassName={cn(
114+
isExpanded ? "h-[300px] overflow-y-auto" : "h-[54px] overflow-hidden"
115+
)}
116+
markdown={markdown}
117+
onChange={onChange}
118+
plugins={[
119+
toolbarPlugin({
120+
toolbarContents: () => (
121+
<>
122+
<UndoRedo />
123+
<Separator />
124+
<BoldItalicUnderlineToggles />
125+
<CodeToggle />
126+
<Separator />
127+
<StrikeThroughSupSubToggles />
128+
<Separator />
129+
<ListsToggle />
130+
<Separator />
131+
<BlockTypeSelect />
132+
<Separator />
133+
<CreateLink />
134+
<InsertImage />
135+
</>
136+
),
137+
}),
138+
headingsPlugin(),
139+
imagePlugin(),
140+
linkPlugin(),
141+
linkDialogPlugin(),
142+
tablePlugin(),
143+
listsPlugin(),
144+
thematicBreakPlugin(),
145+
markdownShortcutPlugin(),
146+
codeBlockPlugin({ defaultCodeBlockLanguage: "txt" }),
147+
quotePlugin(),
148+
]}
149+
/>
150+
</div>
151+
);
152+
}

ui/admin/app/components/ui/textarea.tsx

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ const useAutosizeTextArea = ({
112112
maxHeight = Number.MAX_SAFE_INTEGER,
113113
minHeight = 0,
114114
}: UseAutosizeTextAreaProps) => {
115-
const [init, setInit] = React.useState(true);
115+
const initRef = React.useRef(true);
116116

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

122122
const offsetBorder = 2;
123123

124-
if (init) {
124+
if (initRef.current) {
125125
node.style.minHeight = `${minHeight + offsetBorder}px`;
126126
if (maxHeight > minHeight) {
127127
node.style.maxHeight = `${maxHeight}px`;
128128
}
129129
node.style.height = `${minHeight + offsetBorder}px`;
130-
setInit(false);
130+
initRef.current = false;
131+
return;
131132
}
132133

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

138139
node.style.height = `${newHeight}px`;
139140
},
140-
[maxHeight, minHeight, setInit, init]
141+
[maxHeight, minHeight]
141142
);
142143

143144
const initResizer = React.useCallback(
144145
(node: HTMLTextAreaElement) => {
145-
node.onkeyup = () => resize(node);
146-
node.onfocus = () => resize(node);
147-
node.oninput = () => resize(node);
148-
node.onresize = () => resize(node);
149-
node.onchange = () => resize(node);
146+
const handleResize = () => resize(node);
147+
148+
node.addEventListener("input", handleResize);
149+
node.addEventListener("focus", handleResize);
150+
node.addEventListener("change", handleResize);
151+
node.addEventListener("keyup", handleResize);
152+
node.addEventListener("resize", handleResize);
153+
if (initRef.current) {
154+
resize(node);
155+
}
150156

151-
resize(node);
157+
// Cleanup function to remove event listeners
158+
return () => {
159+
node.removeEventListener("input", handleResize);
160+
node.removeEventListener("focus", handleResize);
161+
node.removeEventListener("change", handleResize);
162+
node.removeEventListener("keyup", handleResize);
163+
node.removeEventListener("resize", handleResize);
164+
};
152165
},
153166
[resize]
154167
);
155168

156169
React.useEffect(() => {
157170
if (textAreaRef) {
158-
initResizer(textAreaRef);
171+
const cleanup = initResizer(textAreaRef);
172+
return cleanup;
159173
}
160174
}, [initResizer, textAreaRef]);
161175

ui/admin/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"@dnd-kit/utilities": "^3.2.2",
2323
"@gptscript-ai/gptscript": "^0.9.5-rc5",
2424
"@hookform/resolvers": "^3.9.0",
25+
"@mdxeditor/editor": "^3.23.2",
2526
"@monaco-editor/react": "^4.6.0",
2627
"@radix-ui/react-accordion": "^1.2.0",
2728
"@radix-ui/react-alert-dialog": "^1.1.2",

0 commit comments

Comments
 (0)