diff --git a/keep-notes/components/note-input.tsx b/keep-notes/components/note-input.tsx
index 792a26a..a217843 100644
--- a/keep-notes/components/note-input.tsx
+++ b/keep-notes/components/note-input.tsx
@@ -494,7 +494,6 @@ export function NoteInput({
reminder: currentReminder,
isMarkdown,
labels: selectedLabels.length > 0 ? selectedLabels : undefined,
- sharedWith: collaborators.length > 0 ? collaborators : undefined,
notebookId: currentNotebookId, // Assign note to current notebook if in one
})
diff --git a/keep-notes/components/notes-tabs-view.tsx b/keep-notes/components/notes-tabs-view.tsx
index 50bbbfc..1e3801b 100644
--- a/keep-notes/components/notes-tabs-view.tsx
+++ b/keep-notes/components/notes-tabs-view.tsx
@@ -217,7 +217,7 @@ function SortableNoteListItem({
{timeAgo}
- {note.labels && note.labels.length > 0 && (
+ {Array.isArray(note.labels) && note.labels.length > 0 && (
<>
·
@@ -307,8 +307,8 @@ export function NotesTabsView({ notes, onEdit, currentNotebookId }: NotesTabsVie
try {
const newNote = await createNote({
content: '',
- title: null,
- notebookId: currentNotebookId || null,
+ title: undefined,
+ notebookId: currentNotebookId || undefined,
skipRevalidation: true
})
if (!newNote) return
diff --git a/keep-notes/components/ui/alert-dialog.tsx b/keep-notes/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..dad3273
--- /dev/null
+++ b/keep-notes/components/ui/alert-dialog.tsx
@@ -0,0 +1,196 @@
+"use client"
+
+import * as React from "react"
+import { AlertDialog as AlertDialogPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+
+function AlertDialog({
+ ...props
+}: React.ComponentProps
) {
+ return
+}
+
+function AlertDialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogContent({
+ className,
+ size = "default",
+ ...props
+}: React.ComponentProps & {
+ size?: "default" | "sm"
+}) {
+ return (
+
+
+
+
+ )
+}
+
+function AlertDialogHeader({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AlertDialogFooter({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AlertDialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogMedia({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AlertDialogAction({
+ className,
+ variant = "default",
+ size = "default",
+ ...props
+}: React.ComponentProps &
+ Pick, "variant" | "size">) {
+ return (
+
+ )
+}
+
+function AlertDialogCancel({
+ className,
+ variant = "outline",
+ size = "default",
+ ...props
+}: React.ComponentProps &
+ Pick, "variant" | "size">) {
+ return (
+
+ )
+}
+
+export {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogMedia,
+ AlertDialogOverlay,
+ AlertDialogPortal,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+}
diff --git a/keep-notes/components/ui/button.tsx b/keep-notes/components/ui/button.tsx
index 37a7d4b..4d38506 100644
--- a/keep-notes/components/ui/button.tsx
+++ b/keep-notes/components/ui/button.tsx
@@ -1,19 +1,19 @@
import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
+import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
- "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
outline:
- "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
@@ -22,9 +22,11 @@ const buttonVariants = cva(
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
- sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+ xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
+ sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
+ "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
@@ -46,7 +48,7 @@ function Button({
VariantProps & {
asChild?: boolean
}) {
- const Comp = asChild ? Slot : "button"
+ const Comp = asChild ? Slot.Root : "button"
return (
) {
+ return
+}
+
+function CollapsibleTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CollapsibleContent({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent }
diff --git a/keep-notes/docker-compose.yml b/keep-notes/docker-compose.yml
index f7f54b1..afdfe97 100644
--- a/keep-notes/docker-compose.yml
+++ b/keep-notes/docker-compose.yml
@@ -1,6 +1,26 @@
version: '3.8'
services:
+ postgres:
+ image: postgres:16-alpine
+ container_name: keep-postgres
+ restart: unless-stopped
+ environment:
+ POSTGRES_USER: keepnotes
+ POSTGRES_PASSWORD: keepnotes
+ POSTGRES_DB: keepnotes
+ volumes:
+ - postgres-data:/var/lib/postgresql/data
+ ports:
+ - "5432:5432"
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U keepnotes"]
+ interval: 5s
+ timeout: 5s
+ retries: 5
+ networks:
+ - keep-network
+
keep-notes:
build:
context: .
@@ -12,7 +32,7 @@ services:
- "3000:3000"
environment:
# Database
- - DATABASE_URL=file:/app/prisma/dev.db
+ - DATABASE_URL=postgresql://keepnotes:keepnotes@postgres:5432/keepnotes
- NODE_ENV=production
# Application (IMPORTANT: Change these!)
@@ -29,14 +49,14 @@ services:
# - OLLAMA_BASE_URL=http://ollama:11434
# - OLLAMA_MODEL=granite4:latest
volumes:
- # Persist SQLite database
- - keep-db:/app/prisma
-
# Persist uploaded images and files
- keep-uploads:/app/public/uploads
# Optional: Mount custom SSL certificates
# - ./certs:/app/certs:ro
+ depends_on:
+ postgres:
+ condition: service_healthy
networks:
- keep-network
# Optional: Resource limits for Proxmox VM
@@ -82,7 +102,7 @@ networks:
driver: bridge
volumes:
- keep-db:
+ postgres-data:
driver: local
keep-uploads:
driver: local
diff --git a/keep-notes/fix-locales.js b/keep-notes/fix-locales.js
new file mode 100644
index 0000000..b84510e
--- /dev/null
+++ b/keep-notes/fix-locales.js
@@ -0,0 +1,27 @@
+const fs = require('fs');
+
+function updateLocale(file, lang) {
+ const content = fs.readFileSync(file, 'utf8');
+ const data = JSON.parse(content);
+
+ if (lang === 'fr') {
+ data.ai.clarifyDesc = "Rendre le propos plus clair et compréhensible";
+ data.ai.shortenDesc = "Résumer le texte et aller à l'essentiel";
+ data.ai.improve = "Améliorer la rédaction";
+ data.ai.improveDesc = "Corriger les fautes et le style";
+ data.ai.toMarkdown = "Formater en Markdown";
+ data.ai.toMarkdownDesc = "Ajouter des titres, des puces et structurer le texte";
+ } else if (lang === 'en') {
+ data.ai.clarifyDesc = "Make the text clearer and easier to understand";
+ data.ai.shortenDesc = "Summarize the text and get to the point";
+ data.ai.improve = "Improve writing";
+ data.ai.improveDesc = "Fix grammar and enhance style";
+ data.ai.toMarkdown = "Format as Markdown";
+ data.ai.toMarkdownDesc = "Add headings, bullet points and structure the text";
+ }
+
+ fs.writeFileSync(file, JSON.stringify(data, null, 2));
+}
+
+updateLocale('./locales/fr.json', 'fr');
+updateLocale('./locales/en.json', 'en');
diff --git a/keep-notes/fix_ai_lang.py b/keep-notes/fix_ai_lang.py
new file mode 100644
index 0000000..718a142
--- /dev/null
+++ b/keep-notes/fix_ai_lang.py
@@ -0,0 +1,70 @@
+import re
+
+# 1. Update types.ts
+with open('lib/ai/types.ts', 'r') as f:
+ types_content = f.read()
+
+types_content = types_content.replace(
+ 'generateTags(content: string): Promise',
+ 'generateTags(content: string, language?: string): Promise'
+)
+with open('lib/ai/types.ts', 'w') as f:
+ f.write(types_content)
+
+# 2. Update OllamaProvider
+with open('lib/ai/providers/ollama.ts', 'r') as f:
+ ollama_content = f.read()
+
+ollama_content = ollama_content.replace(
+ 'async generateTags(content: string): Promise',
+ 'async generateTags(content: string, language: string = "en"): Promise'
+)
+
+# Replace the hardcoded prompt build logic
+prompt_logic = """
+ const promptText = language === 'fa'
+ ? `متن زیر را تحلیل کن و مفاهیم کلیدی را به عنوان برچسب استخراج کن (حداکثر ۱-۳ کلمه).\nقوانین:\n- کلمات ربط را حذف کن.\n- عبارات ترکیبی را حفظ کن.\n- حداکثر ۵ برچسب.\nپاسخ فقط به صورت لیست JSON با فرمت [{"tag": "string", "confidence": number}]\nمتن: "${content}"`
+ : language === 'fr'
+ ? `Analyse la note suivante et extrais les concepts clés sous forme de tags courts (1-3 mots max).\nRègles:\n- Pas de mots de liaison.\n- Garde les expressions composées ensemble.\n- Normalise en minuscules sauf noms propres.\n- Maximum 5 tags.\nRéponds UNIQUEMENT sous forme de liste JSON d'objets : [{"tag": "string", "confidence": number}].\nContenu de la note: "${content}"`
+ : `Analyze the following note and extract key concepts as short tags (1-3 words max).\nRules:\n- No stop words.\n- Keep compound expressions together.\n- Lowercase unless proper noun.\n- Max 5 tags.\nRespond ONLY as a JSON list of objects: [{"tag": "string", "confidence": number}].\nNote content: "${content}"`;
+
+ const response = await fetch(`${this.baseUrl}/generate`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ model: this.modelName,
+ prompt: promptText,
+ stream: false,
+ }),
+ });
+"""
+
+# The original has:
+# const response = await fetch(`${this.baseUrl}/generate`, {
+# method: 'POST',
+# headers: { 'Content-Type': 'application/json' },
+# body: JSON.stringify({
+# model: this.modelName,
+# prompt: `Analyse la note suivante...
+
+ollama_content = re.sub(
+ r'const response = await fetch\(`\$\{this\.baseUrl\}/generate`.*?\}\),\n\s*\}\);',
+ prompt_logic.strip(),
+ ollama_content,
+ flags=re.DOTALL
+)
+
+with open('lib/ai/providers/ollama.ts', 'w') as f:
+ f.write(ollama_content)
+
+# 3. Update route.ts
+with open('app/api/ai/tags/route.ts', 'r') as f:
+ route_content = f.read()
+
+route_content = route_content.replace(
+ 'const tags = await provider.generateTags(content);',
+ 'const tags = await provider.generateTags(content, language);'
+)
+with open('app/api/ai/tags/route.ts', 'w') as f:
+ f.write(route_content)
+
diff --git a/keep-notes/fix_api_labels.py b/keep-notes/fix_api_labels.py
new file mode 100644
index 0000000..ccc8bd0
--- /dev/null
+++ b/keep-notes/fix_api_labels.py
@@ -0,0 +1,25 @@
+with open('app/api/labels/[id]/route.ts', 'r') as f:
+ content = f.read()
+
+# Fix targetUserId logic
+content = content.replace(
+ 'if (name && name.trim() !== currentLabel.name && currentLabel.userId) {',
+ 'const targetUserIdPut = currentLabel.userId || currentLabel.notebook?.userId || session.user.id;\n if (name && name.trim() !== currentLabel.name && targetUserIdPut) {'
+)
+content = content.replace(
+ 'userId: currentLabel.userId,',
+ 'userId: targetUserIdPut,'
+)
+
+content = content.replace(
+ 'if (label.userId) {',
+ 'const targetUserIdDel = label.userId || label.notebook?.userId || session.user.id;\n if (targetUserIdDel) {'
+)
+content = content.replace(
+ 'userId: label.userId,',
+ 'userId: targetUserIdDel,'
+)
+
+with open('app/api/labels/[id]/route.ts', 'w') as f:
+ f.write(content)
+
diff --git a/keep-notes/fix_auto_tag_hook.py b/keep-notes/fix_auto_tag_hook.py
new file mode 100644
index 0000000..ccd87fa
--- /dev/null
+++ b/keep-notes/fix_auto_tag_hook.py
@@ -0,0 +1,18 @@
+with open('hooks/use-auto-tagging.ts', 'r') as f:
+ content = f.read()
+
+if 'useLanguage' not in content:
+ content = "import { useLanguage } from '@/lib/i18n'\n" + content
+
+content = content.replace(
+ 'export function useAutoTagging(notebookId?: string | null) {',
+ 'export function useAutoTagging(notebookId?: string | null) {\n const { language } = useLanguage();'
+)
+
+content = content.replace(
+ "language: document.documentElement.lang || 'en',",
+ "language: language || document.documentElement.lang || 'en',"
+)
+
+with open('hooks/use-auto-tagging.ts', 'w') as f:
+ f.write(content)
diff --git a/keep-notes/fix_date_locale.py b/keep-notes/fix_date_locale.py
new file mode 100644
index 0000000..475f2f7
--- /dev/null
+++ b/keep-notes/fix_date_locale.py
@@ -0,0 +1,41 @@
+import re
+
+files_to_fix = [
+ 'components/note-inline-editor.tsx',
+ 'components/notes-tabs-view.tsx',
+ 'components/note-card.tsx'
+]
+
+replacement_func = """import { faIR } from 'date-fns/locale'
+
+function getDateLocale(language: string) {
+ if (language === 'fr') return fr
+ if (language === 'fa') return faIR
+ return enUS
+}"""
+
+for file in files_to_fix:
+ with open(file, 'r') as f:
+ content = f.read()
+
+ # 1. Replace the getDateLocale function
+ content = re.sub(
+ r'function getDateLocale\(language: string\) \{\s*if \(language === \'fr\'\) return fr\s*return enUS\s*\}',
+ "function getDateLocale(language: string) {\n if (language === 'fr') return fr;\n if (language === 'fa') return require('date-fns/locale').faIR;\n return enUS;\n}",
+ content
+ )
+
+ # Also fix translations for "Modifiée" and "Créée" in inline editor (they are currently hardcoded)
+ if 'note-inline-editor.tsx' in file:
+ content = content.replace(
+ "Modifiée {formatDistanceToNow(new Date(note.updatedAt), { addSuffix: true, locale: dateLocale })}",
+ "{t('notes.modified') || 'Modifiée'} {formatDistanceToNow(new Date(note.updatedAt), { addSuffix: true, locale: dateLocale })}"
+ )
+ content = content.replace(
+ "Créée {formatDistanceToNow(new Date(note.createdAt), { addSuffix: true, locale: dateLocale })}",
+ "{t('notes.created') || 'Créée'} {formatDistanceToNow(new Date(note.createdAt), { addSuffix: true, locale: dateLocale })}"
+ )
+
+ with open(file, 'w') as f:
+ f.write(content)
+
diff --git a/keep-notes/fix_dialog.py b/keep-notes/fix_dialog.py
new file mode 100644
index 0000000..c531b8f
--- /dev/null
+++ b/keep-notes/fix_dialog.py
@@ -0,0 +1,102 @@
+import re
+
+with open('components/label-management-dialog.tsx', 'r') as f:
+ content = f.read()
+
+# Add useNoteRefresh import
+if 'useNoteRefresh' not in content:
+ content = content.replace("import { useLanguage } from '@/lib/i18n'", "import { useLanguage } from '@/lib/i18n'\nimport { useNoteRefresh } from '@/context/NoteRefreshContext'")
+
+# Add useNoteRefresh to component
+content = content.replace("const { t } = useLanguage()", "const { t } = useLanguage()\n const { triggerRefresh } = useNoteRefresh()\n const [confirmDeleteId, setConfirmDeleteId] = useState(null)")
+
+# Modify handleDeleteLabel
+old_delete = """ const handleDeleteLabel = async (id: string) => {
+ if (confirm(t('labels.confirmDelete'))) {
+ try {
+ await deleteLabel(id)
+ } catch (error) {
+ console.error('Failed to delete label:', error)
+ }
+ }
+ }"""
+new_delete = """ const handleDeleteLabel = async (id: string) => {
+ try {
+ await deleteLabel(id)
+ triggerRefresh()
+ setConfirmDeleteId(null)
+ } catch (error) {
+ console.error('Failed to delete label:', error)
+ }
+ }"""
+content = content.replace(old_delete, new_delete)
+
+# Also adding triggerRefresh() on addLabel and updateLabel:
+content = content.replace(
+ "await addLabel(trimmed, 'gray')",
+ "await addLabel(trimmed, 'gray')\n triggerRefresh()"
+)
+content = content.replace(
+ "await updateLabel(id, { color })",
+ "await updateLabel(id, { color })\n triggerRefresh()"
+)
+
+# Inline confirm UI: Change the Trash2 button area
+old_div = """
+
+
+
"""
+
+new_div = """ {confirmDeleteId === label.id ? (
+
+ {t('labels.confirmDeleteShort') || 'Confirmer ?'}
+
+
+
+ ) : (
+
+
+
+
+ )}"""
+
+content = content.replace(old_div, new_div)
+
+with open('components/label-management-dialog.tsx', 'w') as f:
+ f.write(content)
diff --git a/keep-notes/fix_dialog_dir.py b/keep-notes/fix_dialog_dir.py
new file mode 100644
index 0000000..e6c51a3
--- /dev/null
+++ b/keep-notes/fix_dialog_dir.py
@@ -0,0 +1,22 @@
+with open('components/label-management-dialog.tsx', 'r') as f:
+ content = f.read()
+
+# Add language to useLanguage()
+content = content.replace(
+ 'const { t } = useLanguage()',
+ 'const { t, language } = useLanguage()'
+)
+
+# Add dir to Dialog
+content = content.replace(
+ '