fix(editor): custom image width parsing, fix image paste, add AI submenu features

This commit is contained in:
2026-05-03 00:08:28 +02:00
parent d0387cd9a0
commit 54b7b4fcf1
19 changed files with 3890 additions and 3490 deletions

View File

@@ -17,7 +17,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Feature disabled' }, { status: 403 })
}
const { text, option, format } = await request.json()
const { text, option, format, language } = await request.json()
// Validation
if (!text || typeof text !== 'string') {
@@ -25,16 +25,19 @@ export async function POST(request: NextRequest) {
}
// Map option to refactor mode
const modeMap: Record<string, 'clarify' | 'shorten' | 'improveStyle'> = {
const modeMap: Record<string, 'clarify' | 'shorten' | 'improveStyle' | 'fix_grammar' | 'translate' | 'explain'> = {
'clarify': 'clarify',
'shorten': 'shorten',
'improve': 'improveStyle'
'improve': 'improveStyle',
'fix_grammar': 'fix_grammar',
'translate': 'translate',
'explain': 'explain'
}
const mode = modeMap[option]
if (!mode) {
return NextResponse.json(
{ error: 'Invalid option. Use: clarify, shorten, or improve' },
{ error: 'Invalid option. Use: clarify, shorten, improve, fix_grammar, translate, or explain' },
{ status: 400 }
)
}
@@ -50,7 +53,7 @@ export async function POST(request: NextRequest) {
}
// Use the ParagraphRefactorService
const result = await paragraphRefactorService.refactor(text, mode, format === 'html' ? 'html' : 'markdown')
const result = await paragraphRefactorService.refactor(text, mode, format === 'html' ? 'html' : 'markdown', language)
return NextResponse.json({
originalText: result.original,
@@ -66,3 +69,4 @@ export async function POST(request: NextRequest) {
)
}
}

View File

@@ -1366,6 +1366,9 @@ html.font-system * {
color: var(--foreground);
transition: background 0.1s ease, transform 0.08s ease;
}
[dir="rtl"] .notion-slash-item {
text-align: right;
}
.notion-slash-item:hover,
.notion-slash-item-selected {
background: var(--accent);

View File

@@ -124,8 +124,8 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) {
<FeatureToggle
name="IA Note"
description="Active le bouton de chat IA et les outils d'amélioration du texte"
name={t('aiSettings.aiNote')}
description={t('aiSettings.aiNoteDesc')}
checked={settings.paragraphRefactor}
onChange={(checked) => handleToggle('paragraphRefactor', checked)}
/>
@@ -167,37 +167,37 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) {
{/* Language Detection Toggle */}
<FeatureToggle
name="Détection de langue"
description="Détecte automatiquement la langue de vos notes"
name={t('aiSettings.languageDetection')}
description={t('aiSettings.languageDetectionDesc')}
checked={settings.languageDetection ?? true}
onChange={(checked) => handleToggle('languageDetection', checked)}
/>
{/* Auto Labeling Toggle */}
<FeatureToggle
name="Suggestion des labels"
description="Suggère et applique des étiquettes automatiquement à vos notes"
name={t('aiSettings.autoLabeling')}
description={t('aiSettings.autoLabelingDesc')}
checked={settings.autoLabeling ?? true}
onChange={(checked) => handleToggle('autoLabeling', checked)}
/>
<FeatureToggle
name="Historique des notes"
description="Active les snapshots de versions et la restauration depuis History"
name={t('aiSettings.noteHistory')}
description={t('aiSettings.noteHistoryDesc')}
checked={settings.noteHistory ?? false}
onChange={(checked) => handleToggle('noteHistory', checked)}
/>
{settings.noteHistory && (
<div className="space-y-2 rounded-lg border border-border/50 bg-muted/30 p-3">
<p className="text-sm font-medium">{t('notes.historyMode') || 'Mode d\'historique'}</p>
<p className="text-sm font-medium">{t('notes.historyMode')}</p>
<RadioGroup
value={settings.noteHistoryMode ?? 'manual'}
onValueChange={(value) => {
const mode = value as 'manual' | 'auto'
setSettings((s) => ({ ...s, noteHistoryMode: mode }))
updateAISettings({ noteHistoryMode: mode }).then(() => {
toast.success(t('settings.settingsSaved') || 'Saved')
toast.success(t('settings.settingsSaved'))
})
}}
className="space-y-2"
@@ -206,10 +206,10 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) {
<RadioGroupItem value="manual" id="history-manual" />
<div className="grid gap-0.5 leading-none">
<Label htmlFor="history-manual" className="text-sm font-medium">
{t('notes.historyModeManual') || 'Manuel'}
{t('notes.historyModeManual')}
</Label>
<p className="text-xs text-muted-foreground">
{t('notes.historyModeManualDesc') || 'Créer des snapshots avec le bouton commit'}
{t('notes.historyModeManualDesc')}
</p>
</div>
</div>
@@ -217,10 +217,10 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) {
<RadioGroupItem value="auto" id="history-auto" />
<div className="grid gap-0.5 leading-none">
<Label htmlFor="history-auto" className="text-sm font-medium">
{t('notes.historyModeAuto') || 'Automatique'}
{t('notes.historyModeAuto')}
</Label>
<p className="text-xs text-muted-foreground">
{t('notes.historyModeAutoDesc') || 'Snapshots automatiques avec détection intelligente'}
{t('notes.historyModeAutoDesc')}
</p>
</div>
</div>

View File

@@ -12,7 +12,7 @@ import {
Lightbulb, Minimize2, AlignLeft, Wand2,
Globe, BookOpen, FileText, RotateCcw, Check,
Maximize2, ImageIcon, Link2, Download, ArrowDownToLine,
GitMerge, PlusCircle, Eye, Code,
GitMerge, PlusCircle, Eye, Code, Languages,
} from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
import { MarkdownContent } from '@/components/markdown-content'
@@ -64,6 +64,7 @@ const ACTION_IDS = [
{ id: 'clarify', icon: Lightbulb, apiPath: '/api/ai/reformulate', body: (content: string) => ({ text: content, option: 'clarify' }), resultKey: 'reformulatedText', i18nKey: 'ai.action.clarify' },
{ id: 'shorten', icon: Minimize2, apiPath: '/api/ai/reformulate', body: (content: string) => ({ text: content, option: 'shorten' }), resultKey: 'reformulatedText', i18nKey: 'ai.action.shorten' },
{ id: 'improve', icon: AlignLeft, apiPath: '/api/ai/reformulate', body: (content: string) => ({ text: content, option: 'improve' }), resultKey: 'reformulatedText', i18nKey: 'ai.action.improve' },
{ id: 'translate', icon: Languages, apiPath: '/api/ai/reformulate', body: (content: string, _images?: string[], lang?: string) => ({ text: content, option: 'translate', language: lang || 'fr' }), resultKey: 'reformulatedText', i18nKey: 'ai.action.translate' },
{ id: 'markdown', icon: Wand2, apiPath: '/api/ai/transform-markdown', body: (content: string) => ({ text: content }), resultKey: 'transformedText', i18nKey: 'ai.action.toMarkdown' },
{ id: 'describe-images', icon: ImageIcon, apiPath: '/api/ai/describe-image', body: (_content: string, images?: string[], lang?: string) => ({ imageUrls: images || [], mode: 'description', language: lang || 'fr' }), resultKey: 'descriptions', i18nKey: 'ai.action.describeImages', isImageAction: true },
]
@@ -250,18 +251,18 @@ export function ContextualAIChat({
setResourceScraping(true)
try {
const result = await scrapePageText(resourceUrl.trim())
if (!result) { toast.error('Impossible de charger cette URL'); return }
if (!result) { toast.error(t('ai.resource.failedToLoadUrl')); return }
setResourceText(result.text)
toast.success(`Page chargée : ${result.title.slice(0, 40)}`)
toast.success(t('ai.resource.pageLoaded', { title: result.title.slice(0, 40) }))
} catch {
toast.error('Erreur lors du chargement de la page')
toast.error(t('ai.resource.pageLoadError'))
} finally {
setResourceScraping(false)
}
}
const handleResourcePreview = async () => {
if (!resourceText.trim()) { toast.error('Collez du texte ou chargez une URL d\'abord'); return }
if (!resourceText.trim()) { toast.error(t('ai.resource.pasteOrUrlFirst')); return }
if (resourceMode === 'replace') {
setResourcePreview({ text: resourceText, source: 'paste' })
return
@@ -279,10 +280,10 @@ export function ContextualAIChat({
}),
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Erreur IA')
if (!res.ok) throw new Error(data.error || t('ai.resource.enrichError'))
setResourcePreview({ text: data.enrichedContent, source: resourceMode })
} catch (e: any) {
toast.error(e.message || 'Erreur lors de l\'enrichissement')
toast.error(e.message || t('ai.resource.enrichError'))
} finally {
setResourceEnriching(false)
}
@@ -294,7 +295,7 @@ export function ContextualAIChat({
setResourcePreview(null)
setResourceText('')
setResourceUrl('')
toast.success('Contenu appliqué à la note ✓')
toast.success(t('ai.resource.contentApplied'))
}
/** Called from chat hover-actions: inject a chat message into the note */
@@ -326,7 +327,7 @@ export function ContextualAIChat({
if (!res.ok) throw new Error(data.error || 'Erreur IA')
setResourcePreview({ text: data.enrichedContent, source: mode })
} catch (e: any) {
toast.error(e.message || 'Erreur enrichissement')
toast.error(e.message || t('ai.resource.enrichErrorShort'))
} finally {
setResourceEnriching(false)
}
@@ -351,7 +352,7 @@ export function ContextualAIChat({
<div className="min-w-0">
<h2 className="text-base font-semibold text-foreground flex items-center gap-2">
<Sparkles className="h-4 w-4 text-primary shrink-0" />
IA Note
{t('ai.aiNoteTitle')}
</h2>
<p className="text-xs text-muted-foreground truncate">
{noteTitle ? `"${noteTitle}"` : t('ai.currentNote')}
@@ -390,7 +391,7 @@ export function ContextualAIChat({
{([
{ id: 'chat', icon: Bot, label: t('ai.chatTab') },
{ id: 'actions', icon: Wand2, label: t('ai.noteActions') },
{ id: 'resource', icon: ArrowDownToLine, label: 'Ressource' },
{ id: 'resource', icon: ArrowDownToLine, label: t('ai.resourceTab') },
] as const).map(tab => (
<button
key={tab.id}
@@ -466,23 +467,23 @@ export function ContextualAIChat({
<button
onClick={() => handleInjectFromChat(content, 'replace')}
className="flex items-center gap-1 px-2 py-1 rounded-md text-[10px] font-medium bg-muted hover:bg-primary/10 hover:text-primary border border-border/40 transition-colors"
title="Remplacer le contenu de la note par ce message"
title={t('ai.injectReplaceTitle')}
>
<Download className="h-2.5 w-2.5" /> Remplacer
<Download className="h-2.5 w-2.5" /> {t('ai.injectReplace')}
</button>
<button
onClick={() => handleInjectFromChat(content, 'complete')}
className="flex items-center gap-1 px-2 py-1 rounded-md text-[10px] font-medium bg-muted hover:bg-emerald-50 hover:text-emerald-600 dark:hover:bg-emerald-950/30 border border-border/40 transition-colors"
title="Compléter la note avec ce message (IA)"
title={t('ai.injectCompleteTitle')}
>
<PlusCircle className="h-2.5 w-2.5" /> Compléter
<PlusCircle className="h-2.5 w-2.5" /> {t('ai.injectComplete')}
</button>
<button
onClick={() => handleInjectFromChat(content, 'merge')}
className="flex items-center gap-1 px-2 py-1 rounded-md text-[10px] font-medium bg-muted hover:bg-violet-50 hover:text-violet-600 dark:hover:bg-violet-950/30 border border-border/40 transition-colors"
title="Fusionner avec la note (IA)"
title={t('ai.injectMergeTitle')}
>
<GitMerge className="h-2.5 w-2.5" /> Fusionner
<GitMerge className="h-2.5 w-2.5" /> {t('ai.injectMerge')}
</button>
</div>
)}
@@ -695,7 +696,7 @@ export function ContextualAIChat({
<div className="flex flex-col">
<span>{t(action.i18nKey)}</span>
{noteImages.length > 1 && (
<span className="text-[10px] text-muted-foreground">{noteImages.length} images</span>
<span className="text-[10px] text-muted-foreground">{t('ai.imagesCount', { count: noteImages.length })}</span>
)}
</div>
{loading && <span className="ml-auto text-[10px] text-muted-foreground">{t('ai.processingAction')}</span>}
@@ -760,10 +761,10 @@ export function ContextualAIChat({
<div className="px-4 py-2.5 border-b border-border/40 flex items-center justify-between shrink-0">
<div className="flex items-center gap-2">
<p className="text-xs font-semibold text-foreground">
{resourcePreview.source === 'chat' ? '💬 Depuis le chat'
: resourcePreview.source === 'replace' ? '↓ Remplacement'
: resourcePreview.source === 'complete' ? '✦ Complété par IA'
: '⟳ Fusionné par IA'}
{resourcePreview.source === 'chat' ? t('ai.resource.fromChat')
: resourcePreview.source === 'replace' ? t('ai.resource.replacement')
: resourcePreview.source === 'complete' ? t('ai.resource.completedByAI')
: t('ai.resource.mergedByAI')}
</p>
{/* Format toggle */}
<div className="flex rounded-md border border-border/40 overflow-hidden">
@@ -776,7 +777,7 @@ export function ContextualAIChat({
: 'bg-card text-muted-foreground hover:bg-muted',
)}
>
<Eye className="h-2.5 w-2.5" /> Rendu
<Eye className="h-2.5 w-2.5" /> {t('ai.resource.rendered')}
</button>
<button
onClick={() => setResourcePreviewFormat('markdown')}
@@ -814,7 +815,7 @@ export function ContextualAIChat({
className="flex-1 text-xs gap-1.5"
onClick={() => setResourcePreview(null)}
>
<X className="h-3.5 w-3.5" /> Annuler
<X className="h-3.5 w-3.5" /> {t('ai.resource.cancel')}
</Button>
<Button
size="sm"
@@ -822,7 +823,7 @@ export function ContextualAIChat({
onClick={handleApplyResourcePreview}
disabled={!onApplyToNote}
>
<Check className="h-3.5 w-3.5" /> Appliquer à la note
<Check className="h-3.5 w-3.5" /> {t('ai.resource.applyToNote')}
</Button>
</div>
</div>
@@ -832,7 +833,7 @@ export function ContextualAIChat({
{/* URL loader */}
<div>
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground block mb-1.5">
<Link2 className="h-3 w-3 inline mr-1" />URL (optionnel)
<Link2 className="h-3 w-3 inline mr-1" />{t('ai.resource.urlLabel')}
</label>
<div className="flex gap-1.5">
<input
@@ -860,18 +861,18 @@ export function ContextualAIChat({
{/* Text area */}
<div>
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground block mb-1.5">
<FileText className="h-3 w-3 inline mr-1" />Texte de la ressource
<FileText className="h-3 w-3 inline mr-1" />{t('ai.resource.resourceText')}
</label>
<textarea
value={resourceText}
onChange={e => setResourceText(e.target.value)}
placeholder="Collez votre texte ici (markdown, HTML, texte brut…)"
placeholder={t('ai.resource.resourcePlaceholder')}
rows={8}
className="w-full px-2.5 py-2 text-xs bg-card border border-border/60 rounded-lg focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/20 resize-none transition-all font-mono leading-relaxed"
/>
{resourceText && (
<p className="text-[9px] text-muted-foreground mt-0.5 text-right">
{resourceText.split(/\s+/).filter(Boolean).length} mots
{resourceText.split(/\s+/).filter(Boolean).length} {t('ai.resource.words')}
</p>
)}
</div>
@@ -879,13 +880,13 @@ export function ContextualAIChat({
{/* Mode selector */}
<div>
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground block mb-1.5">
Mode d'intégration
{t('ai.resource.integrationMode')}
</label>
<div className="grid grid-cols-3 gap-1.5">
{([
{ id: 'replace', label: 'Remplacer', desc: 'Direct, sans IA', icon: Download, color: 'primary' },
{ id: 'complete', label: 'Compléter', desc: 'Ajoute sans réécrire', icon: PlusCircle, color: 'emerald' },
{ id: 'merge', label: 'Fusionner', desc: 'Réécrit et intègre', icon: GitMerge, color: 'violet' },
{ id: 'replace', label: t('ai.resource.modeReplace'), desc: t('ai.resource.modeReplaceDesc'), icon: Download, color: 'primary' },
{ id: 'complete', label: t('ai.resource.modeComplete'), desc: t('ai.resource.modeCompleteDesc'), icon: PlusCircle, color: 'emerald' },
{ id: 'merge', label: t('ai.resource.modeMerge'), desc: t('ai.resource.modeMergeDesc'), icon: GitMerge, color: 'violet' },
] as const).map(m => {
const Icon = m.icon
const sel = resourceMode === m.id
@@ -920,17 +921,17 @@ export function ContextualAIChat({
disabled={!resourceText.trim() || resourceEnriching}
>
{resourceEnriching
? <><Loader2 className="h-4 w-4 animate-spin" /> IA en cours…</>
? <><Loader2 className="h-4 w-4 animate-spin" /> {t('ai.resource.aiProcessing')}</>
: resourceMode === 'replace'
? <><Eye className="h-4 w-4" /> Aperçu</>
: <><Sparkles className="h-4 w-4" /> Générer l'aperçu</>
? <><Eye className="h-4 w-4" /> {t('ai.resource.preview')}</>
: <><Sparkles className="h-4 w-4" /> {t('ai.resource.generatePreview')}</>
}
</Button>
{/* Hint */}
{!noteContent && resourceMode !== 'replace' && (
<p className="text-[10px] text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800 rounded-lg px-3 py-2">
💡 La note est vide le contenu de la ressource sera intégré directement.
{t('ai.resource.emptyNoteHint')}
</p>
)}
</div>
@@ -941,3 +942,6 @@ export function ContextualAIChat({
</aside>
)
}

View File

@@ -221,6 +221,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
// Paste handler: upload clipboard images
useEffect(() => {
const handlePaste = async (e: ClipboardEvent) => {
if (noteType === 'richtext' && (e.target as HTMLElement)?.closest('.notion-editor')) return;
const items = e.clipboardData?.items
if (!items) return
for (const item of Array.from(items)) {
@@ -237,8 +238,8 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
}
}
}
document.addEventListener('paste', handlePaste)
return () => document.removeEventListener('paste', handlePaste)
document.addEventListener('paste', handlePaste, { capture: true })
return () => document.removeEventListener('paste', handlePaste, { capture: true } as any)
}, [t])
const handleRemoveImage = (index: number) => {
@@ -276,6 +277,11 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
setLinks(links.filter((_, i) => i !== index))
}
const allImages = useMemo(() => {
const extracted = noteType === 'richtext' ? extractImagesFromHTML(content) : [];
return Array.from(new Set([...images, ...extracted]));
}, [images, content, noteType]);
const handleGenerateTitles = async () => {
// Combine content and link metadata for AI
const fullContentForAI = [
@@ -304,7 +310,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
const response = await fetch('/api/ai/title-suggestions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: fullContent }),
body: JSON.stringify({ content: fullContentForAI }),
})
if (!response.ok) {
@@ -733,6 +739,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
content={content}
onChange={setContent}
className="min-h-[200px]"
onImageUpload={uploadImageFile}
/>
) : noteType === 'text' || noteType === 'markdown' ? (
<div className="space-y-2">
@@ -1158,3 +1165,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
</Dialog>
)
}

View File

@@ -394,6 +394,7 @@ export function NoteInlineEditor({
// Paste handler: upload clipboard images
useEffect(() => {
const handlePaste = async (e: ClipboardEvent) => {
if (noteType === 'richtext' && (e.target as HTMLElement)?.closest('.notion-editor')) return;
const items = e.clipboardData?.items
if (!items) return
for (const item of Array.from(items)) {
@@ -412,8 +413,8 @@ export function NoteInlineEditor({
}
}
}
document.addEventListener('paste', handlePaste)
return () => document.removeEventListener('paste', handlePaste)
document.addEventListener('paste', handlePaste, { capture: true })
return () => document.removeEventListener('paste', handlePaste, { capture: true } as any)
}, [note.id, note.images, onChange, t])
const handleRemoveImage = async (index: number) => {
@@ -938,3 +939,5 @@ export function NoteInlineEditor({

View File

@@ -452,6 +452,7 @@ export function NoteInput({
// Paste handler: upload clipboard images
useEffect(() => {
const handlePaste = async (e: ClipboardEvent) => {
if (type === 'richtext' && (e.target as HTMLElement)?.closest('.notion-editor')) return;
const items = e.clipboardData?.items
if (!items) return
for (const item of Array.from(items)) {
@@ -468,8 +469,8 @@ export function NoteInput({
}
}
}
document.addEventListener('paste', handlePaste)
return () => document.removeEventListener('paste', handlePaste)
document.addEventListener('paste', handlePaste, { capture: true })
return () => document.removeEventListener('paste', handlePaste, { capture: true } as any)
}, [t])
// AI title from images
@@ -774,6 +775,7 @@ export function NoteInput({
content={content}
onChange={setContent}
className="min-h-[120px]"
onImageUpload={uploadImageFile}
/>
) : type === 'text' || type === 'markdown' ? (
<>
@@ -1101,3 +1103,4 @@ export function NoteInput({
</div>
)
}

View File

@@ -26,8 +26,9 @@ import {
Sparkles, Wand2, Scissors, Lightbulb, X, Check, ExternalLink,
FileText, Pilcrow, MessageSquare, AlignLeft, AlignCenter, AlignRight,
Superscript as SuperscriptIcon, Subscript as SubscriptIcon, Expand, Plus,
} from 'lucide-react'
SpellCheck, Languages, BookOpen } from 'lucide-react'
import { cn } from '@/lib/utils'
import { toast } from 'sonner'
export interface RichTextEditorHandle {
getEditor: () => Editor | null
@@ -38,6 +39,7 @@ interface RichTextEditorProps {
onChange?: (content: string) => void
className?: string
placeholder?: string
onImageUpload?: (file: File) => Promise<string>
}
type SlashItem = {
@@ -58,6 +60,7 @@ const CustomImage = Image.extend({
...this.parent?.(),
width: {
default: '100%',
parseHTML: element => element.style.width || element.getAttribute('width') || '100%',
renderHTML: attributes => {
if (!attributes.width) return {}
return { style: `width: ${attributes.width}; max-width: 100%; height: auto;` }
@@ -92,11 +95,11 @@ const slashCommands: SlashItem[] = [
{ title: 'Développer', description: 'Élaborer et enrichir le texte', icon: Expand, category: 'IA Note', isAi: true, aiOption: 'clarify', command: () => {} },
]
async function aiReformulate(text: string, option: 'clarify' | 'shorten' | 'improve'): Promise<string> {
async function aiReformulate(text: string, option: string, language?: string): Promise<string> {
const res = await fetch('/api/ai/reformulate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, option, format: 'html' }),
body: JSON.stringify({ text, option, format: 'html', language }),
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'AI failed')
@@ -129,7 +132,7 @@ function useImageInsert() {
}
export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(
function RichTextEditor({ content, onChange, className, placeholder }, ref) {
function RichTextEditor({ content, onChange, className, placeholder, onImageUpload }, ref) {
const { t } = useLanguage()
const imageInsert = useImageInsert()
@@ -152,6 +155,27 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
immediatelyRender: false,
editorProps: {
attributes: { class: 'notion-editor' },
handlePaste: (view, event, slice) => {
if (!onImageUpload) return false;
const items = Array.from(event.clipboardData?.items || []);
const hasImage = items.some(item => item.type.startsWith('image/'));
if (!hasImage) return false;
event.preventDefault();
const images = items.filter(item => item.type.startsWith('image/')).map(item => item.getAsFile()).filter(f => f !== null) as File[];
images.forEach(async (file) => {
try {
toast.info(t('notes.uploading'));
const url = await onImageUpload(file);
const { schema } = view.state;
const node = schema.nodes.image.create({ src: url });
const tr = view.state.tr.replaceSelectionWith(node);
view.dispatch(tr);
} catch (err) {
toast.error(t('notes.uploadFailed'));
}
});
return true;
}
},
onUpdate: ({ editor: e }) => {
const html = e.getHTML()
@@ -253,7 +277,7 @@ function ImageModal({ onConfirm, onCancel }: { onConfirm: (url: string) => void;
}
function BubbleToolbar({ editor }: { editor: Editor | null }) {
const { t } = useLanguage()
const { t, language } = useLanguage()
const [, setTick] = useState(0)
const [aiOpen, setAiOpen] = useState(false)
const [aiLoading, setAiLoading] = useState(false)
@@ -286,15 +310,19 @@ function BubbleToolbar({ editor }: { editor: Editor | null }) {
{ icon: SubscriptIcon, active: editor.isActive('subscript'), action: () => editor.chain().focus().toggleSubscript().run(), title: t('richTextEditor.subscript') },
]
const handleAI = async (option: 'clarify' | 'shorten' | 'improve') => {
const handleAI = async (option: 'clarify' | 'shorten' | 'improve' | 'fix_grammar' | 'translate' | 'explain') => {
const { from, to } = editor.state.selection
const text = editor.state.doc.textBetween(from, to, ' ')
if (!text || text.split(/\s+/).length < 5) return
if (!text || text.split(/\s+/).length < 2) return
setAiLoading(true)
setAiOpen(false)
try {
const result = await aiReformulate(text, option)
editor.chain().focus().insertContentAt({ from, to }, result).run()
const result = await aiReformulate(text, option, language)
if (option === 'explain') {
toast.message(t('ai.action.explain'), { description: result, duration: 10000 })
} else {
editor.chain().focus().insertContentAt({ from, to }, result).run()
}
} catch (err) {
console.error('AI error:', err)
} finally {
@@ -365,6 +393,9 @@ function BubbleToolbar({ editor }: { editor: Editor | null }) {
<button className="notion-ai-subitem" onClick={() => handleAI('clarify')}><Lightbulb className="w-3.5 h-3.5 text-amber-500" /><span>{t('richTextEditor.slashClarify')}</span></button>
<button className="notion-ai-subitem" onClick={() => handleAI('shorten')}><Scissors className="w-3.5 h-3.5 text-blue-500" /><span>{t('richTextEditor.slashShorten')}</span></button>
<button className="notion-ai-subitem" onClick={() => handleAI('improve')}><Wand2 className="w-3.5 h-3.5 text-purple-500" /><span>{t('richTextEditor.slashImprove')}</span></button>
<button className="notion-ai-subitem" onClick={() => handleAI('fix_grammar')}><SpellCheck className="w-3.5 h-3.5 text-green-500" /><span>{t('ai.action.fixGrammar')}</span></button>
<button className="notion-ai-subitem" onClick={() => handleAI('translate')}><Languages className="w-3.5 h-3.5 text-indigo-500" /><span>{t('ai.action.translate')}</span></button>
<button className="notion-ai-subitem" onClick={() => handleAI('explain')}><BookOpen className="w-3.5 h-3.5 text-orange-500" /><span>{t('ai.action.explain')}</span></button>
</div>
)}
</div>
@@ -631,3 +662,7 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@@ -10,7 +10,7 @@ import { LanguageDetectionService } from './language-detection.service'
import { getTagsProvider } from '../factory'
import { getSystemConfig } from '@/lib/config'
export type RefactorMode = 'clarify' | 'shorten' | 'improveStyle'
export type RefactorMode = 'clarify' | 'shorten' | 'improveStyle' | 'fix_grammar' | 'translate' | 'explain'
export interface RefactorOption {
mode: RefactorMode
@@ -68,7 +68,8 @@ export class ParagraphRefactorService {
async refactor(
content: string,
mode: RefactorMode,
format: 'html' | 'markdown' = 'markdown'
format: 'html' | 'markdown' = 'markdown',
targetLanguage?: string
): Promise<RefactorResult> {
// Validate word count
const wordCount = content.split(/\s+/).length
@@ -78,8 +79,9 @@ export class ParagraphRefactorService {
)
}
// Detect language
const { language } = await this.languageDetection.detectLanguage(content)
// Detect language or use provided target language
const { language: detectedLanguage } = await this.languageDetection.detectLanguage(content)
const language = targetLanguage || detectedLanguage
try {
// Build prompts
@@ -233,7 +235,24 @@ Remove fluff, repetition, and unnecessary words, but keep the substance.${format
Your goal: Enhance the text's style, vocabulary, sentence structure, and overall quality.
CRITICAL LANGUAGE RULE: You MUST respond in the EXACT SAME LANGUAGE as the input text. If input is French, output MUST be French. If input is German, output MUST be German. NEVER translate to English.
Maintain similar length but make it sound more professional and polished.${formatInstruction}`
Maintain similar length but make it sound more professional and polished.${formatInstruction}`,
fix_grammar: `You are an expert proofreader.
Your goal: Fix spelling, grammar, and punctuation errors in the text without changing its meaning, tone, or style.
CRITICAL LANGUAGE RULE: You MUST respond in the EXACT SAME LANGUAGE as the input text. NEVER translate to English.
Make minimal changes, only correcting errors.${formatInstruction}`,
translate: `You are a professional translator.
Your goal: Translate the text perfectly into the target language requested by the user.
Ensure the translation sounds natural and preserves the original tone and formatting.${formatInstruction}`,
explain: `You are an expert teacher and encyclopedia.
Your goal: Explain the selected text, concept, or word clearly and concisely.
Provide context, definitions, or relevant information to help the user understand it.
CRITICAL LANGUAGE RULE: You MUST explain it in the requested language (which is the user's interface language).
Keep it concise but informative.${formatInstruction}`
}
return prompts[mode]
@@ -254,7 +273,17 @@ Please shorten this ${language} text while keeping all key information:`,
improveStyle: `IMPORTANT: The text below is in ${language}. Your response MUST be in ${language}. Do NOT translate to English.
Please improve the style and readability of this ${language} text:`
Please improve the style and readability of this ${language} text:`,
fix_grammar: `IMPORTANT: The text below is in ${language}. Your response MUST be in ${language}. Do NOT translate to English.
Please fix any spelling, grammar, or punctuation errors in this ${language} text:`,
translate: `Please translate the following text into ${language}.
Only return the translated text, nothing else.`,
explain: `Please explain the following text/concept in ${language}.
Keep the explanation clear, educational, and concise.`
}
return `${instructions[mode]}

File diff suppressed because it is too large Load Diff

View File

@@ -40,7 +40,7 @@
"noLabelsInNotebook": "هنوز برچسبی در این دفترچه وجود ندارد",
"archive": "بایگانی",
"trash": "زباله‌دان",
"clearFilter": "Remove filter"
"clearFilter": "حذف فیلتر"
},
"notes": {
"title": "یادداشت‌ها",
@@ -88,8 +88,8 @@
"itemOrMediaRequired": "لطفا حداقل یک آیتم یا رسانه اضافه کنید",
"noteCreated": "یادداشت با موفقیت ایجاد شد",
"noteCreateFailed": "ایجاد یادداشت شکست خورد",
"deleted": "Note deleted",
"deleteFailed": "Failed to delete note",
"deleted": "یادداشت حذف شد",
"deleteFailed": "حذف یادداشت ناموفق بود",
"aiAssistant": "دستیار هوش مصنوعی",
"changeSize": "تغییر اندازه",
"backgroundOptions": "گزینه‌های پس‌زمینه",
@@ -176,10 +176,10 @@
"history": "تاریخچه",
"historyRestored": "نسخه بازیابی شد",
"historyEnabled": "تاریخچه فعال شد",
"historyDisabledTitle": "Version history",
"historyDisabledTitle": "تاریخچه نسخه‌ها",
"historyDisabledDesc": "تاریخچه برای حساب شما غیرفعال است.",
"historyEnabledTitle": "History enabled!",
"historyEnabledDesc": "Versions of this note will now be recorded.",
"historyEnabledTitle": "تاریخچه فعال شد!",
"historyEnabledDesc": "نسخه‌های این یادداشت از این پس ثبت خواهد شد.",
"enableHistory": "فعال‌سازی تاریخچه",
"historyEmpty": "نسخه‌ای موجود نیست",
"historySelectVersion": "نسخه‌ای را برای پیش‌نمایش انتخاب کنید",
@@ -188,32 +188,37 @@
"sortDateAsc": "تاریخ (قدیمی‌ترین)",
"sortTitleAsc": "عنوان الف ← ی",
"sortTitleDesc": "عنوان ی ← الف",
"suggestTitle": "AI title",
"generateTitleFromImage": "Generate title from image",
"titleGenerated": "Title generated",
"content": "Content",
"restore": "Restore",
"createFailed": "Failed to create note",
"updateFailed": "Failed to update note",
"archived": "Note archived",
"archiveFailed": "Failed to archive",
"sort": "Sort",
"confirmDeleteTitle": "Delete note",
"leftShare": "Share removed",
"dismissed": "Note dismissed from recent",
"generalNotes": "General Notes",
"suggestTitle": "عنوان هوش مصنوعی",
"generateTitleFromImage": "تولید عنوان از تصویر",
"titleGenerated": "عنوان تولید شد",
"content": "محتوا",
"restore": "بازیابی",
"createFailed": "ایجاد یادداشت ناموفق بود",
"updateFailed": "به‌روزرسانی یادداشت ناموفق بود",
"archived": "یادداشت بایگانی شد",
"archiveFailed": "بایگانی ناموفق بود",
"sort": "مرتب‌سازی",
"confirmDeleteTitle": "حذف یادداشت",
"leftShare": "اشتراک‌گذاری حذف شد",
"dismissed": "یادداشت از اخیرها حذف شد",
"generalNotes": "یادداشت‌های عمومی",
"noteType": "نوع یادداشت",
"typeText": "متن",
"typeMarkdown": "مارکداون",
"typeRichText": "متن غنی",
"typeChecklist": "چک‌لیست",
"convertedToRichText": "Converted to rich text",
"conversionFailed": "Conversion failed, staying in Markdown",
"convertedToRichText": "به متن غنی تبدیل شد",
"conversionFailed": "تبدیل ناموفق بود، در مارک‌داون باقی می‌ماند",
"richTextPlaceholder": "یادداشتی بنویسید...",
"switchTypeTitle": "نوع یادداشت تغییر کند؟",
"switchTypeWarning": "هنگام تغییر به {type} ممکن است برخی قالب‌بندی‌ها از بین بروند.",
"switchTypeContentPreserved": "محتوای شما به عنوان متن ساده حفظ می‌شود.",
"switchType": "تغییر به {type}"
"switchType": "تغییر به {type}",
"deleteVersionDesc": "این عمل قابل بازگشت نیست. این نسخه برای همیشه از تاریخچه حذف خواهد شد.",
"currentVersion": "فعلی",
"compareVersions": "مقایسه",
"diffTitle": "مقایسه",
"diffSelectHint": "برای مقایسه روی ۲ نسخه در لیست کلیک کنید"
},
"pagination": {
"previous": "←",
@@ -221,35 +226,35 @@
"next": "→"
},
"labels": {
"title": "Labels",
"filter": "Filter by Label",
"manage": "Manage Labels",
"manageTooltip": "Manage Labels",
"title": "برچسب‌ها",
"filter": "فیلتر بر اساس برچسب",
"manage": "مدیریت برچسب‌ها",
"manageTooltip": "مدیریت برچسب‌ها",
"changeColor": "تغییر رنگ",
"changeColorTooltip": "تغییر رنگ",
"delete": "Delete",
"deleteTooltip": "Delete label",
"delete": "حذف",
"deleteTooltip": "حذف برچسب",
"confirmDelete": "آیا مطمئن هستید که می‌خواهید این برچسب را حذف کنید؟",
"newLabelPlaceholder": "Create new label",
"namePlaceholder": "Enter label name",
"newLabelPlaceholder": "ایجاد برچسب جدید",
"namePlaceholder": "نام برچسب را وارد کنید",
"addLabel": "افزودن برچسب",
"createLabel": "Create label",
"labelName": "Label name",
"labelColor": "Label color",
"manageLabels": "Manage labels",
"manageLabelsDescription": "Add or remove labels for this note. Click on a label to change its color.",
"selectedLabels": "Selected Labels",
"createLabel": "ایجاد برچسب",
"labelName": "نام برچسب",
"labelColor": "رنگ برچسب",
"manageLabels": "مدیریت برچسب‌ها",
"manageLabelsDescription": "برچسب‌های این یادداشت را اضافه یا حذف کنید. برای تغییر رنگ روی برچسب کلیک کنید.",
"selectedLabels": "برچسب‌های انتخاب شده",
"allLabels": "همه برچسب‌ها",
"clearAll": "پاک کردن همه",
"filterByLabel": "Filter by label",
"tagAdded": "Tag \"{tag}\" added",
"showLess": "Show less",
"showMore": "Show more",
"editLabels": "Edit Labels",
"editLabelsDescription": "Create, edit colors, or delete labels.",
"noLabelsFound": "No labels found.",
"loading": "Loading...",
"notebookRequired": "⚠️ Labels are only available in notebooks. Move this note to a notebook first.",
"filterByLabel": "فیلتر بر اساس برچسب",
"tagAdded": "برچسب «{tag}» اضافه شد",
"showLess": "نمایش کمتر",
"showMore": "نمایش بیشتر",
"editLabels": "ویرایش برچسب‌ها",
"editLabelsDescription": "ایجاد، تغییر رنگ یا حذف برچسب‌ها.",
"noLabelsFound": "برچسبی یافت نشد.",
"loading": "در حال بارگذاری...",
"notebookRequired": "⚠️ برچسب‌ها فقط در دفترچه‌ها موجود هستند. ابتدا این یادداشت را به یک دفترچه منتقل کنید.",
"count": "{count} برچسب",
"noLabels": "بدون برچسب",
"confirmDeleteShort": "تأیید؟",
@@ -417,14 +422,14 @@
"transformationsDesc": "تبدیل‌ها — مستقیماً در یادداشت اعمال می‌شوند",
"writeMinWordsAction": "حداقل ۵ کلمه بنویسید تا عملیات هوش مصنوعی فعال شود.",
"processingAction": "در حال پردازش...",
"noImagesError": "No images in this note",
"overview": "Overview",
"noImagesError": "تصویری در این یادداشت وجود ندارد",
"overview": "نمای کلی",
"action": {
"clarify": "روشن کردن",
"shorten": "خلاصه کردن",
"improve": "بهبود",
"toMarkdown": "به مارک‌داون",
"describeImages": "Describe images"
"describeImages": "توصیف تصاویر"
},
"openAssistant": "باز کردن دستیار هوش مصنوعی",
"poweredByMomento": "پشتیبانی شده توسط Momento AI",
@@ -440,8 +445,50 @@
"insightsTab": "بینش‌ها",
"aiCopilot": "دستیار هوشمند",
"suggestTitle": "پیشنهاد عنوان با هوش مصنوعی",
"generateTitleFromImage": "Generate title from image",
"titleGenerated": "Title generated from image"
"generateTitleFromImage": "تولید عنوان از تصویر",
"titleGenerated": "عنوان از تصویر تولید شد",
"wordCountMin": "حداقل {min} کلمه برای بازنویسی انتخاب کنید (فعلاً {current} کلمه)",
"wordCountMax": "حداکثر {max} کلمه برای بازنویسی انتخاب کنید (فعلاً {current} کلمه)",
"resourceTab": "منبع",
"aiNoteTitle": "یادداشت هوش مصنوعی",
"injectReplace": "جایگزینی",
"injectReplaceTitle": "محتوای یادداشت را با این پیام جایگزین کنید",
"injectComplete": "تکمیل",
"injectCompleteTitle": "یادداشت را با این پیام تکمیل کنید (هوش مصنوعی)",
"injectMerge": "ادغام",
"injectMergeTitle": "با یادداشت ادغام کنید (هوش مصنوعی)",
"imagesCount": "{count} تصویر",
"resource": {
"failedToLoadUrl": "بارگذاری این URL ناموفق بود",
"pageLoaded": "صفحه بارگذاری شد: {title}",
"pageLoadError": "خطا در بارگذاری صفحه",
"pasteOrUrlFirst": "ابتدا متن را بچسبانید یا یک URL بارگذاری کنید",
"enrichError": "خطا در غنی‌سازی",
"enrichErrorShort": "خطای غنی‌سازی",
"contentApplied": "محتوا در یادداشت اعمال شد ✓",
"fromChat": "💬 از چت",
"replacement": "↓ جایگزینی",
"completedByAI": "✦ تکمیل شده توسط هوش مصنوعی",
"mergedByAI": "⟳ ادغام شده توسط هوش مصنوعی",
"rendered": "رندر شده",
"cancel": "لغو",
"applyToNote": "اعمال در یادداشت",
"urlLabel": "URL (اختیاری)",
"resourceText": "متن منبع",
"resourcePlaceholder": "متن خود را اینجا بچسبانید (مارک‌داون، HTML، متن ساده…)",
"words": "کلمه",
"integrationMode": "حالت یکپارچه‌سازی",
"modeReplace": "جایگزینی",
"modeReplaceDesc": "مستقیم، بدون هوش مصنوعی",
"modeComplete": "تکمیل",
"modeCompleteDesc": "بدون بازنویسی اضافه می‌کند",
"modeMerge": "ادغام",
"modeMergeDesc": "بازنویسی و یکپارچه می‌کند",
"aiProcessing": "هوش مصنوعی در حال پردازش…",
"preview": "پیش‌نمایش",
"generatePreview": "تولید پیش‌نمایش",
"emptyNoteHint": "💡 یادداشت خالی است — محتوای منبع مستقیماً یکپارچه خواهد شد."
}
},
"titleSuggestions": {
"available": "پیشنهادات عنوان",
@@ -539,10 +586,10 @@
}
},
"notification": {
"accept": "Accept",
"accepted": "Share accepted",
"decline": "Decline",
"noNotifications": "No new notifications",
"accept": "پذیرش",
"accepted": "اشتراک پذیرفته شد",
"decline": "رد کردن",
"noNotifications": "اعلان جدیدی نیست",
"shared": "\"{title}\" به اشتراک گذاشته شد",
"untitled": "بدون عنوان",
"notifications": "اعلان‌ها",
@@ -603,11 +650,11 @@
"about": "درباره",
"version": "نسخه",
"settingsSaved": "تنظیمات ذخیره شد",
"cardSizeMode": "Note Size",
"cardSizeModeDescription": "Choose between variable sizes or uniform size",
"selectCardSizeMode": "Select display mode",
"cardSizeVariable": "Variable sizes (small/medium/large)",
"cardSizeUniform": "Uniform size",
"cardSizeMode": "اندازه یادداشت",
"cardSizeModeDescription": "انتخاب بین اندازه متغیر یا یکنواخت",
"selectCardSizeMode": "انتخاب حالت نمایش",
"cardSizeVariable": "اندازه متغیر (کوچک/متوسط/بزرگ)",
"cardSizeUniform": "اندازه یکنواخت",
"settingsError": "خطا در ذخیره تنظیمات",
"maintenance": "نگهداری",
"maintenanceDescription": "ابزارهایی برای حفظ سلامت پایگاه داده",
@@ -695,7 +742,15 @@
"providerDesc": "فروشنده هوش مصنوعی مورد نظر خود را انتخاب کنید",
"providerAutoDesc": "Ollama در صورت وجود، در غیر این صورت OpenAI",
"providerOllamaDesc": "۱۰۰٪ خصوصی، به صورت محلی اجرا می‌شود",
"providerOpenAIDesc": "دقیق‌ترین، نیاز به کلید API دارد"
"providerOpenAIDesc": "دقیق‌ترین، نیاز به کلید API دارد",
"aiNote": "یادداشت هوش مصنوعی",
"aiNoteDesc": "فعال‌سازی دکمه چت هوش مصنوعی و ابزارهای بهبود متن",
"languageDetection": "تشخیص زبان",
"languageDetectionDesc": "به طور خودکار زبان یادداشت‌های شما را تشخیص می‌دهد",
"autoLabeling": "پیشنهاد برچسب",
"autoLabelingDesc": "به طور خودکار برچسب‌ها را به یادداشت‌های شما پیشنهاد و اعمال می‌کند",
"noteHistory": "تاریخچه یادداشت",
"noteHistoryDesc": "فعال‌سازی اسنپ‌شات نسخه‌ها و بازیابی از تاریخچه"
},
"general": {
"loading": "در حال بارگذاری...",
@@ -752,7 +807,9 @@
"markDone": "علامت‌گذاری به عنوان انجام شده",
"markUndone": "علامت‌گذاری به عنوان انجام نشده",
"todayAt": "امروز ساعت {time}",
"tomorrowAt": "فردا ساعت {time}"
"tomorrowAt": "فردا ساعت {time}",
"clearCompleted": "پاک کردن تکمیل شده‌ها",
"viewAll": "مشاهده همه یادآوری‌ها"
},
"notebook": {
"create": "ایجاد دفترچه",
@@ -783,7 +840,7 @@
"confidence": "اطمینان",
"savingReminder": "شکست در ذخیره یادآوری",
"removingReminder": "شکست در حذف یادآوری",
"generatingDescription": "Please wait..."
"generatingDescription": "لطفاً صبر کنید..."
},
"notebookSuggestion": {
"title": "انتقال به {name}؟",
@@ -896,14 +953,14 @@
"description": "پیکربندی ارسال ایمیل برای اعلان‌های عامل و بازنشانی رمز عبور.",
"provider": "ارائه‌دهنده ایمیل",
"saveSettings": "ذخیره تنظیمات ایمیل",
"status": "Service Status",
"keySet": "key configured",
"activeAuto": "Auto mode: Resend will be used first, SMTP as fallback.",
"activeSmtp": "Auto mode: SMTP will be used (Resend not configured).",
"noneConfigured": "No email service configured. Set up Resend or SMTP.",
"activeProvider": "Active provider",
"testOk": "test passed",
"testFail": "test failed"
"status": "وضعیت سرویس",
"keySet": "کلید پیکربندی شده",
"activeAuto": "حالت خودکار: ابتدا Resend استفاده می‌شود، SMTP به عنوان جایگزین.",
"activeSmtp": "حالت خودکار: SMTP استفاده می‌شود (Resend پیکربندی نشده).",
"noneConfigured": "هیچ سرویس ایمیلی پیکربندی نشده. Resend یا SMTP را تنظیم کنید.",
"activeProvider": "ارائه‌دهنده فعال",
"testOk": "تست موفق",
"testFail": "تست ناموفق"
},
"smtp": {
"title": "پیکربندی SMTP",
@@ -1182,7 +1239,7 @@
"notesViewLabel": "چیدمان یادداشت‌ها",
"notesViewTabs": "زبانه‌ها (سبک OneNote)",
"notesViewMasonry": "کارت‌ها (شبکه‌ای)",
"selectTheme": "Select theme",
"selectTheme": "انتخاب تم",
"fontFamilyLabel": "خانواده فونت",
"fontFamilyDescription": "فونت استفاده شده در سراسر برنامه را انتخاب کنید",
"selectFontFamily": "Inter برای خوانایی بهینه شده است، سیستم از فونت بومی سیستم‌عامل شما استفاده می‌کند",
@@ -1376,10 +1433,10 @@
"subtitle": "خودکارسازی وظایف پایش و تحقیق شما",
"newAgent": "عامل جدید",
"myAgents": "عامل‌های من",
"searchPlaceholder": "Search agents...",
"filterAll": "All",
"newBadge": "New",
"noResults": "No agents match your search.",
"searchPlaceholder": "جستجوی عامل‌ها...",
"filterAll": "همه",
"newBadge": "جدید",
"noResults": "هیچ عاملی با جستجوی شما مطابقت ندارد.",
"noAgents": "بدون عامل",
"noAgentsDescription": "اولین عامل خود را بسازید یا قالب زیر را نصب کنید تا وظایف پایش خود را خودکار کنید.",
"types": {
@@ -1423,8 +1480,8 @@
"researchTopicPlaceholder": "مثال: آخرین پیشرفت‌ها در هوش مصنوعی",
"notifyEmail": "اعلان ایمیل",
"notifyEmailHint": "پس از هر اجرا، ایمیل حاوی نتایج عامل دریافت کنید",
"includeImages": "Include images",
"includeImagesHint": "Extract images from scraped pages and attach them to the generated note"
"includeImages": "شامل تصاویر",
"includeImagesHint": "استخراج تصاویر از صفحات استخراج شده و پیوست به یادداشت تولید شده"
},
"frequencies": {
"manual": "دستی",
@@ -1434,19 +1491,19 @@
"monthly": "ماهانه"
},
"schedule": {
"nextRun": "Next run",
"pending": "Pending trigger",
"time": "Time",
"dayOfWeek": "Day of week",
"dayOfMonth": "Day of month",
"nextRun": "اجرای بعدی",
"pending": "در انتظار راه‌اندازی",
"time": "زمان",
"dayOfWeek": "روز هفته",
"dayOfMonth": "روز ماه",
"days": {
"mon": "Monday",
"tue": "Tuesday",
"wed": "Wednesday",
"thu": "Thursday",
"fri": "Friday",
"sat": "Saturday",
"sun": "Sunday"
"mon": "دوشنبه",
"tue": "سه‌شنبه",
"wed": "چهارشنبه",
"thu": "پنجشنبه",
"fri": "جمعه",
"sat": "شنبه",
"sun": "یکشنبه"
}
},
"status": {
@@ -1478,8 +1535,8 @@
"installSuccess": "\"{name}\" نصب شد",
"installError": "خطا در حین نصب",
"saveError": "خطا در ذخیره",
"autoRunSuccess": "Agent \"{name}\" executed automatically with success",
"autoRunError": "Agent \"{name}\" failed during automatic execution"
"autoRunSuccess": "عامل «{name}» با موفقیت به صورت خودکار اجرا شد",
"autoRunError": "عامل «{name}» در اجرای خودکار شکست خورد"
},
"templates": {
"title": "قالب‌ها",
@@ -1593,7 +1650,7 @@
"searching": "در حال جستجو...",
"noNotesFoundForContext": "هیچ یادداشت مرتبطی برای این سوال یافت نشد. با دانش عمومی خود پاسخ دهید.",
"webSearch": "جستجوی وب",
"timeoutWarning": "Response is taking longer than expected..."
"timeoutWarning": "پاسخ بیشتر از حد انتظار طول می‌کشد..."
},
"labHeader": {
"title": "آزمایشگاه",
@@ -1611,10 +1668,81 @@
"deleteSpace": "حذف فضا",
"deleted": "فضا حذف شد",
"deleteError": "خطا در حذف",
"rename": "Rename"
"rename": "تغییر نام"
},
"lab": {
"initializing": "راه‌اندازی فضای کاری",
"loadingIdeas": "بارگذاری ایده‌های شما..."
},
"richTextEditor": {
"slashHint": "↑↓ ناوبری · Enter درج · Tab تغییر بخش",
"slashLoading": "هوش مصنوعی در حال فکر کردن...",
"slashTabAll": "همه",
"slashCatBasic": "بلوک‌های پایه",
"slashCatMedia": "رسانه",
"slashCatFormatting": "قالب‌بندی",
"slashCatAi": "هوش مصنوعی",
"insertImage": "درج تصویر",
"imageUrlPlaceholder": "https://example.com/image.png",
"preview": "پیش‌نمایش",
"cancel": "لغو",
"insert": "درج",
"slashText": "متن",
"slashTextDesc": "پاراگراف ساده",
"slashH1": "عنوان ۱",
"slashH1Desc": "عنوان بخش بزرگ",
"slashH2": "عنوان ۲",
"slashH2Desc": "عنوان بخش متوسط",
"slashH3": "عنوان ۳",
"slashH3Desc": "عنوان بخش کوچک",
"slashBullet": "فهرست نقطه‌ای",
"slashBulletDesc": "فهرست بدون ترتیب",
"slashNumbered": "فهرست شماره‌دار",
"slashNumberedDesc": "فهرست مرتب شماره‌دار",
"slashTodo": "فهرست وظایف",
"slashTodoDesc": "وظایف با چک‌باکس",
"slashQuote": "نقل قول",
"slashQuoteDesc": "ثبت یک نقل قول",
"slashCode": "بلوک کد",
"slashCodeDesc": "قطعه کد",
"slashDivider": "جداکننده",
"slashDividerDesc": "جداکننده افقی",
"slashImage": "تصویر",
"slashImageDesc": "درج تصویر از URL",
"slashAlignLeft": "تراز چپ",
"slashAlignLeftDesc": "تراز متن به چپ",
"slashAlignCenter": "مرکز",
"slashAlignCenterDesc": "مرکز کردن متن",
"slashAlignRight": "تراز راست",
"slashAlignRightDesc": "تراز متن به راست",
"slashSuperscript": "بالانویس",
"slashSuperscriptDesc": "متن بالاتر از خط اصلی",
"slashSubscript": "زیرنویس",
"slashSubscriptDesc": "متن پایین‌تر از خط اصلی",
"slashClarify": "شفاف‌سازی",
"slashClarifyDesc": "روشن‌تر کردن متن",
"slashShorten": "خلاصه کردن",
"slashShortenDesc": "فشرده کردن متن",
"slashImprove": "بهبود",
"slashImproveDesc": "بهبود سبک نوشتار",
"slashExpand": "بسط دادن",
"slashExpandDesc": "توسعه و غنی‌سازی متن",
"imageModalTitle": "درج تصویر",
"imageModalPreview": "پیش‌نمایش",
"imageModalCancel": "لغو",
"imageModalInsert": "درج",
"imageModalInvalidUrl": "لطفاً یک URL معتبر وارد کنید",
"imageModalLoadFailed": "بارگذاری تصویر ناموفق بود",
"linkPlaceholder": "یک لینک بچسبانید یا تایپ کنید...",
"bold": "درشت",
"italic": "کج",
"underline": "زیرخط",
"strike": "خط‌خورده",
"code": "کد",
"highlight": "برجسته",
"superscript": "بالانویس",
"subscript": "زیرنویس",
"addBlock": "افزودن بلوک",
"placeholder": "برای دستورات '/' تایپ کنید..."
}
}

File diff suppressed because it is too large Load Diff