feat(notes): liens internes, onglet Réseau, living blocks et consentement IA
Some checks failed
CI / Lint, Test & Build (push) Failing after 1m19s
CI / Deploy production (on server) (push) Has been skipped

Rend les liens entre notes visibles et persistants (sync NoteLink au save, auto-save, graphe réseau rafraîchi), ajoute living blocks, Memory Echo, recherche globale, consentement IA explicite et consolide les prototypes design en architectural-grid.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Antigravity
2026-05-24 14:27:29 +00:00
parent 077e665dfc
commit e2672cd2c2
323 changed files with 20670 additions and 42431 deletions

View File

@@ -0,0 +1,32 @@
/** Seuil Memory Echo (prod) — connexions sous ce score ne sont pas proposées. */
export const SEMANTIC_SIMILARITY_FLOOR = 0.75
export const SEMANTIC_SIMILARITY_FLOOR_DEMO = 0.5
/** Ratio 0 (seuil) → 1 (identique), pour étaler laffichage au-dessus du seuil. */
export function semanticProximityRatio(
similarity: number,
floor = SEMANTIC_SIMILARITY_FLOOR,
): number {
const clamped = Math.max(floor, Math.min(1, similarity))
if (floor >= 1) return 1
return (clamped - floor) / (1 - floor)
}
/** Pourcentage « proximité » affiché (0100), étalé entre le seuil et 100 %. */
export function semanticProximityPercent(
similarity: number,
floor = SEMANTIC_SIMILARITY_FLOOR,
): number {
return Math.round(semanticProximityRatio(similarity, floor) * 100)
}
/** Rayon orbite : plus la proximité est forte, plus le nœud est proche du centre. */
export function semanticOrbitRadius(
similarity: number,
floor = SEMANTIC_SIMILARITY_FLOOR,
): number {
const MIN_R = 34
const MAX_R = 94
const t = semanticProximityRatio(similarity, floor)
return MAX_R - t * (MAX_R - MIN_R)
}

View File

@@ -1,8 +1,11 @@
/**
* Chart Suggestion Service
* Frontend service for calling the AI chart suggestions API
* NOW WITH FAST PARSER - skips AI when possible (< 50ms vs 2 minutes)
*/
import { parseChartData, generateChartSuggestions } from '@/lib/chart/parser'
export interface ChartSuggestion {
type: 'bar' | 'horizontal-bar' | 'line' | 'area' | 'pie' | 'radar' | 'funnel' | 'gauge'
title: string
@@ -64,6 +67,7 @@ function setCached(key: string, data: SuggestChartsResponse): void {
/**
* Call the AI chart suggestions API
* FAST PATH: Try regex parser first (< 50ms), only call AI if no data found
* @param request - The request parameters
* @returns Promise with the chart suggestions
*/
@@ -78,7 +82,29 @@ export async function suggestCharts(request: SuggestChartsRequest): Promise<Sugg
return cached
}
console.log('[suggestCharts] CACHE MISS - calling API')
console.log('[suggestCharts] CACHE MISS - trying fast parser first')
// FAST PATH: Try regex parser first - NO AI call needed!
const textToParse = request.selection || request.content || ''
const parsed = parseChartData(textToParse)
if (parsed.hasData && parsed.confidence > 0.3) {
console.log('[suggestCharts] FAST PATH - regex parser found data, skipping AI!')
const suggestions = generateChartSuggestions(parsed.data)
const response: SuggestChartsResponse = {
suggestions,
analyzedText: textToParse.substring(0, 200),
detectedData: `${parsed.data.length} data points found`,
hasData: true,
}
// Cache the fast result
setCached(cacheKey, response)
return response
}
console.log('[suggestCharts] Parser found no good data, calling AI API (slow...)')
try {
const response = await fetch('/api/ai/suggest-charts', {

View File

@@ -96,6 +96,16 @@ export class MemoryEchoService {
* Find meaningful connections between user's notes
*/
async findConnections(userId: string, demoMode: boolean = false): Promise<NoteConnection[]> {
// GDPR AI Consent check — compliance skip if not granted (AC6)
const userSettings = await prisma.userAISettings.findUnique({
where: { userId },
select: { aiProcessingConsent: true },
})
if (!userSettings?.aiProcessingConsent) {
console.log(`[MemoryEchoService] User ${userId} has not given AI consent. Skipping connection generation for compliance.`)
return []
}
// Ensure all notes have embeddings before searching for connections
await this.ensureEmbeddings(userId)

View File

@@ -31,7 +31,8 @@ const RECIPES: Record<string, Recipe> = {
function resolveRecipe(name?: string): Recipe {
if (!name || name === 'auto') return RECIPES['architectural-saas']
const key = name.toLowerCase().replace(/[^a-z]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')
// Normalize: underscores to dashes for consistency
const key = name.toLowerCase().replace(/[\s_]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')
return RECIPES[key] ?? RECIPES['architectural-saas']
}
@@ -345,30 +346,45 @@ function renderRadarChart(data: { label: string; value: number }[], r: Recipe):
const cx = 150, cy = 150, radius = 110
const max = Math.max(...data.map(d => d.value), 1)
const angleStep = (2 * Math.PI) / n
// Choose grid color based on theme darkness
const gridColor = r.isDark ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.15)'
const labelColor = r.isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'
// Grid
const gridLevels = [0.25, 0.5, 0.75, 1].map(f => {
const pts = Array.from({ length: n }, (_, i) => {
const a = i * angleStep - Math.PI / 2
return `${cx + Math.cos(a) * radius * f},${cy + Math.sin(a) * radius * f}`
}).join(' ')
return `<polygon points="${pts}" fill="none" stroke="${r.svgGrid}" stroke-width="1"/>`
return `<polygon points="${pts}" fill="none" stroke="${gridColor}" stroke-width="1"/>`
}).join('')
// Axis lines from center
const axisLines = Array.from({ length: n }, (_, i) => {
const a = i * angleStep - Math.PI / 2
return `<line x1="${cx}" y1="${cy}" x2="${cx + Math.cos(a) * radius}" y2="${cy + Math.sin(a) * radius}" stroke="${gridColor}" stroke-width="1"/>`
}).join('')
// Data polygon
const dataPts = data.map((d, i) => {
const a = i * angleStep - Math.PI / 2
const r2 = (d.value / max) * radius
return `${cx + Math.cos(a) * r2},${cy + Math.sin(a) * r2}`
}).join(' ')
// Labels
// Labels with better contrast
const labels = data.map((d, i) => {
const a = i * angleStep - Math.PI / 2
const lx = cx + Math.cos(a) * (radius + 20)
const ly = cy + Math.sin(a) * (radius + 20)
return `<text x="${lx}" y="${ly}" text-anchor="middle" font-size="10" fill="${r.textMuted}">${esc(d.label)}</text>`
return `<text x="${lx}" y="${ly}" text-anchor="middle" font-size="11" font-weight="600" fill="${labelColor}">${esc(d.label)}</text>`
}).join('')
return `<svg viewBox="0 0 300 300" style="width:100%;max-width:320px;height:auto;margin:0 auto;display:block;">
${axisLines}
${gridLevels}
<polygon points="${dataPts}" fill="${r.accent1}" fill-opacity="0.15" stroke="${r.accent1}" stroke-width="2"/>
<polygon points="${dataPts}" fill="${r.accent1}" fill-opacity="0.2" stroke="${r.accent1}" stroke-width="2.5"/>
${labels}
</svg>`
}

View File

@@ -43,21 +43,21 @@ export const PALETTE_ALIASES: Record<string, string> = {
premium: 'platinum_white_gold', clean: 'vibrant_tech', stage: 'stage_dark',
architectural: 'architectural_mono', silk: 'minimal_silk',
black: 'keynote', white: 'platinum_white_gold', nuit: 'galaxy', sombre: 'stage_dark',
// Recipe explicit theme mappings
architectural_saas: 'architectural_mono',
midnight_cathedral: 'keynote',
aurora_borealis: 'galaxy',
tokyo_neon: 'vibrant_tech',
sunlit_gallery: 'bohemian',
clinical_precision: 'modern_wellness',
venture_pitch: 'vibrant_orange_mint',
forest_floor: 'forest_eco',
steel_glass: 'luxury_mystery',
cyberpunk_terminal: 'tech_night',
editorial_ink: 'vintage_academic',
coastal_morning: 'coastal_coral',
paper_studio: 'craft_artisan',
// Recipe explicit theme mappings (both underscore and dash variants)
architectural_saas: 'architectural_mono', 'architectural-saas': 'architectural_mono',
midnight_cathedral: 'keynote', 'midnight-cathedral': 'keynote',
aurora_borealis: 'galaxy', 'aurora-borealis': 'galaxy',
tokyo_neon: 'vibrant_tech', 'tokyo-neon': 'vibrant_tech',
sunlit_gallery: 'bohemian', 'sunlit-gallery': 'bohemian',
clinical_precision: 'modern_wellness', 'clinical-precision': 'modern_wellness',
venture_pitch: 'vibrant_orange_mint', 'venture-pitch': 'vibrant_orange_mint',
forest_floor: 'forest_eco', 'forest-floor': 'forest_eco',
steel_glass: 'luxury_mystery', 'steel-glass': 'luxury_mystery',
cyberpunk_terminal: 'tech_night', 'cyberpunk-terminal': 'tech_night',
editorial_ink: 'vintage_academic', 'editorial-ink': 'vintage_academic',
coastal_morning: 'coastal_coral', 'coastal-morning': 'coastal_coral',
paper_studio: 'craft_artisan', 'paper-studio': 'craft_artisan',
}
export const THEME_NAMES: Record<string, string> = {
@@ -74,7 +74,8 @@ export const THEME_NAMES: Record<string, string> = {
}
export function resolvePalette(spec: Pick<PresentationSpec, 'theme'>): { palette: Palette; key: string } {
const name = (spec.theme || '').toLowerCase().replace(/[\s-]/g, '_')
// Normalize theme name: handle both dashes and underscores
const name = (spec.theme || '').toLowerCase().replace(/[\s-]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '')
const key = PALETTE_ALIASES[name] || (PALETTES[name] ? name : 'keynote')
return { palette: PALETTES[key]!, key }
}