fix(chart): improve error handling and color variety
- Add quotaExceeded flag to response for better error UX - Show dedicated quota exceeded state with upgrade button - Improve AI prompt to better detect data patterns - Add chart type-specific colors (blue, indigo, emerald, violet, etc.) - Replace generic primary/10 colors with varied accent colors Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1027,6 +1027,38 @@ You MUST use the task_extract tool. Do NOT respond with text, call the tool dire
|
||||
},
|
||||
}
|
||||
|
||||
function extractJsonFromText(text: string): any {
|
||||
if (!text) return null
|
||||
|
||||
// Try direct parsing first
|
||||
try {
|
||||
const parsed = JSON.parse(text.trim())
|
||||
if (parsed && typeof parsed === 'object') return parsed
|
||||
} catch (e) {}
|
||||
|
||||
// Try extracting markdown code block
|
||||
const jsonBlockRegex = /```json\s*([\s\S]*?)\s*```/i
|
||||
const match = text.match(jsonBlockRegex)
|
||||
if (match && match[1]) {
|
||||
try {
|
||||
const parsed = JSON.parse(match[1].trim())
|
||||
if (parsed && typeof parsed === 'object') return parsed
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Try extracting any { ... } or [ ... ] block
|
||||
const braceRegex = /(\{[\s\S]*\}|\[[\s\S]*\])/
|
||||
const braceMatch = text.match(braceRegex)
|
||||
if (braceMatch && braceMatch[1]) {
|
||||
try {
|
||||
const parsed = JSON.parse(braceMatch[1].trim())
|
||||
if (parsed && typeof parsed === 'object') return parsed
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// --- Tool-Use Agent ---
|
||||
|
||||
async function executeToolUseAgent(
|
||||
@@ -1354,6 +1386,15 @@ async function executeToolUseAgent(
|
||||
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
// Check if AI already created a note via note_create tool
|
||||
// Or if excalidraw/slide generator created a canvas
|
||||
let existingNoteId: string | null = null
|
||||
let canvasId: string | null = null
|
||||
const scrapedUrls: string[] = []
|
||||
let specificToolCalled = false
|
||||
let fallbackSuccess = false
|
||||
let parsedFallbackJson: any = null
|
||||
|
||||
// Détecte si le modèle ne supporte pas le function calling
|
||||
// (il retourne le JSON de l'outil comme texte brut au lieu de l'exécuter)
|
||||
const totalToolCallsCheck = result.steps.reduce((acc, s) => acc + s.toolCalls.length, 0)
|
||||
@@ -1371,16 +1412,96 @@ async function executeToolUseAgent(
|
||||
}
|
||||
if (agentType === 'slide-generator' || agentType === 'excalidraw-generator') {
|
||||
const toolName = agentType === 'slide-generator' ? 'generate_slides' : 'generate_excalidraw'
|
||||
await prisma.agentAction.update({
|
||||
where: { id: actionId },
|
||||
data: {
|
||||
status: 'failure',
|
||||
log: lang === 'fr'
|
||||
? `L'IA n'a pas appelé l'outil ${toolName}. Le modèle a répondu avec du texte au lieu de générer le fichier. Modèle: "${sysConfig.AI_MODEL_CHAT}". Essayez un modèle compatible avec le function calling.`
|
||||
: `The AI did not call the ${toolName} tool. The model responded with text instead of generating the file. Model: "${sysConfig.AI_MODEL_CHAT}". Try a model that supports function calling.`,
|
||||
parsedFallbackJson = extractJsonFromText(result.text)
|
||||
|
||||
if (parsedFallbackJson) {
|
||||
try {
|
||||
if (agentType === 'slide-generator') {
|
||||
let slides: any[] = []
|
||||
let title = agent.name || "Présentation"
|
||||
let theme = agent.slideTheme || "architectural-saas"
|
||||
|
||||
if (Array.isArray(parsedFallbackJson)) {
|
||||
slides = parsedFallbackJson
|
||||
} else if (parsedFallbackJson && typeof parsedFallbackJson === 'object') {
|
||||
if (Array.isArray(parsedFallbackJson.slides)) {
|
||||
slides = parsedFallbackJson.slides
|
||||
} else if (parsedFallbackJson.slides && typeof parsedFallbackJson.slides === 'object') {
|
||||
// nested structure support
|
||||
} else {
|
||||
if (parsedFallbackJson.type) {
|
||||
slides = [parsedFallbackJson]
|
||||
} else {
|
||||
const arrays = Object.values(parsedFallbackJson).filter(val => Array.isArray(val))
|
||||
if (arrays.length > 0) {
|
||||
slides = arrays[0] as any[]
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof parsedFallbackJson.title === 'string') title = parsedFallbackJson.title
|
||||
if (typeof parsedFallbackJson.theme === 'string') theme = parsedFallbackJson.theme
|
||||
}
|
||||
|
||||
const registered = toolRegistry.get('generate_slides')
|
||||
if (registered) {
|
||||
console.log('[AgentExecutor] Running manual fallback execution for generate_slides')
|
||||
const slideTool = registered.buildTool(ctx)
|
||||
const executionResult = await slideTool.execute({ title, theme, slides })
|
||||
if (executionResult && executionResult.success && executionResult.canvasId) {
|
||||
canvasId = executionResult.canvasId
|
||||
specificToolCalled = true
|
||||
fallbackSuccess = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let diagramStr = ""
|
||||
let title = agent.name || "Diagramme"
|
||||
if (parsedFallbackJson && typeof parsedFallbackJson === 'object') {
|
||||
if (typeof parsedFallbackJson.diagram === 'string') {
|
||||
diagramStr = parsedFallbackJson.diagram
|
||||
if (typeof parsedFallbackJson.title === 'string') title = parsedFallbackJson.title
|
||||
} else if (parsedFallbackJson.diagram && typeof parsedFallbackJson.diagram === 'object') {
|
||||
diagramStr = JSON.stringify(parsedFallbackJson.diagram)
|
||||
if (typeof parsedFallbackJson.title === 'string') title = parsedFallbackJson.title
|
||||
} else {
|
||||
diagramStr = JSON.stringify(parsedFallbackJson)
|
||||
if (typeof parsedFallbackJson.title === 'string') title = parsedFallbackJson.title
|
||||
}
|
||||
} else if (typeof parsedFallbackJson === 'string') {
|
||||
diagramStr = parsedFallbackJson
|
||||
}
|
||||
|
||||
if (diagramStr) {
|
||||
const registered = toolRegistry.get('generate_excalidraw')
|
||||
if (registered) {
|
||||
console.log('[AgentExecutor] Running manual fallback execution for generate_excalidraw')
|
||||
const excalidrawTool = registered.buildTool(ctx)
|
||||
const executionResult = await excalidrawTool.execute({ title, diagram: diagramStr })
|
||||
if (executionResult && executionResult.success && executionResult.canvasId) {
|
||||
canvasId = executionResult.canvasId
|
||||
specificToolCalled = true
|
||||
fallbackSuccess = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[AgentExecutor] Fallback execution failed:', err)
|
||||
}
|
||||
})
|
||||
return { success: false, actionId, error: `AI did not call ${toolName} tool` }
|
||||
}
|
||||
|
||||
if (!fallbackSuccess) {
|
||||
await prisma.agentAction.update({
|
||||
where: { id: actionId },
|
||||
data: {
|
||||
status: 'failure',
|
||||
log: lang === 'fr'
|
||||
? `L'IA n'a pas appelé l'outil ${toolName}. Le modèle a répondu avec du texte au lieu de générer le fichier. Modèle: "${sysConfig.AI_MODEL_CHAT}". Essayez un modèle compatible avec le function calling.`
|
||||
: `The AI did not call the ${toolName} tool. The model responded with text instead of generating the file. Model: "${sysConfig.AI_MODEL_CHAT}". Try a model that supports function calling.`,
|
||||
}
|
||||
})
|
||||
return { success: false, actionId, error: `AI did not call ${toolName} tool` }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1395,12 +1516,27 @@ async function executeToolUseAgent(
|
||||
})),
|
||||
}))
|
||||
|
||||
// Check if AI already created a note via note_create tool
|
||||
// Or if excalidraw/slide generator created a canvas
|
||||
let existingNoteId: string | null = null
|
||||
let canvasId: string | null = null
|
||||
const scrapedUrls: string[] = []
|
||||
let specificToolCalled = false
|
||||
if (fallbackSuccess) {
|
||||
toolLog.push({
|
||||
step: toolLog.length + 1,
|
||||
text: "Manual JSON parsing & fallback execution succeeded.",
|
||||
toolCalls: [{
|
||||
id: "fallback",
|
||||
type: "function",
|
||||
function: {
|
||||
name: agentType === 'slide-generator' ? 'generate_slides' : 'generate_excalidraw',
|
||||
arguments: JSON.stringify(parsedFallbackJson),
|
||||
}
|
||||
}] as any,
|
||||
toolResults: [{
|
||||
toolCallId: "fallback",
|
||||
toolName: agentType === 'slide-generator' ? 'generate_slides' : 'generate_excalidraw',
|
||||
type: "tool-result",
|
||||
result: { success: true, canvasId }
|
||||
}] as any
|
||||
})
|
||||
}
|
||||
|
||||
const requiredTool = isFileGenerator
|
||||
? (agentType === 'slide-generator' ? ['generate_slides'] : ['generate_excalidraw'])
|
||||
: null
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface SuggestChartsResponse {
|
||||
detectedData: string
|
||||
hasData: boolean
|
||||
error?: string
|
||||
quotaExceeded?: boolean
|
||||
}
|
||||
|
||||
export interface SuggestChartsRequest {
|
||||
@@ -42,12 +43,16 @@ export async function suggestCharts(request: SuggestChartsRequest): Promise<Sugg
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }))
|
||||
const isQuotaError = response.status === 402 || errorData.error === 'QUOTA_EXCEEDED'
|
||||
return {
|
||||
suggestions: [],
|
||||
analyzedText: '',
|
||||
detectedData: '',
|
||||
hasData: false,
|
||||
error: errorData.error || `HTTP ${response.status}`,
|
||||
error: isQuotaError
|
||||
? 'AI quota exceeded. Please upgrade your plan to continue using AI features.'
|
||||
: (errorData.error || `HTTP ${response.status}`),
|
||||
quotaExceeded: isQuotaError,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,14 +8,14 @@ import { z } from 'zod'
|
||||
import { toolRegistry } from './registry'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
// Simple chart insertion tool - does everything in one call
|
||||
// Simple chart generation tool - returns markdown chart
|
||||
toolRegistry.register({
|
||||
name: 'insert_chart',
|
||||
description: 'Generate a chart and insert it directly into a note. Use when the user asks for a chart, graph, or visualization.',
|
||||
description: 'Generate a chart and return it as markdown. Use when the user asks for a chart, graph, or visualization.',
|
||||
isInternal: true,
|
||||
buildTool: (ctx) =>
|
||||
tool({
|
||||
description: `Generate a chart and insert it directly into the note content.
|
||||
description: `Generate a chart as markdown that will be rendered as an interactive chart.
|
||||
|
||||
Available chart types:
|
||||
- "bar": Vertical bar chart (default for comparisons)
|
||||
@@ -25,11 +25,9 @@ Available chart types:
|
||||
- "pie": Pie chart (use for proportions/percentages)
|
||||
- "radar": Radar chart (use for comparing multiple dimensions)
|
||||
|
||||
IMPORTANT: When the user asks for a chart/graph/visualization:
|
||||
1. Extract the data from the note or user request
|
||||
2. Choose the appropriate chart type based on the data
|
||||
3. Generate the chart markdown using this format:
|
||||
CRITICAL - NEVER use Mermaid, flowchart, or other markdown diagram formats. ALWAYS use this tool to generate charts.
|
||||
|
||||
Chart format:
|
||||
\`\`\`chart
|
||||
{chartType}
|
||||
{title}
|
||||
@@ -47,38 +45,22 @@ Feb: 7500
|
||||
Mar: 6200
|
||||
\`\`\`
|
||||
|
||||
4. Call this tool with the noteId, the chart markdown, and where to insert it`,
|
||||
Example for "show MRR progression":
|
||||
\`\`\`chart
|
||||
line
|
||||
MRR Growth
|
||||
Month 1: 2000
|
||||
Month 2: 4500
|
||||
Month 3: 8900
|
||||
\`\`\``,
|
||||
inputSchema: z.object({
|
||||
noteId: z.string().describe('The note ID to update'),
|
||||
chartMarkdown: z.string().describe('The complete chart markdown block to insert (including the ```chart fences)'),
|
||||
insertLocation: z.enum(['append', 'prepend']).default('append').describe('append: add to end, prepend: add to start'),
|
||||
chartMarkdown: z.string().describe('The complete chart markdown block to render (including the ```chart fences)'),
|
||||
}),
|
||||
execute: async ({ noteId, chartMarkdown, insertLocation }) => {
|
||||
try {
|
||||
const note = await prisma.note.findFirst({
|
||||
where: { id: noteId, userId: ctx.userId },
|
||||
select: { content: true },
|
||||
})
|
||||
|
||||
if (!note) return { error: 'Note not found' }
|
||||
|
||||
const updatedContent = insertLocation === 'append'
|
||||
? `${note.content}\n\n${chartMarkdown}`
|
||||
: `${chartMarkdown}\n\n${note.content}`
|
||||
|
||||
await prisma.note.update({
|
||||
where: { id: noteId },
|
||||
data: { content: updatedContent },
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Chart ${insertLocation === 'append' ? 'appended' : 'prepended'} to note.`,
|
||||
chartMarkdown,
|
||||
}
|
||||
} catch (e: any) {
|
||||
return { success: false, error: `Failed: ${e.message}` }
|
||||
execute: async ({ chartMarkdown }) => {
|
||||
return {
|
||||
chartMarkdown,
|
||||
message: 'Chart generated successfully',
|
||||
}
|
||||
},
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface ToolContext {
|
||||
actionId?: string
|
||||
conversationId?: string
|
||||
notebookId?: string
|
||||
noteId?: string
|
||||
webSearch?: boolean
|
||||
config: Record<string, string>
|
||||
}
|
||||
|
||||
@@ -43,6 +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',
|
||||
}
|
||||
|
||||
export const THEME_NAMES: Record<string, string> = {
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
*/
|
||||
import { normalizeThemeId } from './apply-document-theme'
|
||||
|
||||
export function getThemeScript(serverTheme: string = 'light') {
|
||||
export function getThemeScript(serverTheme: string = 'light', serverAccentColor: string | null = null) {
|
||||
const fallback = normalizeThemeId(serverTheme)
|
||||
const defaultAccent = serverAccentColor || '#A47148'
|
||||
return `
|
||||
(function() {
|
||||
try {
|
||||
@@ -28,7 +29,8 @@ export function getThemeScript(serverTheme: string = 'light') {
|
||||
if (theme === 'midnight') root.classList.add('dark');
|
||||
}
|
||||
var accentStored = localStorage.getItem('accent-color');
|
||||
if (accentStored) root.style.setProperty('--color-brand-accent', accentStored);
|
||||
var effectiveAccent = accentStored || ${JSON.stringify(defaultAccent)};
|
||||
root.style.setProperty('--color-brand-accent', effectiveAccent);
|
||||
} catch (e) {
|
||||
console.error('Theme script error', e);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user