diff --git a/CLAUDE.md b/CLAUDE.md
index 3fa2eee..fa3b08f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -7,22 +7,26 @@ Read this before writing any code, suggesting any feature, or making any archite
## What DumpIt Is
-DumpIt is a personal knowledge vault with AI-powered retrieval. The core loop:
+DumpIt is a personal knowledge vault with AI-powered retrieval, built toward a larger vision: **a portable layer of personal context that makes every AI interaction actually personal.**
+The core loop today:
1. User saves something (a link, a note, a PDF)
2. User asks a question in natural language
3. DumpIt returns a cited answer grounded in what the user actually saved — not hallucinated from general knowledge
-It is **not** a general-purpose chatbot. It is **not** a task manager. It is **not** an autonomous agent.
-It is a save-and-query tool that happens to have excellent AI retrieval underneath.
+The vision it's building toward: over time, what a user saves, reads, and asks about builds a real, living picture of them — their interests, work, how they think and talk, what they care about now versus before. Answers get sharper and more personal because the system actually knows the person asking, not because it's a smarter generic model.
-**One-line positioning:** "Save anything. Ask anything. Get answers from your own knowledge — not a stranger's AI."
+It is **not** a general-purpose chatbot. It is **not** a task manager. It is **not** an autonomous agent. It does **not** act on the user's behalf or impersonate them to others — it makes the user sharper, it does not replace them. This is a deliberate, permanent boundary, not a v1 limitation to relax later.
+
+**One-line positioning (product):** "Save anything. Ask anything. Get answers from your own knowledge — not a stranger's AI."
+
+**One-line positioning (vision):** "The layer of context that makes AI actually know you — once, portably, everywhere you use it."
---
## Who It's For
-Primary persona: knowledge workers, developers, indie hackers, researchers, students — anyone who saves things online and later can't find or remember them.
+Primary persona: knowledge workers, developers, indie hackers, researchers, students — anyone who saves things online and later can't find, remember, or get real value back from them.
The Canva/Claude parallel is intentional: **simple enough for a non-technical user to start in 60 seconds, deep enough for a developer to plug into their own stack via MCP or API.**
@@ -132,7 +136,8 @@ types/ Shared TypeScript types
| Mobile share sheet | 📋 Planned |
| MCP server (query vault from Claude/Cursor) | 📋 Planned — key differentiator |
| Dodo Payments integration | 🔨 In progress |
-| Tone/preference profile (personalized answers) | 📋 Planned — v2 |
+| Personal context / tone profile (shapes how answers are given) | 📋 Planned — v2, core to the vision |
+| Portable context export (take your profile to other tools) | 📋 Planned — v3, long-term vision |
---
@@ -149,6 +154,15 @@ V1 is complete when:
---
+## The Vision, Staged
+
+- **v1 (now):** Capture + cited Q&A. Prove the core loop works and retains real users.
+- **v2 (next):** Personal context/tone profile — DumpIt learns how the user writes, what they care about, and shapes *how it answers them* accordingly. This is the first real step toward "AI that knows you," and the layer that differentiates DumpIt from bare RAG tools.
+- **v3 (later, only after v1/v2 prove retention):** Portability — the user's context isn't locked into DumpIt's own chat UI. It can be queried from Claude, Cursor, or other tools the user already uses (via MCP/API), so the value compounds wherever they work rather than requiring them to live inside DumpIt.
+- **Never on the roadmap as currently scoped:** autonomous action on the user's behalf, or any feature that impersonates the user to other people. If this is ever revisited, it must be a deliberate, separate decision — not an incremental feature add.
+
+---
+
## What Has Been Explicitly Ruled Out
These decisions were made deliberately. Do not reopen them without a strong reason.
@@ -157,12 +171,12 @@ These decisions were made deliberately. Do not reopen them without a strong reas
- Email/password auth was removed. Google Auth only. Rationale: simpler onboarding, zero password-reset overhead.
### Autonomous agents / "act on my behalf"
-- DumpIt answers questions. It does not send emails, book things, or take irreversible actions on behalf of the user.
-- This is a v1 and v2 constraint. If this ever changes it must be a deliberate, separate decision — not a feature added incrementally.
+- DumpIt answers questions and shapes answers around who the user is. It does not send emails, book things, or take irreversible actions on behalf of the user.
+- This applies at every stage of the roadmap, not just v1. Reversibility is the line: answering, even personalized answering, is low-risk if imperfect; acting is not.
### Digital clone / impersonation
-- DumpIt does not generate content that impersonates the user to other people.
-- The tone/preference profile (v2) shapes *how DumpIt answers the user*, not how it represents the user externally.
+- DumpIt does not generate content that impersonates the user to other people, and never will under the current vision.
+- The personal context/tone profile (v2) shapes *how DumpIt answers the user*, not how it represents the user externally. This is the core trust boundary of the whole product — do not blur it, even in service of a "wow" feature.
### OS-level integration
- Not a v1 or v2 item. The browser extension covers the majority of the capture use case with a fraction of the complexity.
@@ -185,11 +199,13 @@ These decisions were made deliberately. Do not reopen them without a strong reas
| Hermes + OMI + Obsidian | Power-user second brain stack | 30-45 min technical setup, not consumer-friendly |
| Notion AI / Obsidian | Note-taking + AI | Organization-first, not retrieval-first; no capture layer |
| Orchid | Consumer AI assistant via text | Acting-on-your-behalf positioning, not knowledge retrieval; receiving backlash |
-| Claude / ChatGPT memory | Native AI memory | No capture layer, no extension, no email-forward; setup-heavy for real knowledge base use |
+| Claude / ChatGPT memory | Native AI memory | Locked to one app, requires manual context-feeding, no capture layer; DumpIt's vision is to be the portable layer underneath all of these, not a competitor to any one of them |
-**The gap DumpIt fills:** retrieval-first, consumer-simple, with a developer escape hatch (MCP/API). Nobody has shipped this cleanly.
+**The gap DumpIt fills today:** retrieval-first, consumer-simple, with a developer escape hatch (MCP/API).
-**The risk:** platform memory features (Claude Projects, ChatGPT Memory) are improving fast. The window for "it just works out of the box" as a moat is real but not permanent. Ship fast, get real retention data, and decide whether this is a 2-3 year window product or something with a deeper moat.
+**The gap DumpIt is building toward:** a personal context layer that isn't locked into one AI app — portable, user-owned, and useful wherever the user chooses to work.
+
+**The risk:** platform memory features (Claude Projects, ChatGPT Memory) are improving fast, and every major AI is racing toward "remembers you natively." The out-of-the-box simplicity moat is real but not permanent. Ship v1 fast, get real retention data, and let that data determine whether to push hard into the v2/v3 personalization-and-portability vision or treat this as a smaller, time-boxed product.
---
@@ -243,6 +259,13 @@ Payment processor: Dodo Payments (MoR — handles all global tax compliance auto
---
+## Git Workflow
+
+- Claude may push directly to feature branches without asking each time.
+- `main` always goes through a PR — never push directly to `main`.
+
+---
+
## What Good Looks Like
A session is successful if:
@@ -250,6 +273,7 @@ A session is successful if:
- A new user can be saving and querying within 3 minutes of signing up
- No feature outside v1 scope was added
- No fake numbers appear anywhere in the product or on the landing page
+- Any personalization work strictly shapes *answers to the user*, never generates content *as* the user to others
- Code follows the existing zinc/Space Grotesk design system
- All Gemini calls go through rate-limited API routes, never client-side
diff --git a/app/RootLayoutClient.tsx b/app/RootLayoutClient.tsx
index c0fcc9a..c476e5f 100644
--- a/app/RootLayoutClient.tsx
+++ b/app/RootLayoutClient.tsx
@@ -4,6 +4,7 @@ import { AuthProvider } from './contexts/AuthContext'
import { ThemeProvider } from './contexts/ThemeContext'
import { ReactNode, useEffect } from 'react'
import { initializeTelemetry } from './lib/telemetry'
+import { ToastProvider } from './components/ui/Toast'
export function RootLayoutClient({ children }: { children: ReactNode }) {
useEffect(() => {
@@ -13,7 +14,9 @@ export function RootLayoutClient({ children }: { children: ReactNode }) {
return (
- {children}
+
+ {children}
+
)
diff --git a/app/api/_utils/auth.ts b/app/api/_utils/auth.ts
index 59a3a74..c61cd9d 100644
--- a/app/api/_utils/auth.ts
+++ b/app/api/_utils/auth.ts
@@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from 'next/server';
+import { createHash } from 'crypto';
import { DecodedIdToken } from 'firebase-admin/auth';
-import { getServerAuth } from './firebaseAdmin';
+import { getServerAuth, getServerFirestore } from './firebaseAdmin';
+
+export const API_KEY_PREFIX = 'dk_live_';
export class AuthError extends Error {
status = 401;
@@ -41,3 +44,45 @@ export const requireAuth = async (request: NextRequest): Promise
export const isAuthError = (error: unknown): error is AuthError => (
error instanceof AuthError
);
+
+export const hashApiKey = (rawKey: string): string => (
+ createHash('sha256').update(rawKey).digest('hex')
+);
+
+/**
+ * Resolves the caller's uid from either a Firebase ID token (dashboard/web app)
+ * or a long-lived `dk_live_...` API key (external API / MCP server clients).
+ * Firebase ID tokens expire in ~1hr and can't power a persistent MCP config,
+ * hence the separate API key path.
+ */
+export const requireAuthOrApiKey = async (request: NextRequest): Promise<{ uid: string }> => {
+ const token = getBearerToken(request);
+ if (!token) {
+ throw new AuthError();
+ }
+
+ if (token.startsWith(API_KEY_PREFIX)) {
+ const db = getServerFirestore();
+ const hash = hashApiKey(token);
+ const snapshot = await db.collection('api_keys').where('hash', '==', hash).limit(1).get();
+ if (snapshot.empty) {
+ throw new AuthError();
+ }
+
+ const keyDoc = snapshot.docs[0];
+ const uid = keyDoc.data().uid as string | undefined;
+ if (!uid) {
+ throw new AuthError();
+ }
+
+ keyDoc.ref.update({ last_used_at: new Date() }).catch(() => {});
+ return { uid };
+ }
+
+ try {
+ const decoded = await getServerAuth().verifyIdToken(token);
+ return { uid: decoded.uid };
+ } catch {
+ throw new AuthError();
+ }
+};
diff --git a/app/api/ai/ask/route.ts b/app/api/ai/ask/route.ts
index 6a3fef7..4cea31b 100644
--- a/app/api/ai/ask/route.ts
+++ b/app/api/ai/ask/route.ts
@@ -1,13 +1,13 @@
import { NextRequest, NextResponse } from 'next/server';
import { buildAnswerContext, normalizeSearchMode, searchResourceChunks } from '../../_utils/aiSearch';
-import { isAuthError, requireAuth, unauthorizedResponse } from '../../_utils/auth';
+import { isAuthError, requireAuthOrApiKey, unauthorizedResponse } from '../../_utils/auth';
import { generateAnswer } from '../../_utils/gemini';
import { checkAiRateLimit } from '../../_utils/rateLimit';
import { checkAiQueryLimit, incrementAiQueryCount } from '../../_utils/subscription';
export async function POST(request: NextRequest) {
try {
- const authUser = await requireAuth(request);
+ const authUser = await requireAuthOrApiKey(request);
// Rate limit: 10 AI queries per minute per authenticated user
const rateLimitResponse = await checkAiRateLimit(request, authUser.uid);
diff --git a/app/api/ai/search/route.ts b/app/api/ai/search/route.ts
index 52334cc..0620a82 100644
--- a/app/api/ai/search/route.ts
+++ b/app/api/ai/search/route.ts
@@ -1,10 +1,10 @@
import { NextRequest, NextResponse } from 'next/server';
-import { isAuthError, requireAuth, unauthorizedResponse } from '../../_utils/auth';
+import { isAuthError, requireAuthOrApiKey, unauthorizedResponse } from '../../_utils/auth';
import { normalizeSearchMode, searchResourceChunks } from '../../_utils/aiSearch';
export async function POST(request: NextRequest) {
try {
- const authUser = await requireAuth(request);
+ const authUser = await requireAuthOrApiKey(request);
const { query, mode, limit } = await request.json();
if (!query || typeof query !== 'string') {
diff --git a/app/api/extension/capture/route.ts b/app/api/extension/capture/route.ts
new file mode 100644
index 0000000..cba95f7
--- /dev/null
+++ b/app/api/extension/capture/route.ts
@@ -0,0 +1,166 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { isAuthError, requireAuth, unauthorizedResponse } from '../../_utils/auth';
+import { getServerFirestore } from '../../_utils/firebaseAdmin';
+import { getPreviewFromUrl } from '../../_utils/linkPreview';
+import { checkAuthenticatedRateLimit } from '../../_utils/rateLimit';
+import { indexResource } from '../../_utils/resourceIndexer';
+import { checkResourceLimit } from '../../_utils/subscription';
+
+// POST /api/extension/capture - Create a resource from the browser extension,
+// preserving capture-specific metadata (selected text, canonical URL, source type)
+export async function POST(request: NextRequest) {
+ try {
+ const authUser = await requireAuth(request);
+
+ // Rate limit: 60 authenticated requests per minute per user
+ const authRateLimitResponse = await checkAuthenticatedRateLimit(request, authUser.uid);
+ if (authRateLimitResponse) return authRateLimitResponse;
+
+ // Check Free tier resource limit
+ const resourceLimitCheck = await checkResourceLimit(authUser.uid);
+ if (resourceLimitCheck.isLimited) {
+ return NextResponse.json(
+ {
+ error: `Free tier resource limit (${resourceLimitCheck.max}) reached. Upgrade to Pro for unlimited resource saves.`,
+ code: 'UPGRADE_REQUIRED',
+ },
+ { status: 403 }
+ );
+ }
+
+ const body = await request.json();
+ const {
+ title,
+ link,
+ note,
+ tag,
+ is_public,
+ collection_ids,
+ captured_text,
+ selected_text,
+ canonical_url,
+ source_type,
+ } = body;
+
+ // Validate required fields
+ if (!link && !note && !title && !captured_text) {
+ return NextResponse.json(
+ { error: 'Missing content: at least a link, title, note, or text is required.' },
+ { status: 400 }
+ );
+ }
+
+ // Validate link format if link is provided
+ if (link && !link.startsWith('http://') && !link.startsWith('https://')) {
+ return NextResponse.json(
+ { error: 'Link must start with http:// or https://' },
+ { status: 400 }
+ );
+ }
+
+ const db = getServerFirestore();
+
+ // Duplicate resource detection
+ if (link) {
+ const duplicateQuery = await db.collection('resources')
+ .where('user_id', '==', authUser.uid)
+ .where('link', '==', link)
+ .limit(1)
+ .get();
+
+ if (!duplicateQuery.empty) {
+ return NextResponse.json(
+ { error: 'Duplicate resource: this link has already been saved to your vault.' },
+ { status: 409 }
+ );
+ }
+ }
+
+ // If title or tag is missing, attempt to auto-enrich from link
+ let enrichedTitle = title || '';
+ let enrichedTag = tag || '';
+ if (link && (!enrichedTitle || !enrichedTag)) {
+ try {
+ const metadata = await getPreviewFromUrl(link);
+ if (!enrichedTitle && metadata.title) enrichedTitle = metadata.title;
+ if (!enrichedTag && metadata.suggestedTag) enrichedTag = metadata.suggestedTag;
+ } catch (err) {
+ console.warn('Failed to enrich link:', err);
+ }
+ }
+ if (!enrichedTitle) {
+ enrichedTitle = link ? 'Untitled Link' : 'Untitled Note';
+ }
+ if (!enrichedTag) {
+ enrichedTag = link ? 'Article' : 'Note';
+ }
+
+ const normalizedCollectionIds: string[] = Array.isArray(collection_ids)
+ ? collection_ids.filter((id: unknown): id is string => typeof id === 'string' && id.trim().length > 0)
+ : [];
+
+ const resourceRef = db.collection('resources').doc();
+ const now = new Date();
+
+ const resourceData = {
+ user_id: authUser.uid,
+ title: enrichedTitle,
+ link: link || null,
+ note: note || null,
+ tag: enrichedTag || 'Note',
+ is_public: is_public ?? false,
+ collection_ids: normalizedCollectionIds,
+ captured_text: captured_text || null,
+ extension_metadata: {
+ selected_text: selected_text || null,
+ canonical_url: canonical_url || null,
+ source_type: source_type || 'chrome_extension',
+ },
+ index_status: 'pending',
+ index_error: null,
+ created_at: now,
+ updated_at: now,
+ };
+
+ let createdCollectionId: string | null = null;
+ await db.runTransaction(async (transaction) => {
+ normalizedCollectionIds.forEach((collectionId: string) => {
+ const membershipRef = db
+ .collection('users')
+ .doc(authUser.uid)
+ .collection('collections')
+ .doc(collectionId)
+ .collection('resources')
+ .doc(resourceRef.id);
+
+ transaction.set(membershipRef, {
+ resource_id: resourceRef.id,
+ added_at: now,
+ });
+ });
+
+ transaction.set(resourceRef, resourceData);
+ });
+
+ const indexing = await indexResource({ resourceId: resourceRef.id, uid: authUser.uid });
+
+ return NextResponse.json({
+ success: true,
+ message: 'Resource captured successfully',
+ resourceId: resourceRef.id,
+ createdCollectionId,
+ indexing,
+ });
+
+ } catch (error) {
+ if (isAuthError(error)) {
+ return unauthorizedResponse();
+ }
+
+ console.error('Error capturing extension resource:', error);
+ return NextResponse.json(
+ { error: 'Internal server error' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/resources/route.ts b/app/api/resources/route.ts
index bcc448d..b183684 100644
--- a/app/api/resources/route.ts
+++ b/app/api/resources/route.ts
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
-import { isAuthError, requireAuth, unauthorizedResponse } from '../_utils/auth';
+import { isAuthError, requireAuth, requireAuthOrApiKey, unauthorizedResponse } from '../_utils/auth';
import { getServerFirestore } from '../_utils/firebaseAdmin';
import { getPreviewFromUrl } from '../_utils/linkPreview';
import { checkAuthenticatedRateLimit, checkPublicRateLimit } from '../_utils/rateLimit';
@@ -121,7 +121,7 @@ export async function GET(request: NextRequest) {
// POST /api/resources - Create new resource
export async function POST(request: NextRequest) {
try {
- const authUser = await requireAuth(request);
+ const authUser = await requireAuthOrApiKey(request);
// Check Free tier resource limit
const resourceLimitCheck = await checkResourceLimit(authUser.uid);
diff --git a/app/api/settings/api-keys/route.ts b/app/api/settings/api-keys/route.ts
new file mode 100644
index 0000000..1e88834
--- /dev/null
+++ b/app/api/settings/api-keys/route.ts
@@ -0,0 +1,119 @@
+import { randomBytes } from 'crypto';
+import { NextRequest, NextResponse } from 'next/server';
+import { API_KEY_PREFIX, hashApiKey, isAuthError, requireAuth, unauthorizedResponse } from '../../_utils/auth';
+import { getServerFirestore } from '../../_utils/firebaseAdmin';
+
+// GET /api/settings/api-keys - List the authenticated user's API keys (masked)
+export async function GET(request: NextRequest) {
+ try {
+ const authUser = await requireAuth(request);
+ const db = getServerFirestore();
+
+ const snapshot = await db.collection('api_keys')
+ .where('uid', '==', authUser.uid)
+ .orderBy('created_at', 'desc')
+ .get();
+
+ const keys = snapshot.docs.map((doc) => {
+ const data = doc.data();
+ return {
+ id: doc.id,
+ prefix: data.prefix,
+ label: data.label || null,
+ created_at: data.created_at,
+ last_used_at: data.last_used_at || null,
+ };
+ });
+
+ return NextResponse.json({ success: true, keys });
+ } catch (error) {
+ if (isAuthError(error)) {
+ return unauthorizedResponse();
+ }
+
+ console.error('Error listing API keys:', error);
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
+
+// POST /api/settings/api-keys - Generate a new API key (raw key returned once)
+export async function POST(request: NextRequest) {
+ try {
+ const authUser = await requireAuth(request);
+ const body = await request.json().catch(() => ({}));
+ const label = typeof body.label === 'string' ? body.label.slice(0, 60) : null;
+
+ const db = getServerFirestore();
+
+ // Cap at 5 keys per user to keep this lightweight
+ const existing = await db.collection('api_keys').where('uid', '==', authUser.uid).select().get();
+ if (existing.size >= 5) {
+ return NextResponse.json(
+ { error: 'API key limit (5) reached. Revoke an existing key first.' },
+ { status: 400 }
+ );
+ }
+
+ const rawKey = `${API_KEY_PREFIX}${randomBytes(24).toString('hex')}`;
+ const hash = hashApiKey(rawKey);
+ const prefix = `${rawKey.slice(0, API_KEY_PREFIX.length + 6)}...`;
+ const now = new Date();
+
+ const keyRef = db.collection('api_keys').doc();
+ await keyRef.set({
+ uid: authUser.uid,
+ hash,
+ prefix,
+ label,
+ created_at: now,
+ last_used_at: null,
+ });
+
+ return NextResponse.json({
+ success: true,
+ key: rawKey,
+ id: keyRef.id,
+ prefix,
+ message: 'Save this key now — it will not be shown again.',
+ });
+ } catch (error) {
+ if (isAuthError(error)) {
+ return unauthorizedResponse();
+ }
+
+ console.error('Error creating API key:', error);
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
+
+// DELETE /api/settings/api-keys?id= - Revoke an API key
+export async function DELETE(request: NextRequest) {
+ try {
+ const authUser = await requireAuth(request);
+ const { searchParams } = new URL(request.url);
+ const id = searchParams.get('id');
+
+ if (!id) {
+ return NextResponse.json({ error: 'Missing id parameter' }, { status: 400 });
+ }
+
+ const db = getServerFirestore();
+ const keyRef = db.collection('api_keys').doc(id);
+ const snapshot = await keyRef.get();
+
+ if (!snapshot.exists || snapshot.data()?.uid !== authUser.uid) {
+ return NextResponse.json({ error: 'API key not found' }, { status: 404 });
+ }
+
+ await keyRef.delete();
+
+ return NextResponse.json({ success: true, message: 'API key revoked' });
+ } catch (error) {
+ if (isAuthError(error)) {
+ return unauthorizedResponse();
+ }
+
+ console.error('Error revoking API key:', error);
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
diff --git a/app/components/AddResource.tsx b/app/components/AddResource.tsx
index a59582b..0acf684 100644
--- a/app/components/AddResource.tsx
+++ b/app/components/AddResource.tsx
@@ -6,6 +6,7 @@ import { useAuth } from '../contexts/AuthContext'
import { useCollections } from '../contexts/CollectionsContext'
import { jsonAuthFetch } from '../lib/authFetch'
import { ShareModal } from './ui/ShareModal'
+import { isUpgradeRequiredError, UpgradePrompt } from './ui/UpgradePrompt'
interface AddResourceProps {
onSuccess: () => void
@@ -36,6 +37,7 @@ export function AddResource({ onSuccess }: AddResourceProps) {
const [isPublic, setIsPublic] = useState(false)
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
+ const [upgradeError, setUpgradeError] = useState(false)
const [enriching, setEnriching] = useState(false)
const [selectedCollectionId, setSelectedCollectionId] = useState('none')
const [newCollectionName, setNewCollectionName] = useState('')
@@ -102,6 +104,7 @@ export function AddResource({ onSuccess }: AddResourceProps) {
setLoading(true)
setError('')
+ setUpgradeError(false)
try {
if (resourceType === 'pdf') {
@@ -131,6 +134,12 @@ export function AddResource({ onSuccess }: AddResourceProps) {
if (!response.ok) {
const errorData = await response.json()
+ if (isUpgradeRequiredError(response.status, errorData)) {
+ setError(errorData.error || 'Upgrade required')
+ setUpgradeError(true)
+ setLoading(false)
+ return
+ }
throw new Error(errorData.error || 'Failed to process PDF upload')
}
@@ -167,6 +176,12 @@ export function AddResource({ onSuccess }: AddResourceProps) {
if (!response.ok) {
const errorData = await response.json()
+ if (isUpgradeRequiredError(response.status, errorData)) {
+ setError(errorData.error || 'Upgrade required')
+ setUpgradeError(true)
+ setLoading(false)
+ return
+ }
throw new Error(errorData.error || 'Failed to add resource')
}
@@ -201,32 +216,36 @@ export function AddResource({ onSuccess }: AddResourceProps) {
Capture
-
Save a source for AI search
-
+
Save a source for AI search
+
Add links, notes, or PDFs. DumpIt indexes the content and makes it available to Ask DumpIt according to its visibility.