chore(ci): correct Gitea runner to runs-on ubuntu-24.04 and feat(billing): implement US-3.7 billing/subscription UX
Some checks failed
CI / Deploy production (on server) (push) Has been cancelled
CI / Lint, Unit Tests & Build (push) Has been cancelled

This commit is contained in:
Antigravity
2026-05-28 21:39:08 +00:00
parent 4bfa7c6b69
commit 3b2570d981
11 changed files with 180 additions and 71 deletions

View File

@@ -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"
)}
/>
<GhostTags
suggestions={state.filteredSuggestions}
addedTags={state.labels}
isAnalyzing={state.isAnalyzingSuggestions}
onSelectTag={actions.handleSelectGhostTag}
onDismissTag={actions.handleDismissGhostTag}
/>
{state.quotaExceededFeature === 'auto_tag' ? (
<InlinePaywall
feature="auto_tag"
onDismiss={() => actions.setQuotaExceededFeature(null)}
/>
) : (
<GhostTags
suggestions={state.filteredSuggestions}
addedTags={state.labels}
isAnalyzing={state.isAnalyzingSuggestions}
onSelectTag={actions.handleSelectGhostTag}
onDismissTag={actions.handleDismissGhostTag}
/>
)}
</div>
)
}
@@ -128,13 +136,20 @@ export function NoteContentArea() {
noteTitle={state.title || note.title || undefined}
sourceUrl={note.sourceUrl}
/>
<GhostTags
suggestions={state.filteredSuggestions}
addedTags={state.labels}
isAnalyzing={state.isAnalyzingSuggestions}
onSelectTag={actions.handleSelectGhostTag}
onDismissTag={actions.handleDismissGhostTag}
/>
{state.quotaExceededFeature === 'auto_tag' ? (
<InlinePaywall
feature="auto_tag"
onDismiss={() => actions.setQuotaExceededFeature(null)}
/>
) : (
<GhostTags
suggestions={state.filteredSuggestions}
addedTags={state.labels}
isAnalyzing={state.isAnalyzingSuggestions}
onSelectTag={actions.handleSelectGhostTag}
onDismissTag={actions.handleDismissGhostTag}
/>
)}
</div>
)
}

View File

@@ -50,6 +50,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
}
}, [session?.user?.id])
const [quotaExceededFeature, setQuotaExceededFeature] = useState<string | null>(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(() => ({

View File

@@ -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' && (
<InlinePaywall
feature="reformulate"
onDismiss={() => actions.setQuotaExceededFeature(null)}
/>
)}
<NoteContentArea />
{/* Metadata Section */}

View File

@@ -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 */}
<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">
{state.labels.map((label) => (
<LabelBadge
@@ -99,13 +100,20 @@ export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
/>
))}
{!readOnly && (
<GhostTags
suggestions={state.filteredSuggestions}
addedTags={state.labels}
isAnalyzing={state.isAnalyzingSuggestions}
onSelectTag={actions.handleSelectGhostTag}
onDismissTag={actions.handleDismissGhostTag}
/>
state.quotaExceededFeature === 'auto_tag' ? (
<InlinePaywall
feature="auto_tag"
onDismiss={() => actions.setQuotaExceededFeature(null)}
/>
) : (
<GhostTags
suggestions={state.filteredSuggestions}
addedTags={state.labels}
isAnalyzing={state.isAnalyzingSuggestions}
onSelectTag={actions.handleSelectGhostTag}
onDismissTag={actions.handleDismissGhostTag}
/>
)
)}
</div>
)}
@@ -124,6 +132,12 @@ export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
{/* Content area — max-w-3xl for wider reading column */}
<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 />
{!readOnly && (

View File

@@ -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 && (
<GhostTags
suggestions={state.filteredSuggestions}
addedTags={state.labels}
isAnalyzing={state.isAnalyzingSuggestions}
onSelectTag={actions.handleSelectGhostTag}
onDismissTag={actions.handleDismissGhostTag}
/>
state.quotaExceededFeature === 'auto_tag' ? (
<InlinePaywall
feature="auto_tag"
onDismiss={() => actions.setQuotaExceededFeature(null)}
/>
) : (
<GhostTags
suggestions={state.filteredSuggestions}
addedTags={state.labels}
isAnalyzing={state.isAnalyzingSuggestions}
onSelectTag={actions.handleSelectGhostTag}
onDismissTag={actions.handleDismissGhostTag}
/>
)
)}
{/* Color indicator */}

View File

@@ -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 (
<div className="space-y-4">
{state.quotaExceededFeature === 'auto_title' && (
<InlinePaywall
feature="auto_title"
onDismiss={() => actions.setQuotaExceededFeature(null)}
/>
)}
{/* Title — auto-resizing textarea, adaptive size */}
<div className="group relative">
<textarea
@@ -84,6 +91,10 @@ export function NoteTitleBlock() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: plain }),
})
if (res.status === 402) {
actions.setQuotaExceededFeature('auto_title')
return
}
if (res.ok) {
const data = await res.json()
const s = data.suggestions?.[0]?.title ?? ''
@@ -127,32 +138,40 @@ export function NoteTitleBlock() {
// Dialog mode title block
return (
<div className="relative">
<input
dir={titleDir}
lang={titleLang}
placeholder={t('notes.titlePlaceholder')}
value={state.title}
onChange={(e) => actions.setTitle(e.target.value)}
disabled={readOnly}
className={cn(
'w-full text-lg font-semibold border-0 focus-visible:ring-0 px-0 bg-transparent',
titleIsRtl ? 'text-right font-[family-name:var(--font-sans)] ps-10' : 'text-left pe-10',
readOnly && 'cursor-default',
)}
/>
<button
onClick={actions.handleGenerateTitles}
disabled={state.isGeneratingTitles || readOnly}
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" />
) : (
<Sparkles className="w-4 h-4 text-purple-600 hover:text-purple-700 dark:text-purple-400" />
)}
</button>
<div className="space-y-2">
<div className="relative">
<input
dir={titleDir}
lang={titleLang}
placeholder={t('notes.titlePlaceholder')}
value={state.title}
onChange={(e) => actions.setTitle(e.target.value)}
disabled={readOnly}
className={cn(
'w-full text-lg font-semibold border-0 focus-visible:ring-0 px-0 bg-transparent',
titleIsRtl ? 'text-right font-[family-name:var(--font-sans)] ps-10' : 'text-left pe-10',
readOnly && 'cursor-default',
)}
/>
<button
onClick={actions.handleGenerateTitles}
disabled={state.isGeneratingTitles || readOnly}
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" />
) : (
<Sparkles className="w-4 h-4 text-purple-600 hover:text-purple-700 dark:text-purple-400" />
)}
</button>
</div>
{state.quotaExceededFeature === 'auto_title' && (
<InlinePaywall
feature="auto_title"
onDismiss={() => actions.setQuotaExceededFeature(null)}
/>
)}
</div>
)
}

View File

@@ -51,6 +51,7 @@ export interface NoteEditorState {
isMarkdown: boolean
allImages: string[]
colorClasses: typeof NOTE_COLORS[keyof typeof NOTE_COLORS]
quotaExceededFeature: string | null
}
export interface NoteEditorActions {
@@ -119,6 +120,7 @@ export interface NoteEditorActions {
setIsGeneratingTitles: (generating: boolean) => void
setIsAnalyzingSuggestions: (analyzing: boolean) => void
setPreviousContentForCopilot: (content: string | null) => void
setQuotaExceededFeature: (feature: string | null) => void
}
export interface NoteEditorContextValue {