CognxSafeTrack commited on
Commit
9f0410f
·
1 Parent(s): 6e7afa2

feat: implement AI campaign generation logic and frontend integration

Browse files
apps/admin/src/pages/CrmConversationalDashboard.tsx CHANGED
@@ -24,8 +24,9 @@ export default function CrmConversationalDashboard() {
24
  ]);
25
  const [input, setInput] = useState('');
26
  const [isUploading, setIsUploading] = useState(false);
 
27
  const [isDragging, setIsDragging] = useState(false);
28
- const [uploadedFile, setUploadedFile] = useState<{ name: string, listName: string } | null>(null);
29
  const messagesEndRef = useRef<HTMLDivElement>(null);
30
 
31
  const scrollToBottom = () => {
@@ -42,8 +43,6 @@ export default function CrmConversationalDashboard() {
42
  setIsUploading(true);
43
  const formData = new FormData();
44
  formData.append('file', file);
45
- // listName is optional, but we can pass one if we want
46
- // formData.append('listName', 'Import Client ' + new Date().toLocaleDateString());
47
 
48
  try {
49
  const res = await fetch(`${import.meta.env.VITE_API_URL}/v1/organizations/${selectedOrgId}/lists/import`, {
@@ -56,9 +55,8 @@ export default function CrmConversationalDashboard() {
56
 
57
  if (res.ok) {
58
  const data = await res.json();
59
- setUploadedFile({ name: file.name, listName: data.listName });
60
 
61
- // Add AI response as requested
62
  const aiMsg: Message = {
63
  id: Date.now().toString(),
64
  role: 'assistant',
@@ -97,9 +95,9 @@ export default function CrmConversationalDashboard() {
97
  }
98
  };
99
 
100
- const handleSendMessage = (e?: React.FormEvent) => {
101
  e?.preventDefault();
102
- if (!input.trim()) return;
103
 
104
  const userMsg: Message = {
105
  id: Date.now().toString(),
@@ -108,18 +106,54 @@ export default function CrmConversationalDashboard() {
108
  timestamp: new Date()
109
  };
110
  setMessages(prev => [...prev, userMsg]);
 
111
  setInput('');
 
112
 
113
- // For now, just a placeholder AI response
114
- setTimeout(() => {
115
- const aiMsg: Message = {
116
- id: (Date.now() + 1).toString(),
117
- role: 'assistant',
118
- content: "C'est noté. Je prépare le brouillon de votre message pour la liste sélectionnée. (En cours de développement)",
119
- timestamp: new Date()
120
- };
121
- setMessages(prev => [...prev, aiMsg]);
122
- }, 1000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  };
124
 
125
  return (
@@ -238,9 +272,35 @@ export default function CrmConversationalDashboard() {
238
  }`}>
239
  {msg.content}
240
  </div>
 
 
 
 
 
 
 
 
 
 
241
  </div>
242
  </motion.div>
243
  ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  <div ref={messagesEndRef} />
245
  </div>
246
 
 
24
  ]);
25
  const [input, setInput] = useState('');
26
  const [isUploading, setIsUploading] = useState(false);
27
+ const [isGenerating, setIsGenerating] = useState(false);
28
  const [isDragging, setIsDragging] = useState(false);
29
+ const [uploadedFile, setUploadedFile] = useState<{ name: string, listId: string, listName: string } | null>(null);
30
  const messagesEndRef = useRef<HTMLDivElement>(null);
31
 
32
  const scrollToBottom = () => {
 
43
  setIsUploading(true);
44
  const formData = new FormData();
45
  formData.append('file', file);
 
 
46
 
47
  try {
48
  const res = await fetch(`${import.meta.env.VITE_API_URL}/v1/organizations/${selectedOrgId}/lists/import`, {
 
55
 
56
  if (res.ok) {
57
  const data = await res.json();
58
+ setUploadedFile({ name: file.name, listId: data.listId, listName: data.listName });
59
 
 
60
  const aiMsg: Message = {
61
  id: Date.now().toString(),
62
  role: 'assistant',
 
95
  }
96
  };
97
 
98
+ const handleSendMessage = async (e?: React.FormEvent) => {
99
  e?.preventDefault();
100
+ if (!input.trim() || !token || !selectedOrgId) return;
101
 
102
  const userMsg: Message = {
103
  id: Date.now().toString(),
 
106
  timestamp: new Date()
107
  };
108
  setMessages(prev => [...prev, userMsg]);
109
+ const currentInput = input;
110
  setInput('');
111
+ setIsGenerating(true);
112
 
113
+ try {
114
+ const res = await fetch(`${import.meta.env.VITE_API_URL}/v1/organizations/${selectedOrgId}/campaigns/generate`, {
115
+ method: 'POST',
116
+ headers: {
117
+ 'Content-Type': 'application/json',
118
+ 'Authorization': `Bearer ${token}`
119
+ },
120
+ body: JSON.stringify({
121
+ prompt: currentInput,
122
+ listId: uploadedFile?.listId
123
+ })
124
+ });
125
+
126
+ if (res.ok) {
127
+ const data = await res.json();
128
+ const aiMsg: Message = {
129
+ id: Date.now().toString(),
130
+ role: 'assistant',
131
+ content: data.message,
132
+ timestamp: new Date()
133
+ };
134
+ setMessages(prev => [...prev, aiMsg]);
135
+ } else {
136
+ const errorMsg: Message = {
137
+ id: Date.now().toString(),
138
+ role: 'assistant',
139
+ content: "Désolé, je n'ai pas pu générer le message. Pouvez-vous reformuler votre demande ?",
140
+ timestamp: new Date()
141
+ };
142
+ setMessages(prev => [...prev, errorMsg]);
143
+ }
144
+ } catch (err) {
145
+ console.error("Generation failed:", err);
146
+ } finally {
147
+ setIsGenerating(false);
148
+ }
149
+ };
150
+
151
+ const handleValidateAndSend = (_message: string) => {
152
+ if (!uploadedFile) {
153
+ alert("Veuillez d'abord importer une liste de contacts.");
154
+ return;
155
+ }
156
+ alert(`Campagne validée ! Envoi du message vers la liste "${uploadedFile.listName}"... (Action à implémenter)`);
157
  };
158
 
159
  return (
 
272
  }`}>
273
  {msg.content}
274
  </div>
275
+ {msg.role === 'assistant' && msg.id !== '1' && !isGenerating && messages[messages.length - 1].id === msg.id && (
276
+ <motion.button
277
+ initial={{ opacity: 0, scale: 0.9 }}
278
+ animate={{ opacity: 1, scale: 1 }}
279
+ onClick={() => handleValidateAndSend(msg.content)}
280
+ className="mt-3 bg-emerald-500 text-white px-6 py-3 rounded-2xl font-black text-xs shadow-lg shadow-emerald-100 hover:bg-emerald-600 hover:scale-105 active:scale-95 transition-all flex items-center gap-2"
281
+ >
282
+ <CheckCircle2 className="w-4 h-4" /> Valider et Envoyer à la liste
283
+ </motion.button>
284
+ )}
285
  </div>
286
  </motion.div>
287
  ))}
288
+ {isGenerating && (
289
+ <motion.div
290
+ initial={{ opacity: 0, y: 10 }}
291
+ animate={{ opacity: 1, y: 0 }}
292
+ className="flex justify-start"
293
+ >
294
+ <div className="flex gap-4 max-w-[80%]">
295
+ <div className="w-10 h-10 rounded-2xl flex items-center justify-center bg-indigo-600 text-white shadow-sm flex-shrink-0 animate-pulse">
296
+ <Bot className="w-6 h-6" />
297
+ </div>
298
+ <div className="bg-white border border-slate-200 p-5 rounded-[1.8rem] rounded-tl-none flex items-center gap-3 text-slate-400 font-bold text-xs animate-pulse">
299
+ <Loader2 className="w-4 h-4 animate-spin" /> L'IA rédige votre campagne...
300
+ </div>
301
+ </div>
302
+ </motion.div>
303
+ )}
304
  <div ref={messagesEndRef} />
305
  </div>
306
 
apps/api/src/index.ts CHANGED
@@ -14,6 +14,7 @@ import { analyticsRoutes } from './routes/analytics';
14
  import { notificationRoutes } from './routes/notifications';
15
  import { internalRoutes } from './routes/internal';
16
  import { authRoutes } from './routes/auth';
 
17
  import { setupRateLimit } from './middleware/rateLimit';
18
  import { startCleanupCron } from './services/cleanup';
19
 
@@ -109,6 +110,7 @@ const registerRoutes = async () => {
109
  scope.register(paymentRoutes, { prefix: '/v1/payments' });
110
  scope.register(analyticsRoutes, { prefix: '/v1/analytics' });
111
  scope.register(notificationRoutes, { prefix: '/v1/notifications' });
 
112
  scope.register(internalRoutes);
113
  });
114
 
 
14
  import { notificationRoutes } from './routes/notifications';
15
  import { internalRoutes } from './routes/internal';
16
  import { authRoutes } from './routes/auth';
17
+ import campaignRoutes from './routes/campaigns';
18
  import { setupRateLimit } from './middleware/rateLimit';
19
  import { startCleanupCron } from './services/cleanup';
20
 
 
110
  scope.register(paymentRoutes, { prefix: '/v1/payments' });
111
  scope.register(analyticsRoutes, { prefix: '/v1/analytics' });
112
  scope.register(notificationRoutes, { prefix: '/v1/notifications' });
113
+ scope.register(campaignRoutes, { prefix: '/v1/organizations' });
114
  scope.register(internalRoutes);
115
  });
116
 
apps/api/src/routes/campaigns.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FastifyInstance } from 'fastify';
2
+ import { aiService } from '../services/ai';
3
+
4
+ export default async function campaignRoutes(fastify: FastifyInstance) {
5
+ // Generate AI Message for Campaign
6
+ fastify.post('/:id/campaigns/generate', async (req, reply) => {
7
+ const { prompt, listId } = req.body as { prompt: string, listId?: string };
8
+
9
+ if (!prompt) {
10
+ return reply.code(400).send({ error: 'Prompt is required' });
11
+ }
12
+
13
+ try {
14
+ const { text, aiSource } = await aiService.generateBroadcastMessage(prompt);
15
+
16
+ return {
17
+ ok: true,
18
+ message: text,
19
+ aiSource,
20
+ listId // Echoing back the listId for frontend use
21
+ };
22
+ } catch (err) {
23
+ fastify.log.error(err);
24
+ return reply.code(500).send({ error: 'AI Generation failed' });
25
+ }
26
+ });
27
+ }
apps/api/src/services/ai/index.ts CHANGED
@@ -284,6 +284,11 @@ export class AIService {
284
  return { ...data, aiSource: source };
285
  }
286
 
 
 
 
 
 
287
  async extractBusinessProfile(userInput: string, dayNumber: number, userLanguage: string = 'FR'): Promise<any> {
288
  const personality = await this.getTenantPersonality();
289
  const prompt = PromptLoader.compile('business-profile-extraction', {
 
284
  return { ...data, aiSource: source };
285
  }
286
 
287
+ async generateBroadcastMessage(userPrompt: string): Promise<{ text: string, aiSource: string }> {
288
+ const systemPrompt = "Tu es un expert en marketing WhatsApp. Rédige un message court, percutant, avec des emojis pertinents, basé sur les instructions de l'utilisateur. Le message doit être prêt à être envoyé à une liste de diffusion.";
289
+ return this.generateText(systemPrompt, userPrompt, 0.8);
290
+ }
291
+
292
  async extractBusinessProfile(userInput: string, dayNumber: number, userLanguage: string = 'FR'): Promise<any> {
293
  const personality = await this.getTenantPersonality();
294
  const prompt = PromptLoader.compile('business-profile-extraction', {