mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-14 22:40:59 +00:00
fix(web): keep feedback dialog interactive
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import base64
|
||||||
|
|
||||||
import quart
|
import quart
|
||||||
|
|
||||||
from .. import group
|
from .. import group
|
||||||
@@ -36,8 +38,6 @@ class SurveyRouterGroup(group.RouterGroup):
|
|||||||
json_data = await quart.request.get_json(silent=True) or {}
|
json_data = await quart.request.get_json(silent=True) or {}
|
||||||
content = str(json_data.get('content', '')).strip()
|
content = str(json_data.get('content', '')).strip()
|
||||||
attachments = json_data.get('attachments', [])
|
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:
|
if not content:
|
||||||
return self.fail(1, 'content required')
|
return self.fail(1, 'content required')
|
||||||
@@ -57,7 +57,11 @@ class SurveyRouterGroup(group.RouterGroup):
|
|||||||
name = str(item.get('name', ''))[:255]
|
name = str(item.get('name', ''))[:255]
|
||||||
if not data_url.startswith('data:image/'):
|
if not data_url.startswith('data:image/'):
|
||||||
continue
|
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')
|
return self.fail(5, 'attachment too large')
|
||||||
normalized_attachments.append({'name': name, 'mime_type': mime_type, 'data_url': data_url})
|
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(
|
ok = await self.ap.survey.submit_feedback(
|
||||||
content=content,
|
content=content,
|
||||||
attachments=normalized_attachments,
|
attachments=normalized_attachments,
|
||||||
page_url=page_url,
|
|
||||||
user_agent=user_agent,
|
|
||||||
user_email=user_email,
|
user_email=user_email,
|
||||||
)
|
)
|
||||||
if ok:
|
if ok:
|
||||||
|
|||||||
@@ -200,8 +200,6 @@ class SurveyManager:
|
|||||||
self,
|
self,
|
||||||
content: str,
|
content: str,
|
||||||
attachments: list[dict],
|
attachments: list[dict],
|
||||||
page_url: str,
|
|
||||||
user_agent: str,
|
|
||||||
user_email: str | None = None,
|
user_email: str | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Submit an on-demand user feedback item to Space."""
|
"""Submit an on-demand user feedback item to Space."""
|
||||||
@@ -210,12 +208,6 @@ class SurveyManager:
|
|||||||
try:
|
try:
|
||||||
url = f'{self._space_url}/api/v1/survey/feedback'
|
url = f'{self._space_url}/api/v1/survey/feedback'
|
||||||
metadata = await self._build_base_metadata(user_email)
|
metadata = await self._build_base_metadata(user_email)
|
||||||
metadata.update(
|
|
||||||
{
|
|
||||||
'page_url': page_url,
|
|
||||||
'user_agent': user_agent,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
payload = {
|
payload = {
|
||||||
'instance_id': constants.instance_id,
|
'instance_id': constants.instance_id,
|
||||||
'content': content,
|
'content': content,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
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 { useTranslation } from 'react-i18next';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -7,7 +7,7 @@ import { Textarea } from '@/components/ui/textarea';
|
|||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
|
|
||||||
const MAX_ATTACHMENTS = 3;
|
const MAX_ATTACHMENTS = 3;
|
||||||
const MAX_IMAGE_BYTES = 2 * 1024 * 1024;
|
const MAX_IMAGE_BYTES = 1024 * 1024;
|
||||||
|
|
||||||
type FeedbackAttachment = {
|
type FeedbackAttachment = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -15,11 +15,6 @@ type FeedbackAttachment = {
|
|||||||
data_url: string;
|
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<FeedbackAttachment> {
|
function readImageFile(file: File): Promise<FeedbackAttachment> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!file.type.startsWith('image/')) {
|
if (!file.type.startsWith('image/')) {
|
||||||
@@ -48,38 +43,7 @@ function readImageFile(file: File): Promise<FeedbackAttachment> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function captureScreen(): Promise<FeedbackAttachment> {
|
const FEEDBACK_I18N_PREFIX = 'monitoring.feedback';
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FeedbackPopoverContent({
|
export function FeedbackPopoverContent({
|
||||||
onSubmitted,
|
onSubmitted,
|
||||||
@@ -87,17 +51,20 @@ export function FeedbackPopoverContent({
|
|||||||
onSubmitted?: () => void;
|
onSubmitted?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const tf = useCallback(
|
||||||
|
(key: string) => t(`${FEEDBACK_I18N_PREFIX}.${key}`),
|
||||||
|
[t],
|
||||||
|
);
|
||||||
const [content, setContent] = useState('');
|
const [content, setContent] = useState('');
|
||||||
const [attachments, setAttachments] = useState<FeedbackAttachment[]>([]);
|
const [attachments, setAttachments] = useState<FeedbackAttachment[]>([]);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [capturing, setCapturing] = useState(false);
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const addFiles = useCallback(
|
const addFiles = useCallback(
|
||||||
async (files: File[]) => {
|
async (files: File[]) => {
|
||||||
const slots = MAX_ATTACHMENTS - attachments.length;
|
const slots = MAX_ATTACHMENTS - attachments.length;
|
||||||
if (slots <= 0) {
|
if (slots <= 0) {
|
||||||
toast.error(t('feedback.tooManyImages'));
|
toast.error(tf('tooManyImages'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const picked = files.slice(0, slots);
|
const picked = files.slice(0, slots);
|
||||||
@@ -108,9 +75,7 @@ export function FeedbackPopoverContent({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const msg = error instanceof Error ? error.message : '';
|
const msg = error instanceof Error ? error.message : '';
|
||||||
toast.error(
|
toast.error(
|
||||||
msg === 'too_large'
|
msg === 'too_large' ? tf('imageTooLarge') : tf('imageOnly'),
|
||||||
? t('feedback.imageTooLarge')
|
|
||||||
: t('feedback.imageOnly'),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,7 +83,7 @@ export function FeedbackPopoverContent({
|
|||||||
setAttachments((prev) => [...prev, ...next].slice(0, MAX_ATTACHMENTS));
|
setAttachments((prev) => [...prev, ...next].slice(0, MAX_ATTACHMENTS));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[attachments.length, t],
|
[attachments.length, tf],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -135,31 +100,10 @@ export function FeedbackPopoverContent({
|
|||||||
return () => window.removeEventListener('paste', onPaste);
|
return () => window.removeEventListener('paste', onPaste);
|
||||||
}, [addFiles]);
|
}, [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 handleSubmit = async () => {
|
||||||
const trimmed = content.trim();
|
const trimmed = content.trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
toast.error(t('feedback.contentRequired'));
|
toast.error(tf('contentRequired'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -167,34 +111,32 @@ export function FeedbackPopoverContent({
|
|||||||
await httpClient.submitFeedback({
|
await httpClient.submitFeedback({
|
||||||
content: trimmed,
|
content: trimmed,
|
||||||
attachments,
|
attachments,
|
||||||
page_url: window.location.href,
|
|
||||||
user_agent: navigator.userAgent,
|
|
||||||
});
|
});
|
||||||
toast.success(t('feedback.submitSuccess'));
|
toast.success(tf('submitSuccess'));
|
||||||
setContent('');
|
setContent('');
|
||||||
setAttachments([]);
|
setAttachments([]);
|
||||||
onSubmitted?.();
|
onSubmitted?.();
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(t('feedback.submitFailed'));
|
toast.error(tf('submitFailed'));
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-80 space-y-3 p-1" onClick={(e) => e.stopPropagation()}>
|
<div className="space-y-3" onClick={(e) => e.stopPropagation()}>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium">{t('feedback.title')}</div>
|
<div className="text-sm font-medium">{tf('title')}</div>
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
{t('feedback.description')}
|
{tf('description')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Textarea
|
<Textarea
|
||||||
value={content}
|
value={content}
|
||||||
onChange={(e) => setContent(e.target.value)}
|
onChange={(e) => setContent(e.target.value)}
|
||||||
placeholder={t('feedback.placeholder')}
|
placeholder={tf('placeholder')}
|
||||||
maxLength={5000}
|
maxLength={5000}
|
||||||
className="min-h-28 resize-none text-sm"
|
className="min-h-32 resize-none text-sm"
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{attachments.map((item, index) => (
|
{attachments.map((item, index) => (
|
||||||
@@ -213,7 +155,7 @@ export function FeedbackPopoverContent({
|
|||||||
setAttachments((prev) => prev.filter((_, i) => i !== index))
|
setAttachments((prev) => prev.filter((_, i) => i !== index))
|
||||||
}
|
}
|
||||||
className="absolute right-1 top-1 rounded-full bg-black/60 p-0.5 text-white"
|
className="absolute right-1 top-1 rounded-full bg-black/60 p-0.5 text-white"
|
||||||
aria-label={t('feedback.removeImage')}
|
aria-label={tf('removeImage')}
|
||||||
>
|
>
|
||||||
<X className="size-3" />
|
<X className="size-3" />
|
||||||
</button>
|
</button>
|
||||||
@@ -240,21 +182,7 @@ export function FeedbackPopoverContent({
|
|||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
>
|
>
|
||||||
<ImagePlus className="mr-1 size-4" />
|
<ImagePlus className="mr-1 size-4" />
|
||||||
{t('feedback.attachImage')}
|
{tf('attachImage')}
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleCapture}
|
|
||||||
disabled={capturing}
|
|
||||||
>
|
|
||||||
{capturing ? (
|
|
||||||
<Loader2 className="mr-1 size-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Camera className="mr-1 size-4" />
|
|
||||||
)}
|
|
||||||
{t('feedback.screenshot')}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||||
@@ -268,10 +196,10 @@ export function FeedbackPopoverContent({
|
|||||||
) : (
|
) : (
|
||||||
<Send className="mr-2 size-4" />
|
<Send className="mr-2 size-4" />
|
||||||
)}
|
)}
|
||||||
{t('feedback.submit')}
|
{tf('submit')}
|
||||||
</Button>
|
</Button>
|
||||||
<p className="text-[11px] leading-relaxed text-muted-foreground">
|
<p className="text-[11px] leading-relaxed text-muted-foreground">
|
||||||
{t('feedback.privacyHint')}
|
{tf('privacyHint')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1570,6 +1570,7 @@ export default function HomeSidebar({
|
|||||||
);
|
);
|
||||||
const [hasNewVersion, setHasNewVersion] = useState(false);
|
const [hasNewVersion, setHasNewVersion] = useState(false);
|
||||||
const [versionDialogOpen, setVersionDialogOpen] = useState(false);
|
const [versionDialogOpen, setVersionDialogOpen] = useState(false);
|
||||||
|
const [feedbackOpen, setFeedbackOpen] = useState(false);
|
||||||
const [userEmail, setUserEmail] = useState<string>('');
|
const [userEmail, setUserEmail] = useState<string>('');
|
||||||
const [starCount, setStarCount] = useState<number | null>(null);
|
const [starCount, setStarCount] = useState<number | null>(null);
|
||||||
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||||
@@ -2040,25 +2041,15 @@ export default function HomeSidebar({
|
|||||||
<CircleHelp />
|
<CircleHelp />
|
||||||
{t('common.helpDocs')}
|
{t('common.helpDocs')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<Popover>
|
<DropdownMenuItem
|
||||||
<PopoverTrigger asChild>
|
onClick={() => {
|
||||||
<DropdownMenuItem
|
setUserMenuOpen(false);
|
||||||
onSelect={(event) => event.preventDefault()}
|
setFeedbackOpen(true);
|
||||||
>
|
}}
|
||||||
<Lightbulb />
|
>
|
||||||
{t('common.featureRequest')}
|
<Lightbulb />
|
||||||
</DropdownMenuItem>
|
{t('common.featureRequest')}
|
||||||
</PopoverTrigger>
|
</DropdownMenuItem>
|
||||||
<PopoverContent
|
|
||||||
side={isMobile ? 'bottom' : 'right'}
|
|
||||||
align="start"
|
|
||||||
className="w-auto p-3"
|
|
||||||
>
|
|
||||||
<FeedbackPopoverContent
|
|
||||||
onSubmitted={() => setUserMenuOpen(false)}
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.open(
|
window.open(
|
||||||
@@ -2105,6 +2096,18 @@ export default function HomeSidebar({
|
|||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
|
||||||
|
<Dialog open={feedbackOpen} onOpenChange={setFeedbackOpen}>
|
||||||
|
<DialogContent className="w-[calc(100vw-2rem)] sm:max-w-[380px]">
|
||||||
|
<DialogHeader className="sr-only">
|
||||||
|
<DialogTitle>{t('monitoring.feedback.title')}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t('monitoring.feedback.description')}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<FeedbackPopoverContent onSubmitted={() => setFeedbackOpen(false)} />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<SettingsDialog
|
<SettingsDialog
|
||||||
open={settingsOpen}
|
open={settingsOpen}
|
||||||
onOpenChange={handleSettingsOpenChange}
|
onOpenChange={handleSettingsOpenChange}
|
||||||
|
|||||||
@@ -1334,8 +1334,6 @@ export class BackendClient extends BaseHttpClient {
|
|||||||
|
|
||||||
public submitFeedback(data: {
|
public submitFeedback(data: {
|
||||||
content: string;
|
content: string;
|
||||||
page_url?: string;
|
|
||||||
user_agent?: string;
|
|
||||||
attachments?: Array<{
|
attachments?: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
mime_type: string;
|
mime_type: string;
|
||||||
|
|||||||
@@ -1363,7 +1363,7 @@ const enUS = {
|
|||||||
platform: 'Platform',
|
platform: 'Platform',
|
||||||
exportFeedback: 'Export Feedback',
|
exportFeedback: 'Export Feedback',
|
||||||
description:
|
description:
|
||||||
'Tell us what went wrong or what could be better. Instance UUID, login account, and page info are included for diagnosis.',
|
'Tell us what went wrong or what could be better. Instance UUID and login account are included for diagnosis.',
|
||||||
placeholder: 'Describe your suggestion, issue, or reproduction steps...',
|
placeholder: 'Describe your suggestion, issue, or reproduction steps...',
|
||||||
attachImage: 'Add image',
|
attachImage: 'Add image',
|
||||||
screenshot: 'Screenshot',
|
screenshot: 'Screenshot',
|
||||||
@@ -1372,7 +1372,7 @@ const enUS = {
|
|||||||
'Do not include secrets, passwords, or private chat content.',
|
'Do not include secrets, passwords, or private chat content.',
|
||||||
contentRequired: 'Please enter feedback first',
|
contentRequired: 'Please enter feedback first',
|
||||||
imageOnly: 'Only image attachments are supported',
|
imageOnly: 'Only image attachments are supported',
|
||||||
imageTooLarge: 'Each image must be under 2MB',
|
imageTooLarge: 'Each image must be under 1MB',
|
||||||
tooManyImages: 'You can attach up to 3 images',
|
tooManyImages: 'You can attach up to 3 images',
|
||||||
screenshotFailed: 'Screenshot failed. Try pasting or uploading an image.',
|
screenshotFailed: 'Screenshot failed. Try pasting or uploading an image.',
|
||||||
submitSuccess: 'Feedback submitted. Thanks!',
|
submitSuccess: 'Feedback submitted. Thanks!',
|
||||||
|
|||||||
@@ -1395,6 +1395,22 @@ const esES = {
|
|||||||
inaccurateReasons: 'Razones de inexactitud',
|
inaccurateReasons: 'Razones de inexactitud',
|
||||||
platform: 'Plataforma',
|
platform: 'Plataforma',
|
||||||
exportFeedback: 'Exportar comentarios',
|
exportFeedback: 'Exportar comentarios',
|
||||||
|
description:
|
||||||
|
'Tell us what went wrong or what could be better. Instance UUID and login account are included for diagnosis.',
|
||||||
|
placeholder: 'Describe your suggestion, issue, or reproduction steps...',
|
||||||
|
attachImage: 'Add image',
|
||||||
|
screenshot: 'Screenshot',
|
||||||
|
submit: 'Submit feedback',
|
||||||
|
privacyHint:
|
||||||
|
'Do not include secrets, passwords, or private chat content.',
|
||||||
|
contentRequired: 'Please enter feedback first',
|
||||||
|
imageOnly: 'Only image attachments are supported',
|
||||||
|
imageTooLarge: 'Each image must be under 1MB',
|
||||||
|
tooManyImages: 'You can attach up to 3 images',
|
||||||
|
screenshotFailed: 'Screenshot failed. Try pasting or uploading an image.',
|
||||||
|
submitSuccess: 'Feedback submitted. Thanks!',
|
||||||
|
submitFailed: 'Failed to submit feedback. Please try again later.',
|
||||||
|
removeImage: 'Remove image',
|
||||||
},
|
},
|
||||||
queries: {
|
queries: {
|
||||||
title: 'Consultas',
|
title: 'Consultas',
|
||||||
|
|||||||
@@ -1371,6 +1371,22 @@ const ruRU = {
|
|||||||
inaccurateReasons: 'Причины неточности',
|
inaccurateReasons: 'Причины неточности',
|
||||||
platform: 'Платформа',
|
platform: 'Платформа',
|
||||||
exportFeedback: 'Экспорт отзывов',
|
exportFeedback: 'Экспорт отзывов',
|
||||||
|
description:
|
||||||
|
'Tell us what went wrong or what could be better. Instance UUID and login account are included for diagnosis.',
|
||||||
|
placeholder: 'Describe your suggestion, issue, or reproduction steps...',
|
||||||
|
attachImage: 'Add image',
|
||||||
|
screenshot: 'Screenshot',
|
||||||
|
submit: 'Submit feedback',
|
||||||
|
privacyHint:
|
||||||
|
'Do not include secrets, passwords, or private chat content.',
|
||||||
|
contentRequired: 'Please enter feedback first',
|
||||||
|
imageOnly: 'Only image attachments are supported',
|
||||||
|
imageTooLarge: 'Each image must be under 1MB',
|
||||||
|
tooManyImages: 'You can attach up to 3 images',
|
||||||
|
screenshotFailed: 'Screenshot failed. Try pasting or uploading an image.',
|
||||||
|
submitSuccess: 'Feedback submitted. Thanks!',
|
||||||
|
submitFailed: 'Failed to submit feedback. Please try again later.',
|
||||||
|
removeImage: 'Remove image',
|
||||||
},
|
},
|
||||||
queries: {
|
queries: {
|
||||||
title: 'Запросы',
|
title: 'Запросы',
|
||||||
|
|||||||
@@ -1340,6 +1340,22 @@ const thTH = {
|
|||||||
inaccurateReasons: 'เหตุผลที่ไม่ถูกต้อง',
|
inaccurateReasons: 'เหตุผลที่ไม่ถูกต้อง',
|
||||||
platform: 'แพลตฟอร์ม',
|
platform: 'แพลตฟอร์ม',
|
||||||
exportFeedback: 'ส่งออกความคิดเห็น',
|
exportFeedback: 'ส่งออกความคิดเห็น',
|
||||||
|
description:
|
||||||
|
'Tell us what went wrong or what could be better. Instance UUID and login account are included for diagnosis.',
|
||||||
|
placeholder: 'Describe your suggestion, issue, or reproduction steps...',
|
||||||
|
attachImage: 'Add image',
|
||||||
|
screenshot: 'Screenshot',
|
||||||
|
submit: 'Submit feedback',
|
||||||
|
privacyHint:
|
||||||
|
'Do not include secrets, passwords, or private chat content.',
|
||||||
|
contentRequired: 'Please enter feedback first',
|
||||||
|
imageOnly: 'Only image attachments are supported',
|
||||||
|
imageTooLarge: 'Each image must be under 1MB',
|
||||||
|
tooManyImages: 'You can attach up to 3 images',
|
||||||
|
screenshotFailed: 'Screenshot failed. Try pasting or uploading an image.',
|
||||||
|
submitSuccess: 'Feedback submitted. Thanks!',
|
||||||
|
submitFailed: 'Failed to submit feedback. Please try again later.',
|
||||||
|
removeImage: 'Remove image',
|
||||||
},
|
},
|
||||||
queries: {
|
queries: {
|
||||||
title: 'คำค้นหา',
|
title: 'คำค้นหา',
|
||||||
|
|||||||
@@ -1364,6 +1364,22 @@ const viVN = {
|
|||||||
inaccurateReasons: 'Lý do không chính xác',
|
inaccurateReasons: 'Lý do không chính xác',
|
||||||
platform: 'Nền tảng',
|
platform: 'Nền tảng',
|
||||||
exportFeedback: 'Xuất phản hồi',
|
exportFeedback: 'Xuất phản hồi',
|
||||||
|
description:
|
||||||
|
'Tell us what went wrong or what could be better. Instance UUID and login account are included for diagnosis.',
|
||||||
|
placeholder: 'Describe your suggestion, issue, or reproduction steps...',
|
||||||
|
attachImage: 'Add image',
|
||||||
|
screenshot: 'Screenshot',
|
||||||
|
submit: 'Submit feedback',
|
||||||
|
privacyHint:
|
||||||
|
'Do not include secrets, passwords, or private chat content.',
|
||||||
|
contentRequired: 'Please enter feedback first',
|
||||||
|
imageOnly: 'Only image attachments are supported',
|
||||||
|
imageTooLarge: 'Each image must be under 1MB',
|
||||||
|
tooManyImages: 'You can attach up to 3 images',
|
||||||
|
screenshotFailed: 'Screenshot failed. Try pasting or uploading an image.',
|
||||||
|
submitSuccess: 'Feedback submitted. Thanks!',
|
||||||
|
submitFailed: 'Failed to submit feedback. Please try again later.',
|
||||||
|
removeImage: 'Remove image',
|
||||||
},
|
},
|
||||||
queries: {
|
queries: {
|
||||||
title: 'Truy vấn',
|
title: 'Truy vấn',
|
||||||
|
|||||||
@@ -1302,7 +1302,7 @@ const zhHans = {
|
|||||||
platform: '平台',
|
platform: '平台',
|
||||||
exportFeedback: '导出反馈',
|
exportFeedback: '导出反馈',
|
||||||
description:
|
description:
|
||||||
'告诉我们遇到的问题或想要的改进。提交时会附带实例 UUID、登录账号和当前页面信息,方便定位。',
|
'告诉我们遇到的问题或想要的改进。提交时会附带实例 UUID 和登录账号,方便定位。',
|
||||||
placeholder: '请描述你的建议、问题或复现步骤...',
|
placeholder: '请描述你的建议、问题或复现步骤...',
|
||||||
attachImage: '添加图片',
|
attachImage: '添加图片',
|
||||||
screenshot: '截图',
|
screenshot: '截图',
|
||||||
@@ -1310,7 +1310,7 @@ const zhHans = {
|
|||||||
privacyHint: '请勿提交敏感密钥、密码或私人聊天内容。',
|
privacyHint: '请勿提交敏感密钥、密码或私人聊天内容。',
|
||||||
contentRequired: '请先填写反馈内容',
|
contentRequired: '请先填写反馈内容',
|
||||||
imageOnly: '仅支持图片附件',
|
imageOnly: '仅支持图片附件',
|
||||||
imageTooLarge: '单张图片不能超过 2MB',
|
imageTooLarge: '单张图片不能超过 1MB',
|
||||||
tooManyImages: '最多添加 3 张图片',
|
tooManyImages: '最多添加 3 张图片',
|
||||||
screenshotFailed: '截图失败,请尝试粘贴或上传图片',
|
screenshotFailed: '截图失败,请尝试粘贴或上传图片',
|
||||||
submitSuccess: '反馈已提交,感谢!',
|
submitSuccess: '反馈已提交,感谢!',
|
||||||
|
|||||||
@@ -1300,6 +1300,21 @@ const zhHant = {
|
|||||||
inaccurateReasons: '不準確原因',
|
inaccurateReasons: '不準確原因',
|
||||||
platform: '平台',
|
platform: '平台',
|
||||||
exportFeedback: '匯出反饋',
|
exportFeedback: '匯出反饋',
|
||||||
|
description:
|
||||||
|
'告訴我們遇到的問題或想要的改進。提交時會附帶實例 UUID 和登入帳號,方便定位。',
|
||||||
|
placeholder: '請描述你的建議、問題或重現步驟...',
|
||||||
|
attachImage: '新增圖片',
|
||||||
|
screenshot: '截圖',
|
||||||
|
submit: '提交反饋',
|
||||||
|
privacyHint: '請勿提交敏感金鑰、密碼或私人聊天內容。',
|
||||||
|
contentRequired: '請先填寫反饋內容',
|
||||||
|
imageOnly: '僅支援圖片附件',
|
||||||
|
imageTooLarge: '單張圖片不能超過 1MB',
|
||||||
|
tooManyImages: '最多新增 3 張圖片',
|
||||||
|
screenshotFailed: '截圖失敗,請嘗試貼上或上傳圖片',
|
||||||
|
submitSuccess: '反饋已提交,感謝!',
|
||||||
|
submitFailed: '反饋提交失敗,請稍後再試',
|
||||||
|
removeImage: '移除圖片',
|
||||||
},
|
},
|
||||||
messageDetails: {
|
messageDetails: {
|
||||||
noData: '此查詢沒有LLM調用或錯誤記錄',
|
noData: '此查詢沒有LLM調用或錯誤記錄',
|
||||||
|
|||||||
Reference in New Issue
Block a user