Files
ai-agent/web/components/content-editor/markdown-editor.tsx
T

169 lines
4.2 KiB
TypeScript
Raw Normal View History

2026-04-09 10:01:23 +08:00
"use client"
import {
forwardRef,
useId,
useImperativeHandle,
useMemo,
useRef,
} from "react"
import { Maximize2Icon, Minimize2Icon } from "lucide-react"
import { MdEditor, NormalToolbar, type ExposeParam } from "md-editor-rt"
import { useTheme } from "next-themes"
import "./markdown-editor.css"
import { EditorModeSwitch } from "./editor-mode-switch"
import type { ContentMode, UploadImageHandler } from "./types"
2026-05-25 12:06:15 +08:00
import { useI18n } from "@/i18n/provider"
2026-04-09 10:01:23 +08:00
export type MarkdownEditorRef = {
focus: () => void
}
type MarkdownEditorProps = {
value: string
onChange: (nextValue: string) => void
mode: ContentMode
allowedModes: ReadonlyArray<ContentMode>
2026-04-09 10:01:23 +08:00
onModeChange: (nextMode: ContentMode) => void
fullscreen: boolean
onToggleFullscreen: () => void
placeholder?: string
disabled?: boolean
onUploadImage?: UploadImageHandler
height: string
}
export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>(
function MarkdownEditor(
{
value,
onChange,
mode,
allowedModes,
2026-04-09 10:01:23 +08:00
onModeChange,
fullscreen,
onToggleFullscreen,
placeholder = "",
disabled = false,
onUploadImage,
height,
},
ref
) {
2026-05-25 12:06:15 +08:00
const t = useI18n()
2026-04-09 10:01:23 +08:00
const editorId = useId()
const editorRef = useRef<ExposeParam>(null)
const { resolvedTheme } = useTheme()
const showModeSwitch = allowedModes.length > 1
2026-04-09 10:01:23 +08:00
const defToolbars = useMemo(
() => {
const fullscreenToolbar = (
<NormalToolbar
2026-04-09 10:01:23 +08:00
key="toggle-fullscreen"
2026-05-25 12:06:15 +08:00
title={fullscreen ? t("editor.exitFullscreen") : t("editor.fullscreen")}
2026-04-09 10:01:23 +08:00
disabled={disabled}
onClick={onToggleFullscreen}
>
{fullscreen ? (
<Minimize2Icon className="h-[16px] w-[16px]" />
) : (
<Maximize2Icon className="h-[16px] w-[16px]" />
)}
</NormalToolbar>
)
if (!showModeSwitch) {
return [fullscreenToolbar]
}
return [
<EditorModeSwitch
key="mode-switch"
value={mode}
allowedModes={allowedModes}
disabled={disabled}
onChange={onModeChange}
/>,
fullscreenToolbar,
]
},
[allowedModes, disabled, fullscreen, mode, onModeChange, onToggleFullscreen, showModeSwitch, t]
)
const toolbars = useMemo(
() => [
...(showModeSwitch ? [0, "-"] : []),
"bold",
"underline",
"italic",
"strikeThrough",
"-",
"title",
"quote",
"unorderedList",
"orderedList",
"-",
"codeRow",
"code",
"link",
"image",
"-",
"revoke",
"next",
showModeSwitch ? 1 : 0,
"=",
"preview",
"previewOnly",
2026-04-09 10:01:23 +08:00
],
[showModeSwitch]
2026-04-09 10:01:23 +08:00
)
useImperativeHandle(ref, () => ({
focus() {
editorRef.current?.focus()
},
}))
return (
<div
className="w-full rounded-lg border bg-background"
style={{ height }}
>
<div className="content-editor-markdown h-full">
<MdEditor
ref={editorRef}
id={editorId}
value={value}
onChange={onChange}
theme={resolvedTheme === "dark" ? "dark" : "light"}
preview={false}
toolbars={toolbars}
2026-04-09 10:01:23 +08:00
defToolbars={defToolbars}
footers={[]}
noMermaid
noKatex
noHighlight
placeholder={placeholder}
disabled={disabled}
style={{ height: "100%" }}
onUploadImg={
onUploadImage
? async (files, callback) => {
const uploadedUrls: string[] = []
for (const file of files) {
const uploaded = await onUploadImage(file)
if (uploaded?.url) {
uploadedUrls.push(uploaded.url)
}
}
callback(uploadedUrls)
}
: undefined
}
/>
</div>
</div>
)
}
)