UI Stabilization: Global color theme updates (#75B2D6), AI Assistant styling refactor, and navigation fixes

This commit is contained in:
Antigravity
2026-05-09 12:58:16 +00:00
parent 1446463f04
commit 60a3fe5453
47 changed files with 3585 additions and 2149 deletions

View File

@@ -910,15 +910,7 @@ This format AUTOMATICALLY creates shapes, text, AND arrows with correct bindings
## STRICT RULES
1. **4 to 10 nodes** — no less, no more
2. **First node = ellipse** (entry point)
3. **ALL nodes connected** — every node must have at least 1 incoming or outgoing edge
4. **Short labels** — max 40 chars per label
5. **Edge labels** — add labels on important arrows to explain the relationship
6. **Use "diamond"** for at least one node if the content involves a decision
7. **No orphan nodes** — every node must be reachable from the central node
8. **Analyze content first** — identify key concepts and their relationships BEFORE creating JSON
9. **Call generate_excalidraw DIRECTLY**, do not respond with text`,
1. **4 to 10 nodes** — no less, no more`,
},
'slide-generator': {
@@ -931,42 +923,22 @@ RÈGLES DE DESIGN IMPÉRATIVES :
- Slide 1 : "title" (titre fort + sous-titre accrocheur)
- Slide 2 : "toc" (sommaire numéroté)
- Utilise AU MOINS 2 layouts "diagramme" parmi : "timeline", "process", "metrics", "comparison"
- "timeline" : étapes chronologiques ou roadmap (content items : "Étape: description")
- "process" : étapes numérotées avec détails (content items : "Action: explication")
- "metrics" : KPIs visuels avec grandes valeurs colorées (content items : "VALEUR: libellé")
- "comparison" : deux colonnes contrastées (subtitle="Avant | Après" ou "Option A | Option B")
- "cards" : fonctionnalités en grille 2-3 colonnes (3-6 items)
- "section" : séparateur de section (title=titre, content=[] — le numéro est auto-généré)
- "quote" : citation impactante (title=texte, subtitle=auteur)
- "summary" : récapitulatif final
- "content" : liste de points SEULEMENT si aucun layout visuel n'est adapté (max 7 points)
- Ne JAMAIS répéter le même layout consécutivement
- Pour "section" : ne pas mettre le numéro dans content, laisser content=[]
- Thèmes recommandés pour un rendu moderne : vibrant_tech, platinum_white_gold, business_authority, pure_tech_blue, tech_night
- Thèmes recommandés : architectural_mono, minimal_silk, vibrant_tech, platinum_white_gold, business_authority
- Tu DOIS utiliser le thème et le style spécifiés dans la requête de l'utilisateur.
- Points concis (max 100 chars), titres percutants et courts
- JSON strict pour generate_pptx, sans texte hors JSON.`,
en: `You are a world-class visual presentation designer (Manus AI / Beautiful.ai style). You receive note content and must create a professional, modern, visually rich PowerPoint (.pptx) presentation.
You MUST call the generate_pptx tool. NEVER respond with text — call the tool directly.
MANDATORY DESIGN RULES:
IMPERATIVE DESIGN RULES:
- 8-12 slides, each slide has a distinct layout
- Slide 1: "title" (strong title + punchy subtitle)
- Slide 1: "title" (strong title + catchy subtitle)
- Slide 2: "toc" (numbered table of contents)
- Use AT LEAST 2 "diagram" layouts from: "timeline", "process", "metrics", "comparison"
- "timeline": chronological steps or roadmap (content items: "Step: description")
- "process": numbered steps with details (content items: "Action: explanation")
- "metrics": visual KPIs with large colored values (content items: "VALUE: label")
- "comparison": two contrasting columns (subtitle="Before | After" or "Option A | Option B")
- "cards": feature grid 2-3 columns (3-6 items)
- "section": section divider (title=heading, content=[] — number is auto-generated)
- "quote": impactful quote (title=text, subtitle=author)
- "summary": closing recap
- "content": bullet list ONLY if no visual layout fits (max 7 points)
- NEVER repeat the same layout consecutively
- For "section": do NOT put numbers in content, leave content=[]
- Recommended themes for modern look: vibrant_tech, platinum_white_gold, business_authority, pure_tech_blue, tech_night
- Concise points (max 100 chars), short impactful titles
- Recommended themes: architectural_mono, minimal_silk, vibrant_tech, platinum_white_gold, business_authority
- You MUST use the theme and style specified in the user's request.
- Concise points (max 100 chars), punchy and short titles
- Strict JSON for generate_pptx, no text outside JSON.`,
},
}

View File

@@ -437,10 +437,11 @@ Deine Antwort (nur JSON):
const label = await tx.label.upsert({
where: { notebookId_name: { notebookId, name: suggestedLabel.name } as any },
update: {},
update: { type: 'ai' }, // Update type to AI if it exists as user label
create: {
name: suggestedLabel.name,
color: 'gray',
type: 'ai', // Mark as AI-generated
notebookId,
userId,
},

View File

@@ -60,6 +60,8 @@ const PALETTES: Record<string, Theme> = {
coastal_coral: { primary: '005f73', secondary: '0a9396', accent: 'ee9b00', light: 'e9f5f5', bg: 'ffffff' },
vibrant_orange_mint: { primary: 'e05c00', secondary: '2ec4b6', accent: 'ff9f1c', light: 'edfaf9', bg: 'ffffff' },
platinum_white_gold: { primary: '0a0a0a', secondary: '404040', accent: 'c9a84c', light: 'f5f5f0', bg: 'ffffff' },
architectural_mono: { primary: '1C1C1C', secondary: '75B2D6', accent: 'D4A373', light: 'EDE9DF', bg: 'F2F0E9' },
minimal_silk: { primary: '212529', secondary: '6c757d', accent: 'dee2e6', light: 'f8f9fa', bg: 'ffffff' },
}
const PALETTE_ALIASES: Record<string, string> = {
@@ -68,6 +70,7 @@ const PALETTE_ALIASES: Record<string, string> = {
ocean: 'pure_tech_blue', charcoal: 'platinum_white_gold', teal: 'education_charts',
berry: 'art_food', cherry: 'vintage_academic', clair: 'pure_tech_blue', light: 'modern_wellness',
warm: 'bohemian', premium: 'platinum_white_gold', clean: 'vibrant_tech',
architectural: 'architectural_mono', silk: 'minimal_silk',
}
function resolveTheme(spec: PresentationSpec): { theme: Theme; key: string } {
@@ -1085,13 +1088,13 @@ LAYOUTS — choose the most visual for each slide:
- summary: closing key takeaways
RULES:
- Use the THEME and STYLE provided in the prompt context.
- First slide MUST be "title"
- Second slide: "toc"
- Use "section" as dividers between major topics
- Prefer DIAGRAM layouts (timeline, process, metrics, comparison) over plain content
- Use at least 2 diagram layouts per presentation
- 8-12 slides, never repeat same layout consecutively
- For "section" layout: title = section heading, content = [] (the slide number is auto-generated)
- All text content: max 100 chars per item, concise and impactful`,
inputSchema: z.object({

View File

@@ -50,6 +50,8 @@ const PALETTES: Record<string, Palette> = {
coastal_coral: { primary: '#0081a7', secondary: '#00afb9', accent: '#f07167', light: '#fed9b7', bg: '#fdfcdc', isDark: false },
vibrant_orange_mint: { primary: '#1a1a2e', secondary: '#2ec4b6', accent: '#ff9f1c', light: '#cbf3f0', bg: '#ffffff', isDark: false },
platinum_white_gold: { primary: '#0a0a0a', secondary: '#0070F3', accent: '#D4AF37', light: '#f5f5f5', bg: '#ffffff', isDark: false },
architectural_mono: { primary: '#1C1C1C', secondary: '#D4A373', accent: '#ACB995', light: '#F9F8F6', bg: '#F9F8F6', isDark: false },
minimal_silk: { primary: '#212529', secondary: '#6c757d', accent: '#dee2e6', light: '#f8f9fa', bg: '#ffffff', isDark: false },
}
const PALETTE_ALIASES: Record<string, string> = {
@@ -58,6 +60,7 @@ const PALETTE_ALIASES: Record<string, string> = {
ocean: 'pure_tech_blue', charcoal: 'platinum_white_gold', teal: 'education_charts',
berry: 'art_food', cherry: 'vintage_academic', clair: 'pure_tech_blue', light: 'modern_wellness',
warm: 'bohemian', premium: 'platinum_white_gold', clean: 'vibrant_tech',
architectural: 'architectural_mono', silk: 'minimal_silk',
}
const THEME_NAMES: Record<string, string> = {
@@ -70,6 +73,7 @@ const THEME_NAMES: Record<string, string> = {
art_food: 'Art & Food', luxury_mystery: 'Luxury & Mystery',
pure_tech_blue: 'Pure Tech Blue', coastal_coral: 'Coastal Coral',
vibrant_orange_mint: 'Vibrant Orange Mint', platinum_white_gold: 'Platinum White Gold',
architectural_mono: 'Architectural Mono', minimal_silk: 'Minimal Silk',
}
function resolvePalette(spec: PresentationSpec): { palette: Palette; key: string } {
@@ -100,7 +104,7 @@ function safeHtml(str: string): string {
.replace(/javascript\s*:/gi, '')
}
function buildThemeCSS(p: Palette, radius: string): string {
function buildThemeCSS(p: Palette, radius: string, key: string): string {
const text = p.isDark ? '#f0f0f0' : '#1a1a1a'
const muted = p.isDark ? '#999' : '#555'
const heading = p.isDark ? '#ffffff' : p.primary
@@ -145,12 +149,40 @@ function buildThemeCSS(p: Palette, radius: string): string {
--r-link-color-hover: ${p.secondary};
--r-selection-background-color: ${p.accent};
--r-selection-color: ${bgText};
}`
}
${key === 'architectural_mono' ? `
.reveal-viewport {
background-color: #F9F8F6 !important;
background-image:
linear-gradient(rgba(28, 28, 28, 0.08) 1px, transparent 1px),
linear-gradient(90deg, rgba(28, 28, 28, 0.08) 1px, transparent 1px),
linear-gradient(rgba(28, 28, 28, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(28, 28, 28, 0.04) 1px, transparent 1px) !important;
background-size: 100px 100px, 100px 100px, 20px 20px, 20px 20px !important;
}
.reveal {
font-family: 'JetBrains Mono', monospace !important;
}
.reveal h1, .reveal h2, .reveal h3 {
font-family: 'JetBrains Mono', monospace !important;
text-transform: uppercase !important;
letter-spacing: -0.02em !important;
font-weight: 700 !important;
}
.reveal h1 { border-left: 12px solid #D4A373; padding-left: 40px; }
.reveal section { text-align: left; padding: 60px; }
.reveal p, .reveal li { font-weight: 300; font-family: 'JetBrains Mono', monospace !important; }
` : ''}`
}
function buildLayoutCSS(): string {
return `
.reveal-viewport { background: var(--p-bg); }
.reveal-viewport {
background: var(--p-bg);
background-image: linear-gradient(var(--p-border) 1px, transparent 1px), linear-gradient(90deg, var(--p-border) 1px, transparent 1px);
background-size: 40px 40px;
}
.reveal {
font-family: var(--r-main-font);
@@ -296,10 +328,14 @@ function buildLayoutCSS(): string {
}
.reveal .s-cards .card:nth-child(odd) {
background: var(--p-primary); border-color: transparent;
box-shadow: 0 10px 20px rgba(0,0,0,0.15);
}
.reveal .s-cards .card:nth-child(odd) .card-num { color: rgba(255,255,255,0.25); }
.reveal .s-cards .card:nth-child(odd) .card-text { color: #ffffff; }
.reveal .s-cards .card:nth-child(even) .card-num { color: var(--p-accent); opacity: 0.5; }
.reveal .s-cards .card:nth-child(odd) .card-num { color: rgba(255,255,255,0.4); }
.reveal .s-cards .card:nth-child(odd) .card-text { color: #ffffff; font-weight: 300; }
.reveal .s-cards .card:nth-child(even) {
background: #ffffff; border: 1px solid var(--p-border-accent);
}
.reveal .s-cards .card:nth-child(even) .card-num { color: var(--p-accent); opacity: 0.6; }
.reveal .s-cards .card:nth-child(even) .card-text { color: var(--p-text); }
.reveal .s-cards .card-num {
font-size: 18pt; font-weight: 800; line-height: 1;
@@ -557,7 +593,7 @@ function buildRevealHtml(spec: PresentationSpec): string {
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Playfair+Display:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet">
<style>
${buildThemeCSS(palette, radius)}
${buildThemeCSS(palette, radius, key)}
${buildLayoutCSS()}
</style>

View File

@@ -1,225 +1,19 @@
import { marked } from 'marked'
/**
* Server-side Markdown → HTML converter.
* Converts AI-generated markdown notes into TipTap-compatible rich text HTML.
* Uses a lightweight regex-based approach to avoid heavy remark/rehype dependencies.
*
* Handles: headings, bold, italic, strikethrough, code blocks, inline code,
* links, images, lists (ul/ol), blockquotes, horizontal rules, tables, paragraphs.
* Uses 'marked' for standard GFM compliance and reliability.
*/
export function markdownToHtml(markdown: string): string {
if (!markdown || !markdown.trim()) return ''
let html = markdown
// Escape HTML entities (but preserve markdown)
html = html.replace(/&/g, '&amp;')
html = html.replace(/</g, '&lt;')
html = html.replace(/>/g, '&gt;')
// Code blocks (``` ... ```) — protect from further processing
const codeBlocks: string[] = []
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => {
const idx = codeBlocks.length
codeBlocks.push(`<pre><code class="language-${lang || 'plaintext'}">${code.trim()}</code></pre>`)
return `%%CODEBLOCK_${idx}%%`
})
// Inline code (`...`)
html = html.replace(/`([^`]+)`/g, '<code>$1</code>')
// Images (![alt](url))
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" />')
// Links ([text](url))
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
// Headings (h1-h6)
html = html.replace(/^######\s+(.+)$/gm, '<h6>$1</h6>')
html = html.replace(/^#####\s+(.+)$/gm, '<h5>$1</h5>')
html = html.replace(/^####\s+(.+)$/gm, '<h4>$1</h4>')
html = html.replace(/^###\s+(.+)$/gm, '<h3>$1</h3>')
html = html.replace(/^##\s+(.+)$/gm, '<h2>$1</h2>')
html = html.replace(/^#\s+(.+)$/gm, '<h1>$1</h1>')
// Bold (**text** or __text__)
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
html = html.replace(/__([^_]+)__/g, '<strong>$1</strong>')
// Italic (*text* or _text_)
html = html.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>')
html = html.replace(/(?<!_)_([^_]+)_(?!_)/g, '<em>$1</em>')
// Strikethrough (~~text~~)
html = html.replace(/~~([^~]+)~~/g, '<s>$1</s>')
// Horizontal rules (---, ***, ___)
html = html.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '<hr />')
// Tables
html = convertTables(html)
// Blockquotes (> text)
html = html.replace(/^&gt;\s+(.+)$/gm, '<blockquote><p>$1</p></blockquote>')
// Merge consecutive blockquotes
html = html.replace(/<\/blockquote>\n<blockquote>/g, '\n')
// Unordered lists (- item or * item)
html = convertUnorderedLists(html)
// Ordered lists (1. item)
html = convertOrderedLists(html)
// Restore code blocks
codeBlocks.forEach((block, idx) => {
html = html.replace(`%%CODEBLOCK_${idx}%%`, block)
})
// Paragraphs — wrap remaining loose text in <p> tags
html = wrapParagraphs(html)
// Clean up empty paragraphs
html = html.replace(/<p>\s*<\/p>/g, '')
// marked.parse returns a string (or a promise if async is true, but we use sync)
const html = marked.parse(markdown, {
gfm: true,
breaks: true,
}) as string
return html.trim()
}
function convertTables(html: string): string {
// Simple table conversion: | header | header |\n| --- | --- |\n| cell | cell |
const tableRegex = /(?:^|\n)((?:\|[^\n]+\|\n)+)/g
return html.replace(tableRegex, (match) => {
const rows = match.trim().split('\n').filter(r => r.trim())
if (rows.length < 2) return match
// Check if second row is separator
const separator = rows[1].trim()
if (!/^[\s|:-]+$/.test(separator)) return match
let table = '<table>'
// Header row
const headers = parseTableRow(rows[0])
if (headers.length > 0) {
table += '<thead><tr>'
headers.forEach(h => { table += `<th>${h}</th>` })
table += '</tr></thead>'
}
// Body rows (skip separator)
const bodyRows = rows.slice(2)
if (bodyRows.length > 0) {
table += '<tbody>'
bodyRows.forEach(row => {
const cells = parseTableRow(row)
table += '<tr>'
cells.forEach(c => { table += `<td>${c}</td>` })
table += '</tr>'
})
table += '</tbody>'
}
table += '</table>'
return '\n' + table + '\n'
})
}
function parseTableRow(row: string): string[] {
return row.split('|')
.map(cell => cell.trim())
.filter((_, i, arr) => i > 0 && i < arr.length) // Skip first and last empty from leading/trailing |
}
function convertUnorderedLists(html: string): string {
const lines = html.split('\n')
const result: string[] = []
let inList = false
for (const line of lines) {
const listMatch = line.match(/^(\s*)[-*]\s+(.+)$/)
if (listMatch) {
if (!inList) {
result.push('<ul>')
inList = true
}
result.push(`<li>${listMatch[2]}</li>`)
} else {
if (inList) {
result.push('</ul>')
inList = false
}
result.push(line)
}
}
if (inList) result.push('</ul>')
return result.join('\n')
}
function convertOrderedLists(html: string): string {
const lines = html.split('\n')
const result: string[] = []
let inList = false
for (const line of lines) {
const listMatch = line.match(/^(\s*)\d+\.\s+(.+)$/)
if (listMatch) {
if (!inList) {
result.push('<ol>')
inList = true
}
result.push(`<li>${listMatch[2]}</li>`)
} else {
if (inList) {
result.push('</ol>')
inList = false
}
result.push(line)
}
}
if (inList) result.push('</ol>')
return result.join('\n')
}
function wrapParagraphs(html: string): string {
const blockTags = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote', 'pre', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr', 'p', 'div', 'img'])
const lines = html.split('\n')
const result: string[] = []
let buffer: string[] = []
const flushBuffer = () => {
const text = buffer.join('\n').trim()
if (text) {
// Don't double-wrap if already starts with a block tag
const firstTag = text.match(/^<(\w+)/)
if (firstTag && blockTags.has(firstTag[1].toLowerCase())) {
result.push(text)
} else {
result.push(`<p>${text}</p>`)
}
}
buffer = []
}
for (const line of lines) {
const trimmed = line.trim()
// Check if this line is a block-level element
const isBlockLine = trimmed.startsWith('<') && (() => {
const tag = trimmed.match(/^<(\w+)/)
return tag ? blockTags.has(tag[1].toLowerCase()) : false
})()
if (isBlockLine || trimmed === '') {
flushBuffer()
if (isBlockLine) result.push(trimmed)
} else {
buffer.push(trimmed)
}
}
flushBuffer()
return result.join('\n')
}

View File

@@ -28,6 +28,7 @@ export interface Label {
id: string;
name: string;
color: LabelColorName;
type?: 'ai' | 'user'; // "ai" for auto-generated, "user" for manually created
notebookId: string | null; // NEW: Belongs to a notebook
userId?: string | null; // DEPRECATED: Kept for backward compatibility
createdAt: Date;