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 || '—' const category = ctx.categoryName || '—'
if (ctx.locale === 'fa') { if (ctx.locale === 'fa') {
return `یک توضیحات فنی محصول به صورت HTML بنویس. return `یک توضیحات فنی محصول به صورت HTML برای ادیتور متن پیشرفته بنویس.
نام محصول (فارسی): ${nameFa} نام محصول (فارسی): ${nameFa}
نام محصول (انگلیسی): ${nameEn} نام محصول (انگلیسی): ${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 (FA): ${nameFa}
Product name (EN): ${nameEn} Product name (EN): ${nameEn}
@@ -97,7 +98,8 @@ Requirements:
- Avoid CTAs, discounts, "best choice", and vague marketing claims. - 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. - Do not invent specs; if details are unknown, focus on what the product is and typical uses.
- 24 short paragraphs in Persian (Farsi). - 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 { interface ProductFieldAiPromptModalProps {
@@ -1,9 +1,13 @@
.wrapper { .wrapper {
width: 100%;
max-width: 100%;
min-width: 0;
border: 1px solid rgba(148, 163, 184, 0.35); border: 1px solid rgba(148, 163, 184, 0.35);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
background: rgba(255, 255, 255, 0.7); background: rgba(255, 255, 255, 0.7);
overflow: hidden; overflow: hidden;
transition: border-color 0.2s, box-shadow 0.2s; transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
} }
.wrapper:focus-within { .wrapper:focus-within {
@@ -45,15 +49,28 @@
.editorShell { .editorShell {
position: relative; position: relative;
width: 100%;
max-width: 100%;
min-width: 0;
overflow: hidden;
} }
.editor { .editor {
width: 100%;
max-width: 100%;
min-width: 0;
min-height: 140px; min-height: 140px;
padding: var(--field-padding-y) var(--field-padding-x); padding: var(--field-padding-y) var(--field-padding-x);
padding-bottom: 40px;
font-size: var(--field-font-size); font-size: var(--field-font-size);
line-height: 1.5; line-height: 1.5;
color: var(--text-primary); color: var(--text-primary);
outline: none; outline: none;
box-sizing: border-box;
overflow-x: hidden;
overflow-wrap: break-word;
word-break: break-word;
unicode-bidi: isolate;
} }
.editor:empty::before { .editor:empty::before {
@@ -62,9 +79,15 @@
pointer-events: none; pointer-events: none;
} }
.editor > * {
max-width: 100%;
box-sizing: border-box;
}
.editor ul, .editor ul,
.editor ol { .editor ol {
padding-left: 1.5em; padding-inline-start: 1.5em;
padding-inline-end: 0;
margin: 0.5em 0; margin: 0.5em 0;
} }
@@ -1,5 +1,6 @@
import { useRef, useEffect, useState, useCallback } from 'react' import { useRef, useEffect, useState, useCallback } from 'react'
import { Bold, Italic, Underline, List, ListOrdered, ImagePlus } from 'lucide-react' import { Bold, Italic, Underline, List, ListOrdered, ImagePlus } from 'lucide-react'
import { useLocale } from '@meshkee/dashboard-ui'
import { uploadMediaFiles } from '../services/mediaService' import { uploadMediaFiles } from '../services/mediaService'
import styles from './RichTextEditor.module.css' import styles from './RichTextEditor.module.css'
@@ -20,6 +21,7 @@ export function RichTextEditor({
allowImages = false, allowImages = false,
editorMinHeight = 140, editorMinHeight = 140,
}: RichTextEditorProps) { }: RichTextEditorProps) {
const { locale, dir } = useLocale()
const editorShellRef = useRef<HTMLDivElement>(null) const editorShellRef = useRef<HTMLDivElement>(null)
const editorRef = useRef<HTMLDivElement>(null) const editorRef = useRef<HTMLDivElement>(null)
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
@@ -203,8 +205,8 @@ export function RichTextEditor({
} }
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper} dir={dir} lang={locale}>
<div className={styles.toolbar}> <div className={styles.toolbar} dir="ltr">
<button type="button" onClick={() => exec('bold')} title="Bold" aria-label="Bold"> <button type="button" onClick={() => exec('bold')} title="Bold" aria-label="Bold">
<Bold size={16} /> <Bold size={16} />
</button> </button>
@@ -256,6 +258,8 @@ export function RichTextEditor({
contentEditable contentEditable
role="textbox" role="textbox"
aria-multiline="true" aria-multiline="true"
dir={dir}
lang={locale}
data-placeholder={placeholder} data-placeholder={placeholder}
onInput={syncChange} onInput={syncChange}
suppressContentEditableWarning suppressContentEditableWarning
@@ -82,6 +82,8 @@
.fieldWithAi { .fieldWithAi {
position: relative; position: relative;
min-width: 0; min-width: 0;
width: 100%;
max-width: 100%;
} }
.aiFillAnchor { .aiFillAnchor {
@@ -352,6 +352,7 @@ export function AddNewProductPage() {
value={description} value={description}
onChange={setDescription} onChange={setDescription}
placeholder={t('products.form.descriptionPlaceholder')} placeholder={t('products.form.descriptionPlaceholder')}
editorMinHeight={180}
/> />
<span className={styles.aiFillAnchor}> <span className={styles.aiFillAnchor}>
<Tooltip label={t('products.form.aiFill')}> <Tooltip label={t('products.form.aiFill')}>