@consilioweb/payload-support 0.9.10 → 0.9.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -77,22 +77,41 @@ function useAI(messages, client, ticketSubject, replyBody, setReplyBody, setRepl
77
77
  };
78
78
  const handleAiRewrite = async (style = "auto") => {
79
79
  if (!replyBody.trim()) return;
80
+ const sel = typeof window !== "undefined" ? window.getSelection() : null;
81
+ const selectedText = sel?.toString().trim() || "";
82
+ const hasSelection = selectedText.length > 3;
83
+ const textToRewrite = hasSelection ? selectedText : replyBody;
80
84
  setAiRewriting(true);
81
85
  try {
82
86
  const res = await fetch("/api/support/ai", {
83
87
  method: "POST",
84
88
  headers: { "Content-Type": "application/json" },
85
89
  credentials: "include",
86
- body: JSON.stringify({ action: "rewrite", text: replyBody, style })
90
+ body: JSON.stringify({ action: "rewrite", text: textToRewrite, style })
87
91
  });
88
92
  if (res.ok) {
89
93
  const data = await res.json();
90
94
  const rewritten = data.rewritten || "";
91
95
  if (rewritten) {
92
- setReplyBody(rewritten);
93
- setReplyHtml(rewritten.replace(/\n/g, "<br/>"));
94
- if (replyEditorRef.current?.setContent) {
95
- replyEditorRef.current.setContent(rewritten.replace(/\n/g, "<br/>"));
96
+ if (hasSelection && sel && sel.rangeCount > 0) {
97
+ const range = sel.getRangeAt(0);
98
+ range.deleteContents();
99
+ range.insertNode(document.createTextNode(rewritten));
100
+ const editorEl = replyEditorRef.current;
101
+ editorEl?.focus?.();
102
+ const rootEl = range.commonAncestorContainer.closest?.("[contenteditable]");
103
+ if (rootEl) {
104
+ const newHtml = rootEl.innerHTML;
105
+ const newText = rootEl.innerText?.trim() || "";
106
+ setReplyBody(newText);
107
+ setReplyHtml(newHtml);
108
+ }
109
+ } else {
110
+ setReplyBody(rewritten);
111
+ setReplyHtml(rewritten.replace(/\n/g, "<br/>"));
112
+ if (replyEditorRef.current?.setContent) {
113
+ replyEditorRef.current.setContent(rewritten.replace(/\n/g, "<br/>"));
114
+ }
96
115
  }
97
116
  }
98
117
  }
@@ -76,22 +76,41 @@ function useAI(messages, client, ticketSubject, replyBody, setReplyBody, setRepl
76
76
  };
77
77
  const handleAiRewrite = async (style = "auto") => {
78
78
  if (!replyBody.trim()) return;
79
+ const sel = typeof window !== "undefined" ? window.getSelection() : null;
80
+ const selectedText = sel?.toString().trim() || "";
81
+ const hasSelection = selectedText.length > 3;
82
+ const textToRewrite = hasSelection ? selectedText : replyBody;
79
83
  setAiRewriting(true);
80
84
  try {
81
85
  const res = await fetch("/api/support/ai", {
82
86
  method: "POST",
83
87
  headers: { "Content-Type": "application/json" },
84
88
  credentials: "include",
85
- body: JSON.stringify({ action: "rewrite", text: replyBody, style })
89
+ body: JSON.stringify({ action: "rewrite", text: textToRewrite, style })
86
90
  });
87
91
  if (res.ok) {
88
92
  const data = await res.json();
89
93
  const rewritten = data.rewritten || "";
90
94
  if (rewritten) {
91
- setReplyBody(rewritten);
92
- setReplyHtml(rewritten.replace(/\n/g, "<br/>"));
93
- if (replyEditorRef.current?.setContent) {
94
- replyEditorRef.current.setContent(rewritten.replace(/\n/g, "<br/>"));
95
+ if (hasSelection && sel && sel.rangeCount > 0) {
96
+ const range = sel.getRangeAt(0);
97
+ range.deleteContents();
98
+ range.insertNode(document.createTextNode(rewritten));
99
+ const editorEl = replyEditorRef.current;
100
+ editorEl?.focus?.();
101
+ const rootEl = range.commonAncestorContainer.closest?.("[contenteditable]");
102
+ if (rootEl) {
103
+ const newHtml = rootEl.innerHTML;
104
+ const newText = rootEl.innerText?.trim() || "";
105
+ setReplyBody(newText);
106
+ setReplyHtml(newHtml);
107
+ }
108
+ } else {
109
+ setReplyBody(rewritten);
110
+ setReplyHtml(rewritten.replace(/\n/g, "<br/>"));
111
+ if (replyEditorRef.current?.setContent) {
112
+ replyEditorRef.current.setContent(rewritten.replace(/\n/g, "<br/>"));
113
+ }
95
114
  }
96
115
  }
97
116
  }
package/dist/index.cjs CHANGED
@@ -283,15 +283,20 @@ R\xE9dige une r\xE9ponse appropri\xE9e au dernier message du client. Sois concis
283
283
  const { text, style } = body;
284
284
  if (!text?.trim()) return Response.json({ error: "text required" }, { status: 400 });
285
285
  const styleInstructions = {
286
- auto: "Garde le m\xEAme ton (tutoiement/vouvoiement).",
287
- tutoyer: "Utilise le tutoiement. Si le texte vouvoie, convertis en tutoiement.",
288
- vouvoyer: "Utilise le vouvoiement. Si le texte tutoie, convertis en vouvoiement.",
289
- formel: "Adopte un ton formel et professionnel avec vouvoiement.",
290
- court: "Raccourcis le texte au maximum tout en gardant le sens. Sois concis et direct.",
291
- amical: "Adopte un ton chaleureux et amical avec tutoiement."
286
+ auto: { tone: "Garde le ton actuel (neutre professionnel).", person: "IMPORTANT: Pr\xE9serve EXACTEMENT le tutoiement ou vouvoiement du texte original." },
287
+ tutoyer: { tone: "Ton d\xE9contract\xE9 et direct, mais correct.", person: 'CRITIQUE: Utilise IMP\xC9RATIVEMENT le tutoiement partout (tu, ton, te, toi). Si le texte vouvoie, convertis TOUT en tutoiement. Exemple: "vous pouvez" \u2192 "tu peux", "votre" \u2192 "ton".' },
288
+ vouvoyer: { tone: "Ton neutre et poli.", person: "CRITIQUE: Utilise IMP\xC9RATIVEMENT le vouvoiement partout (vous, votre, etc). Si le texte tutoie, convertis TOUT en vouvoiement." },
289
+ formel: { tone: "Ton formel et institutionnel.", person: "Utilise le vouvoiement partout." },
290
+ court: { tone: "Style concis et direct, phrases courtes, aller \xE0 l'essentiel.", person: "Pr\xE9serve le tutoiement/vouvoiement du texte original." },
291
+ amical: { tone: "Ton chaleureux, amical, sympathique, avec un peu de cordialit\xE9.", person: "CRITIQUE: Utilise IMP\xC9RATIVEMENT le tutoiement partout. Convertis le vouvoiement en tutoiement." }
292
292
  };
293
293
  const styleGuide = styleInstructions[style || "auto"] || styleInstructions.auto;
294
- const prompt = `Tu es un agent de support technique professionnel. Reformule le texte ci-dessous de mani\xE8re plus professionnelle et corrige les fautes d'orthographe/grammaire. ${styleGuide} Ne change pas le fond du message, am\xE9liore uniquement la forme. R\xE9ponds UNIQUEMENT avec le texte reformul\xE9, sans commentaire ni explication.
294
+ const prompt = `Reformule le texte ci-dessous en corrigeant les fautes d'orthographe/grammaire. Ne change pas le fond du message, am\xE9liore uniquement la forme.
295
+
296
+ TON REQUIS: ${styleGuide.tone}
297
+ FORME REQUISE: ${styleGuide.person}
298
+
299
+ R\xE9ponds UNIQUEMENT avec le texte reformul\xE9, sans commentaire ni explication.
295
300
 
296
301
  Texte original :
297
302
  ${text}`;
package/dist/index.js CHANGED
@@ -277,15 +277,20 @@ R\xE9dige une r\xE9ponse appropri\xE9e au dernier message du client. Sois concis
277
277
  const { text, style } = body;
278
278
  if (!text?.trim()) return Response.json({ error: "text required" }, { status: 400 });
279
279
  const styleInstructions = {
280
- auto: "Garde le m\xEAme ton (tutoiement/vouvoiement).",
281
- tutoyer: "Utilise le tutoiement. Si le texte vouvoie, convertis en tutoiement.",
282
- vouvoyer: "Utilise le vouvoiement. Si le texte tutoie, convertis en vouvoiement.",
283
- formel: "Adopte un ton formel et professionnel avec vouvoiement.",
284
- court: "Raccourcis le texte au maximum tout en gardant le sens. Sois concis et direct.",
285
- amical: "Adopte un ton chaleureux et amical avec tutoiement."
280
+ auto: { tone: "Garde le ton actuel (neutre professionnel).", person: "IMPORTANT: Pr\xE9serve EXACTEMENT le tutoiement ou vouvoiement du texte original." },
281
+ tutoyer: { tone: "Ton d\xE9contract\xE9 et direct, mais correct.", person: 'CRITIQUE: Utilise IMP\xC9RATIVEMENT le tutoiement partout (tu, ton, te, toi). Si le texte vouvoie, convertis TOUT en tutoiement. Exemple: "vous pouvez" \u2192 "tu peux", "votre" \u2192 "ton".' },
282
+ vouvoyer: { tone: "Ton neutre et poli.", person: "CRITIQUE: Utilise IMP\xC9RATIVEMENT le vouvoiement partout (vous, votre, etc). Si le texte tutoie, convertis TOUT en vouvoiement." },
283
+ formel: { tone: "Ton formel et institutionnel.", person: "Utilise le vouvoiement partout." },
284
+ court: { tone: "Style concis et direct, phrases courtes, aller \xE0 l'essentiel.", person: "Pr\xE9serve le tutoiement/vouvoiement du texte original." },
285
+ amical: { tone: "Ton chaleureux, amical, sympathique, avec un peu de cordialit\xE9.", person: "CRITIQUE: Utilise IMP\xC9RATIVEMENT le tutoiement partout. Convertis le vouvoiement en tutoiement." }
286
286
  };
287
287
  const styleGuide = styleInstructions[style || "auto"] || styleInstructions.auto;
288
- const prompt = `Tu es un agent de support technique professionnel. Reformule le texte ci-dessous de mani\xE8re plus professionnelle et corrige les fautes d'orthographe/grammaire. ${styleGuide} Ne change pas le fond du message, am\xE9liore uniquement la forme. R\xE9ponds UNIQUEMENT avec le texte reformul\xE9, sans commentaire ni explication.
288
+ const prompt = `Reformule le texte ci-dessous en corrigeant les fautes d'orthographe/grammaire. Ne change pas le fond du message, am\xE9liore uniquement la forme.
289
+
290
+ TON REQUIS: ${styleGuide.tone}
291
+ FORME REQUISE: ${styleGuide.person}
292
+
293
+ R\xE9ponds UNIQUEMENT avec le texte reformul\xE9, sans commentaire ni explication.
289
294
 
290
295
  Texte original :
291
296
  ${text}`;
@@ -141,7 +141,9 @@ const TicketDetailClient = () => {
141
141
  const [sendAsClient, setSendAsClient] = React.useState(false);
142
142
  const [editingMsgId, setEditingMsgId] = React.useState(null);
143
143
  const [editingBody, setEditingBody] = React.useState("");
144
+ const [editingHtml, setEditingHtml] = React.useState("");
144
145
  const [editSaving, setEditSaving] = React.useState(false);
146
+ const editEditorRef = React.useRef(null);
145
147
  const [sending, setSending] = React.useState(false);
146
148
  const [showMenu, setShowMenu] = React.useState(false);
147
149
  const [clientTyping, setClientTyping] = React.useState(false);
@@ -483,10 +485,12 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
483
485
  const startEditMessage = (msg) => {
484
486
  setEditingMsgId(msg.id);
485
487
  setEditingBody(msg.body || "");
488
+ setEditingHtml(msg.bodyHtml || (msg.body || "").replace(/\n/g, "<br/>"));
486
489
  };
487
490
  const cancelEditMessage = () => {
488
491
  setEditingMsgId(null);
489
492
  setEditingBody("");
493
+ setEditingHtml("");
490
494
  };
491
495
  const saveEditMessage = async () => {
492
496
  if (editingMsgId === null) return;
@@ -496,11 +500,12 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
496
500
  method: "PATCH",
497
501
  headers: { "Content-Type": "application/json" },
498
502
  credentials: "include",
499
- body: JSON.stringify({ body: editingBody, bodyHtml: null, skipNotification: true })
503
+ body: JSON.stringify({ body: editingBody, bodyHtml: editingHtml || null, skipNotification: true })
500
504
  });
501
505
  if (res.ok) {
502
506
  setEditingMsgId(null);
503
507
  setEditingBody("");
508
+ setEditingHtml("");
504
509
  fetchAll();
505
510
  }
506
511
  } catch {
@@ -719,12 +724,29 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
719
724
  ] }),
720
725
  editingMsgId === msg.id ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
721
726
  /* @__PURE__ */ jsxRuntime.jsx(
722
- "textarea",
727
+ index.RichTextEditor,
723
728
  {
724
- value: editingBody,
725
- onChange: (e) => setEditingBody(e.target.value),
726
- style: { width: "100%", minHeight: 120, padding: 10, fontSize: 13, lineHeight: 1.5, fontFamily: "inherit", border: "1px solid var(--theme-elevation-200)", borderRadius: 6, background: "var(--theme-elevation-0)", color: "var(--theme-text)" },
727
- autoFocus: true
729
+ ref: editEditorRef,
730
+ initialValue: editingHtml,
731
+ onChange: (html, text) => {
732
+ setEditingHtml(html);
733
+ setEditingBody(text);
734
+ },
735
+ placeholder: "\xC9diter le message...",
736
+ minHeight: 120,
737
+ onFileUpload: async (file) => {
738
+ try {
739
+ const formData = new FormData();
740
+ formData.append("file", file);
741
+ formData.append("_payload", JSON.stringify({ alt: file.name }));
742
+ const ur = await fetch("/api/media", { method: "POST", credentials: "include", body: formData });
743
+ if (!ur.ok) return null;
744
+ const ud = await ur.json();
745
+ return ud.doc?.url || null;
746
+ } catch {
747
+ return null;
748
+ }
749
+ }
728
750
  }
729
751
  ),
730
752
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 6, justifyContent: "flex-end" }, children: [
@@ -134,7 +134,9 @@ const TicketDetailClient = () => {
134
134
  const [sendAsClient, setSendAsClient] = useState(false);
135
135
  const [editingMsgId, setEditingMsgId] = useState(null);
136
136
  const [editingBody, setEditingBody] = useState("");
137
+ const [editingHtml, setEditingHtml] = useState("");
137
138
  const [editSaving, setEditSaving] = useState(false);
139
+ const editEditorRef = useRef(null);
138
140
  const [sending, setSending] = useState(false);
139
141
  const [showMenu, setShowMenu] = useState(false);
140
142
  const [clientTyping, setClientTyping] = useState(false);
@@ -476,10 +478,12 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
476
478
  const startEditMessage = (msg) => {
477
479
  setEditingMsgId(msg.id);
478
480
  setEditingBody(msg.body || "");
481
+ setEditingHtml(msg.bodyHtml || (msg.body || "").replace(/\n/g, "<br/>"));
479
482
  };
480
483
  const cancelEditMessage = () => {
481
484
  setEditingMsgId(null);
482
485
  setEditingBody("");
486
+ setEditingHtml("");
483
487
  };
484
488
  const saveEditMessage = async () => {
485
489
  if (editingMsgId === null) return;
@@ -489,11 +493,12 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
489
493
  method: "PATCH",
490
494
  headers: { "Content-Type": "application/json" },
491
495
  credentials: "include",
492
- body: JSON.stringify({ body: editingBody, bodyHtml: null, skipNotification: true })
496
+ body: JSON.stringify({ body: editingBody, bodyHtml: editingHtml || null, skipNotification: true })
493
497
  });
494
498
  if (res.ok) {
495
499
  setEditingMsgId(null);
496
500
  setEditingBody("");
501
+ setEditingHtml("");
497
502
  fetchAll();
498
503
  }
499
504
  } catch {
@@ -712,12 +717,29 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
712
717
  ] }),
713
718
  editingMsgId === msg.id ? /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
714
719
  /* @__PURE__ */ jsx(
715
- "textarea",
720
+ RichTextEditor,
716
721
  {
717
- value: editingBody,
718
- onChange: (e) => setEditingBody(e.target.value),
719
- style: { width: "100%", minHeight: 120, padding: 10, fontSize: 13, lineHeight: 1.5, fontFamily: "inherit", border: "1px solid var(--theme-elevation-200)", borderRadius: 6, background: "var(--theme-elevation-0)", color: "var(--theme-text)" },
720
- autoFocus: true
722
+ ref: editEditorRef,
723
+ initialValue: editingHtml,
724
+ onChange: (html, text) => {
725
+ setEditingHtml(html);
726
+ setEditingBody(text);
727
+ },
728
+ placeholder: "\xC9diter le message...",
729
+ minHeight: 120,
730
+ onFileUpload: async (file) => {
731
+ try {
732
+ const formData = new FormData();
733
+ formData.append("file", file);
734
+ formData.append("_payload", JSON.stringify({ alt: file.name }));
735
+ const ur = await fetch("/api/media", { method: "POST", credentials: "include", body: formData });
736
+ if (!ur.ok) return null;
737
+ const ud = await ur.json();
738
+ return ud.doc?.url || null;
739
+ } catch {
740
+ return null;
741
+ }
742
+ }
721
743
  }
722
744
  ),
723
745
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6, justifyContent: "flex-end" }, children: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consilioweb/payload-support",
3
- "version": "0.9.10",
3
+ "version": "0.9.11",
4
4
  "description": "Payload CMS plugin — professional support & ticketing system with AI, SLA, time tracking, live chat, and more",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -89,22 +89,46 @@ export function useAI(
89
89
 
90
90
  const handleAiRewrite = async (style: string = 'auto') => {
91
91
  if (!replyBody.trim()) return
92
+ // Detect selection inside the editor — if present, rewrite only the selected text
93
+ const sel = typeof window !== 'undefined' ? window.getSelection() : null
94
+ const selectedText = sel?.toString().trim() || ''
95
+ const hasSelection = selectedText.length > 3
96
+ const textToRewrite = hasSelection ? selectedText : replyBody
97
+
92
98
  setAiRewriting(true)
93
99
  try {
94
100
  const res = await fetch('/api/support/ai', {
95
101
  method: 'POST',
96
102
  headers: { 'Content-Type': 'application/json' },
97
103
  credentials: 'include',
98
- body: JSON.stringify({ action: 'rewrite', text: replyBody, style }),
104
+ body: JSON.stringify({ action: 'rewrite', text: textToRewrite, style }),
99
105
  })
100
106
  if (res.ok) {
101
107
  const data = await res.json()
102
108
  const rewritten = data.rewritten || ''
103
109
  if (rewritten) {
104
- setReplyBody(rewritten)
105
- setReplyHtml(rewritten.replace(/\n/g, '<br/>'))
106
- if (replyEditorRef.current?.setContent) {
107
- replyEditorRef.current.setContent(rewritten.replace(/\n/g, '<br/>'))
110
+ if (hasSelection && sel && sel.rangeCount > 0) {
111
+ // Replace only the selected text in the contentEditable
112
+ const range = sel.getRangeAt(0)
113
+ range.deleteContents()
114
+ range.insertNode(document.createTextNode(rewritten))
115
+ // Trigger input event so parent state updates via onInput
116
+ const editorEl = replyEditorRef.current as unknown as { focus?: () => void } | null
117
+ editorEl?.focus?.()
118
+ // Sync state from DOM
119
+ const rootEl = (range.commonAncestorContainer as HTMLElement).closest?.('[contenteditable]') as HTMLElement | null
120
+ if (rootEl) {
121
+ const newHtml = rootEl.innerHTML
122
+ const newText = rootEl.innerText?.trim() || ''
123
+ setReplyBody(newText)
124
+ setReplyHtml(newHtml)
125
+ }
126
+ } else {
127
+ setReplyBody(rewritten)
128
+ setReplyHtml(rewritten.replace(/\n/g, '<br/>'))
129
+ if (replyEditorRef.current?.setContent) {
130
+ replyEditorRef.current.setContent(rewritten.replace(/\n/g, '<br/>'))
131
+ }
108
132
  }
109
133
  }
110
134
  }
@@ -172,17 +172,22 @@ Rédige une réponse appropriée au dernier message du client. Sois concis (3-5
172
172
  const { text, style } = body as { text: string; style?: string }
173
173
  if (!text?.trim()) return Response.json({ error: 'text required' }, { status: 400 })
174
174
 
175
- const styleInstructions: Record<string, string> = {
176
- auto: 'Garde le même ton (tutoiement/vouvoiement).',
177
- tutoyer: 'Utilise le tutoiement. Si le texte vouvoie, convertis en tutoiement.',
178
- vouvoyer: 'Utilise le vouvoiement. Si le texte tutoie, convertis en vouvoiement.',
179
- formel: 'Adopte un ton formel et professionnel avec vouvoiement.',
180
- court: 'Raccourcis le texte au maximum tout en gardant le sens. Sois concis et direct.',
181
- amical: 'Adopte un ton chaleureux et amical avec tutoiement.',
175
+ const styleInstructions: Record<string, { tone: string; person: string }> = {
176
+ auto: { tone: 'Garde le ton actuel (neutre professionnel).', person: 'IMPORTANT: Préserve EXACTEMENT le tutoiement ou vouvoiement du texte original.' },
177
+ tutoyer: { tone: 'Ton décontracté et direct, mais correct.', person: 'CRITIQUE: Utilise IMPÉRATIVEMENT le tutoiement partout (tu, ton, te, toi). Si le texte vouvoie, convertis TOUT en tutoiement. Exemple: "vous pouvez" → "tu peux", "votre" → "ton".' },
178
+ vouvoyer: { tone: 'Ton neutre et poli.', person: 'CRITIQUE: Utilise IMPÉRATIVEMENT le vouvoiement partout (vous, votre, etc). Si le texte tutoie, convertis TOUT en vouvoiement.' },
179
+ formel: { tone: 'Ton formel et institutionnel.', person: 'Utilise le vouvoiement partout.' },
180
+ court: { tone: 'Style concis et direct, phrases courtes, aller à l\'essentiel.', person: 'Préserve le tutoiement/vouvoiement du texte original.' },
181
+ amical: { tone: 'Ton chaleureux, amical, sympathique, avec un peu de cordialité.', person: 'CRITIQUE: Utilise IMPÉRATIVEMENT le tutoiement partout. Convertis le vouvoiement en tutoiement.' },
182
182
  }
183
183
  const styleGuide = styleInstructions[style || 'auto'] || styleInstructions.auto
184
184
 
185
- const prompt = `Tu es un agent de support technique professionnel. Reformule le texte ci-dessous de manière plus professionnelle et corrige les fautes d'orthographe/grammaire. ${styleGuide} Ne change pas le fond du message, améliore uniquement la forme. Réponds UNIQUEMENT avec le texte reformulé, sans commentaire ni explication.
185
+ const prompt = `Reformule le texte ci-dessous en corrigeant les fautes d'orthographe/grammaire. Ne change pas le fond du message, améliore uniquement la forme.
186
+
187
+ TON REQUIS: ${styleGuide.tone}
188
+ FORME REQUISE: ${styleGuide.person}
189
+
190
+ Réponds UNIQUEMENT avec le texte reformulé, sans commentaire ni explication.
186
191
 
187
192
  Texte original :
188
193
  ${text}`
@@ -136,7 +136,9 @@ export const TicketDetailClient: React.FC = () => {
136
136
  // Inline message edit
137
137
  const [editingMsgId, setEditingMsgId] = useState<string | number | null>(null)
138
138
  const [editingBody, setEditingBody] = useState('')
139
+ const [editingHtml, setEditingHtml] = useState('')
139
140
  const [editSaving, setEditSaving] = useState(false)
141
+ const editEditorRef = useRef<RichTextEditorHandle>(null)
140
142
  const [sending, setSending] = useState(false)
141
143
 
142
144
  const [showMenu, setShowMenu] = useState(false)
@@ -449,11 +451,14 @@ const [clientTyping, setClientTyping] = useState(false)
449
451
  const startEditMessage = (msg: Message) => {
450
452
  setEditingMsgId(msg.id)
451
453
  setEditingBody(msg.body || '')
454
+ // Prefer existing bodyHtml for WYSIWYG edit, fallback to body with <br/> conversion
455
+ setEditingHtml(msg.bodyHtml || (msg.body || '').replace(/\n/g, '<br/>'))
452
456
  }
453
457
 
454
458
  const cancelEditMessage = () => {
455
459
  setEditingMsgId(null)
456
460
  setEditingBody('')
461
+ setEditingHtml('')
457
462
  }
458
463
 
459
464
  const saveEditMessage = async () => {
@@ -464,11 +469,12 @@ const [clientTyping, setClientTyping] = useState(false)
464
469
  method: 'PATCH',
465
470
  headers: { 'Content-Type': 'application/json' },
466
471
  credentials: 'include',
467
- body: JSON.stringify({ body: editingBody, bodyHtml: null, skipNotification: true }),
472
+ body: JSON.stringify({ body: editingBody, bodyHtml: editingHtml || null, skipNotification: true }),
468
473
  })
469
474
  if (res.ok) {
470
475
  setEditingMsgId(null)
471
476
  setEditingBody('')
477
+ setEditingHtml('')
472
478
  fetchAll()
473
479
  }
474
480
  } catch { /* silent */ } finally {
@@ -654,11 +660,23 @@ const [clientTyping, setClientTyping] = useState(false)
654
660
  </div>
655
661
  {editingMsgId === msg.id ? (
656
662
  <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
657
- <textarea
658
- value={editingBody}
659
- onChange={(e) => setEditingBody(e.target.value)}
660
- style={{ width: '100%', minHeight: 120, padding: 10, fontSize: 13, lineHeight: 1.5, fontFamily: 'inherit', border: '1px solid var(--theme-elevation-200)', borderRadius: 6, background: 'var(--theme-elevation-0)', color: 'var(--theme-text)' }}
661
- autoFocus
663
+ <RichTextEditor
664
+ ref={editEditorRef}
665
+ initialValue={editingHtml}
666
+ onChange={(html, text) => { setEditingHtml(html); setEditingBody(text) }}
667
+ placeholder="Éditer le message..."
668
+ minHeight={120}
669
+ onFileUpload={async (file) => {
670
+ try {
671
+ const formData = new FormData()
672
+ formData.append('file', file)
673
+ formData.append('_payload', JSON.stringify({ alt: file.name }))
674
+ const ur = await fetch('/api/media', { method: 'POST', credentials: 'include', body: formData })
675
+ if (!ur.ok) return null
676
+ const ud = await ur.json()
677
+ return ud.doc?.url || null
678
+ } catch { return null }
679
+ }}
662
680
  />
663
681
  <div style={{ display: 'flex', gap: 6, justifyContent: 'flex-end' }}>
664
682
  <button onClick={cancelEditMessage} disabled={editSaving} style={{ padding: '5px 12px', fontSize: 12, borderRadius: 5, border: '1px solid var(--theme-elevation-200)', background: 'var(--theme-elevation-0)', color: 'var(--theme-text)', cursor: 'pointer' }}>Annuler</button>