Normalize AI product HTML for the rich text description editor.

Sanitize model output to an allowlisted fragment and convert plain/markdown fallbacks so description text stays inside the advanced editor.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-10 20:44:35 +03:30
co-authored by Cursor
parent 8d59166fe8
commit 404c008ced
5 changed files with 39 additions and 7 deletions
@@ -60,7 +60,7 @@ export function buildProductDescriptionAiPrompt(ctx: {
const category = ctx.categoryName || '—'
if (ctx.locale === 'fa') {
return `یک توضیحات فنی محصول به صورت HTML بنویس.
return `یک توضیحات فنی محصول به صورت HTML برای ادیتور متن پیشرفته بنویس.
نام محصول (فارسی): ${nameFa}
نام محصول (انگلیسی): ${nameEn}
@@ -77,10 +77,11 @@ export function buildProductDescriptionAiPrompt(ctx: {
- از دعوت به خرید، تخفیف، «بهترین انتخاب»، و جملات بازاریابی مبهم پرهیز کن.
- مشخصات ساختگی ننویس؛ اگر جزئیات دقیق نداری، روی تعریف محصول و کاربردهای معمول تمرکز کن.
- ۲ تا ۴ پاراگراف کوتاه به فارسی.
- فقط HTML با تگ‌های <p>، <ul>، <li>، <strong> و <em> برگردان.`
- خروجی باید HTML خام باشد (نه markdown، نه کد بلوک، نه تگ‌های escapeشده) تا داخل ادیتور پیشرفته قرار بگیرد.
- فقط تگ‌های <p>، <ul>، <li>، <strong> و <em> مجاز است.`
}
return `Write a technical product description as HTML.
return `Write a technical product description as HTML for the advanced rich-text editor.
Product name (FA): ${nameFa}
Product name (EN): ${nameEn}
@@ -97,7 +98,8 @@ Requirements:
- Avoid CTAs, discounts, "best choice", and vague marketing claims.
- Do not invent specs; if details are unknown, focus on what the product is and typical uses.
- 24 short paragraphs in Persian (Farsi).
- Return HTML only using <p>, <ul>, <li>, <strong>, and <em>.`
- Return raw HTML (not markdown, not code fences, not escaped entities) so it renders inside the advanced editor.
- HTML only using <p>, <ul>, <li>, <strong>, and <em>.`
}
interface ProductFieldAiPromptModalProps {
@@ -1,9 +1,13 @@
.wrapper {
width: 100%;
max-width: 100%;
min-width: 0;
border: 1px solid rgba(148, 163, 184, 0.35);
border-radius: var(--radius-sm);
background: rgba(255, 255, 255, 0.7);
overflow: hidden;
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
}
.wrapper:focus-within {
@@ -45,15 +49,28 @@
.editorShell {
position: relative;
width: 100%;
max-width: 100%;
min-width: 0;
overflow: hidden;
}
.editor {
width: 100%;
max-width: 100%;
min-width: 0;
min-height: 140px;
padding: var(--field-padding-y) var(--field-padding-x);
padding-bottom: 40px;
font-size: var(--field-font-size);
line-height: 1.5;
color: var(--text-primary);
outline: none;
box-sizing: border-box;
overflow-x: hidden;
overflow-wrap: break-word;
word-break: break-word;
unicode-bidi: isolate;
}
.editor:empty::before {
@@ -62,9 +79,15 @@
pointer-events: none;
}
.editor > * {
max-width: 100%;
box-sizing: border-box;
}
.editor ul,
.editor ol {
padding-left: 1.5em;
padding-inline-start: 1.5em;
padding-inline-end: 0;
margin: 0.5em 0;
}
@@ -1,5 +1,6 @@
import { useRef, useEffect, useState, useCallback } from 'react'
import { Bold, Italic, Underline, List, ListOrdered, ImagePlus } from 'lucide-react'
import { useLocale } from '@meshkee/dashboard-ui'
import { uploadMediaFiles } from '../services/mediaService'
import styles from './RichTextEditor.module.css'
@@ -20,6 +21,7 @@ export function RichTextEditor({
allowImages = false,
editorMinHeight = 140,
}: RichTextEditorProps) {
const { locale, dir } = useLocale()
const editorShellRef = useRef<HTMLDivElement>(null)
const editorRef = useRef<HTMLDivElement>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
@@ -203,8 +205,8 @@ export function RichTextEditor({
}
return (
<div className={styles.wrapper}>
<div className={styles.toolbar}>
<div className={styles.wrapper} dir={dir} lang={locale}>
<div className={styles.toolbar} dir="ltr">
<button type="button" onClick={() => exec('bold')} title="Bold" aria-label="Bold">
<Bold size={16} />
</button>
@@ -256,6 +258,8 @@ export function RichTextEditor({
contentEditable
role="textbox"
aria-multiline="true"
dir={dir}
lang={locale}
data-placeholder={placeholder}
onInput={syncChange}
suppressContentEditableWarning
@@ -82,6 +82,8 @@
.fieldWithAi {
position: relative;
min-width: 0;
width: 100%;
max-width: 100%;
}
.aiFillAnchor {
@@ -352,6 +352,7 @@ export function AddNewProductPage() {
value={description}
onChange={setDescription}
placeholder={t('products.form.descriptionPlaceholder')}
editorMinHeight={180}
/>
<span className={styles.aiFillAnchor}>
<Tooltip label={t('products.form.aiFill')}>