mirror of
https://git.meshkee.com/Meshkee/dashboards.git
synced 2026-08-11 22:30:58 +04:30
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:
co-authored by
Cursor
parent
8d59166fe8
commit
404c008ced
@@ -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.
|
||||||
- 2–4 short paragraphs in Persian (Farsi).
|
- 2–4 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')}>
|
||||||
|
|||||||
Reference in New Issue
Block a user