@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.
- package/dist/components/TicketConversation/hooks/useAI.cjs +24 -5
- package/dist/components/TicketConversation/hooks/useAI.js +24 -5
- package/dist/index.cjs +12 -7
- package/dist/index.js +12 -7
- package/dist/views/TicketDetailView/client.cjs +28 -6
- package/dist/views/TicketDetailView/client.js +28 -6
- package/package.json +1 -1
- package/src/components/TicketConversation/hooks/useAI.ts +29 -5
- package/src/endpoints/ai.ts +13 -8
- package/src/views/TicketDetailView/client.tsx +24 -6
|
@@ -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:
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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:
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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:
|
|
290
|
-
court: "
|
|
291
|
-
amical:
|
|
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 = `
|
|
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
|
|
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:
|
|
284
|
-
court: "
|
|
285
|
-
amical:
|
|
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 = `
|
|
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
|
-
|
|
727
|
+
index.RichTextEditor,
|
|
723
728
|
{
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
|
|
720
|
+
RichTextEditor,
|
|
716
721
|
{
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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.
|
|
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:
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
}
|
package/src/endpoints/ai.ts
CHANGED
|
@@ -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
|
|
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:
|
|
180
|
-
court: '
|
|
181
|
-
amical:
|
|
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 = `
|
|
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
|
-
<
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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>
|