From b825bdb8b2f7dfed05b8679161780472ab354db3 Mon Sep 17 00:00:00 2001 From: Antigravity Date: Fri, 29 May 2026 13:40:59 +0000 Subject: [PATCH] fix: boucle infinie Maximum update depth dans useAutoTagging + toolbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - use-auto-tagging: onQuotaExceeded via ref stable → n'invalide plus useCallback analyzeContent à chaque render parent - note-editor-context: filteredSuggestions et existingLabelsLower stabilisés avec useMemo (était recalculé sans memo → nouvelle ref à chaque render → état useMemo state se réexécutait → boucle) - deepseek.ts: generateTags via generateText (pas generateObject) pour éviter response_format:json_schema non supporté Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../note-editor/note-editor-context.tsx | 10 +++++--- memento-note/hooks/use-auto-tagging.ts | 9 ++++--- memento-note/lib/ai/providers/deepseek.ts | 24 ++++++++++--------- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/memento-note/components/note-editor/note-editor-context.tsx b/memento-note/components/note-editor/note-editor-context.tsx index 480dd36..73cf535 100644 --- a/memento-note/components/note-editor/note-editor-context.tsx +++ b/memento-note/components/note-editor/note-editor-context.tsx @@ -211,11 +211,15 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o const [comparisonNotes, setComparisonNotes] = useState>>([]) const [fusionNotes, setFusionNotes] = useState>>([]) - const existingLabelsLower = (note.labels || []).map((l) => l.toLowerCase()) - const filteredSuggestions = suggestions.filter(s => { + const existingLabelsLower = useMemo( + () => (note.labels || []).map((l) => l.toLowerCase()), + // eslint-disable-next-line react-hooks/exhaustive-deps + [JSON.stringify(note.labels)] + ) + const filteredSuggestions = useMemo(() => suggestions.filter(s => { if (!s || !s.tag) return false return !dismissedTags.includes(s.tag) && !existingLabelsLower.includes(s.tag.toLowerCase()) - }) + }), [suggestions, dismissedTags, existingLabelsLower]) const colorClasses = NOTE_COLORS[color as NoteColor] || NOTE_COLORS.default diff --git a/memento-note/hooks/use-auto-tagging.ts b/memento-note/hooks/use-auto-tagging.ts index 88f48c5..81d80c4 100644 --- a/memento-note/hooks/use-auto-tagging.ts +++ b/memento-note/hooks/use-auto-tagging.ts @@ -25,6 +25,9 @@ export function useAutoTagging({ content, notebookId, enabled = true, onQuotaExc const previousNotebookId = useRef(notebookId); // AbortController for cancelling in-flight requests const abortRef = useRef(null); + // Stable ref for onQuotaExceeded — avoids re-creating analyzeContent on every render + const onQuotaExceededRef = useRef(onQuotaExceeded); + onQuotaExceededRef.current = onQuotaExceeded; const analyzeContent = useCallback(async (contentToAnalyze: string, currentNotebookId?: string | null, currentLanguage?: string) => { if (!contentToAnalyze || contentToAnalyze.length < 10) { @@ -63,8 +66,8 @@ export function useAutoTagging({ content, notebookId, enabled = true, onQuotaExc if (controller.signal.aborted) return; if (!response.ok) { - if (response.status === 402 && onQuotaExceeded) { - onQuotaExceeded(); + if (response.status === 402) { + onQuotaExceededRef.current?.(); } throw new Error('Error during analysis'); } @@ -79,7 +82,7 @@ export function useAutoTagging({ content, notebookId, enabled = true, onQuotaExc setIsAnalyzing(false); } } - }, [hasAiConsent, requestAiConsent, onQuotaExceeded]); + }, [hasAiConsent, requestAiConsent]); // onQuotaExceeded via ref — stable // Trigger on content change useEffect(() => { diff --git a/memento-note/lib/ai/providers/deepseek.ts b/memento-note/lib/ai/providers/deepseek.ts index ea411b3..f0639a7 100644 --- a/memento-note/lib/ai/providers/deepseek.ts +++ b/memento-note/lib/ai/providers/deepseek.ts @@ -30,21 +30,23 @@ export class DeepSeekProvider implements AIProvider { this.embeddingModel = deepseek.embedding(embeddingModelName); } - async generateTags(content: string): Promise { + async generateTags(content: string, language?: string): Promise { try { - const { object } = await generateObject({ + // DeepSeek doesn't support response_format: json_schema — use generateText + manual parse + const { text } = await aiGenerateText({ model: this.model, - schema: z.object({ - tags: z.array(z.object({ - tag: z.string().describe('Short tag name in lowercase'), - confidence: z.number().min(0).max(1).describe('Confidence level between 0 and 1') - })) - }), - prompt: `Analyze the following note and suggest 1 to 5 relevant tags. - Note content: "${content}"`, + prompt: `Analyze the following note and suggest 1 to 5 relevant tags as a JSON array. +Return ONLY a JSON array like: [{"tag":"example","confidence":0.9}] +Note content: "${content.substring(0, 1500)}"`, }); - return object.tags; + const clean = text.replace(/^```json\n?/, '').replace(/\n?```$/, '').trim(); + const parsed = JSON.parse(clean); + const arr = Array.isArray(parsed) ? parsed : (parsed.tags || []); + return arr.map((t: any) => ({ + tag: t.tag || t.label || t.name || '', + confidence: t.confidence || t.score || 0.7, + })); } catch (e) { console.error('Error generating tags (DeepSeek):', e); return [];