diff --git a/frontend/src/components/shared/SharePanel.tsx b/frontend/src/components/shared/SharePanel.tsx
new file mode 100644
index 0000000..9179dde
--- /dev/null
+++ b/frontend/src/components/shared/SharePanel.tsx
@@ -0,0 +1,172 @@
+import { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Copy, Mail, MessageCircle, Send, Share2, Link as LinkIcon } from 'lucide-react';
+import { trackEvent } from '@/services/analytics';
+
+type ShareVariant = 'page' | 'result';
+
+interface SharePanelProps {
+ title: string;
+ text: string;
+ url: string;
+ variant?: ShareVariant;
+ className?: string;
+}
+
+interface ShareTarget {
+ key: string;
+ label: string;
+ href: string;
+}
+
+function openShareWindow(url: string) {
+ window.open(url, '_blank', 'noopener,noreferrer');
+}
+
+export default function SharePanel({
+ title,
+ text,
+ url,
+ variant = 'page',
+ className = '',
+}: SharePanelProps) {
+ const { t } = useTranslation();
+ const [open, setOpen] = useState(false);
+ const [copied, setCopied] = useState(false);
+ const canNativeShare = typeof navigator !== 'undefined' && typeof navigator.share === 'function';
+
+ if (!url) return null;
+
+ const encodedUrl = encodeURIComponent(url);
+ const encodedTitle = encodeURIComponent(title);
+ const encodedText = encodeURIComponent(text);
+
+ const targets: ShareTarget[] = [
+ {
+ key: 'whatsapp',
+ label: t('share.targets.whatsapp'),
+ href: `https://wa.me/?text=${encodeURIComponent(`${title}\n${url}`)}`,
+ },
+ {
+ key: 'facebook',
+ label: t('share.targets.facebook'),
+ href: `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`,
+ },
+ {
+ key: 'telegram',
+ label: t('share.targets.telegram'),
+ href: `https://t.me/share/url?url=${encodedUrl}&text=${encodedTitle}`,
+ },
+ {
+ key: 'x',
+ label: t('share.targets.x'),
+ href: `https://twitter.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`,
+ },
+ {
+ key: 'linkedin',
+ label: t('share.targets.linkedin'),
+ href: `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`,
+ },
+ {
+ key: 'email',
+ label: t('share.targets.email'),
+ href: `mailto:?subject=${encodedTitle}&body=${encodedText}%0A%0A${encodedUrl}`,
+ },
+ ];
+
+ const handleNativeShare = async () => {
+ if (!canNativeShare) return;
+
+ try {
+ await navigator.share({ title, text, url });
+ trackEvent('share_clicked', { variant, target: 'native' });
+ } catch {
+ // Ignore cancelation and rely on the fallback actions below.
+ }
+ };
+
+ const handleCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(url);
+ setCopied(true);
+ window.setTimeout(() => setCopied(false), 1800);
+ trackEvent('share_clicked', { variant, target: 'copy' });
+ } catch {
+ setCopied(false);
+ }
+ };
+
+ return (
+
+
+
+ {open && (
+
+
+
+ {variant === 'result' ? t('share.resultLabel') : t('share.toolLabel')}
+
+
{title}
+
{text}
+
+
+
+ {canNativeShare && (
+
+ )}
+
+
+
+
+
+ {targets.map((target) => (
+
+ ))}
+
+
+
{t('share.note')}
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/i18n/ar.json b/frontend/src/i18n/ar.json
index d2f1507..ca3093a 100644
--- a/frontend/src/i18n/ar.json
+++ b/frontend/src/i18n/ar.json
@@ -62,6 +62,50 @@
"noToken": "رابط غير صالح. يرجى طلب رابط جديد."
}
},
+ "assistant": {
+ "badge": "دليل ذكي",
+ "title": "هل تحتاج مساعدة لاختيار الأداة المناسبة؟",
+ "subtitle": "اسأل عن تدفقات PDF أو الصور أو الفيديو أو النصوص، وستحصل على إجابة موجهة فوراً.",
+ "dataNotice": "قد يتم حفظ الرسائل المرسلة هنا حتى نطوّر المساعد ونفهم احتياجات المستخدمين بشكل أفضل.",
+ "greeting": "مرحباً، أستطيع مساعدتك في اختيار الأداة المناسبة أو شرح طريقة عمل الموقع.",
+ "greetingWithTool": "مرحباً، أستطيع مساعدتك في استخدام {{tool}} أو ترشيح أداة أنسب إذا لزم الأمر.",
+ "emptyState": "اسأل عن أفضل أداة، أو سير العمل المناسب، أو كيفية مشاركة النتيجة وتنزيلها.",
+ "inputPlaceholder": "اسأل عن أي أداة أو سير عمل...",
+ "send": "إرسال الرسالة",
+ "thinking": "جارٍ التفكير...",
+ "unavailable": "المساعد غير متاح مؤقتاً. يرجى المحاولة بعد قليل.",
+ "close": "إغلاق المساعد",
+ "fabTitle": "اسأل SaaS-PDF",
+ "fabSubtitle": "مساعدة ذكية عبر جميع الأدوات",
+ "prompts": {
+ "currentTool": "كيف أستخدم {{tool}}؟",
+ "alternativeTool": "هل توجد أداة أفضل لهذه المهمة؟",
+ "share": "هل يمكنني مشاركة نتيجة هذه الأداة؟",
+ "findTool": "ما الأداة المناسبة لملفي؟",
+ "pdfWorkflows": "ما أفضل أدوات PDF هنا؟",
+ "imageWorkflows": "ما الأدوات التي تعمل مع الصور وOCR؟"
+ }
+ },
+ "share": {
+ "shareTool": "مشاركة هذه الأداة",
+ "shareResult": "مشاركة النتيجة",
+ "toolLabel": "رابط الأداة",
+ "resultLabel": "رابط التنزيل",
+ "native": "مشاركة",
+ "copyLink": "نسخ الرابط",
+ "copied": "تم النسخ",
+ "note": "شارك روابط التنزيل فقط عندما تكون مرتاحاً لمنح الآخرين إمكانية الوصول إلى الملف الناتج.",
+ "resultFallbackTitle": "الملف الناتج",
+ "resultDescription": "شارك وصولاً مباشراً إلى {{filename}}.",
+ "targets": {
+ "whatsapp": "واتساب",
+ "facebook": "فيسبوك",
+ "telegram": "تيليجرام",
+ "x": "X",
+ "linkedin": "لينكدإن",
+ "email": "البريد الإلكتروني"
+ }
+ },
"home": {
"hero": "كل ما تحتاجه للتعامل مع ملفات PDF — فوراً وبخطوات بسيطة",
"heroSub": "ارفع ملفك أو اسحبه هنا، وسنكتشف نوعه تلقائيًا ونقترح الأدوات الملائمة — التحرير، التحويل، الضغط وغير ذلك. لا حاجة لتسجيل حساب لبدء الاستخدام.",
@@ -140,7 +184,7 @@
"title": "سياسة الخصوصية",
"lastUpdated": "آخر تحديث: {{date}}",
"dataCollectionTitle": "1. جمع البيانات",
- "dataCollectionText": "نقوم فقط بمعالجة الملفات التي ترفعها عمداً. لا نطلب التسجيل ولا نجمع معلومات شخصية أثناء معالجة الملفات. إذا أنشأت حساباً، نخزن فقط بريدك الإلكتروني وكلمة المرور المشفرة.",
+ "dataCollectionText": "نقوم فقط بمعالجة الملفات التي ترفعها عمداً. لا نطلب التسجيل ولا نجمع معلومات شخصية أثناء معالجة الملفات. إذا أنشأت حساباً، نخزن فقط بريدك الإلكتروني وكلمة المرور المشفرة. وإذا استخدمت مساعد الموقع، فقد يتم حفظ رسائلك وردود المساعد لتحسين جودة الدعم، ودراسة استخدام المنتج، وتطوير تجربة المساعد.",
"fileHandlingTitle": "2. معالجة الملفات والتخزين",
"fileHandlingItems": [
"تتم معالجة الملفات المرفوعة على خوادمنا الآمنة.",
@@ -155,7 +199,8 @@
"thirdPartyItems": [
"Google AdSense — لعرض الإعلانات",
"Google Analytics — لإحصائيات الاستخدام المجهولة",
- "التخزين السحابي — لتخزين الملفات المؤقت والمشفر"
+ "التخزين السحابي — لتخزين الملفات المؤقت والمشفر",
+ "مزودو نماذج الذكاء الاصطناعي — لإنشاء ردود المساعد عند تفعيلها"
],
"securityTitle": "5. الأمان",
"securityText": "نستخدم إجراءات أمنية وفق معايير الصناعة تشمل تشفير HTTPS والتحقق من الملفات وتحديد المعدل وتعقيم المدخلات وتنظيف الملفات التلقائي. جميع البيانات أثناء النقل مشفرة وتتم معالجة الملفات في بيئات معزولة.",
diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json
index 160b2a0..fd671a1 100644
--- a/frontend/src/i18n/en.json
+++ b/frontend/src/i18n/en.json
@@ -62,6 +62,50 @@
"noToken": "Invalid reset link. Please request a new one."
}
},
+ "assistant": {
+ "badge": "AI Guide",
+ "title": "Need help choosing the right tool?",
+ "subtitle": "Ask about PDF, image, video, or text workflows and get a guided answer right away.",
+ "dataNotice": "Messages sent here may be stored so we can improve the assistant and understand user needs better.",
+ "greeting": "Hi, I can help you find the right tool or explain how the site works.",
+ "greetingWithTool": "Hi, I can help you use {{tool}} or point you to a better fit if needed.",
+ "emptyState": "Ask for the best tool, the right workflow, or how to share and download your result.",
+ "inputPlaceholder": "Ask about any tool or workflow...",
+ "send": "Send message",
+ "thinking": "Thinking...",
+ "unavailable": "The assistant is temporarily unavailable. Please try again in a moment.",
+ "close": "Close assistant",
+ "fabTitle": "Ask SaaS-PDF",
+ "fabSubtitle": "Smart help across all tools",
+ "prompts": {
+ "currentTool": "How do I use {{tool}}?",
+ "alternativeTool": "Is there a better tool for this task?",
+ "share": "Can I share the result from this tool?",
+ "findTool": "Which tool should I use for my file?",
+ "pdfWorkflows": "What are the best PDF tools here?",
+ "imageWorkflows": "Which tools work for images and OCR?"
+ }
+ },
+ "share": {
+ "shareTool": "Share this tool",
+ "shareResult": "Share result",
+ "toolLabel": "Tool link",
+ "resultLabel": "Download link",
+ "native": "Share",
+ "copyLink": "Copy link",
+ "copied": "Copied",
+ "note": "Only share download links when you are comfortable giving others access to the processed file.",
+ "resultFallbackTitle": "Processed file",
+ "resultDescription": "Share direct access to {{filename}}.",
+ "targets": {
+ "whatsapp": "WhatsApp",
+ "facebook": "Facebook",
+ "telegram": "Telegram",
+ "x": "X",
+ "linkedin": "LinkedIn",
+ "email": "Email"
+ }
+ },
"home": {
"hero": "Everything You Need to Work with PDF Files — Instantly",
"heroSub": "Upload or drag & drop your file, and we'll auto-detect its type and suggest the right tools — edit, convert, compress, and more. No registration required.",
@@ -140,7 +184,7 @@
"title": "Privacy Policy",
"lastUpdated": "Last updated: {{date}}",
"dataCollectionTitle": "1. Data Collection",
- "dataCollectionText": "We only process files you intentionally upload. We do not require registration, and no personal information is collected during file processing. If you create an account, we store only your email address and hashed password.",
+ "dataCollectionText": "We only process files you intentionally upload. We do not require registration, and no personal information is collected during file processing. If you create an account, we store only your email address and hashed password. If you use the site assistant, your messages and assistant replies may be stored to improve support quality, study product usage, and refine the assistant experience.",
"fileHandlingTitle": "2. File Processing & Storage",
"fileHandlingItems": [
"Uploaded files are processed on our secure servers.",
@@ -155,7 +199,8 @@
"thirdPartyItems": [
"Google AdSense — for displaying advertisements",
"Google Analytics — for anonymous usage statistics",
- "Cloud storage — for temporary encrypted file storage"
+ "Cloud storage — for temporary encrypted file storage",
+ "AI model providers — for generating assistant replies when enabled"
],
"securityTitle": "5. Security",
"securityText": "We employ industry-standard security measures including HTTPS encryption, file validation, rate limiting, input sanitization, and automatic file cleanup. All data in transit is encrypted, and files are processed in isolated environments.",
diff --git a/frontend/src/i18n/fr.json b/frontend/src/i18n/fr.json
index 9571694..859be2e 100644
--- a/frontend/src/i18n/fr.json
+++ b/frontend/src/i18n/fr.json
@@ -62,6 +62,50 @@
"noToken": "Lien invalide. Veuillez en demander un nouveau."
}
},
+ "assistant": {
+ "badge": "Guide IA",
+ "title": "Besoin d'aide pour choisir le bon outil ?",
+ "subtitle": "Posez des questions sur les workflows PDF, image, vidéo ou texte et obtenez une réponse guidée immédiatement.",
+ "dataNotice": "Les messages envoyés ici peuvent être conservés afin d'améliorer l'assistant et de mieux comprendre les besoins des utilisateurs.",
+ "greeting": "Bonjour, je peux vous aider à trouver le bon outil ou à comprendre comment utiliser le site.",
+ "greetingWithTool": "Bonjour, je peux vous aider à utiliser {{tool}} ou vous orienter vers un meilleur choix si nécessaire.",
+ "emptyState": "Demandez le meilleur outil, le bon workflow, ou comment partager et télécharger votre résultat.",
+ "inputPlaceholder": "Posez une question sur un outil ou un workflow...",
+ "send": "Envoyer le message",
+ "thinking": "Réflexion en cours...",
+ "unavailable": "L'assistant est temporairement indisponible. Veuillez réessayer dans un instant.",
+ "close": "Fermer l'assistant",
+ "fabTitle": "Demander à SaaS-PDF",
+ "fabSubtitle": "Aide intelligente sur tous les outils",
+ "prompts": {
+ "currentTool": "Comment utiliser {{tool}} ?",
+ "alternativeTool": "Existe-t-il un meilleur outil pour cette tâche ?",
+ "share": "Puis-je partager le résultat de cet outil ?",
+ "findTool": "Quel outil convient à mon fichier ?",
+ "pdfWorkflows": "Quels sont les meilleurs outils PDF ici ?",
+ "imageWorkflows": "Quels outils fonctionnent avec les images et l'OCR ?"
+ }
+ },
+ "share": {
+ "shareTool": "Partager cet outil",
+ "shareResult": "Partager le résultat",
+ "toolLabel": "Lien de l'outil",
+ "resultLabel": "Lien de téléchargement",
+ "native": "Partager",
+ "copyLink": "Copier le lien",
+ "copied": "Copié",
+ "note": "Ne partagez les liens de téléchargement que si vous acceptez de donner à d'autres accès au fichier traité.",
+ "resultFallbackTitle": "Fichier traité",
+ "resultDescription": "Partager un accès direct à {{filename}}.",
+ "targets": {
+ "whatsapp": "WhatsApp",
+ "facebook": "Facebook",
+ "telegram": "Telegram",
+ "x": "X",
+ "linkedin": "LinkedIn",
+ "email": "E-mail"
+ }
+ },
"home": {
"hero": "Tout ce dont vous avez besoin pour vos fichiers PDF — instantanément",
"heroSub": "Déposez votre fichier ici, nous détecterons automatiquement son type et proposerons les outils adaptés — édition, conversion, compression et plus. Aucune inscription requise.",
@@ -140,7 +184,7 @@
"title": "Politique de confidentialité",
"lastUpdated": "Dernière mise à jour : {{date}}",
"dataCollectionTitle": "1. Collecte de données",
- "dataCollectionText": "Nous ne traitons que les fichiers que vous téléchargez intentionnellement. Nous n'exigeons pas d'inscription et aucune information personnelle n'est collectée lors du traitement des fichiers. Si vous créez un compte, nous ne stockons que votre adresse e-mail et votre mot de passe chiffré.",
+ "dataCollectionText": "Nous ne traitons que les fichiers que vous téléchargez intentionnellement. Nous n'exigeons pas d'inscription et aucune information personnelle n'est collectée lors du traitement des fichiers. Si vous créez un compte, nous ne stockons que votre adresse e-mail et votre mot de passe chiffré. Si vous utilisez l'assistant du site, vos messages et les réponses de l'assistant peuvent être conservés pour améliorer la qualité du support, étudier l'usage du produit et affiner l'expérience assistant.",
"fileHandlingTitle": "2. Traitement et stockage des fichiers",
"fileHandlingItems": [
"Les fichiers téléchargés sont traités sur nos serveurs sécurisés.",
@@ -155,7 +199,8 @@
"thirdPartyItems": [
"Google AdSense — pour l'affichage de publicités",
"Google Analytics — pour les statistiques d'utilisation anonymes",
- "Stockage cloud — pour le stockage temporaire chiffré des fichiers"
+ "Stockage cloud — pour le stockage temporaire chiffré des fichiers",
+ "Fournisseurs de modèles IA — pour générer les réponses de l'assistant lorsqu'elles sont activées"
],
"securityTitle": "5. Sécurité",
"securityText": "Nous employons des mesures de sécurité conformes aux normes de l'industrie, incluant le chiffrement HTTPS, la validation des fichiers, la limitation de débit, l'assainissement des entrées et le nettoyage automatique des fichiers. Toutes les données en transit sont chiffrées et les fichiers sont traités dans des environnements isolés.",
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
index 6f2ab2a..d7be3b7 100644
--- a/frontend/src/services/api.ts
+++ b/frontend/src/services/api.ts
@@ -122,6 +122,80 @@ export interface HistoryEntry {
created_at: string;
}
+export interface AssistantHistoryMessage {
+ role: 'user' | 'assistant';
+ content: string;
+}
+
+export interface AssistantChatRequest {
+ message: string;
+ session_id?: string;
+ fingerprint: string;
+ tool_slug?: string;
+ page_url?: string;
+ locale?: string;
+ history?: AssistantHistoryMessage[];
+}
+
+export interface AssistantChatResponse {
+ session_id: string;
+ reply: string;
+ stored: boolean;
+}
+
+interface AssistantStreamHandlers {
+ onSession?: (sessionId: string) => void;
+ onChunk?: (chunk: string) => void;
+}
+
+interface AssistantStreamEvent {
+ event: string;
+ data: Record
;
+}
+
+
+function parseAssistantStreamEvent(rawEvent: string): AssistantStreamEvent | null {
+ const lines = rawEvent.split(/\r?\n/);
+ let event = 'message';
+ const dataLines: string[] = [];
+
+ for (const line of lines) {
+ if (!line) {
+ continue;
+ }
+ if (line.startsWith('event:')) {
+ event = line.slice(6).trim();
+ continue;
+ }
+ if (line.startsWith('data:')) {
+ dataLines.push(line.slice(5).trim());
+ }
+ }
+
+ if (!dataLines.length) {
+ return null;
+ }
+
+ return {
+ event,
+ data: JSON.parse(dataLines.join('\n')) as Record,
+ };
+}
+
+
+function normalizeStreamError(status: number, bodyText: string): Error {
+ if (!bodyText.trim()) {
+ return new Error(`Request failed (${status}).`);
+ }
+
+ try {
+ const parsed = JSON.parse(bodyText) as { error?: string; message?: string };
+ return new Error(parsed.error || parsed.message || `Request failed (${status}).`);
+ } catch {
+ return new Error(bodyText.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim());
+ }
+}
+
/**
* Upload a file and start a processing task.
*/
@@ -242,6 +316,104 @@ export async function getTaskStatus(taskId: string): Promise {
return response.data;
}
+/**
+ * Send one message to the site assistant.
+ */
+export async function chatWithAssistant(
+ payload: AssistantChatRequest
+): Promise {
+ const response = await api.post('/assistant/chat', payload);
+ return response.data;
+}
+
+
+/**
+ * Stream one assistant response incrementally over SSE.
+ */
+export async function streamAssistantChat(
+ payload: AssistantChatRequest,
+ handlers: AssistantStreamHandlers = {}
+): Promise {
+ const response = await fetch('/api/assistant/chat/stream', {
+ method: 'POST',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'text/event-stream',
+ },
+ body: JSON.stringify(payload),
+ });
+
+ if (!response.ok) {
+ const bodyText = await response.text();
+ throw normalizeStreamError(response.status, bodyText);
+ }
+
+ if (!response.body) {
+ throw new Error('Streaming is not supported by this browser.');
+ }
+
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let buffer = '';
+ let finalResponse: AssistantChatResponse | null = null;
+
+ while (true) {
+ const { value, done } = await reader.read();
+ buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
+
+ let boundary = buffer.indexOf('\n\n');
+ while (boundary !== -1) {
+ const rawEvent = buffer.slice(0, boundary);
+ buffer = buffer.slice(boundary + 2);
+ const parsedEvent = parseAssistantStreamEvent(rawEvent);
+
+ if (parsedEvent?.event === 'session') {
+ const sessionId = parsedEvent.data.session_id;
+ if (typeof sessionId === 'string') {
+ handlers.onSession?.(sessionId);
+ }
+ }
+
+ if (parsedEvent?.event === 'chunk') {
+ const chunk = parsedEvent.data.content;
+ if (typeof chunk === 'string' && chunk) {
+ handlers.onChunk?.(chunk);
+ }
+ }
+
+ if (parsedEvent?.event === 'done') {
+ const sessionId = parsedEvent.data.session_id;
+ const reply = parsedEvent.data.reply;
+ const stored = parsedEvent.data.stored;
+ if (
+ typeof sessionId === 'string' &&
+ typeof reply === 'string' &&
+ typeof stored === 'boolean'
+ ) {
+ finalResponse = {
+ session_id: sessionId,
+ reply,
+ stored,
+ };
+ }
+ }
+
+ boundary = buffer.indexOf('\n\n');
+ }
+
+ if (done) {
+ break;
+ }
+ }
+
+ if (!finalResponse) {
+ throw new Error('Assistant stream ended unexpectedly.');
+ }
+
+ return finalResponse;
+}
+
/**
* Check API health.
*/