diff --git a/src/langbot/pkg/api/http/controller/groups/survey.py b/src/langbot/pkg/api/http/controller/groups/survey.py index 2d5882673..a65d51a85 100644 --- a/src/langbot/pkg/api/http/controller/groups/survey.py +++ b/src/langbot/pkg/api/http/controller/groups/survey.py @@ -1,3 +1,5 @@ +import base64 + import quart from .. import group @@ -36,8 +38,6 @@ class SurveyRouterGroup(group.RouterGroup): json_data = await quart.request.get_json(silent=True) or {} content = str(json_data.get('content', '')).strip() attachments = json_data.get('attachments', []) - page_url = str(json_data.get('page_url', ''))[:2048] - user_agent = str(json_data.get('user_agent', ''))[:512] if not content: return self.fail(1, 'content required') @@ -57,7 +57,11 @@ class SurveyRouterGroup(group.RouterGroup): name = str(item.get('name', ''))[:255] if not data_url.startswith('data:image/'): continue - if len(data_url) > 2_800_000: + try: + payload = data_url.split(',', 1)[1] + if len(base64.b64decode(payload, validate=True)) > 1024 * 1024: + return self.fail(5, 'attachment too large') + except Exception: return self.fail(5, 'attachment too large') normalized_attachments.append({'name': name, 'mime_type': mime_type, 'data_url': data_url}) @@ -65,8 +69,6 @@ class SurveyRouterGroup(group.RouterGroup): ok = await self.ap.survey.submit_feedback( content=content, attachments=normalized_attachments, - page_url=page_url, - user_agent=user_agent, user_email=user_email, ) if ok: diff --git a/src/langbot/pkg/survey/manager.py b/src/langbot/pkg/survey/manager.py index b5ea31cd5..34689625e 100644 --- a/src/langbot/pkg/survey/manager.py +++ b/src/langbot/pkg/survey/manager.py @@ -200,8 +200,6 @@ class SurveyManager: self, content: str, attachments: list[dict], - page_url: str, - user_agent: str, user_email: str | None = None, ) -> bool: """Submit an on-demand user feedback item to Space.""" @@ -210,12 +208,6 @@ class SurveyManager: try: url = f'{self._space_url}/api/v1/survey/feedback' metadata = await self._build_base_metadata(user_email) - metadata.update( - { - 'page_url': page_url, - 'user_agent': user_agent, - } - ) payload = { 'instance_id': constants.instance_id, 'content': content, diff --git a/web/src/app/home/components/home-sidebar/FeedbackPopover.tsx b/web/src/app/home/components/home-sidebar/FeedbackPopover.tsx index cbd1bcbf2..5aba435fb 100644 --- a/web/src/app/home/components/home-sidebar/FeedbackPopover.tsx +++ b/web/src/app/home/components/home-sidebar/FeedbackPopover.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState } from 'react'; -import { Camera, ImagePlus, Loader2, Paperclip, Send, X } from 'lucide-react'; +import { ImagePlus, Loader2, Paperclip, Send, X } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; @@ -7,7 +7,7 @@ import { Textarea } from '@/components/ui/textarea'; import { httpClient } from '@/app/infra/http/HttpClient'; const MAX_ATTACHMENTS = 3; -const MAX_IMAGE_BYTES = 2 * 1024 * 1024; +const MAX_IMAGE_BYTES = 1024 * 1024; type FeedbackAttachment = { name: string; @@ -15,11 +15,6 @@ type FeedbackAttachment = { data_url: string; }; -function bytesFromDataUrl(dataUrl: string): number { - const payload = dataUrl.split(',')[1] || ''; - return Math.ceil((payload.length * 3) / 4); -} - function readImageFile(file: File): Promise { return new Promise((resolve, reject) => { if (!file.type.startsWith('image/')) { @@ -48,38 +43,7 @@ function readImageFile(file: File): Promise { }); } -async function captureScreen(): Promise { - if (!navigator.mediaDevices?.getDisplayMedia) { - throw new Error('unsupported'); - } - const stream = await navigator.mediaDevices.getDisplayMedia({ - video: true, - audio: false, - }); - try { - const video = document.createElement('video'); - video.srcObject = stream; - await video.play(); - await new Promise((resolve) => requestAnimationFrame(resolve)); - const canvas = document.createElement('canvas'); - canvas.width = video.videoWidth; - canvas.height = video.videoHeight; - const ctx = canvas.getContext('2d'); - if (!ctx) throw new Error('canvas_unavailable'); - ctx.drawImage(video, 0, 0, canvas.width, canvas.height); - const dataUrl = canvas.toDataURL('image/png'); - if (bytesFromDataUrl(dataUrl) > MAX_IMAGE_BYTES) { - throw new Error('too_large'); - } - return { - name: `screenshot-${Date.now()}.png`, - mime_type: 'image/png', - data_url: dataUrl, - }; - } finally { - stream.getTracks().forEach((track) => track.stop()); - } -} +const FEEDBACK_I18N_PREFIX = 'monitoring.feedback'; export function FeedbackPopoverContent({ onSubmitted, @@ -87,17 +51,20 @@ export function FeedbackPopoverContent({ onSubmitted?: () => void; }) { const { t } = useTranslation(); + const tf = useCallback( + (key: string) => t(`${FEEDBACK_I18N_PREFIX}.${key}`), + [t], + ); const [content, setContent] = useState(''); const [attachments, setAttachments] = useState([]); const [submitting, setSubmitting] = useState(false); - const [capturing, setCapturing] = useState(false); const fileInputRef = useRef(null); const addFiles = useCallback( async (files: File[]) => { const slots = MAX_ATTACHMENTS - attachments.length; if (slots <= 0) { - toast.error(t('feedback.tooManyImages')); + toast.error(tf('tooManyImages')); return; } const picked = files.slice(0, slots); @@ -108,9 +75,7 @@ export function FeedbackPopoverContent({ } catch (error) { const msg = error instanceof Error ? error.message : ''; toast.error( - msg === 'too_large' - ? t('feedback.imageTooLarge') - : t('feedback.imageOnly'), + msg === 'too_large' ? tf('imageTooLarge') : tf('imageOnly'), ); } } @@ -118,7 +83,7 @@ export function FeedbackPopoverContent({ setAttachments((prev) => [...prev, ...next].slice(0, MAX_ATTACHMENTS)); } }, - [attachments.length, t], + [attachments.length, tf], ); useEffect(() => { @@ -135,31 +100,10 @@ export function FeedbackPopoverContent({ return () => window.removeEventListener('paste', onPaste); }, [addFiles]); - const handleCapture = async () => { - try { - if (attachments.length >= MAX_ATTACHMENTS) { - toast.error(t('feedback.tooManyImages')); - return; - } - setCapturing(true); - const screenshot = await captureScreen(); - setAttachments((prev) => [...prev, screenshot].slice(0, MAX_ATTACHMENTS)); - } catch (error) { - const msg = error instanceof Error ? error.message : ''; - toast.error( - msg === 'too_large' - ? t('feedback.imageTooLarge') - : t('feedback.screenshotFailed'), - ); - } finally { - setCapturing(false); - } - }; - const handleSubmit = async () => { const trimmed = content.trim(); if (!trimmed) { - toast.error(t('feedback.contentRequired')); + toast.error(tf('contentRequired')); return; } try { @@ -167,34 +111,32 @@ export function FeedbackPopoverContent({ await httpClient.submitFeedback({ content: trimmed, attachments, - page_url: window.location.href, - user_agent: navigator.userAgent, }); - toast.success(t('feedback.submitSuccess')); + toast.success(tf('submitSuccess')); setContent(''); setAttachments([]); onSubmitted?.(); } catch { - toast.error(t('feedback.submitFailed')); + toast.error(tf('submitFailed')); } finally { setSubmitting(false); } }; return ( -
e.stopPropagation()}> +
e.stopPropagation()}>
-
{t('feedback.title')}
+
{tf('title')}

- {t('feedback.description')} + {tf('description')}