Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 9 additions & 17 deletions apps/web/app/(app)/[emailAccountId]/assistant/RulesPrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
saveRulesPromptAction,
generateRulesPromptAction,
} from "@/utils/actions/ai-rule";
import { Input } from "@/components/Input";
import { SimpleRichTextEditor } from "@/components/editor/SimpleRichTextEditor";
import {
saveRulesPromptBody,
type SaveRulesPromptBody,
Expand Down Expand Up @@ -243,27 +243,19 @@ function RulesPromptForm({
</Label>

<div className="mt-1.5 space-y-4">
<Input
className="min-h-[300px] border-input"
<SimpleRichTextEditor
registerProps={register("rulesPrompt", { required: true })}
name="rulesPrompt"
type="text"
autosizeTextarea
rows={30}
maxRows={50}
error={errors.rulesPrompt}
value={currentPrompt}
minHeight={600}
placeholder={`Here's an example of what your prompt might look like:

${personas.other.promptArray.slice(0, 1).join("\n")}

If someone asks about pricing, reply with:
---
Hi NAME!

I'm currently offering a 10% discount for the first 10 customers.

Let me know if you're interested!
---`}
* ${personas.other.promptArray[0]}
* ${personas.other.promptArray[1]}
* If someone asks about pricing, reply with:
> Hi NAME!
> I'm currently offering a 10% discount. Let me know if you're interested!`}
/>

<div className="flex flex-wrap gap-2">
Expand Down
30 changes: 30 additions & 0 deletions apps/web/components/editor/SimpleRichTextEditor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* Tiptap Editor Highlight Styles */
.tiptap-editor .tiptap-highlight,
.simple-rich-editor .simple-editor-highlight,
.simple-rich-editor code {
@apply inline-block align-baseline rounded-md bg-blue-100 px-2 py-0.5 text-sm font-medium text-blue-900;
@apply border border-blue-200;
font-family: inherit; /* Override monospace font */
}

/* Override prose code styles */
.simple-rich-editor.prose :where(code):not(:where([class~="not-prose"] *)) {
@apply bg-blue-100 text-blue-900 border-blue-200;
font-weight: 500;
}

.simple-rich-editor.prose :where(code):not(:where([class~="not-prose"] *))::before,
.simple-rich-editor.prose :where(code):not(:where([class~="not-prose"] *))::after {
content: ""; /* Remove backticks */
}

/* Dark mode highlight styles */
.dark .tiptap-editor .tiptap-highlight,
.dark .simple-rich-editor .simple-editor-highlight,
.dark .simple-rich-editor code {
@apply bg-blue-950 border-blue-800 text-blue-100;
}

.dark .simple-rich-editor.prose :where(code):not(:where([class~="not-prose"] *)) {
@apply bg-blue-950 text-blue-100 border-blue-800;
}
181 changes: 181 additions & 0 deletions apps/web/components/editor/SimpleRichTextEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
"use client";

import { useEditor, EditorContent, type Editor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { Markdown } from "tiptap-markdown";
import { Placeholder } from "@tiptap/extension-placeholder";
import { useCallback, useEffect, useImperativeHandle, forwardRef } from "react";
import { cn } from "@/utils";
import type { UseFormRegisterReturn } from "react-hook-form";
import type { FieldError } from "react-hook-form";
import "./SimpleRichTextEditor.css";

interface SimpleRichTextEditorProps {
registerProps?: UseFormRegisterReturn;
name?: string;
error?: FieldError;
placeholder?: string;
className?: string;
disabled?: boolean;
defaultValue?: string;
value?: string;
minHeight?: number;
}

export interface SimpleRichTextEditorRef {
insertText: (text: string) => void;
appendText: (text: string) => void;
getMarkdown: () => string;
}

export const SimpleRichTextEditor = forwardRef<
SimpleRichTextEditorRef,
SimpleRichTextEditorProps
>(
(
{
registerProps,
name,
error,
placeholder,
className,
disabled,
defaultValue = "",
value,
minHeight = 300,
},
ref,
) => {
const editor = useEditor({
extensions: [
StarterKit.configure({
italic: false,
strike: false,
code: {
HTMLAttributes: {
class: "simple-editor-highlight",
},
},
codeBlock: false,
blockquote: {},
horizontalRule: false,
dropcursor: false,
gapcursor: false,
bulletList: {
keepMarks: true,
keepAttributes: false,
},
orderedList: {
keepMarks: true,
keepAttributes: false,
},
}),
...(placeholder
? [
Placeholder.configure({
placeholder,
showOnlyWhenEditable: true,
showOnlyCurrent: false,
}),
]
: []),
Markdown.configure({
html: false,
transformPastedText: true,
transformCopiedText: true,
breaks: false,
linkify: false,
}),
],
content: defaultValue || "",
onUpdate: useCallback(
({ editor }: { editor: Editor }) => {
const markdown = editor.storage.markdown.getMarkdown();
if (registerProps?.onChange) {
registerProps.onChange({
target: { name: name || registerProps.name, value: markdown },
});
}
},
[registerProps, name],
),
editorProps: {
attributes: {
class: cn(
"p-3 max-w-none focus:outline-none max-w-none simple-rich-editor",
"prose prose-sm",
"prose-headings:font-cal prose-headings:text-foreground",
"prose-p:text-foreground prose-li:text-foreground",
"prose-strong:text-foreground prose-strong:font-semibold",
"prose-ul:text-foreground prose-ol:text-foreground",
"[&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
// Placeholder styles
"[&_p.is-editor-empty:first-child::before]:content-[attr(data-placeholder)]",
"[&_p.is-editor-empty:first-child::before]:float-left",
"[&_p.is-editor-empty:first-child::before]:text-muted-foreground",
"[&_p.is-editor-empty:first-child::before]:pointer-events-none",
"[&_p.is-editor-empty:first-child::before]:h-0",
disabled && "opacity-50 cursor-not-allowed",
),
style: `min-height: ${minHeight}px`,
...(placeholder && { "data-placeholder": placeholder }),
},
},
editable: !disabled,
});

// Expose editor methods via ref
useImperativeHandle(
ref,
() => ({
insertText: (text: string) => {
if (editor) {
editor.chain().focus().insertContent(text).run();
}
},
appendText: (text: string) => {
if (editor) {
const currentContent = editor.storage.markdown.getMarkdown();
const newContent = currentContent
? `${currentContent}\n${text}`
: text;
editor.commands.setContent(newContent);
}
},
getMarkdown: () => {
return editor?.storage.markdown.getMarkdown() || "";
},
}),
[editor],
);

// Update editor content when value prop changes
useEffect(() => {
if (
editor &&
value !== undefined &&
value !== editor.storage.markdown.getMarkdown()
) {
editor.commands.setContent(value);
}
}, [value, editor]);

return (
<div className={cn("relative w-full", className)}>
<div
className={cn(
"rounded-md border border-input bg-background",
"focus-within:border-ring focus-within:ring-1 focus-within:ring-ring",
error &&
"border-red-500 focus-within:border-red-500 focus-within:ring-red-500",
disabled && "cursor-not-allowed opacity-50",
)}
style={{ minHeight }}
>
<EditorContent editor={editor} />
</div>
{error && <p className="mt-1 text-sm text-red-500">{error.message}</p>}
</div>
);
},
);
7 changes: 4 additions & 3 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,10 @@
"@tanstack/react-query": "5.79.0",
"@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "3.13.9",
"@tiptap/pm": "2.12.0",
"@tiptap/react": "2.12.0",
"@tiptap/starter-kit": "2.12.0",
"@tiptap/extension-placeholder": "2.26.1",
"@tiptap/pm": "2.26.1",
"@tiptap/react": "2.26.1",
"@tiptap/starter-kit": "2.26.1",
"@tremor/react": "3.18.7",
"@upstash/qstash": "2.8.1",
"@upstash/redis": "1.34.9",
Expand Down
Loading
Loading