Skip to content

Commit 3fdda27

Browse files
committed
feat: trash system, Sonner notifications + remove embedding pipeline
trash: - soft delete for notes and folders (deletedAt field) - TrashPanel, TrashItem, TrashItemSkeleton, TrashEmptyState components - useTrash hook with infinite pagination - restore by type (note/folder), permanent delete, empty trash - auto-purge after 30 days via /api/cron/purge-trash - storage files cleaned on permanent delete/purge - workspace tree and queries now filter deletedAt records notifications: - replaced old notification system with Sonner across the entire app - toasts for: delete, rename, restore, save errors, version restore, upload, trash actions search: - removed semantic search and embedding pipeline (Voyage AI / Nomic) - deleted: embed-note.ts, embeddings.ts, semantic-search.ts, note-embedding-job.ts - removed /api/notes/[id]/embed route and embedding queue - search modal rebuilt: uses `query` param, removed SearchMode/SearchMatchType types - simplified UI: no recent mode, no match badges, cleaner empty states - autosave no longer marks embeddings stale planner: - new study session flow with FSRS interval calculation - PlannerStudySession.tsx, planner-study-server.ts, planner-study-session.ts - GET /api/planner/session/next with card prefetch and session state - rate limiting and model fallback for flashcard generation settings: - replaced native selects with custom dropdown for model selection - richer usage stats skeleton, improved tooltips and accessibility infra: - Prisma migrations updated for soft delete and embedding queue cleanup - updated .env.example, vercel.json, package.json
1 parent 3343f2b commit 3fdda27

104 files changed

Lines changed: 6275 additions & 2187 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,11 @@ SUPABASE_SERVICE_ROLE_KEY=""
3838
# Used server-side to search note cover images.
3939
UNSPLASH_ACCESS_KEY=""
4040

41-
# ─── Embeddings (search + graph) ─────────────────────────────
42-
# Preferred: OpenAI-compatible embeddings endpoint that serves Nomic `nomic-embed-text-v1.5`
43-
# Example base URL: https://your-provider.example/v1/
44-
EMBEDDINGS_BASE_URL=""
45-
EMBEDDINGS_API_KEY=""
46-
EMBEDDINGS_MODEL="nomic-embed-text-v1.5"
47-
48-
# Backwards-compatible direct Nomic API fallback
49-
NOMIC_API_KEY=""
41+
# ─── GROQ API Key (for search) ───────────────────────────────
42+
# Get your API key at https://groq.com — free tier is enough for development
43+
GROQ_API_KEY=""
44+
45+
# ─── Internal cron secret ───────────────────────────────────────────────
46+
# Must match the Bearer token used by the internal queue processor.
47+
# Generate a strong random secret: run `node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"` in your terminal
48+
INTERNAL_CRON_SECRET=""

eslint.config.mjs

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,22 @@ import nextVitals from "eslint-config-next/core-web-vitals";
33
import nextTs from "eslint-config-next/typescript";
44

55
const eslintConfig = defineConfig([
6-
...nextVitals,
7-
...nextTs,
8-
// Override default ignores of eslint-config-next.
9-
globalIgnores([
10-
// Default ignores of eslint-config-next:
11-
".next/**",
12-
"out/**",
13-
"build/**",
14-
"next-env.d.ts",
15-
]),
6+
...nextVitals,
7+
...nextTs,
8+
{
9+
rules: {
10+
"react-hooks/set-state-in-effect": "off",
11+
},
12+
},
13+
// Override default ignores of eslint-config-next.
14+
globalIgnores([
15+
// Default ignores of eslint-config-next:
16+
".next/**",
17+
"out/**",
18+
"build/**",
19+
".agents/**",
20+
"next-env.d.ts",
21+
]),
1622
]);
1723

1824
export default eslintConfig;

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"resend": "^6.9.3",
5252
"shadcn": "^4.0.7",
5353
"shiki": "^4.0.2",
54+
"sonner": "^2.0.7",
5455
"tailwind-merge": "^3.5.0",
5556
"tw-animate-css": "^1.4.0",
5657
"use-debounce": "^10.1.0"

prisma.config.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,21 @@
33
import "dotenv/config";
44
import { defineConfig } from "prisma/config";
55

6+
function getEnvValue(value: string | undefined): string | undefined {
7+
if (!value) {
8+
return undefined;
9+
}
10+
11+
const trimmed = value.trim();
12+
return trimmed.length > 0 ? trimmed : undefined;
13+
}
14+
615
export default defineConfig({
7-
schema: "prisma/schema.prisma",
8-
migrations: {
9-
path: "prisma/migrations",
10-
},
11-
datasource: {
12-
url: process.env["DATABASE_URL"],
13-
},
16+
schema: "prisma/schema.prisma",
17+
migrations: {
18+
path: "prisma/migrations",
19+
},
20+
datasource: {
21+
url: getEnvValue(process.env["DIRECT_URL"]) ?? getEnvValue(process.env["DATABASE_URL"]),
22+
},
1423
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
ALTER TABLE "folders"
2+
ADD COLUMN "deletedAt" TIMESTAMP(3),
3+
ADD COLUMN "originalParentId" TEXT;
4+
5+
ALTER TABLE "notes"
6+
ADD COLUMN "deletedAt" TIMESTAMP(3),
7+
ADD COLUMN "originalParentId" TEXT;
8+
9+
CREATE INDEX "folders_workspaceId_deletedAt_idx" ON "folders"("workspaceId", "deletedAt");
10+
CREATE INDEX "folders_parentId_deletedAt_idx" ON "folders"("parentId", "deletedAt");
11+
CREATE INDEX "notes_workspaceId_deletedAt_idx" ON "notes"("workspaceId", "deletedAt");
12+
CREATE INDEX "notes_folderId_deletedAt_idx" ON "notes"("folderId", "deletedAt");
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
CREATE EXTENSION IF NOT EXISTS vector;
2+
3+
DO $$
4+
BEGIN
5+
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'EmbeddingStatus') THEN
6+
CREATE TYPE "EmbeddingStatus" AS ENUM ('NONE', 'QUEUED', 'PROCESSING', 'DONE', 'STALE');
7+
END IF;
8+
END $$;
9+
10+
DO $$
11+
BEGIN
12+
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'QueueItemStatus') THEN
13+
CREATE TYPE "QueueItemStatus" AS ENUM ('PENDING', 'PROCESSING', 'DONE', 'FAILED');
14+
END IF;
15+
END $$;
16+
17+
DO $$
18+
DECLARE
19+
embedding_type text;
20+
BEGIN
21+
SELECT format_type(a.atttypid, a.atttypmod)
22+
INTO embedding_type
23+
FROM pg_attribute a
24+
JOIN pg_class c ON c.oid = a.attrelid
25+
JOIN pg_namespace n ON n.oid = c.relnamespace
26+
WHERE n.nspname = 'public'
27+
AND c.relname = 'notes'
28+
AND a.attname = 'embedding'
29+
AND a.attnum > 0
30+
AND NOT a.attisdropped;
31+
32+
IF embedding_type IS NULL THEN
33+
ALTER TABLE "notes" ADD COLUMN "embedding" vector(1024);
34+
ELSIF embedding_type <> 'vector(1024)' THEN
35+
ALTER TABLE "notes" DROP COLUMN "embedding";
36+
ALTER TABLE "notes" ADD COLUMN "embedding" vector(1024);
37+
END IF;
38+
END $$;
39+
40+
ALTER TABLE "notes"
41+
ADD COLUMN IF NOT EXISTS "embeddingModel" TEXT,
42+
ADD COLUMN IF NOT EXISTS "embeddingStatus" "EmbeddingStatus" NOT NULL DEFAULT 'NONE',
43+
ADD COLUMN IF NOT EXISTS "embeddingQueuedAt" TIMESTAMP(3),
44+
ADD COLUMN IF NOT EXISTS "embeddingUpdatedAt" TIMESTAMP(3),
45+
ADD COLUMN IF NOT EXISTS "lastEditedAt" TIMESTAMP(3);
46+
47+
ALTER TABLE "users"
48+
ADD COLUMN IF NOT EXISTS "lastEmbeddingRequestAt" TIMESTAMP(3),
49+
ADD COLUMN IF NOT EXISTS "embeddingRequestsToday" INTEGER NOT NULL DEFAULT 0;
50+
51+
ALTER TABLE "notes" DROP COLUMN IF EXISTS "embeddedAt";
52+
ALTER TABLE "notes" DROP COLUMN IF EXISTS "vectorUpdatedAt";
53+
54+
CREATE TABLE IF NOT EXISTS "embedding_queue_items" (
55+
"id" TEXT NOT NULL,
56+
"noteId" TEXT NOT NULL,
57+
"userId" TEXT NOT NULL,
58+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
59+
"status" "QueueItemStatus" NOT NULL DEFAULT 'PENDING',
60+
"attempts" INTEGER NOT NULL DEFAULT 0,
61+
"tokensUsed" INTEGER,
62+
"lastError" TEXT,
63+
CONSTRAINT "embedding_queue_items_pkey" PRIMARY KEY ("id")
64+
);
65+
66+
DO $$
67+
BEGIN
68+
IF NOT EXISTS (
69+
SELECT 1
70+
FROM pg_constraint
71+
WHERE conname = 'embedding_queue_items_noteId_fkey'
72+
) THEN
73+
ALTER TABLE "embedding_queue_items"
74+
ADD CONSTRAINT "embedding_queue_items_noteId_fkey"
75+
FOREIGN KEY ("noteId") REFERENCES "notes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
76+
END IF;
77+
END $$;
78+
79+
DO $$
80+
BEGIN
81+
IF NOT EXISTS (
82+
SELECT 1
83+
FROM pg_constraint
84+
WHERE conname = 'embedding_queue_items_userId_fkey'
85+
) THEN
86+
ALTER TABLE "embedding_queue_items"
87+
ADD CONSTRAINT "embedding_queue_items_userId_fkey"
88+
FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
89+
END IF;
90+
END $$;
91+
92+
CREATE INDEX IF NOT EXISTS "embedding_queue_items_status_createdAt_idx"
93+
ON "embedding_queue_items" ("status", "createdAt");
94+
95+
CREATE INDEX IF NOT EXISTS "embedding_queue_items_noteId_status_createdAt_idx"
96+
ON "embedding_queue_items" ("noteId", "status", "createdAt");
97+
98+
CREATE INDEX IF NOT EXISTS "embedding_queue_items_userId_createdAt_idx"
99+
ON "embedding_queue_items" ("userId", "createdAt");
100+
101+
CREATE INDEX IF NOT EXISTS "notes_embedding_hnsw"
102+
ON "notes" USING hnsw ("embedding" vector_cosine_ops);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-- Remove Voyage embeddings and queue artifacts
2+
DROP INDEX IF EXISTS "notes_embedding_hnsw";
3+
DROP INDEX IF EXISTS "notes_embedding_idx";
4+
5+
DROP TABLE IF EXISTS "embedding_queue_items";
6+
7+
ALTER TABLE "users"
8+
DROP COLUMN IF EXISTS "lastEmbeddingRequestAt",
9+
DROP COLUMN IF EXISTS "embeddingRequestsToday";
10+
11+
ALTER TABLE "notes"
12+
DROP COLUMN IF EXISTS "embedding",
13+
DROP COLUMN IF EXISTS "embeddingModel",
14+
DROP COLUMN IF EXISTS "embeddingStatus",
15+
DROP COLUMN IF EXISTS "embeddingQueuedAt",
16+
DROP COLUMN IF EXISTS "embeddingUpdatedAt",
17+
DROP COLUMN IF EXISTS "vectorUpdatedAt";
18+
19+
DO $$
20+
BEGIN
21+
IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'EmbeddingStatus') THEN
22+
DROP TYPE "EmbeddingStatus";
23+
END IF;
24+
25+
IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'QueueItemStatus') THEN
26+
DROP TYPE "QueueItemStatus";
27+
END IF;
28+
END $$;

prisma/schema.prisma

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ model Folder {
9292
name String
9393
workspaceId String
9494
parentId String?
95+
deletedAt DateTime?
96+
originalParentId String?
9597
order Int @default(0)
9698
createdAt DateTime @default(now())
9799
updatedAt DateTime @updatedAt
@@ -101,6 +103,8 @@ model Folder {
101103
notes Note[]
102104
103105
@@index([workspaceId])
106+
@@index([workspaceId, deletedAt])
107+
@@index([parentId, deletedAt])
104108
@@map("folders")
105109
}
106110

@@ -115,6 +119,8 @@ model Note {
115119
coverImage String?
116120
coverImageMeta Json?
117121
isArchived Boolean @default(false)
122+
deletedAt DateTime?
123+
originalParentId String?
118124
order Int @default(0)
119125
createdAt DateTime @default(now())
120126
updatedAt DateTime @updatedAt
@@ -124,13 +130,14 @@ model Note {
124130
aiChatSessions AIChatSession[]
125131
flashcardDecks FlashcardDeck[]
126132
searchVector Unsupported("tsvector")?
127-
embedding Unsupported("vector(768)")?
128-
vectorUpdatedAt DateTime?
133+
lastEditedAt DateTime?
129134
folder Folder? @relation(fields: [folderId], references: [id])
130135
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
131136
132137
@@index([searchVector], type: Gin)
133138
@@index([workspaceId, updatedAt(sort: Desc)])
139+
@@index([workspaceId, deletedAt])
140+
@@index([folderId, deletedAt])
134141
@@map("notes")
135142
}
136143

scripts/backfill-embeddings.ts

Lines changed: 0 additions & 53 deletions
This file was deleted.

0 commit comments

Comments
 (0)