feat: brainstorm sessions, PDF document Q&A, embedding fixes, and UI improvements
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 7s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 7s
- Add brainstorm feature with collaborative canvas, AI idea generation, live cursors, playback, and export - Add PDF upload/extraction/ingestion pipeline with pgvector document search (RAG) - Add document Q&A overlay with streaming chat and PDF preview - Add note attachments UI with status polling, grid layout, and auto-scroll - Add task extraction AI tool and agent executor improvements - Fix NoteEmbedding missing updatedAt column, re-index 66 notes with 1536-dim embeddings - Fix brainstorm 'Create Note' button: add success toast and redirect to created note - Fix memory echo notification infinite polling - Fix chat route to always include document_search tool - Add brainstorm i18n keys across all 14 locales - Add socket server for real-time brainstorm collaboration - Add hierarchical notebook selector and organize notebook dialog improvements - Add sidebar brainstorm section with session management - Update prisma schema with brainstorm tables, attachments, and document chunks
This commit is contained in:
163
memento-note/scripts/sync_locales_from_en.py
Normal file
163
memento-note/scripts/sync_locales_from_en.py
Normal file
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Synchronise locales/*.json avec locales/en.json (référence des clés).
|
||||
- Garde les chaînes déjà présentes et non vides dans la locale cible.
|
||||
- Traduit les clés manquantes (Google via deep-translator).
|
||||
Usage : depuis memento-note/ avec le venv :
|
||||
.venv-i18n/bin/python scripts/sync_locales_from_en.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from deep_translator import GoogleTranslator
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
LOCALES = ROOT / "locales"
|
||||
|
||||
# Codes Google Translate pour deep-translator (source=en)
|
||||
LANG_TARGETS = {
|
||||
"fr": "fr",
|
||||
"es": "es",
|
||||
"de": "de",
|
||||
"fa": "fa",
|
||||
"it": "it",
|
||||
"pt": "pt",
|
||||
"ru": "ru",
|
||||
"zh": "zh-CN",
|
||||
"ja": "ja",
|
||||
"ko": "ko",
|
||||
"ar": "ar",
|
||||
"hi": "hi",
|
||||
"nl": "nl",
|
||||
"pl": "pl",
|
||||
}
|
||||
|
||||
|
||||
def flatten_leaves(obj: dict, prefix: str = "") -> dict[str, str]:
|
||||
out: dict[str, str] = {}
|
||||
for k, v in obj.items():
|
||||
path = f"{prefix}.{k}" if prefix else k
|
||||
if isinstance(v, dict):
|
||||
out.update(flatten_leaves(v, path))
|
||||
elif isinstance(v, str):
|
||||
out[path] = v
|
||||
else:
|
||||
raise TypeError(f"Valeur non supportée à {path}: {type(v)}")
|
||||
return out
|
||||
|
||||
|
||||
def unflatten_leaves(flat: dict[str, str]) -> dict:
|
||||
root: dict = {}
|
||||
for path, val in flat.items():
|
||||
parts = path.split(".")
|
||||
cur = root
|
||||
for p in parts[:-1]:
|
||||
cur = cur.setdefault(p, {})
|
||||
cur[parts[-1]] = val
|
||||
return root
|
||||
|
||||
|
||||
def translate_unique(texts: list[str], target_code: str, batch_size: int = 35) -> dict[str, str]:
|
||||
translator = GoogleTranslator(source="en", target=target_code)
|
||||
mapping: dict[str, str] = {}
|
||||
for i in range(0, len(texts), batch_size):
|
||||
batch = texts[i : i + batch_size]
|
||||
try:
|
||||
outs = translator.translate_batch(batch)
|
||||
except Exception as e:
|
||||
print(f" batch erreur ({e}), traduction unitaire…", flush=True)
|
||||
outs = []
|
||||
for t in batch:
|
||||
try:
|
||||
outs.append(translator.translate(t))
|
||||
except Exception:
|
||||
outs.append(t)
|
||||
time.sleep(0.15)
|
||||
if len(outs) != len(batch):
|
||||
outs = batch # fallback
|
||||
for src, dst in zip(batch, outs):
|
||||
mapping[src] = dst if isinstance(dst, str) else src
|
||||
time.sleep(0.6)
|
||||
print(f" … {min(i + batch_size, len(texts))}/{len(texts)}", flush=True)
|
||||
return mapping
|
||||
|
||||
|
||||
def merge_locale(en_flat: dict[str, str], loc_flat: dict[str, str], target_code: str) -> dict[str, str]:
|
||||
text_to_keys: dict[str, list[str]] = {}
|
||||
result: dict[str, str] = {}
|
||||
|
||||
for key, en_val in en_flat.items():
|
||||
loc_val = loc_flat.get(key)
|
||||
if isinstance(loc_val, str) and loc_val.strip():
|
||||
result[key] = loc_val
|
||||
else:
|
||||
text_to_keys.setdefault(en_val, []).append(key)
|
||||
|
||||
if not text_to_keys:
|
||||
return result
|
||||
|
||||
unique = list(text_to_keys.keys())
|
||||
print(f" {len(unique)} textes uniques à traduire ({sum(len(v) for v in text_to_keys.values())} clés)", flush=True)
|
||||
trans_map = translate_unique(unique, target_code)
|
||||
|
||||
for src, keys in text_to_keys.items():
|
||||
tr = trans_map.get(src, src)
|
||||
for k in keys:
|
||||
result[k] = tr
|
||||
return result
|
||||
|
||||
|
||||
def main() -> int:
|
||||
en_path = LOCALES / "en.json"
|
||||
if not en_path.exists():
|
||||
print("locales/en.json introuvable", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
en_obj = json.loads(en_path.read_text(encoding="utf-8"))
|
||||
en_flat = flatten_leaves(en_obj)
|
||||
print(f"Référence en.json : {len(en_flat)} clés feuilles", flush=True)
|
||||
|
||||
skip = {"en"}
|
||||
for code, google_target in LANG_TARGETS.items():
|
||||
if code in skip:
|
||||
continue
|
||||
path = LOCALES / f"{code}.json"
|
||||
if not path.exists():
|
||||
print(f"Absence de {path.name}, ignoré", flush=True)
|
||||
continue
|
||||
|
||||
loc_obj = json.loads(path.read_text(encoding="utf-8"))
|
||||
loc_flat = flatten_leaves(loc_obj)
|
||||
|
||||
before_missing = sum(1 for k in en_flat if k not in loc_flat or not str(loc_flat.get(k, "")).strip())
|
||||
if before_missing == 0:
|
||||
print(f"\n=== {code}.json — déjà complet ({len(en_flat)} clés), rien à faire", flush=True)
|
||||
continue
|
||||
|
||||
print(f"\n=== {code}.json ({google_target}) — manquantes avant: {before_missing}", flush=True)
|
||||
|
||||
merged_flat = merge_locale(en_flat, loc_flat, google_target)
|
||||
|
||||
# Couverture complète des clés en
|
||||
out_flat = dict(en_flat)
|
||||
out_flat.update(merged_flat)
|
||||
|
||||
missing_after = sum(1 for k in en_flat if k not in out_flat or not str(out_flat[k]).strip())
|
||||
if missing_after:
|
||||
print(f"ERREUR: encore {missing_after} clés vides pour {code}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
tree = unflatten_leaves(out_flat)
|
||||
path.write_text(json.dumps(tree, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
print(f" Écrit {path.name}", flush=True)
|
||||
|
||||
print("\nTerminé.", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
41
memento-note/scripts/update-colors.ts
Normal file
41
memento-note/scripts/update-colors.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
async function main() {
|
||||
const OLD_COLOR = '#75B2D6'
|
||||
const NEW_COLOR = '#A47148'
|
||||
|
||||
console.log(`Updating colors from ${OLD_COLOR} to ${NEW_COLOR}...`)
|
||||
|
||||
// Update Notebooks
|
||||
const updatedNotebooks = await prisma.notebook.updateMany({
|
||||
where: { color: OLD_COLOR },
|
||||
data: { color: NEW_COLOR }
|
||||
})
|
||||
console.log(`Updated ${updatedNotebooks.count} notebooks.`)
|
||||
|
||||
// Update Labels (if any use this color)
|
||||
const updatedLabels = await prisma.label.updateMany({
|
||||
where: { color: OLD_COLOR },
|
||||
data: { color: NEW_COLOR }
|
||||
})
|
||||
console.log(`Updated ${updatedLabels.count} labels.`)
|
||||
|
||||
// Update Notes (some notes might have this as a string color in metadata or field)
|
||||
// Note.color is usually "default", but let's check
|
||||
const updatedNotes = await prisma.note.updateMany({
|
||||
where: { color: OLD_COLOR },
|
||||
data: { color: NEW_COLOR }
|
||||
})
|
||||
console.log(`Updated ${updatedNotes.count} notes.`)
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(e => {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect()
|
||||
})
|
||||
Reference in New Issue
Block a user