diff --git a/.cursor/hooks/state/continual-learning.json b/.cursor/hooks/state/continual-learning.json index d47349e..e2c123d 100644 --- a/.cursor/hooks/state/continual-learning.json +++ b/.cursor/hooks/state/continual-learning.json @@ -1,8 +1,8 @@ { "version": 1, "lastRunAtMs": 1779998560332, - "turnsSinceLastRun": 2, + "turnsSinceLastRun": 3, "lastTranscriptMtimeMs": 1779998515529, - "lastProcessedGenerationId": "5e947d58-6d03-4f90-ad7c-4a97606f4d11", + "lastProcessedGenerationId": "250d4e92-b72e-4ca2-b0c1-625292c59d32", "trialStartedAtMs": null } diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index e73920f..9bbe3fc 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -13,7 +13,7 @@ on: jobs: ci: name: Lint, Unit Tests & Build - runs-on: docker-host + runs-on: ubuntu-24.04 defaults: run: working-directory: memento-note @@ -21,6 +21,19 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Setup Node 22 + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Cache node_modules + uses: actions/cache@v3 + with: + path: memento-note/node_modules + key: ${{ runner.os }}-node-${{ hashFiles('memento-note/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + - name: Install dependencies run: npm ci --legacy-peer-deps diff --git a/memento-note/components/note-editor/note-content-area.tsx b/memento-note/components/note-editor/note-content-area.tsx index b07edd8..9b9da29 100644 --- a/memento-note/components/note-editor/note-content-area.tsx +++ b/memento-note/components/note-editor/note-content-area.tsx @@ -12,6 +12,7 @@ import { Checkbox } from '@/components/ui/checkbox' import { X, Plus } from 'lucide-react' import { useLanguage } from '@/lib/i18n' import { cn } from '@/lib/utils' +import { InlinePaywall } from '@/components/settings/inline-paywall' export function NoteContentArea() { const { state, actions, readOnly, fullPage, textareaRef, note, richTextEditorRef } = useNoteEditorContext() @@ -85,13 +86,20 @@ export function NoteContentArea() { readOnly && "cursor-default" )} /> - + {state.quotaExceededFeature === 'auto_tag' ? ( + actions.setQuotaExceededFeature(null)} + /> + ) : ( + + )} ) } @@ -128,13 +136,20 @@ export function NoteContentArea() { noteTitle={state.title || note.title || undefined} sourceUrl={note.sourceUrl} /> - + {state.quotaExceededFeature === 'auto_tag' ? ( + actions.setQuotaExceededFeature(null)} + /> + ) : ( + + )} ) } diff --git a/memento-note/components/note-editor/note-editor-context.tsx b/memento-note/components/note-editor/note-editor-context.tsx index e44eb6b..480dd36 100644 --- a/memento-note/components/note-editor/note-editor-context.tsx +++ b/memento-note/components/note-editor/note-editor-context.tsx @@ -50,6 +50,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o } }, [session?.user?.id]) + const [quotaExceededFeature, setQuotaExceededFeature] = useState(null) const [title, setTitle] = useState(note.title || '') const contentRef = useRef(note.content) const [content, setContentState] = useState(note.content) @@ -93,6 +94,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o const prev = prevNoteRef.current if (note.id !== prev.id) { + setQuotaExceededFeature(null) setTitle(note.title || '') setContentImmediate(note.content) setCheckItems(note.checkItems || []) @@ -165,7 +167,8 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o const { suggestions, isAnalyzing: isAnalyzingSuggestions } = useAutoTagging({ content: content, notebookId: note.notebookId, - enabled: autoTaggingEnabled + enabled: autoTaggingEnabled, + onQuotaExceeded: () => setQuotaExceededFeature('auto_tag') }) const [showReminderDialog, setShowReminderDialog] = useState(false) @@ -382,6 +385,10 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o }) if (!response.ok) { + if (response.status === 402) { + setQuotaExceededFeature('auto_title') + return + } const errorData = await response.json() throw new Error(errorData.error || t('ai.titleGenerationError')) } @@ -454,6 +461,10 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o }) if (!response.ok) { + if (response.status === 402) { + setQuotaExceededFeature('reformulate') + return + } const errorData = await response.json() throw new Error(errorData.error || t('ai.reformulationError')) } @@ -490,6 +501,10 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: content, option: 'clarify' }) }) + if (response.status === 402) { + setQuotaExceededFeature('reformulate') + return + } const data = await response.json() if (!response.ok) throw new Error(data.error || t('notes.clarifyFailed')) setContentImmediate(data.reformulatedText || data.text) @@ -519,6 +534,10 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: content, option: 'shorten' }) }) + if (response.status === 402) { + setQuotaExceededFeature('reformulate') + return + } const data = await response.json() if (!response.ok) throw new Error(data.error || t('notes.shortenFailed')) setContentImmediate(data.reformulatedText || data.text) @@ -548,6 +567,10 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: content, option: 'improve' }) }) + if (response.status === 402) { + setQuotaExceededFeature('reformulate') + return + } const data = await response.json() if (!response.ok) throw new Error(data.error || t('notes.improveFailed')) setContentImmediate(data.reformulatedText || data.text) @@ -864,13 +887,14 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o isMarkdown, allImages, colorClasses, + quotaExceededFeature, }), [ title, content, checkItems, labels, images, links, newLabel, color, size, showMarkdownPreview, removedImageUrls, isSaving, isDirty, isProcessingAI, aiOpen, infoOpen, isGeneratingTitles, titleSuggestions, dismissedTitleSuggestions, isReformulating, reformulationModal, previousContentForCopilot, showReminderDialog, currentReminder, showLinkDialog, linkUrl, comparisonNotes, fusionNotes, dismissedTags, filteredSuggestions, - isAnalyzingSuggestions, isMarkdown, allImages, colorClasses + isAnalyzingSuggestions, isMarkdown, allImages, colorClasses, quotaExceededFeature ]) const actions: NoteEditorActions = useMemo(() => ({ @@ -924,13 +948,14 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o setIsGeneratingTitles, setIsAnalyzingSuggestions: (_a) => { /* handled by useAutoTagging */ }, setPreviousContentForCopilot, + setQuotaExceededFeature, }), [ handleCheckItem, handleUpdateCheckItem, handleAddCheckItem, handleRemoveCheckItem, handleSelectGhostTag, handleDismissGhostTag, handleRemoveLabel, handleImageUpload, handleRemoveImage, handleAddLink, handleRemoveLink, handleReminderSave, handleRemoveReminder, handleGenerateTitles, handleSelectTitle, handleReformulate, handleApplyRefactor, handleClarifyDirect, handleShortenDirect, handleImproveDirect, - handleTransformMarkdown, handleSave, handleSaveInPlace, handleMakeCopy + handleTransformMarkdown, handleSave, handleSaveInPlace, handleMakeCopy, setQuotaExceededFeature ]) const value: NoteEditorContextValue = useMemo(() => ({ diff --git a/memento-note/components/note-editor/note-editor-dialog.tsx b/memento-note/components/note-editor/note-editor-dialog.tsx index e2d2fc8..8e38be3 100644 --- a/memento-note/components/note-editor/note-editor-dialog.tsx +++ b/memento-note/components/note-editor/note-editor-dialog.tsx @@ -4,6 +4,7 @@ import { useNoteEditorContext } from './note-editor-context' import { NoteTitleBlock } from './note-title-block' import { NoteContentArea } from './note-content-area' import { NoteMetadataSection } from './note-metadata-section' +import { InlinePaywall } from '@/components/settings/inline-paywall' import { EditorImages } from '@/components/editor-images' import { ComparisonModal } from '@/components/comparison-modal' import { FusionModal } from '@/components/fusion-modal' @@ -107,6 +108,12 @@ export function NoteEditorDialog({ onClose }: NoteEditorDialogProps) { )} {/* Content Area */} + {state.quotaExceededFeature === 'reformulate' && ( + actions.setQuotaExceededFeature(null)} + /> + )} {/* Metadata Section */} diff --git a/memento-note/components/note-editor/note-editor-full-page.tsx b/memento-note/components/note-editor/note-editor-full-page.tsx index 8969edb..760ca79 100644 --- a/memento-note/components/note-editor/note-editor-full-page.tsx +++ b/memento-note/components/note-editor/note-editor-full-page.tsx @@ -17,6 +17,7 @@ import { ChevronRight } from 'lucide-react' import { toast } from 'sonner' import { Note } from '@/lib/types' import { GhostTags } from '@/components/ghost-tags' +import { InlinePaywall } from '@/components/settings/inline-paywall' import { LabelBadge } from '@/components/label-badge' import { NoteAttachments } from '@/components/note-attachments' import { DocumentQAOverlay } from '@/components/document-qa-overlay' @@ -88,7 +89,7 @@ export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) { {/* Title */} - {(state.labels.length > 0 || (state.filteredSuggestions.length > 0)) && ( + {(state.labels.length > 0 || state.filteredSuggestions.length > 0 || state.quotaExceededFeature === 'auto_tag') && (
{state.labels.map((label) => ( ))} {!readOnly && ( - + state.quotaExceededFeature === 'auto_tag' ? ( + actions.setQuotaExceededFeature(null)} + /> + ) : ( + + ) )}
)} @@ -124,6 +132,12 @@ export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) { {/* Content area — max-w-3xl for wider reading column */}
+ {state.quotaExceededFeature === 'reformulate' && ( + actions.setQuotaExceededFeature(null)} + /> + )} {!readOnly && ( diff --git a/memento-note/components/note-editor/note-metadata-section.tsx b/memento-note/components/note-editor/note-metadata-section.tsx index 65f6624..8f32cad 100644 --- a/memento-note/components/note-editor/note-metadata-section.tsx +++ b/memento-note/components/note-editor/note-metadata-section.tsx @@ -3,6 +3,7 @@ import { useNoteEditorContext } from './note-editor-context' import { LabelBadge } from '../label-badge' import { GhostTags } from '../ghost-tags' +import { InlinePaywall } from '@/components/settings/inline-paywall' import { cn } from '@/lib/utils' export function NoteMetadataSection() { @@ -31,13 +32,20 @@ export function NoteMetadataSection() { {/* Ghost Tags - only show in dialog mode */} {!readOnly && !state.isMarkdown && ( - + state.quotaExceededFeature === 'auto_tag' ? ( + actions.setQuotaExceededFeature(null)} + /> + ) : ( + + ) )} {/* Color indicator */} diff --git a/memento-note/components/note-editor/note-title-block.tsx b/memento-note/components/note-editor/note-title-block.tsx index 738501e..7f5bf92 100644 --- a/memento-note/components/note-editor/note-title-block.tsx +++ b/memento-note/components/note-editor/note-title-block.tsx @@ -8,6 +8,7 @@ import { useAiConsent } from '@/components/legal/ai-consent-provider' import { cn } from '@/lib/utils' import { toast } from 'sonner' import { resolveTitleDirection, resolveTitleLang } from '@/lib/clip/rtl-content' +import { InlinePaywall } from '@/components/settings/inline-paywall' export function NoteTitleBlock() { const { note, state, actions, readOnly, fullPage } = useNoteEditorContext() @@ -30,6 +31,12 @@ export function NoteTitleBlock() { return (
+ {state.quotaExceededFeature === 'auto_title' && ( + actions.setQuotaExceededFeature(null)} + /> + )} {/* Title — auto-resizing textarea, adaptive size */}