feat: 8 AI providers, rich text editor, agent notifications, UI contrast & font settings
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m25s

- Add DeepSeek, OpenRouter, Mistral, Z.AI, LM Studio as AI providers
  with editable model names via Combobox in admin settings
- Fix OpenRouter broken by normalizeProvider bug in config.ts
- Convert agent-created notes from Markdown to HTML (TipTap rich text)
- Add Notification model + in-app notifications for agent results
- Agent notification click opens the created note directly
- Add note count display on notebook and inbox headers
- Fix checklist toggle in card view (persist state via localCheckItems)
- Add checklist creation option in tabs/list view (dropdown on + button)
- Fix image description ENOENT error with HTTP fallback
- Improve UI contrast across all themes (input, border, checkbox visibility)
- Add font family setting (Inter vs System Default) in Appearance settings
- Fix CSS font-sans variable conflict (removed dead Geist references)
- Update README with new features and 8 providers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Sepehr Ramezani
2026-05-01 16:14:07 +02:00
parent 1345403a31
commit dbd49d6fcb
64 changed files with 4124 additions and 1392 deletions

474
scripts/deploy-docker.ps1 Normal file
View File

@@ -0,0 +1,474 @@
# ============================================================
# Memento - Docker Deploy Script (Windows PowerShell)
# ============================================================
# Usage:
# .\scripts\deploy-docker.ps1 # Full setup
# .\scripts\deploy-docker.ps1 -EnvOnly # Generate .env.docker only
# .\scripts\deploy-docker.ps1 -Build # Build + deploy (no env setup)
# .\scripts\deploy-docker.ps1 -Stop # Stop all containers
# .\scripts\deploy-docker.ps1 -Logs # Show logs
# ============================================================
[CmdletBinding()]
param(
[switch]$EnvOnly = $false,
[switch]$Build = $false,
[switch]$Full = $false,
[switch]$Stop = $false,
[switch]$Logs = $false
)
$ErrorActionPreference = "Stop"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$ProjectDir = Split-Path -Parent $ScriptDir
$EnvFile = Join-Path $ProjectDir ".env.docker"
# -----------------------------------------------------------
# Helpers
# -----------------------------------------------------------
function Write-Info($msg) { Write-Host "[INFO] $msg" -ForegroundColor Blue }
function Write-Ok($msg) { Write-Host "[OK] $msg" -ForegroundColor Green }
function Write-Warn($msg) { Write-Host "[WARN] $msg" -ForegroundColor Yellow }
function Write-Err($msg) { Write-Host "[ERROR] $msg" -ForegroundColor Red; exit 1 }
function Write-Step($msg) { Write-Host "`n==> $msg" -ForegroundColor Cyan }
function Get-RandomSecret {
$bytes = New-Object byte[] 32
[Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($bytes)
[Convert]::ToBase64String($bytes)
}
function Get-RandomPassword {
$bytes = New-Object byte[] 16
[Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($bytes)
($bytes | ForEach-Object { $_.ToString("x2") }) -join ""
}
function Ask-Input {
param([string]$Prompt, [string]$Default = "")
if ($Default) {
Write-Host " ? $Prompt [$Default]: " -ForegroundColor Cyan -NoNewline
} else {
Write-Host " ? $Prompt: " -ForegroundColor Cyan -NoNewline
}
$result = Read-Host
if ([string]::IsNullOrWhiteSpace($result)) { $result = $Default }
return $result
}
function Ask-Required {
param([string]$Prompt)
while ($true) {
Write-Host " ? $Prompt (required): " -ForegroundColor Cyan -NoNewline
$result = Read-Host
if (-not [string]::IsNullOrWhiteSpace($result)) { return $result }
Write-Host " This field is required." -ForegroundColor Red
}
}
function Ask-Email {
param([string]$Prompt)
while ($true) {
Write-Host " ? $Prompt (required): " -ForegroundColor Cyan -NoNewline
$result = Read-Host
if (-not [string]::IsNullOrWhiteSpace($result) -and $result -match '^[^@]+@[^@]+\.[^@]+$') {
return $result
}
Write-Host " Please enter a valid email address." -ForegroundColor Red
}
}
# -----------------------------------------------------------
# Check dependencies
# -----------------------------------------------------------
function Check-Deps {
Write-Step "Checking dependencies..."
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
Write-Err "Docker is not installed. Install from: https://docs.docker.com/desktop/install/windows-install/"
}
try {
docker info 2>&1 | Out-Null
} catch {
Write-Err "Docker daemon is not running. Start Docker Desktop first."
}
try {
docker compose version 2>&1 | Out-Null
$script:ComposeCmd = "docker compose"
} catch {
if (Get-Command docker-compose -ErrorAction SilentlyContinue) {
$script:ComposeCmd = "docker-compose"
} else {
Write-Err "Docker Compose is not installed. Install from: https://docs.docker.com/compose/install/"
}
}
Write-Ok "All dependencies met"
}
# -----------------------------------------------------------
# Generate .env.docker
# -----------------------------------------------------------
function Generate-Env {
Write-Step "Configuring Memento for Docker deployment"
if (Test-Path $EnvFile) {
Write-Warn ".env.docker already exists."
$confirm = Ask-Input "Overwrite?" "N"
if ($confirm -notmatch "^[Yy]") {
Write-Info "Keeping existing .env.docker"
return
}
}
Write-Host ""
Write-Host " This wizard will guide you through the configuration." -ForegroundColor White
Write-Host " Press Enter to accept defaults in [brackets]." -ForegroundColor White
Write-Host ""
# ---- Core ----
Write-Step "Core configuration"
$url = Ask-Input "App URL (NEXTAUTH_URL)" "http://localhost:3000"
$secret = Get-RandomSecret
Write-Info "Auto-generated NEXTAUTH_SECRET"
$adminEmail = Ask-Email "Admin email (first user with this email becomes ADMIN)"
$allowReg = Ask-Input "Allow public registration" "true"
if ($allowReg -match "^[Yy]|^[Yy]es|^true|^1") { $allowReg = "true" } else { $allowReg = "false" }
# ---- PostgreSQL ----
Write-Step "PostgreSQL configuration"
$pgPort = Ask-Input "PostgreSQL exposed port" "5433"
$pgDb = Ask-Input "PostgreSQL database name" "memento"
$pgUser = Ask-Input "PostgreSQL username" "memento"
$pgPass = Get-RandomPassword
Write-Info "Auto-generated secure PostgreSQL password"
# ---- AI Provider ----
Write-Step "AI Provider configuration"
Write-Host " Choose your AI provider:"
Write-Host " 1) OpenAI"
Write-Host " 2) Ollama (local, requires Ollama container)"
Write-Host " 3) OpenRouter"
Write-Host " 4) Custom OpenAI-compatible (Groq, Together, Mistral, etc.)"
Write-Host " 5) Skip AI configuration"
Write-Host ""
$aiChoice = Ask-Input "Choice" "5"
$aiTagsProvider = $aiTagsModel = $aiEmbedProvider = $aiEmbedModel = ""
$aiChatProvider = $aiChatModel = $openaiKey = $customKey = $customUrl = $ollamaUrl = ""
switch ($aiChoice) {
"1" {
$aiTagsProvider = "openai"; $aiTagsModel = "gpt-4o-mini"
$aiEmbedProvider = "openai"; $aiEmbedModel = "text-embedding-3-small"
$aiChatProvider = "openai"; $aiChatModel = "gpt-4o-mini"
$openaiKey = Ask-Required "OpenAI API Key"
}
"2" {
$aiTagsProvider = "ollama"; $aiTagsModel = "granite4:latest"
$aiEmbedProvider = "ollama"; $aiEmbedModel = "embeddinggemma:latest"
$aiChatProvider = "ollama"; $aiChatModel = "granite4:latest"
$ollamaUrl = Ask-Input "Ollama base URL" "http://ollama:11434"
}
"3" {
$aiTagsProvider = "custom"; $aiTagsModel = "google/gemma-3-27b-it"
$aiEmbedProvider = "custom"; $aiEmbedModel = "text-embedding-3-small"
$aiChatProvider = "custom"; $aiChatModel = "google/gemma-3-27b-it"
$customUrl = "https://openrouter.ai/api/v1"
$customKey = Ask-Required "OpenRouter API Key"
$customUrl = Ask-Input "OpenRouter base URL" $customUrl
}
"4" {
$aiTagsProvider = "custom"
$aiEmbedProvider = "custom"
$aiChatProvider = "custom"
$customKey = Ask-Required "Custom provider API Key"
$customUrl = Ask-Required "Custom provider base URL"
$aiTagsModel = Ask-Input "Model for tags" "gpt-4o-mini"
$aiEmbedModel = Ask-Input "Model for embeddings" "text-embedding-3-small"
$aiChatModel = Ask-Input "Model for chat" "gpt-4o-mini"
}
"5" {
Write-Info "Skipping AI configuration. You can configure it later in the admin panel."
}
default { Write-Err "Invalid choice" }
}
# ---- MCP Server ----
Write-Step "MCP Server configuration"
Write-Host " The MCP server allows external tools (Claude, Cline, N8N) to interact with Memento."
Write-Host ""
$mcpEnable = Ask-Input "Enable MCP server?" "yes"
$mcpEnable = ($mcpEnable -match "^[Yy]|^[Yy]es|^true|^1")
$mcpPort = "3001"; $mcpServerMode = "sse"; $mcpServerUrl = ""; $mcpApiKey = ""
if ($mcpEnable) {
$mcpPort = Ask-Input "MCP server port" "3001"
$mcpServerMode = Ask-Input "MCP server mode (sse or stdio)" "sse"
$mcpServerUrl = "$($url -replace ':\d+$',''):$mcpPort"
$mcpAuth = Ask-Input "Require MCP authentication?" "yes"
if ($mcpAuth -match "^[Yy]|^[Yy]es|^true|^1") {
$mcpApiKey = Get-RandomPassword
Write-Info "Auto-generated MCP API key"
}
}
# ---- Email ----
Write-Step "Email configuration (optional, needed for password reset)"
Write-Host " Choose an email provider:"
Write-Host " 1) Resend"
Write-Host " 2) SMTP"
Write-Host " 3) Skip"
Write-Host ""
$emailChoice = Ask-Input "Choice" "3"
$resendKey = $smtpHost = $smtpPort = $smtpUser = $smtpPass = $smtpFrom = ""
switch ($emailChoice) {
"1" { $resendKey = Ask-Required "Resend API Key" }
"2" {
$smtpHost = Ask-Required "SMTP Host"
$smtpPort = Ask-Input "SMTP Port" "587"
$smtpUser = Ask-Required "SMTP Username"
$smtpPass = Ask-Required "SMTP Password"
$smtpFrom = Ask-Required "SMTP From email"
}
}
# ---- Ollama container ----
$enableOllama = "no"
if ($aiChoice -eq "2") {
$enableOllama = "yes"
} else {
Write-Step "Ollama container (optional)"
$enableOllama = Ask-Input "Also start Ollama container?" "no"
$enableOllama = $(if ($enableOllama -match "^[Yy]|^[Yy]es|^true|^1") { "yes" } else { "no" })
}
# ---- Web Search ----
Write-Step "Web Search configuration (optional)"
Write-Host " Choose a web search provider:"
Write-Host " 1) SearXNG (self-hosted)"
Write-Host " 2) Brave Search"
Write-Host " 3) Skip"
Write-Host ""
$searchChoice = Ask-Input "Choice" "3"
$searxngUrl = $braveKey = ""
switch ($searchChoice) {
"1" { $searxngUrl = Ask-Required "SearXNG URL" }
"2" { $braveKey = Ask-Required "Brave Search API Key" }
}
# ---- Write .env.docker ----
Write-Step "Writing .env.docker"
$envContent = @"
# =============================================================================
# Memento - Docker Environment (auto-generated by deploy-docker.ps1)
# =============================================================================
# Generated on $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
# =============================================================================
# Core
NEXTAUTH_URL="$url"
NEXTAUTH_SECRET="$secret"
ADMIN_EMAIL="$adminEmail"
ALLOW_REGISTRATION="$allowReg"
# PostgreSQL
POSTGRES_PORT=$pgPort
POSTGRES_DB=$pgDb
POSTGRES_USER=$pgUser
POSTGRES_PASSWORD="$pgPass"
"@
if ($aiChoice -ne "5") {
$envContent += @"
# AI - Tags
AI_PROVIDER_TAGS=$aiTagsProvider
AI_MODEL_TAGS="$aiTagsModel"
# AI - Embeddings
AI_PROVIDER_EMBEDDING=$aiEmbedProvider
AI_MODEL_EMBEDDING="$aiEmbedModel"
# AI - Chat
AI_PROVIDER_CHAT=$aiChatProvider
AI_MODEL_CHAT="$aiChatModel"
"@
if ($openaiKey) { $envContent += "`nOPENAI_API_KEY=`"$openaiKey`"" }
if ($customKey) { $envContent += "`nCUSTOM_OPENAI_API_KEY=`"$customKey`"`nCUSTOM_OPENAI_BASE_URL=`"$customUrl`"" }
if ($ollamaUrl) { $envContent += "`nOLLAMA_BASE_URL=`"$ollamaUrl`"" }
}
if ($mcpEnable) {
$envContent += @"
# MCP Server
MCP_MODE="$mcpServerMode"
MCP_PORT="$mcpPort"
MCP_SERVER_MODE="$mcpServerMode"
MCP_SERVER_URL="$mcpServerUrl"
"@
if ($mcpApiKey) { $envContent += "`nMCP_API_KEY=`"$mcpApiKey`"" }
} else {
$envContent += @"
# MCP Server (disabled)
MCP_SERVER_MODE="disabled"
"@
}
if ($resendKey) { $envContent += "`n`n# Email - Resend`nRESEND_API_KEY=`"$resendKey`"" }
if ($smtpHost) {
$envContent += @"
# Email - SMTP
SMTP_HOST="$smtpHost"
SMTP_PORT="$smtpPort"
SMTP_USER="$smtpUser"
SMTP_PASS="$smtpPass"
SMTP_FROM="$smtpFrom"
"@
}
if ($searxngUrl) { $envContent += "`n`n# Web Search - SearXNG`nWEB_SEARCH_PROVIDER=`"searxng`"`nSEARXNG_URL=`"$searxngUrl`"" }
if ($braveKey) { $envContent += "`n`n# Web Search - Brave`nWEB_SEARCH_PROVIDER=`"brave`"`nBRAVE_SEARCH_API_KEY=`"$braveKey`"" }
$envContent | Set-Content -Path $EnvFile -Encoding UTF8
Write-Ok ".env.docker created at $EnvFile"
Write-Host ""
Write-Host " Configuration summary:" -ForegroundColor White
Write-Host " URL: $url"
Write-Host " Admin email: $adminEmail"
Write-Host " Registration: $allowReg"
Write-Host " PostgreSQL user: $pgUser / db: $pgDb"
Write-Host " AI provider: $(if ($aiChoice -eq '5') { 'skipped' } else { $aiTagsProvider })"
Write-Host " MCP server: $(if ($mcpEnable) { "enabled ($mcpServerMode)" } else { 'disabled' })"
Write-Host " Email: $(if ($emailChoice -eq '3') { 'skipped' } else { 'configured' })"
Write-Host " Ollama container: $enableOllama"
Write-Host " (sensitive values are hidden)"
}
# -----------------------------------------------------------
# Build and deploy
# -----------------------------------------------------------
function Deploy-Containers {
if (-not (Test-Path $EnvFile)) {
Write-Err ".env.docker not found. Run: .\scripts\deploy-docker.ps1 -EnvOnly"
}
Push-Location $ProjectDir
# Determine Ollama profile
$envContent = Get-Content $EnvFile -Raw
$ollamaProfile = ""
if ($envContent -match 'OLLAMA_BASE_URL="http://ollama' -or $envContent -match 'AI_PROVIDER_TAGS=ollama') {
$ollamaProfile = "--profile ollama"
}
Write-Step "Building Docker containers..."
Invoke-Expression "$ComposeCmd build --parallel" 2>&1
Write-Step "Starting containers..."
Invoke-Expression "$ComposeCmd up -d $ollamaProfile" 2>&1
Write-Step "Waiting for services to be healthy..."
$retries = 0
$maxRetries = 45
while ($retries -lt $maxRetries) {
$status = Invoke-Expression "$ComposeCmd ps --format '{{.Status}}'" 2>$null
$unhealthy = ($status | Where-Object { $_ -notmatch "healthy|Up" }).Count
if ($unhealthy -eq 0) { break }
$retries++
Start-Sleep -Seconds 2
Write-Host -NoNewline "."
}
Write-Host ""
if ($retries -ge $maxRetries) {
Write-Warn "Some containers may still be starting. Check status with: .\scripts\deploy-docker.ps1 -Logs"
}
Write-Step "Waiting for database migrations (handled by entrypoint)..."
# The docker-entrypoint.sh runs prisma migrate deploy automatically on every start.
# It handles: fresh DB, existing DB with migrations, and P3005 baseline recovery.
# No manual migration step needed here.
Write-Host ""
Write-Host "============================================" -ForegroundColor Green
Write-Host " Memento is running!" -ForegroundColor Green
Write-Host "============================================" -ForegroundColor Green
Write-Host ""
Invoke-Expression "$ComposeCmd ps"
$appUrl = (Select-String -Path $EnvFile -Pattern '^NEXTAUTH_URL=(.+)$').Matches[0].Groups[1].Value -replace '"',''
$admEmail = (Select-String -Path $EnvFile -Pattern '^ADMIN_EMAIL=(.+)$').Matches[0].Groups[1].Value -replace '"',''
Write-Host " App: $appUrl"
Write-Host " Admin: Register with $admEmail to get admin access"
Write-Host ""
Write-Host " Useful commands:" -ForegroundColor Cyan
Write-Host " .\scripts\deploy-docker.ps1 -Logs View logs"
Write-Host " .\scripts\deploy-docker.ps1 -Stop Stop containers"
Write-Host " .\scripts\deploy-docker.ps1 -EnvOnly Reconfigure .env.docker"
Pop-Location
}
# -----------------------------------------------------------
# Stop containers
# -----------------------------------------------------------
function Stop-Containers {
Push-Location $ProjectDir
Write-Step "Stopping containers..."
Invoke-Expression "$ComposeCmd down" 2>&1
Write-Ok "Containers stopped"
Pop-Location
}
# -----------------------------------------------------------
# Show logs
# -----------------------------------------------------------
function Show-Logs {
Push-Location $ProjectDir
Invoke-Expression "$ComposeCmd logs -f --tail=100"
Pop-Location
}
# -----------------------------------------------------------
# Main
# -----------------------------------------------------------
Check-Deps
if ($EnvOnly) {
Generate-Env
} elseif ($Build) {
Deploy-Containers
} elseif ($Stop) {
Stop-Containers
} elseif ($Logs) {
Show-Logs
} else {
# Default: full setup
Generate-Env
Write-Host ""
Deploy-Containers
}