chore(ci): correct Gitea runner to runs-on ubuntu-24.04 and feat(billing): implement US-3.7 billing/subscription UX
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"lastRunAtMs": 1779998560332,
|
"lastRunAtMs": 1779998560332,
|
||||||
"turnsSinceLastRun": 2,
|
"turnsSinceLastRun": 3,
|
||||||
"lastTranscriptMtimeMs": 1779998515529,
|
"lastTranscriptMtimeMs": 1779998515529,
|
||||||
"lastProcessedGenerationId": "5e947d58-6d03-4f90-ad7c-4a97606f4d11",
|
"lastProcessedGenerationId": "250d4e92-b72e-4ca2-b0c1-625292c59d32",
|
||||||
"trialStartedAtMs": null
|
"trialStartedAtMs": null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
name: Lint, Unit Tests & Build
|
name: Lint, Unit Tests & Build
|
||||||
runs-on: docker-host
|
runs-on: ubuntu-24.04
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: memento-note
|
working-directory: memento-note
|
||||||
@@ -21,6 +21,19 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
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
|
- name: Install dependencies
|
||||||
run: npm ci --legacy-peer-deps
|
run: npm ci --legacy-peer-deps
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Checkbox } from '@/components/ui/checkbox'
|
|||||||
import { X, Plus } from 'lucide-react'
|
import { X, Plus } from 'lucide-react'
|
||||||
import { useLanguage } from '@/lib/i18n'
|
import { useLanguage } from '@/lib/i18n'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { InlinePaywall } from '@/components/settings/inline-paywall'
|
||||||
|
|
||||||
export function NoteContentArea() {
|
export function NoteContentArea() {
|
||||||
const { state, actions, readOnly, fullPage, textareaRef, note, richTextEditorRef } = useNoteEditorContext()
|
const { state, actions, readOnly, fullPage, textareaRef, note, richTextEditorRef } = useNoteEditorContext()
|
||||||
@@ -85,13 +86,20 @@ export function NoteContentArea() {
|
|||||||
readOnly && "cursor-default"
|
readOnly && "cursor-default"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<GhostTags
|
{state.quotaExceededFeature === 'auto_tag' ? (
|
||||||
suggestions={state.filteredSuggestions}
|
<InlinePaywall
|
||||||
addedTags={state.labels}
|
feature="auto_tag"
|
||||||
isAnalyzing={state.isAnalyzingSuggestions}
|
onDismiss={() => actions.setQuotaExceededFeature(null)}
|
||||||
onSelectTag={actions.handleSelectGhostTag}
|
/>
|
||||||
onDismissTag={actions.handleDismissGhostTag}
|
) : (
|
||||||
/>
|
<GhostTags
|
||||||
|
suggestions={state.filteredSuggestions}
|
||||||
|
addedTags={state.labels}
|
||||||
|
isAnalyzing={state.isAnalyzingSuggestions}
|
||||||
|
onSelectTag={actions.handleSelectGhostTag}
|
||||||
|
onDismissTag={actions.handleDismissGhostTag}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -128,13 +136,20 @@ export function NoteContentArea() {
|
|||||||
noteTitle={state.title || note.title || undefined}
|
noteTitle={state.title || note.title || undefined}
|
||||||
sourceUrl={note.sourceUrl}
|
sourceUrl={note.sourceUrl}
|
||||||
/>
|
/>
|
||||||
<GhostTags
|
{state.quotaExceededFeature === 'auto_tag' ? (
|
||||||
suggestions={state.filteredSuggestions}
|
<InlinePaywall
|
||||||
addedTags={state.labels}
|
feature="auto_tag"
|
||||||
isAnalyzing={state.isAnalyzingSuggestions}
|
onDismiss={() => actions.setQuotaExceededFeature(null)}
|
||||||
onSelectTag={actions.handleSelectGhostTag}
|
/>
|
||||||
onDismissTag={actions.handleDismissGhostTag}
|
) : (
|
||||||
/>
|
<GhostTags
|
||||||
|
suggestions={state.filteredSuggestions}
|
||||||
|
addedTags={state.labels}
|
||||||
|
isAnalyzing={state.isAnalyzingSuggestions}
|
||||||
|
onSelectTag={actions.handleSelectGhostTag}
|
||||||
|
onDismissTag={actions.handleDismissGhostTag}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
|||||||
}
|
}
|
||||||
}, [session?.user?.id])
|
}, [session?.user?.id])
|
||||||
|
|
||||||
|
const [quotaExceededFeature, setQuotaExceededFeature] = useState<string | null>(null)
|
||||||
const [title, setTitle] = useState(note.title || '')
|
const [title, setTitle] = useState(note.title || '')
|
||||||
const contentRef = useRef(note.content)
|
const contentRef = useRef(note.content)
|
||||||
const [content, setContentState] = useState(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
|
const prev = prevNoteRef.current
|
||||||
|
|
||||||
if (note.id !== prev.id) {
|
if (note.id !== prev.id) {
|
||||||
|
setQuotaExceededFeature(null)
|
||||||
setTitle(note.title || '')
|
setTitle(note.title || '')
|
||||||
setContentImmediate(note.content)
|
setContentImmediate(note.content)
|
||||||
setCheckItems(note.checkItems || [])
|
setCheckItems(note.checkItems || [])
|
||||||
@@ -165,7 +167,8 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
|||||||
const { suggestions, isAnalyzing: isAnalyzingSuggestions } = useAutoTagging({
|
const { suggestions, isAnalyzing: isAnalyzingSuggestions } = useAutoTagging({
|
||||||
content: content,
|
content: content,
|
||||||
notebookId: note.notebookId,
|
notebookId: note.notebookId,
|
||||||
enabled: autoTaggingEnabled
|
enabled: autoTaggingEnabled,
|
||||||
|
onQuotaExceeded: () => setQuotaExceededFeature('auto_tag')
|
||||||
})
|
})
|
||||||
|
|
||||||
const [showReminderDialog, setShowReminderDialog] = useState(false)
|
const [showReminderDialog, setShowReminderDialog] = useState(false)
|
||||||
@@ -382,6 +385,10 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
if (response.status === 402) {
|
||||||
|
setQuotaExceededFeature('auto_title')
|
||||||
|
return
|
||||||
|
}
|
||||||
const errorData = await response.json()
|
const errorData = await response.json()
|
||||||
throw new Error(errorData.error || t('ai.titleGenerationError'))
|
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.ok) {
|
||||||
|
if (response.status === 402) {
|
||||||
|
setQuotaExceededFeature('reformulate')
|
||||||
|
return
|
||||||
|
}
|
||||||
const errorData = await response.json()
|
const errorData = await response.json()
|
||||||
throw new Error(errorData.error || t('ai.reformulationError'))
|
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' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ text: content, option: 'clarify' })
|
body: JSON.stringify({ text: content, option: 'clarify' })
|
||||||
})
|
})
|
||||||
|
if (response.status === 402) {
|
||||||
|
setQuotaExceededFeature('reformulate')
|
||||||
|
return
|
||||||
|
}
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
if (!response.ok) throw new Error(data.error || t('notes.clarifyFailed'))
|
if (!response.ok) throw new Error(data.error || t('notes.clarifyFailed'))
|
||||||
setContentImmediate(data.reformulatedText || data.text)
|
setContentImmediate(data.reformulatedText || data.text)
|
||||||
@@ -519,6 +534,10 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ text: content, option: 'shorten' })
|
body: JSON.stringify({ text: content, option: 'shorten' })
|
||||||
})
|
})
|
||||||
|
if (response.status === 402) {
|
||||||
|
setQuotaExceededFeature('reformulate')
|
||||||
|
return
|
||||||
|
}
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
if (!response.ok) throw new Error(data.error || t('notes.shortenFailed'))
|
if (!response.ok) throw new Error(data.error || t('notes.shortenFailed'))
|
||||||
setContentImmediate(data.reformulatedText || data.text)
|
setContentImmediate(data.reformulatedText || data.text)
|
||||||
@@ -548,6 +567,10 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ text: content, option: 'improve' })
|
body: JSON.stringify({ text: content, option: 'improve' })
|
||||||
})
|
})
|
||||||
|
if (response.status === 402) {
|
||||||
|
setQuotaExceededFeature('reformulate')
|
||||||
|
return
|
||||||
|
}
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
if (!response.ok) throw new Error(data.error || t('notes.improveFailed'))
|
if (!response.ok) throw new Error(data.error || t('notes.improveFailed'))
|
||||||
setContentImmediate(data.reformulatedText || data.text)
|
setContentImmediate(data.reformulatedText || data.text)
|
||||||
@@ -864,13 +887,14 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
|||||||
isMarkdown,
|
isMarkdown,
|
||||||
allImages,
|
allImages,
|
||||||
colorClasses,
|
colorClasses,
|
||||||
|
quotaExceededFeature,
|
||||||
}), [
|
}), [
|
||||||
title, content, checkItems, labels, images, links, newLabel, color, size,
|
title, content, checkItems, labels, images, links, newLabel, color, size,
|
||||||
showMarkdownPreview, removedImageUrls, isSaving, isDirty, isProcessingAI, aiOpen, infoOpen,
|
showMarkdownPreview, removedImageUrls, isSaving, isDirty, isProcessingAI, aiOpen, infoOpen,
|
||||||
isGeneratingTitles, titleSuggestions, dismissedTitleSuggestions, isReformulating,
|
isGeneratingTitles, titleSuggestions, dismissedTitleSuggestions, isReformulating,
|
||||||
reformulationModal, previousContentForCopilot, showReminderDialog, currentReminder,
|
reformulationModal, previousContentForCopilot, showReminderDialog, currentReminder,
|
||||||
showLinkDialog, linkUrl, comparisonNotes, fusionNotes, dismissedTags, filteredSuggestions,
|
showLinkDialog, linkUrl, comparisonNotes, fusionNotes, dismissedTags, filteredSuggestions,
|
||||||
isAnalyzingSuggestions, isMarkdown, allImages, colorClasses
|
isAnalyzingSuggestions, isMarkdown, allImages, colorClasses, quotaExceededFeature
|
||||||
])
|
])
|
||||||
|
|
||||||
const actions: NoteEditorActions = useMemo(() => ({
|
const actions: NoteEditorActions = useMemo(() => ({
|
||||||
@@ -924,13 +948,14 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
|||||||
setIsGeneratingTitles,
|
setIsGeneratingTitles,
|
||||||
setIsAnalyzingSuggestions: (_a) => { /* handled by useAutoTagging */ },
|
setIsAnalyzingSuggestions: (_a) => { /* handled by useAutoTagging */ },
|
||||||
setPreviousContentForCopilot,
|
setPreviousContentForCopilot,
|
||||||
|
setQuotaExceededFeature,
|
||||||
}), [
|
}), [
|
||||||
handleCheckItem, handleUpdateCheckItem, handleAddCheckItem, handleRemoveCheckItem,
|
handleCheckItem, handleUpdateCheckItem, handleAddCheckItem, handleRemoveCheckItem,
|
||||||
handleSelectGhostTag, handleDismissGhostTag, handleRemoveLabel, handleImageUpload,
|
handleSelectGhostTag, handleDismissGhostTag, handleRemoveLabel, handleImageUpload,
|
||||||
handleRemoveImage, handleAddLink, handleRemoveLink, handleReminderSave,
|
handleRemoveImage, handleAddLink, handleRemoveLink, handleReminderSave,
|
||||||
handleRemoveReminder, handleGenerateTitles, handleSelectTitle, handleReformulate,
|
handleRemoveReminder, handleGenerateTitles, handleSelectTitle, handleReformulate,
|
||||||
handleApplyRefactor, handleClarifyDirect, handleShortenDirect, handleImproveDirect,
|
handleApplyRefactor, handleClarifyDirect, handleShortenDirect, handleImproveDirect,
|
||||||
handleTransformMarkdown, handleSave, handleSaveInPlace, handleMakeCopy
|
handleTransformMarkdown, handleSave, handleSaveInPlace, handleMakeCopy, setQuotaExceededFeature
|
||||||
])
|
])
|
||||||
|
|
||||||
const value: NoteEditorContextValue = useMemo(() => ({
|
const value: NoteEditorContextValue = useMemo(() => ({
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useNoteEditorContext } from './note-editor-context'
|
|||||||
import { NoteTitleBlock } from './note-title-block'
|
import { NoteTitleBlock } from './note-title-block'
|
||||||
import { NoteContentArea } from './note-content-area'
|
import { NoteContentArea } from './note-content-area'
|
||||||
import { NoteMetadataSection } from './note-metadata-section'
|
import { NoteMetadataSection } from './note-metadata-section'
|
||||||
|
import { InlinePaywall } from '@/components/settings/inline-paywall'
|
||||||
import { EditorImages } from '@/components/editor-images'
|
import { EditorImages } from '@/components/editor-images'
|
||||||
import { ComparisonModal } from '@/components/comparison-modal'
|
import { ComparisonModal } from '@/components/comparison-modal'
|
||||||
import { FusionModal } from '@/components/fusion-modal'
|
import { FusionModal } from '@/components/fusion-modal'
|
||||||
@@ -107,6 +108,12 @@ export function NoteEditorDialog({ onClose }: NoteEditorDialogProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Content Area */}
|
{/* Content Area */}
|
||||||
|
{state.quotaExceededFeature === 'reformulate' && (
|
||||||
|
<InlinePaywall
|
||||||
|
feature="reformulate"
|
||||||
|
onDismiss={() => actions.setQuotaExceededFeature(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<NoteContentArea />
|
<NoteContentArea />
|
||||||
|
|
||||||
{/* Metadata Section */}
|
{/* Metadata Section */}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { ChevronRight } from 'lucide-react'
|
|||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { Note } from '@/lib/types'
|
import { Note } from '@/lib/types'
|
||||||
import { GhostTags } from '@/components/ghost-tags'
|
import { GhostTags } from '@/components/ghost-tags'
|
||||||
|
import { InlinePaywall } from '@/components/settings/inline-paywall'
|
||||||
import { LabelBadge } from '@/components/label-badge'
|
import { LabelBadge } from '@/components/label-badge'
|
||||||
import { NoteAttachments } from '@/components/note-attachments'
|
import { NoteAttachments } from '@/components/note-attachments'
|
||||||
import { DocumentQAOverlay } from '@/components/document-qa-overlay'
|
import { DocumentQAOverlay } from '@/components/document-qa-overlay'
|
||||||
@@ -88,7 +89,7 @@ export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
|
|||||||
{/* Title */}
|
{/* Title */}
|
||||||
<NoteTitleBlock />
|
<NoteTitleBlock />
|
||||||
|
|
||||||
{(state.labels.length > 0 || (state.filteredSuggestions.length > 0)) && (
|
{(state.labels.length > 0 || state.filteredSuggestions.length > 0 || state.quotaExceededFeature === 'auto_tag') && (
|
||||||
<div className="flex flex-wrap gap-2 pt-2">
|
<div className="flex flex-wrap gap-2 pt-2">
|
||||||
{state.labels.map((label) => (
|
{state.labels.map((label) => (
|
||||||
<LabelBadge
|
<LabelBadge
|
||||||
@@ -99,13 +100,20 @@ export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<GhostTags
|
state.quotaExceededFeature === 'auto_tag' ? (
|
||||||
suggestions={state.filteredSuggestions}
|
<InlinePaywall
|
||||||
addedTags={state.labels}
|
feature="auto_tag"
|
||||||
isAnalyzing={state.isAnalyzingSuggestions}
|
onDismiss={() => actions.setQuotaExceededFeature(null)}
|
||||||
onSelectTag={actions.handleSelectGhostTag}
|
/>
|
||||||
onDismissTag={actions.handleDismissGhostTag}
|
) : (
|
||||||
/>
|
<GhostTags
|
||||||
|
suggestions={state.filteredSuggestions}
|
||||||
|
addedTags={state.labels}
|
||||||
|
isAnalyzing={state.isAnalyzingSuggestions}
|
||||||
|
onSelectTag={actions.handleSelectGhostTag}
|
||||||
|
onDismissTag={actions.handleDismissGhostTag}
|
||||||
|
/>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -124,6 +132,12 @@ export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
|
|||||||
|
|
||||||
{/* Content area — max-w-3xl for wider reading column */}
|
{/* Content area — max-w-3xl for wider reading column */}
|
||||||
<div className="max-w-3xl mx-auto w-full space-y-8 pb-32">
|
<div className="max-w-3xl mx-auto w-full space-y-8 pb-32">
|
||||||
|
{state.quotaExceededFeature === 'reformulate' && (
|
||||||
|
<InlinePaywall
|
||||||
|
feature="reformulate"
|
||||||
|
onDismiss={() => actions.setQuotaExceededFeature(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<NoteContentArea />
|
<NoteContentArea />
|
||||||
|
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useNoteEditorContext } from './note-editor-context'
|
import { useNoteEditorContext } from './note-editor-context'
|
||||||
import { LabelBadge } from '../label-badge'
|
import { LabelBadge } from '../label-badge'
|
||||||
import { GhostTags } from '../ghost-tags'
|
import { GhostTags } from '../ghost-tags'
|
||||||
|
import { InlinePaywall } from '@/components/settings/inline-paywall'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export function NoteMetadataSection() {
|
export function NoteMetadataSection() {
|
||||||
@@ -31,13 +32,20 @@ export function NoteMetadataSection() {
|
|||||||
|
|
||||||
{/* Ghost Tags - only show in dialog mode */}
|
{/* Ghost Tags - only show in dialog mode */}
|
||||||
{!readOnly && !state.isMarkdown && (
|
{!readOnly && !state.isMarkdown && (
|
||||||
<GhostTags
|
state.quotaExceededFeature === 'auto_tag' ? (
|
||||||
suggestions={state.filteredSuggestions}
|
<InlinePaywall
|
||||||
addedTags={state.labels}
|
feature="auto_tag"
|
||||||
isAnalyzing={state.isAnalyzingSuggestions}
|
onDismiss={() => actions.setQuotaExceededFeature(null)}
|
||||||
onSelectTag={actions.handleSelectGhostTag}
|
/>
|
||||||
onDismissTag={actions.handleDismissGhostTag}
|
) : (
|
||||||
/>
|
<GhostTags
|
||||||
|
suggestions={state.filteredSuggestions}
|
||||||
|
addedTags={state.labels}
|
||||||
|
isAnalyzing={state.isAnalyzingSuggestions}
|
||||||
|
onSelectTag={actions.handleSelectGhostTag}
|
||||||
|
onDismissTag={actions.handleDismissGhostTag}
|
||||||
|
/>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Color indicator */}
|
{/* Color indicator */}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useAiConsent } from '@/components/legal/ai-consent-provider'
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { resolveTitleDirection, resolveTitleLang } from '@/lib/clip/rtl-content'
|
import { resolveTitleDirection, resolveTitleLang } from '@/lib/clip/rtl-content'
|
||||||
|
import { InlinePaywall } from '@/components/settings/inline-paywall'
|
||||||
|
|
||||||
export function NoteTitleBlock() {
|
export function NoteTitleBlock() {
|
||||||
const { note, state, actions, readOnly, fullPage } = useNoteEditorContext()
|
const { note, state, actions, readOnly, fullPage } = useNoteEditorContext()
|
||||||
@@ -30,6 +31,12 @@ export function NoteTitleBlock() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{state.quotaExceededFeature === 'auto_title' && (
|
||||||
|
<InlinePaywall
|
||||||
|
feature="auto_title"
|
||||||
|
onDismiss={() => actions.setQuotaExceededFeature(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{/* Title — auto-resizing textarea, adaptive size */}
|
{/* Title — auto-resizing textarea, adaptive size */}
|
||||||
<div className="group relative">
|
<div className="group relative">
|
||||||
<textarea
|
<textarea
|
||||||
@@ -84,6 +91,10 @@ export function NoteTitleBlock() {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ content: plain }),
|
body: JSON.stringify({ content: plain }),
|
||||||
})
|
})
|
||||||
|
if (res.status === 402) {
|
||||||
|
actions.setQuotaExceededFeature('auto_title')
|
||||||
|
return
|
||||||
|
}
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
const s = data.suggestions?.[0]?.title ?? ''
|
const s = data.suggestions?.[0]?.title ?? ''
|
||||||
@@ -127,32 +138,40 @@ export function NoteTitleBlock() {
|
|||||||
|
|
||||||
// Dialog mode title block
|
// Dialog mode title block
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="space-y-2">
|
||||||
<input
|
<div className="relative">
|
||||||
dir={titleDir}
|
<input
|
||||||
lang={titleLang}
|
dir={titleDir}
|
||||||
placeholder={t('notes.titlePlaceholder')}
|
lang={titleLang}
|
||||||
value={state.title}
|
placeholder={t('notes.titlePlaceholder')}
|
||||||
onChange={(e) => actions.setTitle(e.target.value)}
|
value={state.title}
|
||||||
disabled={readOnly}
|
onChange={(e) => actions.setTitle(e.target.value)}
|
||||||
className={cn(
|
disabled={readOnly}
|
||||||
'w-full text-lg font-semibold border-0 focus-visible:ring-0 px-0 bg-transparent',
|
className={cn(
|
||||||
titleIsRtl ? 'text-right font-[family-name:var(--font-sans)] ps-10' : 'text-left pe-10',
|
'w-full text-lg font-semibold border-0 focus-visible:ring-0 px-0 bg-transparent',
|
||||||
readOnly && 'cursor-default',
|
titleIsRtl ? 'text-right font-[family-name:var(--font-sans)] ps-10' : 'text-left pe-10',
|
||||||
)}
|
readOnly && 'cursor-default',
|
||||||
/>
|
)}
|
||||||
<button
|
/>
|
||||||
onClick={actions.handleGenerateTitles}
|
<button
|
||||||
disabled={state.isGeneratingTitles || readOnly}
|
onClick={actions.handleGenerateTitles}
|
||||||
className="absolute right-0 top-1/2 -translate-y-1/2 p-1 hover:bg-purple-100 dark:hover:bg-purple-900 rounded transition-colors"
|
disabled={state.isGeneratingTitles || readOnly}
|
||||||
title={state.isGeneratingTitles ? t('ai.titleGenerating') : t('ai.titleGenerateWithAI')}
|
className="absolute right-0 top-1/2 -translate-y-1/2 p-1 hover:bg-purple-100 dark:hover:bg-purple-900 rounded transition-colors"
|
||||||
>
|
title={state.isGeneratingTitles ? t('ai.titleGenerating') : t('ai.titleGenerateWithAI')}
|
||||||
{state.isGeneratingTitles ? (
|
>
|
||||||
<div className="w-4 h-4 border-2 border-purple-500 border-t-transparent rounded-full animate-spin" />
|
{state.isGeneratingTitles ? (
|
||||||
) : (
|
<div className="w-4 h-4 border-2 border-purple-500 border-t-transparent rounded-full animate-spin" />
|
||||||
<Sparkles className="w-4 h-4 text-purple-600 hover:text-purple-700 dark:text-purple-400" />
|
) : (
|
||||||
)}
|
<Sparkles className="w-4 h-4 text-purple-600 hover:text-purple-700 dark:text-purple-400" />
|
||||||
</button>
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{state.quotaExceededFeature === 'auto_title' && (
|
||||||
|
<InlinePaywall
|
||||||
|
feature="auto_title"
|
||||||
|
onDismiss={() => actions.setQuotaExceededFeature(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -51,6 +51,7 @@ export interface NoteEditorState {
|
|||||||
isMarkdown: boolean
|
isMarkdown: boolean
|
||||||
allImages: string[]
|
allImages: string[]
|
||||||
colorClasses: typeof NOTE_COLORS[keyof typeof NOTE_COLORS]
|
colorClasses: typeof NOTE_COLORS[keyof typeof NOTE_COLORS]
|
||||||
|
quotaExceededFeature: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NoteEditorActions {
|
export interface NoteEditorActions {
|
||||||
@@ -119,6 +120,7 @@ export interface NoteEditorActions {
|
|||||||
setIsGeneratingTitles: (generating: boolean) => void
|
setIsGeneratingTitles: (generating: boolean) => void
|
||||||
setIsAnalyzingSuggestions: (analyzing: boolean) => void
|
setIsAnalyzingSuggestions: (analyzing: boolean) => void
|
||||||
setPreviousContentForCopilot: (content: string | null) => void
|
setPreviousContentForCopilot: (content: string | null) => void
|
||||||
|
setQuotaExceededFeature: (feature: string | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NoteEditorContextValue {
|
export interface NoteEditorContextValue {
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ export function BillingPlans() {
|
|||||||
let totalUsed = 0;
|
let totalUsed = 0;
|
||||||
let totalLimit = 0;
|
let totalLimit = 0;
|
||||||
if (quotas) {
|
if (quotas) {
|
||||||
Object.entries(quotas).forEach(([_, q]) => {
|
Object.entries(quotas as Record<string, any>).forEach(([_, q]) => {
|
||||||
if (q.limit > 0 && q.limit !== Infinity) {
|
if (q.limit > 0 && q.limit !== Infinity) {
|
||||||
totalUsed += q.used;
|
totalUsed += q.used;
|
||||||
totalLimit += q.limit;
|
totalLimit += q.limit;
|
||||||
@@ -471,7 +471,7 @@ export function BillingPlans() {
|
|||||||
<Loader2 className="h-6 w-6 animate-spin text-brand-accent" />
|
<Loader2 className="h-6 w-6 animate-spin text-brand-accent" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{quotas && Object.entries(quotas).map(([key, q]) => {
|
{quotas && Object.entries(quotas as Record<string, any>).map(([key, q]) => {
|
||||||
const pct = q.limit > 0 && q.limit !== Infinity ? (q.used / q.limit) * 100 : 0;
|
const pct = q.limit > 0 && q.limit !== Infinity ? (q.used / q.limit) * 100 : 0;
|
||||||
const isUnlimited = q.limit === Infinity || q.limit <= 0;
|
const isUnlimited = q.limit === Infinity || q.limit <= 0;
|
||||||
|
|
||||||
@@ -516,12 +516,13 @@ export function BillingPlans() {
|
|||||||
{isPaid && <BillingHistory />}
|
{isPaid && <BillingHistory />}
|
||||||
|
|
||||||
{/* Interval Toggle & Plan Cards */}
|
{/* Interval Toggle & Plan Cards */}
|
||||||
<div className="space-y-8 pt-6 border-t border-border/40">
|
{!isPaid && (
|
||||||
<div className="text-center space-y-2">
|
<div className="space-y-8 pt-6 border-t border-border/40">
|
||||||
<h3 className="text-xs font-bold uppercase tracking-[0.2em] text-concrete">
|
<div className="text-center space-y-2">
|
||||||
{t('billing.upgradePlan') || 'Changer de plan'}
|
<h3 className="text-xs font-bold uppercase tracking-[0.2em] text-concrete">
|
||||||
</h3>
|
{t('billing.upgradePlan') || 'Changer de plan'}
|
||||||
</div>
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
{billingEnabled && (
|
{billingEnabled && (
|
||||||
<div className="flex items-center gap-2 justify-center">
|
<div className="flex items-center gap-2 justify-center">
|
||||||
@@ -603,6 +604,7 @@ export function BillingPlans() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Footer Info */}
|
{/* Footer Info */}
|
||||||
<div className="bg-slate-50 dark:bg-black/20 rounded-[32px] p-8 border border-border/40 flex flex-col md:flex-row items-center justify-between gap-8">
|
<div className="bg-slate-50 dark:bg-black/20 rounded-[32px] p-8 border border-border/40 flex flex-col md:flex-row items-center justify-between gap-8">
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ interface UseAutoTaggingProps {
|
|||||||
content: string;
|
content: string;
|
||||||
notebookId?: string | null;
|
notebookId?: string | null;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
onQuotaExceeded?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAutoTagging({ content, notebookId, enabled = true }: UseAutoTaggingProps) {
|
export function useAutoTagging({ content, notebookId, enabled = true, onQuotaExceeded }: UseAutoTaggingProps) {
|
||||||
const { language } = useLanguage();
|
const { language } = useLanguage();
|
||||||
const { hasAiConsent, requestAiConsent } = useAiConsent();
|
const { hasAiConsent, requestAiConsent } = useAiConsent();
|
||||||
const [suggestions, setSuggestions] = useState<TagSuggestion[]>([]);
|
const [suggestions, setSuggestions] = useState<TagSuggestion[]>([]);
|
||||||
@@ -62,6 +63,9 @@ export function useAutoTagging({ content, notebookId, enabled = true }: UseAutoT
|
|||||||
if (controller.signal.aborted) return;
|
if (controller.signal.aborted) return;
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
if (response.status === 402 && onQuotaExceeded) {
|
||||||
|
onQuotaExceeded();
|
||||||
|
}
|
||||||
throw new Error('Error during analysis');
|
throw new Error('Error during analysis');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +79,7 @@ export function useAutoTagging({ content, notebookId, enabled = true }: UseAutoT
|
|||||||
setIsAnalyzing(false);
|
setIsAnalyzing(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [hasAiConsent, requestAiConsent]);
|
}, [hasAiConsent, requestAiConsent, onQuotaExceeded]);
|
||||||
|
|
||||||
// Trigger on content change
|
// Trigger on content change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user