diff --git a/apps/backend/src/agents/tools/display-map.ts b/apps/backend/src/agents/tools/display-map.ts new file mode 100644 index 000000000..2ff2fea84 --- /dev/null +++ b/apps/backend/src/agents/tools/display-map.ts @@ -0,0 +1,80 @@ +import { buildMapPoints, MAX_MAP_POINTS, resolveColumnName } from '@nao/shared'; +import { displayMap } from '@nao/shared/tools'; + +import { DisplayMapOutput, renderToModelOutput } from '../../components/tool-outputs'; +import { getQueryResult } from '../../services/query-result.service'; +import { createTool } from '../../utils/tools'; + +export default createTool({ + description: + 'Display an interactive map of geographic data from a previous `execute_sql` tool call. The query result must contain latitude and longitude columns in decimal degrees.', + inputSchema: displayMap.InputSchema, + outputSchema: displayMap.OutputSchema, + + execute: async (input, context) => { + const queryResult = await getQueryResult(context, input.query_id); + if (!queryResult) { + return { + _version: '1', + success: false, + error: `Query result "${input.query_id}" not found. Run \`execute_sql\` first and use the id from its output.`, + }; + } + + const latitudeColumn = resolveColumnName(queryResult.columns, input.latitude_key); + const longitudeColumn = resolveColumnName(queryResult.columns, input.longitude_key); + const missingColumn = [latitudeColumn, longitudeColumn].find((column) => !queryResult.columns.includes(column)); + if (missingColumn) { + return { + _version: '1', + success: false, + error: `Column "${missingColumn}" not found in the query result. Available columns: ${queryResult.columns.join(', ')}.`, + }; + } + if (latitudeColumn === longitudeColumn) { + return { + _version: '1', + success: false, + error: 'latitude_key and longitude_key must reference different columns.', + }; + } + + const points = buildMapPoints(queryResult.data, { + ...input, + latitude_key: latitudeColumn, + longitude_key: longitudeColumn, + }); + if (points.length === 0) { + return { + _version: '1', + success: false, + error: 'The query result contains no rows with valid decimal-degree coordinates, so there is nothing to plot.', + }; + } + + const warnings: string[] = []; + const missingPopupKeys = [input.label_key, ...(input.tooltip_keys ?? [])] + .filter((key): key is string => !!key) + .filter((key) => !queryResult.columns.includes(resolveColumnName(queryResult.columns, key))); + if (missingPopupKeys.length > 0) { + warnings.push( + `Popup column(s) ${missingPopupKeys.map((key) => `"${key}"`).join(', ')} not found in the query result, so popups will not show them. Available columns: ${queryResult.columns.join(', ')}.`, + ); + } + if (points.length > MAX_MAP_POINTS) { + warnings.push( + `The map only renders the first ${MAX_MAP_POINTS} points — mention this to the user, or aggregate in SQL to stay under the limit.`, + ); + } + + return { + _version: '1', + success: true, + point_count: points.length, + dropped_row_count: queryResult.data.length - points.length, + ...(warnings.length > 0 && { warning: warnings.join(' ') }), + }; + }, + + toModelOutput: ({ output }) => renderToModelOutput(DisplayMapOutput({ output }), output), +}); diff --git a/apps/backend/src/agents/tools/index.ts b/apps/backend/src/agents/tools/index.ts index 737037d1a..6562963ae 100644 --- a/apps/backend/src/agents/tools/index.ts +++ b/apps/backend/src/agents/tools/index.ts @@ -7,6 +7,7 @@ import { mcpService } from '../../services/mcp'; import { AgentSettings } from '../../types/agent-settings'; import clarification from './clarification'; import displayChart from './display-chart'; +import displayMap from './display-map'; import executePython from './execute-python'; import executeSandboxedCode from './execute-sandboxed-code'; import executeSql from './execute-sql'; @@ -19,10 +20,18 @@ import search from './search'; import story from './story'; import suggestFollowUps from './suggest-follow-ups'; +/** + * Tools whose output only the web chat can render — excluded from automations and the MCP sub-agent. + * Messaging providers keep them and degrade to an "open in nao" link (a card on Slack/Teams/Telegram, + * a plain-text link on WhatsApp). + */ +export const WEB_CHAT_ONLY_TOOLS = ['display_map']; + export const tools = { story, clarification, display_chart: displayChart, + display_map: displayMap, ...(executePython && { execute_python: executePython }), ...(executeSandboxedCode && { execute_sandboxed_code: executeSandboxedCode }), execute_sql: executeSql, @@ -49,6 +58,12 @@ export const getTools = ( * only discover context, not query the warehouse or render charts. */ builtinToolAllowlist?: string[]; + /** + * Drops these built-in tools from the returned set. Used by runs whose + * surface cannot render a tool's output (e.g. `display_map` outside the + * web chat: automations, MCP sub-agent, WhatsApp). + */ + excludeBuiltinTools?: string[]; } = {}, ) => { const configuredServers = new Set(mcpService.getConfiguredServerNames()); @@ -66,6 +81,7 @@ export const getTools = ( execute_sandboxed_code, clarification: clarificationTool, suggest_follow_ups, + display_map: displayMapTool, ...rest } = tools; const baseTools = options.excludeFollowUps ? rest : { ...rest, suggest_follow_ups }; @@ -76,13 +92,30 @@ export const getTools = ( ...mcpTools, ...(agentSettings?.experimental?.pythonSandboxing && execute_python && { execute_python }), ...(agentSettings?.experimental?.sandboxes && execute_sandboxed_code && { execute_sandboxed_code }), + ...(agentSettings?.experimental?.displayMap && { display_map: displayMapTool }), ...extraTools, }; + let result = allTools; if (options.builtinToolAllowlist) { - const allowed = new Set([...options.builtinToolAllowlist, ...Object.keys(extraTools ?? {})]); - return Object.fromEntries(Object.entries(allTools).filter(([name]) => allowed.has(name))) as typeof allTools; + const allowed = new Set(options.builtinToolAllowlist); + result = keepTools(result, extraTools, (name) => allowed.has(name)); } + if (options.excludeBuiltinTools) { + const excluded = new Set(options.excludeBuiltinTools); + result = keepTools(result, extraTools, (name) => !excluded.has(name)); + } + + return result; +}; - return allTools; +const keepTools = >( + toolset: T, + extraTools: Record | undefined, + shouldKeep: (name: string) => boolean, +): T => { + const extraToolNames = new Set(Object.keys(extraTools ?? {})); + return Object.fromEntries( + Object.entries(toolset).filter(([name]) => shouldKeep(name) || extraToolNames.has(name)), + ) as T; }; diff --git a/apps/backend/src/components/ai/system-prompt.tsx b/apps/backend/src/components/ai/system-prompt.tsx index 737466432..579ea6d14 100644 --- a/apps/backend/src/components/ai/system-prompt.tsx +++ b/apps/backend/src/components/ai/system-prompt.tsx @@ -22,6 +22,8 @@ type SystemPromptProps = { mcpServers?: string[]; timezone?: string; testMode?: boolean; + /** Names of the tools in the run's tool set — rules for surface-dependent tools (e.g. display_map) are only emitted when the tool is present. Omit to include every rule. */ + toolNames?: string[]; }; export const MEMORY_TOKEN_LIMIT = 1000; @@ -34,7 +36,9 @@ export function SystemPrompt({ mcpServers = [], timezone, testMode, + toolNames, }: SystemPromptProps) { + const hasTool = (name: string) => !toolNames || toolNames.includes(name); const visibleMemories = getMemoriesInTokenRange(memories, MEMORY_TOKEN_LIMIT); const dialectToolCallRules = getDialectToolCallRules(connections); const dialectSqlQueryRules = getDialectSqlQueryRules(connections); @@ -97,6 +101,13 @@ export function SystemPrompt({ Chart Rules + {hasTool('display_map') && ( + + Use display_map instead of display_chart for spatial point data: query results with latitude and + longitude columns (store locations, delivery points, events). Coordinates must be in decimal + degrees — select or convert them in SQL if needed. + + )} For display_chart x_axis_type: use "date" only when x-axis values are parseable by JavaScript Date (e.g. YYYY-MM-DD). Use "category" for quarter labels (quarter_ending), fiscal periods (FY25-Q1), or diff --git a/apps/backend/src/components/tool-outputs/display-map.tsx b/apps/backend/src/components/tool-outputs/display-map.tsx new file mode 100644 index 000000000..f0e5b318d --- /dev/null +++ b/apps/backend/src/components/tool-outputs/display-map.tsx @@ -0,0 +1,20 @@ +import type { displayMap } from '@nao/shared/tools'; + +import { Block } from '../../lib/markdown'; + +export function DisplayMapOutput({ output }: { output: displayMap.Output }) { + if (output.error) { + return Could not display the map: {output.error}; + } + const points = output.point_count != null ? ` with ${output.point_count} point(s)` : ''; + const dropped = output.dropped_row_count + ? ` ${output.dropped_row_count} row(s) were dropped for missing or invalid coordinates — adjust the SQL if they should appear.` + : ''; + const warning = output.warning ? ` Warning: ${output.warning}` : ''; + return ( + + Map displayed successfully{points}.{dropped} + {warning} + + ); +} diff --git a/apps/backend/src/components/tool-outputs/index.ts b/apps/backend/src/components/tool-outputs/index.ts index 30550df3c..7f94c6b5f 100644 --- a/apps/backend/src/components/tool-outputs/index.ts +++ b/apps/backend/src/components/tool-outputs/index.ts @@ -4,6 +4,7 @@ import type { ReactNode } from 'react'; import { renderToMarkdown } from '../../lib/markdown/render-to-markdown'; export { DisplayChartOutput } from './display-chart'; +export { DisplayMapOutput } from './display-map'; export { ExecuteSqlOutput } from './execute-sql'; export { GrepOutput } from './grep'; export { ListOutput } from './list'; diff --git a/apps/backend/src/handlers/automation.handler.ts b/apps/backend/src/handlers/automation.handler.ts index 458f5531f..3f5515840 100644 --- a/apps/backend/src/handlers/automation.handler.ts +++ b/apps/backend/src/handlers/automation.handler.ts @@ -1,7 +1,7 @@ import type { InferUIMessageChunk } from 'ai'; import { getToolName, isToolUIPart, readUIMessageStream } from 'ai'; -import { getTools } from '../agents/tools'; +import { getTools, WEB_CHAT_ONLY_TOOLS } from '../agents/tools'; import { renderAutomationRunPrompt } from '../components/ai/automation-run-prompt'; import type { DBAutomationRun, DBScheduledJob } from '../db/abstractSchema'; import type { AutomationWithSchedule } from '../queries/automation.queries'; @@ -142,6 +142,7 @@ async function finishAutomationRun(automation: AutomationWithSchedule, run: DBAu mcpEnabled: automation.mcpEnabled, mcpServers: automation.mcpServers, excludeFollowUps: true, + excludeBuiltinTools: WEB_CHAT_ONLY_TOOLS, }, ), }, diff --git a/apps/backend/src/mcp/tools/sub-agent.ts b/apps/backend/src/mcp/tools/sub-agent.ts index 6c8752938..dce033f3a 100644 --- a/apps/backend/src/mcp/tools/sub-agent.ts +++ b/apps/backend/src/mcp/tools/sub-agent.ts @@ -2,9 +2,10 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { type InferUIMessageChunk, readUIMessageStream } from 'ai'; import { z } from 'zod'; +import { WEB_CHAT_ONLY_TOOLS } from '../../agents/tools'; import * as chatQueries from '../../queries/chat.queries'; import * as storyQueries from '../../queries/story.queries'; -import { agentService } from '../../services/agent'; +import { agentService, defaultAgentToolsExcluding } from '../../services/agent'; import { mcpService } from '../../services/mcp'; import { skillService } from '../../services/skill'; import type { UIMessage, UIMessagePart } from '../../types/chat'; @@ -129,7 +130,9 @@ export function registerSubAgentTools(server: McpServer, ctx: McpContext): void const { chat, uiMessages } = await buildChatContext(ctx.projectId, ctx.userId, question, chatId); const naoChatUrl = chatUrl(chat.id); - const agent = await agentService.create(chat); + const agent = await agentService.create(chat, undefined, { + tools: defaultAgentToolsExcluding(WEB_CHAT_ONLY_TOOLS), + }); askNaoRuns.start(chat.id); const runPromise = runAskNaoInBackground(agent, uiMessages, chat.id, naoChatUrl); diff --git a/apps/backend/src/services/agent.ts b/apps/backend/src/services/agent.ts index d84718e51..4bee67cf8 100644 --- a/apps/backend/src/services/agent.ts +++ b/apps/backend/src/services/agent.ts @@ -114,6 +114,12 @@ export type AgentToolsResolver = (context: AgentToolsContext) => AgentTools | Pr export const defaultAgentTools: AgentToolsResolver = ({ chat, agentSettings, webTools }) => getTools(agentSettings, webTools ?? {}, { testMode: chat.testMode }); +/** Default tool set minus the given built-ins — for runs whose surface cannot render them. */ +export const defaultAgentToolsExcluding = + (excludeBuiltinTools: string[]): AgentToolsResolver => + ({ chat, agentSettings, webTools }) => + getTools(agentSettings, webTools ?? {}, { testMode: chat.testMode, excludeBuiltinTools }); + /** * Admin-mode tool set: the same `execute_sql` tool the chat already uses (it * runs against nao's own app database when `ToolContext.adminMode` is set), @@ -589,6 +595,7 @@ class AgentManager { mcpServers, timezone, testMode: this.chat.testMode, + toolNames: Object.keys(this._agentTools), }), ); const renderedPrompt = provider diff --git a/apps/backend/src/services/slack.ts b/apps/backend/src/services/slack.ts index 519e24be6..2201566a1 100644 --- a/apps/backend/src/services/slack.ts +++ b/apps/backend/src/services/slack.ts @@ -30,6 +30,7 @@ import { createFeedbackModal, createImageBlock, createLiveToolCall, + createMapLinkCard, createStopButtonCard, createSummaryToolCalls, createTextBlock, @@ -665,7 +666,7 @@ class ProjectSlackBot { ctx: ConversationContext, ): Promise { const state: StreamState = { - renderedChartIds: new Set(), + renderedToolCallIds: new Set(), sqlOutputs: new Map(), lastUpdateAt: Date.now(), toolGroup: new Map(), @@ -691,6 +692,8 @@ class ProjectSlackBot { this._handleSqlPart(part, state); } else if (part.type === 'tool-display_chart') { await this._handleChartPart(part, state, ctx); + } else if (part.type === 'tool-display_map') { + await this._handleMapPart(part, state, ctx); } } @@ -725,7 +728,7 @@ class ProjectSlackBot { state: StreamState, ctx: ConversationContext, ): Promise { - if (part.state !== 'output-available' || state.renderedChartIds.has(part.toolCallId)) { + if (part.state !== 'output-available' || state.renderedToolCallIds.has(part.toolCallId)) { return; } if (part.input.chart_type === 'table') { @@ -743,7 +746,7 @@ class ProjectSlackBot { dateFormat: displaySettings.dateFormat, }); const chartId = await chartImageQueries.saveChart(part.toolCallId, png.toString('base64')); - state.renderedChartIds.add(part.toolCallId); + state.renderedToolCallIds.add(part.toolCallId); if (this._config.transportMode === 'socket') { await this._uploadChartImageFile(png, sqlOutput.name, ctx); @@ -763,6 +766,33 @@ class ProjectSlackBot { } } + private async _handleMapPart( + part: Extract, + state: StreamState, + ctx: ConversationContext, + ): Promise { + if ( + part.state !== 'output-available' || + !part.output.success || + state.renderedToolCallIds.has(part.toolCallId) + ) { + return; + } + state.renderedToolCallIds.add(part.toolCallId); + try { + const chatUrl = new URL(ctx.chatId, this._redirectUrl).toString(); + ctx.textBlockIndex = -1; + ctx.textBlockCount = 0; + ctx.blocks.push(...createMapLinkCard(part.input.title, chatUrl)); + await ctx.convMessage?.edit(Card({ children: ctx.blocks })); + } catch (error) { + logger.error(`Map link card failed: ${String(error)}`, { + source: 'system', + context: { chatId: ctx.chatId, toolCallId: part.toolCallId }, + }); + } + } + private async _uploadChartImageFile(png: Buffer, name: string | null, ctx: ConversationContext): Promise { const [, channelId, threadTs] = ctx.thread.id.split(':'); const filename = name ? `${name.toLowerCase().replace(/\s+/g, '_')}.png` : 'chart.png'; diff --git a/apps/backend/src/services/teams.ts b/apps/backend/src/services/teams.ts index 0ee884c92..bdb03804b 100644 --- a/apps/backend/src/services/teams.ts +++ b/apps/backend/src/services/teams.ts @@ -23,6 +23,7 @@ import { createCompletionCard, createImageBlock, createLiveToolCall, + createMapLinkCard, createStopButtonCard, createSummaryToolCalls, createTextBlock, @@ -291,7 +292,7 @@ class TeamsService { ctx: ConversationContext, ): Promise { const state: StreamState = { - renderedChartIds: new Set(), + renderedToolCallIds: new Set(), sqlOutputs: new Map(), lastUpdateAt: Date.now(), toolGroup: new Map(), @@ -319,6 +320,8 @@ class TeamsService { this._handleSqlPart(part, state); } else if (part.type === 'tool-display_chart') { await this._handleChartPart(part, state, ctx); + } else if (part.type === 'tool-display_map') { + await this._handleMapPart(part, state, ctx); } lastMessage = uiMessage; } @@ -354,7 +357,7 @@ class TeamsService { state: StreamState, ctx: ConversationContext, ): Promise { - if (part.state !== 'output-available' || state.renderedChartIds.has(part.toolCallId)) { + if (part.state !== 'output-available' || state.renderedToolCallIds.has(part.toolCallId)) { return; } if (part.input.chart_type === 'table') { @@ -371,7 +374,7 @@ class TeamsService { data: sqlOutput.rows, dateFormat: displaySettings.dateFormat, }); - state.renderedChartIds.add(part.toolCallId); + state.renderedToolCallIds.add(part.toolCallId); const chartId = await chartImageQueries.saveChart(part.toolCallId, png.toString('base64')); const imageUrl = new URL(`c/${ctx.chatId}/${chartId}.png`, this._redirectUrl).toString(); @@ -386,6 +389,32 @@ class TeamsService { } } + private async _handleMapPart( + part: Extract, + state: StreamState, + ctx: ConversationContext, + ): Promise { + if ( + part.state !== 'output-available' || + !part.output.success || + state.renderedToolCallIds.has(part.toolCallId) + ) { + return; + } + state.renderedToolCallIds.add(part.toolCallId); + try { + const chatUrl = new URL(ctx.chatId, this._redirectUrl).toString(); + ctx.textBlockIndex = -1; + ctx.blocks.push(...createMapLinkCard(part.input.title, chatUrl)); + await ctx.convMessage?.edit(Card({ children: ctx.blocks })); + } catch (error) { + logger.error(`Map link card failed: ${String(error)}`, { + source: 'system', + context: { chatId: ctx.chatId, toolCallId: part.toolCallId }, + }); + } + } + private async _handleCollapsibleToolPart( part: Extract, state: StreamState, diff --git a/apps/backend/src/services/telegram.ts b/apps/backend/src/services/telegram.ts index 7ac32b66c..77f20dc81 100644 --- a/apps/backend/src/services/telegram.ts +++ b/apps/backend/src/services/telegram.ts @@ -19,6 +19,7 @@ import { createPlainTextBlock, createSummaryToolCalls, createTelegramCompletionCard, + createTelegramMapLinkCard, createTelegramStopButtonCard, EXCLUDED_TOOLS, formatMessagingError, @@ -288,7 +289,7 @@ class TelegramService { ctx: ConversationContext, ): Promise { const state: StreamState = { - renderedChartIds: new Set(), + renderedToolCallIds: new Set(), sqlOutputs: new Map(), lastUpdateAt: Date.now(), toolGroup: new Map(), @@ -316,6 +317,8 @@ class TelegramService { this._handleSqlPart(part, state); } else if (part.type === 'tool-display_chart') { await this._handleChartPart(part, state, ctx); + } else if (part.type === 'tool-display_map') { + await this._handleMapPart(part, state, ctx); } lastMessage = uiMessage; } @@ -367,7 +370,7 @@ class TelegramService { state: StreamState, ctx: ConversationContext, ): Promise { - if (part.state !== 'output-available' || state.renderedChartIds.has(part.toolCallId)) { + if (part.state !== 'output-available' || state.renderedToolCallIds.has(part.toolCallId)) { return; } if (part.input.chart_type === 'table') { @@ -384,7 +387,7 @@ class TelegramService { data: sqlOutput.rows, dateFormat: displaySettings.dateFormat, }); - state.renderedChartIds.add(part.toolCallId); + state.renderedToolCallIds.add(part.toolCallId); ctx.textBlockIndex = -1; await ctx.thread.post({ @@ -400,6 +403,31 @@ class TelegramService { } } + private async _handleMapPart( + part: Extract, + state: StreamState, + ctx: ConversationContext, + ): Promise { + if ( + part.state !== 'output-available' || + !part.output.success || + state.renderedToolCallIds.has(part.toolCallId) + ) { + return; + } + try { + state.renderedToolCallIds.add(part.toolCallId); + const chatUrl = new URL(ctx.chatId, this._redirectUrl).toString(); + ctx.textBlockIndex = -1; + ctx.blocks.push(...createTelegramMapLinkCard(part.input.title, chatUrl)); + if (ctx.convMessage) { + await this._safeEdit(ctx.convMessage, Card({ children: ctx.blocks })); + } + } catch (error) { + console.error('Error rendering map link card:', error); + } + } + private async _handleCollapsibleToolPart( part: Extract, state: StreamState, diff --git a/apps/backend/src/services/whatsapp.ts b/apps/backend/src/services/whatsapp.ts index 148f8f2da..5af9dfeec 100644 --- a/apps/backend/src/services/whatsapp.ts +++ b/apps/backend/src/services/whatsapp.ts @@ -20,7 +20,7 @@ import { ConversationContext, StreamState, ToolCallEntry } from '../types/messag import { createChatTitle } from '../utils/ai'; import { buildImageUrl } from '../utils/image'; import { logger } from '../utils/logger'; -import { EXCLUDED_TOOLS, formatMessagingError } from '../utils/messaging-provider'; +import { createWhatsappMapLink, EXCLUDED_TOOLS, formatMessagingError } from '../utils/messaging-provider'; import { agentService } from './agent'; import { posthog, PostHogEvent } from './posthog'; import * as transcribeService from './transcribe.service'; @@ -435,7 +435,7 @@ class WhatsappService { const chatUrl = new URL(ctx.chatId, this._redirectUrl).toString(); const stream = await this._createAgentStream(chat, ctx, chatUrl); - const { finalText, chartUrls } = await this._readStreamAndUpdateMessage(stream, ctx); + const { finalText, chartUrls, mapLinks } = await this._readStreamAndUpdateMessage(stream, ctx); if (finalText) { await ctx.thread.post(finalText); @@ -445,6 +445,10 @@ class WhatsappService { await this._sendWhatsAppImage(ctx.thread.id, url); } + for (const link of mapLinks) { + await ctx.thread.post(link); + } + posthog.capture(ctx.user!.id, PostHogEvent.MessageSent, { project_id: this._projectId, chat_id: ctx.chatId, @@ -471,9 +475,9 @@ class WhatsappService { private async _readStreamAndUpdateMessage( stream: ReadableStream>, ctx: ConversationContext, - ): Promise<{ finalText: string; chartUrls: string[] }> { + ): Promise<{ finalText: string; chartUrls: string[]; mapLinks: string[] }> { const state: StreamState = { - renderedChartIds: new Set(), + renderedToolCallIds: new Set(), sqlOutputs: new Map(), lastUpdateAt: Date.now(), toolGroup: new Map(), @@ -481,6 +485,7 @@ class WhatsappService { }; const chartUrls: string[] = []; + const mapLinks: string[] = []; let lastMessage: UIMessage | null = null; for await (const uiMessage of readUIMessageStream({ stream })) { @@ -499,6 +504,11 @@ class WhatsappService { if (url) { chartUrls.push(url); } + } else if (part.type === 'tool-display_map') { + const link = this._handleMapPart(part, state, ctx); + if (link) { + mapLinks.push(link); + } } } @@ -507,7 +517,24 @@ class WhatsappService { .map((p) => p.text.replace(CITATION_TAG_REGEX, '')) .join('\n\n'); - return { finalText, chartUrls }; + return { finalText, chartUrls, mapLinks }; + } + + private _handleMapPart( + part: Extract, + state: StreamState, + ctx: ConversationContext, + ): string | null { + if ( + part.state !== 'output-available' || + !part.output.success || + state.renderedToolCallIds.has(part.toolCallId) + ) { + return null; + } + state.renderedToolCallIds.add(part.toolCallId); + const chatUrl = new URL(ctx.chatId, this._redirectUrl).toString(); + return createWhatsappMapLink(part.input.title, chatUrl); } private async _handleChartPart( @@ -515,7 +542,7 @@ class WhatsappService { state: StreamState, ctx: ConversationContext, ): Promise { - if (part.state !== 'output-available' || state.renderedChartIds.has(part.toolCallId)) { + if (part.state !== 'output-available' || state.renderedToolCallIds.has(part.toolCallId)) { return null; } if (part.input.chart_type === 'table') { @@ -533,7 +560,7 @@ class WhatsappService { dateFormat: displaySettings.dateFormat, }); const chartId = await chartImageQueries.saveChart(part.toolCallId, png.toString('base64')); - state.renderedChartIds.add(part.toolCallId); + state.renderedToolCallIds.add(part.toolCallId); return new URL(`c/${ctx.chatId}/${chartId}.png`, this._redirectUrl).toString(); } catch (error) { logger.error(`Chart image generation failed: ${String(error)}`, { diff --git a/apps/backend/src/trpc/project.routes.ts b/apps/backend/src/trpc/project.routes.ts index efbeabaa4..f0f555291 100644 --- a/apps/backend/src/trpc/project.routes.ts +++ b/apps/backend/src/trpc/project.routes.ts @@ -746,6 +746,7 @@ export const projectRoutes = { .object({ pythonSandboxing: z.boolean().optional(), sandboxes: z.boolean().optional(), + displayMap: z.boolean().optional(), }) .optional(), transcribe: z @@ -793,6 +794,7 @@ export const projectRoutes = { sql_dangerously_write_perm_enabled: merged.sql?.dangerouslyWritePermEnabled, python_execution_max_duration_secs: merged.pythonExecution?.maxDurationSecs, python_sandboxing_enabled: merged.experimental?.pythonSandboxing, + display_map_enabled: merged.experimental?.displayMap, memory_enabled: merged.memoryEnabled, web_search_enabled: merged.webSearch?.enabled, web_search_mode: merged.webSearch?.mode, diff --git a/apps/backend/src/types/agent-settings.ts b/apps/backend/src/types/agent-settings.ts index dd4b061e6..48669352c 100644 --- a/apps/backend/src/types/agent-settings.ts +++ b/apps/backend/src/types/agent-settings.ts @@ -5,6 +5,7 @@ export interface AgentSettings { experimental?: { pythonSandboxing?: boolean; sandboxes?: boolean; + displayMap?: boolean; }; transcribe?: { enabled?: boolean; diff --git a/apps/backend/src/types/messaging-provider.ts b/apps/backend/src/types/messaging-provider.ts index 0f77d5426..e241e9c28 100644 --- a/apps/backend/src/types/messaging-provider.ts +++ b/apps/backend/src/types/messaging-provider.ts @@ -28,7 +28,7 @@ export type ToolCallEntry = { }; export type StreamState = { - renderedChartIds: Set; + renderedToolCallIds: Set; sqlOutputs: Map; lastUpdateAt: number; toolGroup: Map; diff --git a/apps/backend/src/utils/messaging-provider.ts b/apps/backend/src/utils/messaging-provider.ts index 2d2a3ac86..26a0f68df 100644 --- a/apps/backend/src/utils/messaging-provider.ts +++ b/apps/backend/src/utils/messaging-provider.ts @@ -6,7 +6,12 @@ import { Actions, Button, Card, CardText, Image, LinkButton, Table } from 'chat' import { ToolCallEntry } from '../types/messaging-provider'; import { BudgetExceededError } from './error'; -export const EXCLUDED_TOOLS = ['tool-suggest_follow_ups', 'tool-display_chart', 'tool-clarification']; +export const EXCLUDED_TOOLS = [ + 'tool-suggest_follow_ups', + 'tool-display_chart', + 'tool-display_map', + 'tool-clarification', +]; export const createLiveToolCall = (toolGroup: Map): CardChild => { const parts = [...countToolsByNoun(toolGroup).entries()].map( @@ -139,6 +144,21 @@ export const createImageBlock = (url: string): CardChild => { return Image({ url, alt: 'image' }); }; +/** Interactive maps cannot be rendered by messaging providers, so they degrade to a link to the nao chat. */ +export const createMapLinkCard = (title: string, chatUrl: string): CardChild[] => [ + CardText(`🗺️ **${title}**`), + Actions([LinkButton({ url: chatUrl, label: 'View interactive map in nao' })]), +]; + +export const createTelegramMapLinkCard = (title: string, chatUrl: string): CardChild[] => [ + createPlainTextBlock(`🗺️ ${title}`), + Actions([LinkButton({ url: chatUrl, label: 'View interactive map in nao' })]), +]; + +/** WhatsApp has no interactive card UI, so a map degrades to a plain-text link to the nao chat. */ +export const createWhatsappMapLink = (title: string, chatUrl: string): string => + `🗺️ ${title}\nView interactive map in nao: ${chatUrl}`; + export const createPlainTextBlock = (text: string): CardChild => { return CardText(stripMarkdown(text)); }; diff --git a/apps/backend/tests/display-map-tool.test.ts b/apps/backend/tests/display-map-tool.test.ts new file mode 100644 index 000000000..8bdb41136 --- /dev/null +++ b/apps/backend/tests/display-map-tool.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('../src/db/db', () => ({ db: {} })); +vi.mock('../src/queries/chat.queries', () => ({ + getQueryResultByQueryId: vi.fn(async () => null), +})); + +import displayMapTool from '../src/agents/tools/display-map'; +import * as chatQueries from '../src/queries/chat.queries'; +import type { QueryResult, ToolContext } from '../src/types/tools'; + +const input = { + query_id: 'q1', + map_type: 'points' as const, + latitude_key: 'lat', + longitude_key: 'lng', + title: 'Test map', +}; + +const rows = [{ lat: 48.85, lng: 2.35 }]; + +function execute(overrides: Partial, queryResults = new Map()) { + const context = { queryResults, chatId: 'chat1' } as unknown as ToolContext; + return displayMapTool.execute!({ ...input, ...overrides }, { + experimental_context: context, + } as Parameters>[1]); +} + +describe('display_map execute', () => { + it('rejects identical latitude and longitude keys', async () => { + const queryResults = new Map([['q1', { columns: ['lat', 'lng'], data: rows }]]); + const output = await execute({ longitude_key: 'lat' }, queryResults); + expect(output).toMatchObject({ success: false }); + expect((output as { error?: string }).error).toContain('different columns'); + }); + + it('rejects a query id that is neither in the run context nor in the chat history', async () => { + const output = await execute({}); + expect(output).toMatchObject({ success: false }); + expect((output as { error?: string }).error).toContain('q1'); + }); + + it('resolves query results from the persisted chat history', async () => { + vi.mocked(chatQueries.getQueryResultByQueryId).mockResolvedValueOnce({ + columns: ['lat', 'lng'], + data: rows, + }); + const output = await execute({}); + expect(output).toMatchObject({ success: true }); + }); + + it('succeeds when the keys match the query columns case-insensitively', async () => { + const queryResults = new Map([['q1', { columns: ['LAT', 'LNG'], data: [{ LAT: 48.85, LNG: 2.35 }] }]]); + const output = await execute({}, queryResults); + expect(output).toMatchObject({ success: true }); + }); + + it('rejects a key missing from the query columns and lists the available ones', async () => { + const queryResults = new Map([['q1', { columns: ['lat', 'city'], data: rows }]]); + const output = await execute({}, queryResults); + expect(output).toMatchObject({ success: false }); + expect((output as { error?: string }).error).toContain('lng'); + expect((output as { error?: string }).error).toContain('lat, city'); + }); + + it('rejects case variants that resolve to the same column', async () => { + const queryResults = new Map([['q1', { columns: ['LAT'], data: [{ LAT: 48.85 }] }]]); + const output = await execute({ latitude_key: 'lat', longitude_key: 'LAT' }, queryResults); + expect(output).toMatchObject({ success: false }); + }); + + it('accepts genuinely distinct case-variant columns via exact matches', async () => { + const queryResults = new Map([['q1', { columns: ['lat', 'LAT'], data: [{ lat: 48.85, LAT: 2.35 }] }]]); + const output = await execute({ latitude_key: 'lat', longitude_key: 'LAT' }, queryResults); + expect(output).toMatchObject({ success: true }); + }); + + it('reports plotted and dropped counts on success', async () => { + const queryResults = new Map([['q1', { columns: ['lat', 'lng'], data: [...rows, { lat: null, lng: null }] }]]); + const output = await execute({}, queryResults); + expect(output).toMatchObject({ success: true, point_count: 1, dropped_row_count: 1 }); + }); + + it('warns about popup keys missing from the query columns without failing', async () => { + const queryResults = new Map([['q1', { columns: ['lat', 'lng', 'city'], data: rows }]]); + const output = await execute({ label_key: 'cty', tooltip_keys: ['city', 'missing'] }, queryResults); + expect(output).toMatchObject({ success: true }); + expect((output as { warning?: string }).warning).toContain('"cty"'); + expect((output as { warning?: string }).warning).toContain('"missing"'); + expect((output as { warning?: string }).warning).not.toContain('"city"'); + }); + + it('warns when the result exceeds the rendered point cap', async () => { + const manyRows = Array.from({ length: 5001 }, (_, i) => ({ + lat: 40 + (i % 100) / 100, + lng: 2 + (i % 100) / 100, + })); + const queryResults = new Map([['q1', { columns: ['lat', 'lng'], data: manyRows }]]); + const output = await execute({}, queryResults); + expect(output).toMatchObject({ success: true, point_count: 5001 }); + expect((output as { warning?: string }).warning).toContain('first 5000 points'); + }); + + it('omits the warning when popup keys resolve', async () => { + const queryResults = new Map([['q1', { columns: ['lat', 'lng', 'CITY'], data: rows }]]); + const output = await execute({ label_key: 'city' }, queryResults); + expect(output).toMatchObject({ success: true }); + expect((output as { warning?: string }).warning).toBeUndefined(); + }); + + it('rejects a query result with no plottable coordinates', async () => { + const queryResults = new Map([['q1', { columns: ['lat', 'lng'], data: [{ lat: null, lng: null }] }]]); + const output = await execute({}, queryResults); + expect(output).toMatchObject({ success: false }); + expect((output as { error?: string }).error).toContain('no rows with valid'); + }); + + it('rejects an empty query result', async () => { + const queryResults = new Map([['q1', { columns: ['lat', 'lng'], data: [] }]]); + const output = await execute({}, queryResults); + expect(output).toMatchObject({ success: false }); + }); +}); diff --git a/apps/backend/tests/system-prompt.test.ts b/apps/backend/tests/system-prompt.test.ts index 09305571c..b8262b8df 100644 --- a/apps/backend/tests/system-prompt.test.ts +++ b/apps/backend/tests/system-prompt.test.ts @@ -76,3 +76,15 @@ describe('SystemPrompt timezone rendering', () => { expect(markdown).toContain('Tuesday, March 10, 2026 (UTC)'); }); }); + +describe('SystemPrompt display_map rules', () => { + it('includes the display_map rule by default', () => { + expect(renderToMarkdown(SystemPrompt({}))).toContain('display_map'); + }); + + it('omits the display_map rule when the run excludes the tool', () => { + expect(renderToMarkdown(SystemPrompt({ toolNames: ['execute_sql', 'display_chart'] }))).not.toContain( + 'display_map', + ); + }); +}); diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 77495843e..aabdaa782 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -55,6 +55,7 @@ "jszip": "^3.10.1", "katex": "^0.16.45", "lucide-react": "^0.562.0", + "maplibre-gl": "^5.24.0", "posthog-js": "^1.336.4", "prompt-mentions": "^0.0.37", "proxy-from-env": "^1.1.0", diff --git a/apps/frontend/src/components/settings-search-index.ts b/apps/frontend/src/components/settings-search-index.ts index 759f766a3..5f9cfac3d 100644 --- a/apps/frontend/src/components/settings-search-index.ts +++ b/apps/frontend/src/components/settings-search-index.ts @@ -267,6 +267,16 @@ export const settingsSearchIndex: SettingsSearchEntry[] = [ keywords: ['boxlite', 'code execution'], adminOnly: true, }, + { + page: '/settings/project/agent', + pageLabel: 'Agent', + section: 'Experimental', + title: 'Display map', + description: + 'Allow the agent to render query results with latitude and longitude columns on an interactive map.', + keywords: ['map', 'geo', 'location', 'latitude', 'longitude', 'coordinates', 'spatial'], + adminOnly: true, + }, { page: '/settings/project/agent', pageLabel: 'Agent', diff --git a/apps/frontend/src/components/settings/experimental.tsx b/apps/frontend/src/components/settings/experimental.tsx index a596c92a3..73066eead 100644 --- a/apps/frontend/src/components/settings/experimental.tsx +++ b/apps/frontend/src/components/settings/experimental.tsx @@ -39,6 +39,7 @@ export function SettingsExperimental({ isAdmin }: SettingsExperimentalProps) { const [pythonExecutionDurationInput, setPythonExecutionDurationInput] = useState( String(pythonExecutionDurationSecs), ); + const displayMapEnabled = agentSettings.data?.experimental?.displayMap ?? false; useEffect(() => { setPythonExecutionDurationInput(String(pythonExecutionDurationSecs)); @@ -82,6 +83,14 @@ export function SettingsExperimental({ isAdmin }: SettingsExperimentalProps) { const pythonExecutionDurationError = getPythonExecutionDurationError(pythonExecutionDurationInput); + const handleDisplayMapChange = (enabled: boolean) => { + updateAgentSettings.mutate({ + experimental: { + displayMap: enabled, + }, + }); + }; + return ( } /> + + } + /> (() => { - if (!chartConfig?.query_id) { - return null; - } - return findLatestExecuteSqlInMessages(messages, chartConfig.query_id); - }, [messages, chartConfig?.query_id]); - - const sourceData = sourceQuery?.output ?? null; + const { sourceQuery, sourceData } = useSourceQuery(chartConfig?.query_id); const sqlQuery = sourceQuery?.input?.sql_query; const handleDownloadPng = async () => { diff --git a/apps/frontend/src/components/tool-calls/display-map-view.tsx b/apps/frontend/src/components/tool-calls/display-map-view.tsx new file mode 100644 index 000000000..207b70a44 --- /dev/null +++ b/apps/frontend/src/components/tool-calls/display-map-view.tsx @@ -0,0 +1,228 @@ +import { useEffect, useRef, useState } from 'react'; +import maplibregl from 'maplibre-gl'; +import { computeMapBounds, labelize } from '@nao/shared'; +import type { MapPoint } from '@nao/shared'; +import type { displayMap } from '@nao/shared/tools'; +import 'maplibre-gl/dist/maplibre-gl.css'; + +const MAP_STYLE_LIGHT = import.meta.env.VITE_MAP_STYLE_URL || 'https://tiles.openfreemap.org/styles/positron'; +const MAP_STYLE_DARK = import.meta.env.VITE_MAP_STYLE_URL_DARK || 'https://tiles.openfreemap.org/styles/dark'; +const POINTS_SOURCE_ID = 'query-points'; +const POINTS_LAYER_ID = 'query-points-circles'; + +interface MapViewProps { + points: MapPoint[]; + config: displayMap.Input; +} + +export default function MapView({ points, config }: MapViewProps) { + const containerRef = useRef(null); + const mapRef = useRef(null); + const pointsRef = useRef(points); + const configRef = useRef(config); + const markerColorRef = useRef('#522bff'); + const styleUrlRef = useRef(''); + const [initFailed, setInitFailed] = useState(false); + const isDark = useIsDark(); + pointsRef.current = points; + configRef.current = config; + + useEffect(() => { + if (!containerRef.current) { + return; + } + + const styleUrl = resolveStyleUrl(document.documentElement.classList.contains('dark')); + styleUrlRef.current = styleUrl; + + let map: maplibregl.Map; + try { + map = new maplibregl.Map({ + container: containerRef.current, + style: styleUrl, + cooperativeGestures: true, + attributionControl: { compact: true }, + }); + } catch { + setInitFailed(true); + return; + } + mapRef.current = map; + map.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-right'); + + map.on('style.load', () => { + markerColorRef.current = resolveCssColor('--primary', '#522bff'); + map.addSource(POINTS_SOURCE_ID, { type: 'geojson', data: toGeoJsonPoints(pointsRef.current) }); + map.addLayer({ + id: POINTS_LAYER_ID, + type: 'circle', + source: POINTS_SOURCE_ID, + paint: { + 'circle-radius': 5, + 'circle-color': markerColorRef.current, + 'circle-opacity': 0.9, + 'circle-stroke-width': 2, + 'circle-stroke-color': resolveCssColor('--background', '#ffffff'), + }, + }); + fitToPoints(map, pointsRef.current); + }); + + const tooltip = new maplibregl.Popup({ + closeButton: false, + closeOnClick: false, + className: 'map-tooltip', + maxWidth: '280px', + offset: 12, + }); + + map.on('mousemove', POINTS_LAYER_ID, (event) => { + const index = event.features?.[0]?.properties?.index; + const point = typeof index === 'number' ? pointsRef.current[index] : undefined; + const content = point ? buildTooltipContent(point, configRef.current, markerColorRef.current) : null; + if (!point || !content) { + tooltip.remove(); + map.getCanvas().style.cursor = ''; + return; + } + map.getCanvas().style.cursor = 'pointer'; + tooltip.setLngLat([point.longitude, point.latitude]).setDOMContent(content).addTo(map); + }); + map.on('mouseleave', POINTS_LAYER_ID, () => { + map.getCanvas().style.cursor = ''; + tooltip.remove(); + }); + + return () => { + mapRef.current = null; + map.remove(); + }; + }, []); + + useEffect(() => { + const map = mapRef.current; + const styleUrl = resolveStyleUrl(isDark); + if (!map || styleUrl === styleUrlRef.current) { + return; + } + styleUrlRef.current = styleUrl; + map.setStyle(styleUrl); + }, [isDark]); + + useEffect(() => { + const map = mapRef.current; + const source = map?.getSource(POINTS_SOURCE_ID) as maplibregl.GeoJSONSource | undefined; + if (!map || !source) { + return; + } + source.setData(toGeoJsonPoints(points)); + fitToPoints(map, points); + }, [points]); + + if (initFailed) { + return ( +
+ Interactive maps are not supported in this browser (WebGL unavailable). +
+ ); + } + + return
; +} + +function useIsDark() { + const [isDark, setIsDark] = useState(() => document.documentElement.classList.contains('dark')); + + useEffect(() => { + const root = document.documentElement; + const observer = new MutationObserver(() => setIsDark(root.classList.contains('dark'))); + observer.observe(root, { attributes: true, attributeFilter: ['class'] }); + return () => observer.disconnect(); + }, []); + + return isDark; +} + +function resolveStyleUrl(isDark: boolean): string { + return isDark ? MAP_STYLE_DARK : MAP_STYLE_LIGHT; +} + +function toGeoJsonPoints(points: MapPoint[]) { + return { + type: 'FeatureCollection' as const, + features: points.map((point, index) => ({ + type: 'Feature' as const, + geometry: { type: 'Point' as const, coordinates: [point.longitude, point.latitude] }, + properties: { index }, + })), + }; +} + +function fitToPoints(map: maplibregl.Map, points: MapPoint[]) { + const bounds = computeMapBounds(points); + if (!bounds) { + return; + } + map.fitBounds( + [ + [bounds.west, bounds.south], + [bounds.east, bounds.north], + ], + { padding: 48, maxZoom: 14, duration: 0 }, + ); +} + +function buildTooltipContent(point: MapPoint, config: displayMap.Input, markerColor: string): HTMLElement | null { + const rows: HTMLElement[] = []; + + if (config.label_key && point.row[config.label_key] != null) { + const title = document.createElement('div'); + title.className = 'flex items-center gap-2 font-medium text-foreground'; + const dot = document.createElement('span'); + dot.className = 'h-2.5 w-2.5 shrink-0 rounded-[2px]'; + dot.style.backgroundColor = markerColor; + const text = document.createElement('span'); + text.textContent = String(point.row[config.label_key]); + title.append(dot, text); + rows.push(title); + } + + for (const key of config.tooltip_keys ?? []) { + if (key === config.label_key || point.row[key] == null) { + continue; + } + const row = document.createElement('div'); + row.className = 'flex items-center justify-between gap-4 leading-none'; + const label = document.createElement('span'); + label.className = 'text-muted-foreground'; + label.textContent = labelize(key); + const value = document.createElement('span'); + value.className = 'text-foreground font-mono font-medium tabular-nums'; + value.textContent = String(point.row[key]); + row.append(label, value); + rows.push(row); + } + + if (rows.length === 0) { + return null; + } + + const container = document.createElement('div'); + container.className = + 'border-border/50 bg-background grid min-w-32 items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl font-sans'; + container.append(...rows); + return container; +} + +function resolveCssColor(variableName: string, fallback: string): string { + const value = getComputedStyle(document.documentElement).getPropertyValue(variableName).trim(); + const context = document.createElement('canvas').getContext('2d', { willReadFrequently: true }); + if (!value || !context) { + return fallback; + } + context.fillStyle = fallback; + context.fillStyle = value; + context.fillRect(0, 0, 1, 1); + const [r, g, b, a] = context.getImageData(0, 0, 1, 1).data; + return `rgba(${r}, ${g}, ${b}, ${a / 255})`; +} diff --git a/apps/frontend/src/components/tool-calls/display-map.tsx b/apps/frontend/src/components/tool-calls/display-map.tsx new file mode 100644 index 000000000..01c21e79f --- /dev/null +++ b/apps/frontend/src/components/tool-calls/display-map.tsx @@ -0,0 +1,179 @@ +import { lazy, Suspense, useMemo, useState } from 'react'; +import { Code, Map as MapIcon, Table as TableIcon } from 'lucide-react'; +import { buildMapPoints, MAX_MAP_POINTS, resolveMapConfig } from '@nao/shared'; +import { Skeleton } from '../ui/skeleton'; +import { TextShimmer } from '../ui/text-shimmer'; +import { Button } from '../ui/button'; +import GraphLoaderAnimated from '../icons/graph-loader-animated'; +import { TableDisplay } from './display-table'; +import { SqlQueryDisplay } from './sql-query-display'; +import { ToolCallWrapper } from './tool-call-wrapper'; +import type { ToolCallComponentProps } from '.'; +import type { ReactNode } from 'react'; +import type { displayMap } from '@nao/shared/tools'; +import type { MapPoint } from '@nao/shared'; +import { cn } from '@/lib/utils'; +import { useSourceQuery } from '@/hooks/use-source-query'; + +const MapView = lazy(() => import('./display-map-view')); + +type MapViewMode = 'map' | 'table' | 'query'; + +export const DisplayMapToolCall = ({ toolPart: { state, input, output } }: ToolCallComponentProps<'display_map'>) => { + const config = state !== 'input-streaming' ? input : undefined; + const { sourceQuery, sourceData } = useSourceQuery(config?.query_id); + const [viewMode, setViewMode] = useState('map'); + + const resolvedConfig = useMemo(() => { + if (!config || !sourceData?.data) { + return config; + } + return resolveMapConfig(sourceData.data, config); + }, [config, sourceData?.data]); + + const points = useMemo(() => { + if (!sourceData?.data || !resolvedConfig) { + return []; + } + return buildMapPoints(sourceData.data, resolvedConfig); + }, [sourceData?.data, resolvedConfig]); + + const visiblePoints = useMemo(() => { + return points.slice(0, MAX_MAP_POINTS); + }, [points]); + + if (output && output.error) { + return ( + +
{output.error}
+
+ ); + } + + if (!config) { + return ( +
+ + + + + +
+ ); + } + + if (!sourceData) { + return ( +
+ Could not display the map because the data is missing. +
+ ); + } + + const mapConfig = resolvedConfig ?? config; + + if (mapConfig.latitude_key === mapConfig.longitude_key) { + return ( +
+ Could not display the map because the latitude and longitude keys resolve to the same column. +
+ ); + } + + if (points.length === 0) { + return ( +
+ Could not display the map because no rows contain valid coordinates. +
+ ); + } + + return ( +
+
+
+ {config.title} +
+ } + title='View map' + isActive={viewMode === 'map'} + onClick={() => setViewMode('map')} + /> + } + title='View data table' + isActive={viewMode === 'table'} + onClick={() => setViewMode('table')} + /> + {sourceQuery?.input && ( + } + title='View SQL query' + isActive={viewMode === 'query'} + onClick={() => setViewMode('query')} + /> + )} +
+
+ + {viewMode === 'map' && ( +
+ }> + + +
+ )} + + {viewMode === 'table' && ( + []} + columns={sourceData.columns} + tableContainerClassName='max-h-[28rem]' + maxRowsBeforePagination={10} + compactFooter + humanizeColumnLabels + /> + )} + + {viewMode === 'query' && sourceQuery?.input && ( +
+ +
+ )} +
+ + {viewMode === 'map' && points.length > MAX_MAP_POINTS && ( + + Showing the first {MAX_MAP_POINTS.toLocaleString()} of {points.length.toLocaleString()} points. + + )} +
+ ); +}; + +interface ViewToggleButtonProps { + icon: ReactNode; + title: string; + isActive: boolean; + onClick: () => void; +} + +function ViewToggleButton({ icon, title, isActive, onClick }: ViewToggleButtonProps) { + return ( + + ); +} diff --git a/apps/frontend/src/components/tool-calls/index.tsx b/apps/frontend/src/components/tool-calls/index.tsx index 6fd5be5f5..879d070cc 100644 --- a/apps/frontend/src/components/tool-calls/index.tsx +++ b/apps/frontend/src/components/tool-calls/index.tsx @@ -3,6 +3,7 @@ import { StoryToolCall } from './story'; import { ClarificationToolCall } from './clarification'; import { DefaultToolCall } from './default'; import { DisplayChartToolCall } from './display-chart'; +import { DisplayMapToolCall } from './display-map'; import { ExecutePythonToolCall } from './execute-python'; import { ExecuteSandboxedCodeToolCall } from './execute-sandboxed-code'; import { ExecuteSqlToolCall } from './execute-sql'; @@ -31,6 +32,7 @@ const toolComponents: Partial<{ story: StoryToolCall, clarification: ClarificationToolCall, display_chart: DisplayChartToolCall, + display_map: DisplayMapToolCall, execute_python: ExecutePythonToolCall, execute_sandboxed_code: ExecuteSandboxedCodeToolCall, execute_sql: ExecuteSqlToolCall, diff --git a/apps/frontend/src/hooks/use-source-query.tsx b/apps/frontend/src/hooks/use-source-query.tsx new file mode 100644 index 000000000..af3ca74d0 --- /dev/null +++ b/apps/frontend/src/hooks/use-source-query.tsx @@ -0,0 +1,47 @@ +import { useMemo, useRef } from 'react'; +import type { executeSql } from '@nao/shared/tools'; +import type { UIMessage } from '@nao/backend/chat'; +import { useOptionalAgentContext } from '@/contexts/agent.provider'; +import { findLatestExecuteSqlInMessages } from '@/lib/execute-sql-messages'; + +const EMPTY_MESSAGES: UIMessage[] = []; + +export type SourceQuery = { input?: executeSql.Input; output: executeSql.Output }; + +/** + * Finds the `execute_sql` tool call a visualization references. The hit is cached in a ref — + * tool outputs are immutable once emitted — so streaming re-renders skip the message scan + * and keep a stable object identity. + */ +export function useSourceQuery(queryId: string | undefined): { + sourceQuery: SourceQuery | null; + sourceData: executeSql.Output | null; +} { + const agent = useOptionalAgentContext(); + const messages = agent?.messages ?? EMPTY_MESSAGES; + const foundRef = useRef<{ queryId: string; result: SourceQuery } | null>(null); + + const sourceQuery = useMemo(() => { + if (!queryId) { + return null; + } + if (foundRef.current?.queryId === queryId) { + return foundRef.current.result; + } + for (const message of messages) { + for (const part of message.parts) { + if (part.type === 'tool-execute_sql' && part.output && part.output.id === queryId) { + const result = { input: part.input, output: part.output }; + foundRef.current = { queryId, result }; + return result; + } + } + } + if (!queryId) { + return null; + } + return findLatestExecuteSqlInMessages(messages, queryId); + }, [messages, queryId]); + + return { sourceQuery, sourceData: sourceQuery?.output ?? null }; +} diff --git a/apps/frontend/src/lib/ai.ts b/apps/frontend/src/lib/ai.ts index 2df9de7c5..369ab3572 100644 --- a/apps/frontend/src/lib/ai.ts +++ b/apps/frontend/src/lib/ai.ts @@ -73,13 +73,14 @@ export const checkIsAgentRunning = (agent: Pick, 'stat /** Tools that should NOT be collapsed (important UI elements), per density setting. */ const NON_COLLAPSIBLE_TOOLS_BY_DENSITY: Record = { - compact: ['story', 'display_chart', 'suggest_follow_ups', 'clarification'], + compact: ['story', 'display_chart', 'display_map', 'suggest_follow_ups', 'clarification'], detailed: [ 'story', 'execute_sql', 'query_app_db', 'record_recommendation', 'display_chart', + 'display_map', 'suggest_follow_ups', 'clarification', 'execute_python', diff --git a/apps/frontend/src/lib/charts.utils.test.ts b/apps/frontend/src/lib/charts.utils.test.ts index cac8c5037..3158734d7 100644 --- a/apps/frontend/src/lib/charts.utils.test.ts +++ b/apps/frontend/src/lib/charts.utils.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; -import { alignChartDataToBaselineX, resolveDataKey, resolvePieTooltipLabel } from './charts.utils'; +import { resolveDataKey } from '@nao/shared'; +import { alignChartDataToBaselineX, resolvePieTooltipLabel } from './charts.utils'; describe('resolveDataKey', () => { it('returns an empty string when the key is undefined (kpi cards have no x-axis)', () => { diff --git a/apps/frontend/src/lib/charts.utils.ts b/apps/frontend/src/lib/charts.utils.ts index c59d30d5c..18d70a642 100644 --- a/apps/frontend/src/lib/charts.utils.ts +++ b/apps/frontend/src/lib/charts.utils.ts @@ -1,3 +1,4 @@ +import { resolveDataKey } from '@nao/shared'; import { getToolName, isToolUIPart } from './ai'; import { hashValue } from './hash'; import type { UIMessage } from '@nao/backend/chat'; @@ -101,20 +102,6 @@ export function resolvePieTooltipLabel(payload?: readonly { name?: unknown }[]): return name == null ? '' : String(name); } -/** Resolves a config key to the matching key in the data, ignoring case. Falls back to the original key. */ -export function resolveDataKey(data: Record[], key: string | undefined): string { - if (key === undefined) { - return ''; - } - const row = data[0]; - if (!row || key in row) { - return key; - } - const lower = key.toLowerCase(); - const match = Object.keys(row).find((dataKey) => dataKey.toLowerCase() === lower); - return match ?? key; -} - /** * Keeps the baseline x-axis categories when filtered SQL drops rows, zero-filling * series values for missing categories so bars/lines animate in place instead of diff --git a/apps/frontend/src/styles.css b/apps/frontend/src/styles.css index fed403d03..ff7a73d49 100644 --- a/apps/frontend/src/styles.css +++ b/apps/frontend/src/styles.css @@ -734,6 +734,71 @@ code.dollar:before { opacity: 0.9; } +/* MapLibre hover tooltip: strip default popup chrome so the inner box matches the chart tooltip */ +.maplibregl-popup.map-tooltip { + pointer-events: none; +} + +.maplibregl-popup.map-tooltip .maplibregl-popup-content { + padding: 0; + background: transparent; + box-shadow: none; + border-radius: 0; +} + +.maplibregl-popup.map-tooltip .maplibregl-popup-tip { + display: none; +} + +/* Round the zoom buttons to match the control group so hover/active fills stay inside the corners */ +.maplibregl-ctrl-group button:first-child { + border-radius: 4px 4px 0 0; +} + +.maplibregl-ctrl-group button:last-child { + border-radius: 0 0 4px 4px; +} + +.maplibregl-ctrl-group button:only-child { + border-radius: inherit; +} + +/* MapLibre controls (zoom +/- and attribution) follow the app light/dark theme */ +.dark .maplibregl-ctrl-group { + background: var(--background); +} + +.dark .maplibregl-ctrl-group button + button { + border-top-color: var(--border); +} + +.dark .maplibregl-ctrl-group button:not(:disabled):hover, +.dark .maplibregl-ctrl-group button:not(:disabled):active { + background-color: var(--accent); +} + +.dark .maplibregl-ctrl button .maplibregl-ctrl-icon, +.dark .maplibregl-ctrl-attrib.maplibregl-compact:after { + filter: invert(1); +} + +.dark .maplibregl-ctrl.maplibregl-ctrl-attrib { + background-color: color-mix(in srgb, var(--background) 50%, transparent); +} + +.dark .maplibregl-ctrl-attrib.maplibregl-compact { + background-color: var(--background); + color: var(--foreground); +} + +.dark .maplibregl-ctrl-attrib.maplibregl-compact-show .maplibregl-ctrl-attrib-button { + background-color: var(--accent); +} + +.dark .maplibregl-ctrl-attrib a { + color: var(--foreground); +} + @keyframes chat-input-border-draw { from { stroke-dashoffset: 100; diff --git a/apps/shared/src/index.ts b/apps/shared/src/index.ts index 85e17bb77..08d15d42d 100644 --- a/apps/shared/src/index.ts +++ b/apps/shared/src/index.ts @@ -4,6 +4,7 @@ export * from './chart-domain'; export * from './citation'; export * from './date'; export * from './execute-sql-parts'; +export * from './map-points'; export * from './mcp'; export * from './mcp-embed'; export * from './mentions'; diff --git a/apps/shared/src/map-points.ts b/apps/shared/src/map-points.ts new file mode 100644 index 000000000..9d1d30a6f --- /dev/null +++ b/apps/shared/src/map-points.ts @@ -0,0 +1,122 @@ +import type * as displayMap from './tools/display-map'; + +export interface MapPoint { + latitude: number; + longitude: number; + row: Record; +} + +export interface MapBounds { + west: number; + south: number; + east: number; + north: number; +} + +/** Web Mercator clamps latitudes beyond this, so points above it would all collapse onto the map edge. */ +export const MERCATOR_MAX_LATITUDE = 85.051129; + +/** The map UI renders at most this many points; the tool warns the model when a result exceeds it. */ +export const MAX_MAP_POINTS = 5000; + +/** Resolves a key to the matching column name, preferring an exact match before case-insensitive lookup. Falls back to the original key. */ +export function resolveColumnName(columns: string[], key: string): string { + if (columns.includes(key)) { + return key; + } + const lower = key.toLowerCase(); + const match = columns.find((column) => column.toLowerCase() === lower); + return match ?? key; +} + +/** Resolves a config key to the matching key in the data, ignoring case. Falls back to the original key. */ +export function resolveDataKey(data: Record[], key: string | undefined): string { + if (key === undefined) { + return ''; + } + const row = data[0]; + if (!row) { + return key; + } + return resolveColumnName(Object.keys(row), key); +} + +/** Resolves the configured column keys against the actual query result columns, ignoring case. */ +export function resolveMapConfig(rows: Record[], config: displayMap.Input): displayMap.Input { + return { + ...config, + latitude_key: resolveDataKey(rows, config.latitude_key), + longitude_key: resolveDataKey(rows, config.longitude_key), + label_key: config.label_key && resolveDataKey(rows, config.label_key), + tooltip_keys: config.tooltip_keys?.map((key) => resolveDataKey(rows, key)), + }; +} + +export function buildMapPoints(rows: Record[], config: displayMap.Input): MapPoint[] { + return rows + .map((row) => ({ + latitude: parseCoordinate(row[config.latitude_key]), + longitude: parseCoordinate(row[config.longitude_key]), + row, + })) + .filter( + (point): point is MapPoint => + point.latitude !== null && + point.longitude !== null && + Math.abs(point.latitude) <= MERCATOR_MAX_LATITUDE && + Math.abs(point.longitude) <= 180, + ); +} + +/** + * Computes the smallest bounds covering the points. Longitudes wrap at the + * antimeridian: for points at 179° and -179° this returns [179, 181] (a 2° + * span, which map libraries accept) instead of the naive [-179, 179]. + */ +export function computeMapBounds(points: MapPoint[]): MapBounds | null { + if (points.length === 0) { + return null; + } + + let south = Infinity; + let north = -Infinity; + for (const point of points) { + south = Math.min(south, point.latitude); + north = Math.max(north, point.latitude); + } + + const longitudes = [...new Set(points.map((point) => point.longitude))].sort((a, b) => a - b); + if (longitudes.length === 1) { + return { west: longitudes[0], south, east: longitudes[0], north }; + } + + let largestGap = longitudes[0] + 360 - longitudes[longitudes.length - 1]; + let largestGapIndex = longitudes.length - 1; + for (let i = 0; i < longitudes.length - 1; i++) { + const gap = longitudes[i + 1] - longitudes[i]; + if (gap > largestGap) { + largestGap = gap; + largestGapIndex = i; + } + } + + const crossesAntimeridian = largestGapIndex !== longitudes.length - 1; + const west = longitudes[(largestGapIndex + 1) % longitudes.length]; + const east = longitudes[largestGapIndex] + (crossesAntimeridian ? 360 : 0); + return { west, south, east, north }; +} + +/** + * Accepts only numbers and decimal-number strings. A blanket `Number()` coercion would turn + * null/blank into 0 (fabricated points at (0,0)), booleans into 0/1 and dates into timestamps. + */ +function parseCoordinate(value: unknown): number | null { + if (typeof value === 'number') { + return Number.isFinite(value) ? value : null; + } + if (typeof value === 'string' && value.trim() !== '') { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; + } + return null; +} diff --git a/apps/shared/src/tools/display-map.ts b/apps/shared/src/tools/display-map.ts new file mode 100644 index 000000000..57b7416f5 --- /dev/null +++ b/apps/shared/src/tools/display-map.ts @@ -0,0 +1,36 @@ +import z from 'zod/v3'; + +export const MapTypeEnum = z.enum(['points']); + +export const InputSchema = z.object({ + query_id: z.string().describe("The id of a previous `execute_sql` tool call's output to get data from."), + map_type: MapTypeEnum.describe('Type of map visualization to display.'), + latitude_key: z.string().describe('Column name containing the latitude in WGS84 decimal degrees.'), + longitude_key: z.string().describe('Column name containing the longitude in WGS84 decimal degrees.'), + label_key: z + .string() + .optional() + .describe('Column name used as the title of the popup shown when a point is clicked.'), + tooltip_keys: z + .array(z.string()) + .optional() + .describe('Additional column names to display as label/value rows in the point popup.'), + title: z + .string() + .describe( + 'A concise and descriptive title of what the map shows. Do not include the type of map in the title or other map configurations.', + ), +}); + +export const OutputSchema = z.object({ + _version: z.literal('1').optional(), + success: z.boolean(), + error: z.string().optional(), + warning: z.string().optional(), + point_count: z.number().optional(), + dropped_row_count: z.number().optional(), +}); + +export type MapType = z.infer; +export type Input = z.infer; +export type Output = z.infer; diff --git a/apps/shared/src/tools/index.ts b/apps/shared/src/tools/index.ts index 2209340ea..1c859a5ba 100644 --- a/apps/shared/src/tools/index.ts +++ b/apps/shared/src/tools/index.ts @@ -1,5 +1,6 @@ export * as clarification from './clarification'; export * as displayChart from './display-chart'; +export * as displayMap from './display-map'; export * as executePython from './execute-python'; export * as executeSandboxedCode from './execute-sandboxed-code'; export * as executeSql from './execute-sql'; diff --git a/apps/shared/tests/map-points.test.ts b/apps/shared/tests/map-points.test.ts new file mode 100644 index 000000000..99961c472 --- /dev/null +++ b/apps/shared/tests/map-points.test.ts @@ -0,0 +1,157 @@ +import { describe, expect, it } from 'vitest'; + +import { buildMapPoints, computeMapBounds, resolveColumnName, resolveMapConfig } from '../src/map-points'; +import type * as displayMap from '../src/tools/display-map'; + +const config: displayMap.Input = { + query_id: 'q1', + map_type: 'points', + latitude_key: 'lat', + longitude_key: 'lng', + title: 'Test map', +}; + +describe('buildMapPoints', () => { + it('builds points from numeric coordinates', () => { + const rows = [{ lat: 48.85, lng: 2.35 }]; + expect(buildMapPoints(rows, config)).toEqual([{ latitude: 48.85, longitude: 2.35, row: rows[0] }]); + }); + + it('parses string coordinates', () => { + const points = buildMapPoints([{ lat: '48.85', lng: '2.35' }], config); + expect(points).toEqual([expect.objectContaining({ latitude: 48.85, longitude: 2.35 })]); + }); + + it('drops rows with null, empty or blank coordinates instead of coercing them to 0', () => { + const rows = [ + { lat: null, lng: 2.35 }, + { lat: 48.85, lng: undefined }, + { lat: '', lng: 2.35 }, + { lat: 48.85, lng: ' ' }, + ]; + expect(buildMapPoints(rows, config)).toEqual([]); + }); + + it('keeps legitimate zero coordinates', () => { + const points = buildMapPoints([{ lat: 0, lng: 0 }], config); + expect(points).toEqual([expect.objectContaining({ latitude: 0, longitude: 0 })]); + }); + + it('drops non-numeric coordinates', () => { + expect(buildMapPoints([{ lat: 'Paris', lng: 2.35 }], config)).toEqual([]); + }); + + it('drops coercible non-numeric types instead of plotting them', () => { + const rows = [ + { lat: true, lng: false }, + { lat: new Date('2026-01-01'), lng: 2.35 }, + { lat: [48.85], lng: 2.35 }, + ]; + expect(buildMapPoints(rows, config)).toEqual([]); + }); + + it('drops out-of-range coordinates', () => { + const rows = [ + { lat: 91, lng: 2.35 }, + { lat: -90.5, lng: 2.35 }, + { lat: 48.85, lng: 180.1 }, + { lat: 48.85, lng: -181 }, + ]; + expect(buildMapPoints(rows, config)).toEqual([]); + }); + + it('drops latitudes beyond the Web Mercator limit, which would collapse onto the map edge', () => { + const rows = [ + { lat: 86, lng: 2.35 }, + { lat: -90, lng: 2.35 }, + ]; + expect(buildMapPoints(rows, config)).toEqual([]); + }); + + it('keeps boundary coordinates', () => { + const rows = [{ lat: 85.051129, lng: -180 }]; + expect(buildMapPoints(rows, config)).toHaveLength(1); + }); +}); + +describe('resolveColumnName', () => { + it('prefers an exact match over a case-insensitive one', () => { + expect(resolveColumnName(['Lat', 'lat'], 'lat')).toBe('lat'); + expect(resolveColumnName(['lat', 'LAT'], 'LAT')).toBe('LAT'); + }); + + it('falls back to a case-insensitive match, then to the original key', () => { + expect(resolveColumnName(['LAT'], 'lat')).toBe('LAT'); + expect(resolveColumnName(['city'], 'lat')).toBe('lat'); + }); +}); + +describe('resolveMapConfig', () => { + it('resolves configured keys against uppercase result columns', () => { + const rows = [{ LAT: 48.85, LNG: 2.35, CITY: 'Paris', COUNT: 3 }]; + const resolved = resolveMapConfig(rows, { + ...config, + label_key: 'city', + tooltip_keys: ['count'], + }); + expect(resolved.latitude_key).toBe('LAT'); + expect(resolved.longitude_key).toBe('LNG'); + expect(resolved.label_key).toBe('CITY'); + expect(resolved.tooltip_keys).toEqual(['COUNT']); + }); + + it('keeps keys unchanged when they match or when no column matches', () => { + const rows = [{ lat: 1, lng: 2 }]; + const resolved = resolveMapConfig(rows, { ...config, label_key: 'missing' }); + expect(resolved.latitude_key).toBe('lat'); + expect(resolved.label_key).toBe('missing'); + }); + + it('resolves case variants of the same column to one key, so callers can detect the collision', () => { + const rows = [{ LATITUDE: 48.85, city: 'Paris' }]; + const resolved = resolveMapConfig(rows, { + ...config, + latitude_key: 'latitude', + longitude_key: 'LATITUDE', + }); + expect(resolved.latitude_key).toBe('LATITUDE'); + expect(resolved.longitude_key).toBe('LATITUDE'); + }); + + it('resolves case-insensitive keys end to end with buildMapPoints', () => { + const rows = [{ LATITUDE: 48.85, LONGITUDE: 2.35 }]; + const resolved = resolveMapConfig(rows, { + ...config, + latitude_key: 'latitude', + longitude_key: 'longitude', + }); + expect(buildMapPoints(rows, resolved)).toHaveLength(1); + }); +}); + +describe('computeMapBounds', () => { + const point = (latitude: number, longitude: number) => ({ latitude, longitude, row: {} }); + + it('returns null for no points', () => { + expect(computeMapBounds([])).toBeNull(); + }); + + it('returns a degenerate box for a single point', () => { + expect(computeMapBounds([point(48.85, 2.35)])).toEqual({ west: 2.35, south: 48.85, east: 2.35, north: 48.85 }); + }); + + it('returns plain min/max bounds for points on one side of the antimeridian', () => { + const bounds = computeMapBounds([point(48.85, 2.35), point(52.5, 13.4), point(40.4, -3.7)]); + expect(bounds).toEqual({ west: -3.7, south: 40.4, east: 13.4, north: 52.5 }); + }); + + it('wraps across the antimeridian instead of spanning the whole world', () => { + const bounds = computeMapBounds([point(-17, 179), point(-18, -179)]); + expect(bounds).toEqual({ west: 179, south: -18, east: 181, north: -17 }); + }); + + it('keeps the wrapped interval minimal with several points near the antimeridian', () => { + const bounds = computeMapBounds([point(60, 170), point(62, 179), point(61, -170)]); + expect(bounds).toEqual({ west: 170, south: 60, east: 190, north: 62 }); + }); +}); diff --git a/package-lock.json b/package-lock.json index 0615b374d..c63bf4c80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -120,6 +120,7 @@ "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.19.0" } @@ -291,6 +292,7 @@ "jszip": "^3.10.1", "katex": "^0.16.45", "lucide-react": "^0.562.0", + "maplibre-gl": "^5.24.0", "posthog-js": "^1.336.4", "prompt-mentions": "^0.0.37", "proxy-from-env": "^1.1.0", @@ -1998,6 +2000,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2279,6 +2282,7 @@ "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.6.3.tgz", "integrity": "sha512-HefGR2SNfAi2RhT6XvSYViH4a0xoCGGL10bSDiv6sQGrmY6ulEQEV1X4nebTHeG0P6jdBmXAoEW3k37nhpk99w==", "license": "MIT", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", @@ -2411,6 +2415,7 @@ "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.4.0.tgz", "integrity": "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA==", "license": "MIT", + "peer": true, "dependencies": { "@noble/hashes": "^2.0.1" } @@ -2418,7 +2423,8 @@ "node_modules/@better-fetch/fetch": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.21.tgz", - "integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==" + "integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==", + "peer": true }, "node_modules/@boxlite-ai/boxlite": { "version": "0.3.0", @@ -2478,6 +2484,12 @@ "node": ">=18.0.0" } }, + "node_modules/@boxlite-ai/boxlite/node_modules/@boxlite-ai/boxlite-darwin-x64": { + "optional": true + }, + "node_modules/@boxlite-ai/boxlite/node_modules/@boxlite-ai/boxlite-linux-arm64-gnu": { + "optional": true + }, "node_modules/@braintree/sanitize-url": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", @@ -2682,6 +2694,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -2730,6 +2743,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -2757,6 +2771,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -2906,6 +2921,7 @@ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "license": "MIT", + "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" @@ -2916,6 +2932,7 @@ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -2953,7 +2970,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=12" } @@ -2970,7 +2986,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=12" } @@ -2987,7 +3002,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=12" } @@ -3004,7 +3018,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=12" } @@ -3021,7 +3034,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=12" } @@ -3038,7 +3050,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=12" } @@ -3055,7 +3066,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=12" } @@ -3072,7 +3082,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -3089,7 +3098,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -3106,7 +3114,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -3123,7 +3130,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -3140,7 +3146,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -3157,7 +3162,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -3174,7 +3178,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -3191,7 +3194,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -3208,7 +3210,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -3225,7 +3226,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=12" } @@ -3242,7 +3242,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=12" } @@ -3259,7 +3258,6 @@ "os": [ "sunos" ], - "peer": true, "engines": { "node": ">=12" } @@ -3276,7 +3274,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=12" } @@ -3293,7 +3290,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=12" } @@ -3310,7 +3306,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=12" } @@ -3377,7 +3372,6 @@ "os": [ "aix" ], - "peer": true, "engines": { "node": ">=18" } @@ -3394,7 +3388,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -3411,7 +3404,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -3428,7 +3420,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -3445,7 +3436,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -3462,7 +3452,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -3479,7 +3468,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -3496,7 +3484,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -3513,7 +3500,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -3530,7 +3516,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -3547,7 +3532,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -3564,7 +3548,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -3581,7 +3564,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -3598,7 +3580,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -3615,7 +3596,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -3632,7 +3612,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -3665,7 +3644,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -3682,7 +3660,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -3699,7 +3676,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -3716,7 +3692,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -3733,7 +3708,6 @@ "os": [ "openharmony" ], - "peer": true, "engines": { "node": ">=18" } @@ -3750,7 +3724,6 @@ "os": [ "sunos" ], - "peer": true, "engines": { "node": ">=18" } @@ -3767,7 +3740,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -3784,7 +3756,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -3801,7 +3772,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -4342,6 +4312,7 @@ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", "license": "MIT", + "peer": true, "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" @@ -4573,6 +4544,119 @@ "node": ">=8" } }, + "node_modules/@mapbox/jsonlint-lines-primitives": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.3.tgz", + "integrity": "sha512-0SElaV0uMxEnxzBhhX9WTuPyUeMsAN/SS0i16tjuba4/mio63MG9khjC1a0JAiPGXAwvwm4UfHJURCN7nyudQg==", + "license": "MIT", + "engines": { + "node": ">= 22" + } + }, + "node_modules/@mapbox/point-geometry": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz", + "integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==", + "license": "ISC" + }, + "node_modules/@mapbox/tiny-sdf": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.2.0.tgz", + "integrity": "sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/vector-tile": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.5.tgz", + "integrity": "sha512-pXj8m7KTsqZt+1jsE0xIpGvqTSbblfkuEJL/NJmNePMtEwxO8V3XMDo9WMSfDeqHvCtBI9Lmt4mGcGR10zecmw==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "~1.1.0", + "@types/geojson": "^7946.0.16", + "pbf": "^4.0.2" + } + }, + "node_modules/@mapbox/whoots-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", + "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@maplibre/geojson-vt": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.1.1.tgz", + "integrity": "sha512-FVMOcmSP/yqol45t7StApEyTL5/vmqBCuFhH9n+fFuINenhaX+YgHHIt1yJ86S8kln3uJLcMvmEU2cfn6E2eCQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.1.0" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "24.10.0", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.10.0.tgz", + "integrity": "sha512-lichxSiagMEBBrqHF0trtMQH9RKh+9jUlIJl0qW0QHvt2H/tbvUWdE+ZzI2Jd0/pT7j/iavLonlPu7EQ/ixTOw==", + "license": "ISC", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/unitbezier": "^1.0.0", + "json-stringify-pretty-compact": "^4.0.0", + "minimist": "^1.2.8", + "quickselect": "^3.0.0", + "tinyqueue": "^3.0.0" + }, + "bin": { + "gl-style-format": "dist/gl-style-format.mjs", + "gl-style-migrate": "dist/gl-style-migrate.mjs", + "gl-style-validate": "dist/gl-style-validate.mjs" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/@mapbox/unitbezier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-1.0.0.tgz", + "integrity": "sha512-fqd515fjBmANKGGsQ286E2Wvj/XvDFpGzwJxq4CI6jMQue6Oy04uCKp+JWKF00xRTmk6cEu1jPJ9p3xqH8YWqQ==", + "license": "BSD-2-Clause" + }, + "node_modules/@maplibre/mlt": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.12.tgz", + "integrity": "sha512-ZeK5w2TTeHOajcLaEQs1KZXw2V9wIKo1PmThlxlsHoXsQsYlBqLJzPOd6tJHRtGTChUY3DPPmjXRArYVvAbmZw==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0" + } + }, + "node_modules/@maplibre/vt-pbf": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.3.2.tgz", + "integrity": "sha512-j6p0AdjvAR19Z3XaCysle7A4ZSo08tYOzxD0Y9NQylwPAkwJJeYub5b2eVucdeDh7erhv69DahoLOevDRERRUw==", + "license": "MIT", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0", + "@types/geojson": "^7946.0.16", + "pbf": "^5.1.0" + } + }, + "node_modules/@maplibre/vt-pbf/node_modules/pbf": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-5.1.2.tgz", + "integrity": "sha512-mnvGdvOrIvJOBGUEdGkrVXjN8E/VkIJCkf2eS1DH2yv82ORUlLttmDt0rWY38yYZmVwciZwBUvHM20qxBZf40w==", + "license": "BSD-3-Clause", + "dependencies": { + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, "node_modules/@mermaid-js/parser": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.0.tgz", @@ -4745,6 +4829,7 @@ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", "license": "MIT", + "peer": true, "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", @@ -4897,6 +4982,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -4930,6 +5016,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -4964,7 +5051,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.220.0.tgz", "integrity": "sha512-/+ExB3lRkf+erv4PnoywyL7RHKITidxtUpUTS55k7OQ0dB42S7gEF1gry7swb9MSm1hYLUhJg4QQh9W8SpwwqA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/otlp-exporter-base": "0.220.0", @@ -4984,7 +5070,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz", "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/api": "^1.3.0" }, @@ -4997,7 +5082,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -5013,7 +5097,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.220.0.tgz", "integrity": "sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/otlp-transformer": "0.220.0" @@ -5030,7 +5113,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.220.0.tgz", "integrity": "sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/api-logs": "0.220.0", "@opentelemetry/core": "2.9.0", @@ -5051,7 +5133,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.220.0.tgz", "integrity": "sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/api-logs": "0.220.0", "@opentelemetry/core": "2.9.0", @@ -5070,7 +5151,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/resources": "2.9.0" @@ -5253,6 +5333,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", @@ -7306,6 +7387,7 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.12.0.tgz", "integrity": "sha512-rFtRx68ZOtFO6aQwEfMaje3dh/x6BOr0kAauIiTimUw+A6RVbx5QVLbYHQXL+hlxondnm8dbNV6lsgzShg3yKQ==", "license": "MIT", + "peer": true, "dependencies": { "cluster-key-slot": "1.1.2" }, @@ -9800,6 +9882,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.99.0.tgz", "integrity": "sha512-OY2bCqPemT1LlqJ8Y2CUau4KELnIhhG9Ol3ZndPbdnB095pRbPo1cHuXTndg8iIwtoHTgwZjyaDnQ0xD0mYwAw==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.99.0" }, @@ -9816,6 +9899,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.168.21.tgz", "integrity": "sha512-slnitYiHHmU52eMWtW8JbV9EMT5q6mRMbTA5yepBmJAnj5WZDrDRsLY/TuUrdD97A4W7/25tEQRoqc1G2X0oCw==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", @@ -9904,6 +9988,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.168.15.tgz", "integrity": "sha512-Wr0424NDtD8fT/uALobMZ9DdcfsTyXtW5IPR++7zvW8/7RaIOeaqXpVDId8ywaGtqPWLWOfaUg2zUtYtukoXYA==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", @@ -10112,6 +10197,7 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -10159,6 +10245,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.22.3.tgz", "integrity": "sha512-Dv9MKK5BDWCF0N2l6/Pxv3JNCce2kwuWf2cKMBc2bEetx0Pn6o7zlFmSxMvYK4UtG1Tw9Yg/ZHi6QOFWK0Zm9Q==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -10436,6 +10523,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.22.3.tgz", "integrity": "sha512-rqvv/dtqwbX+8KnPv0eMYp6PnBcuhPMol5cv1GlS8Nq/Cxt68EWGUHBuTFesw+hdnRQLmKwzoO1DlRn7PhxYRQ==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -10570,6 +10658,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.22.3.tgz", "integrity": "sha512-s5eiMq0m5N6N+W7dU6rd60KgZyyCD7FvtPNNswISfPr12EQwJBfbjWwTqd0UKNzA4fNrhQEERXnzORkykttPeA==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -10601,6 +10690,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.22.3.tgz", "integrity": "sha512-NjfWjZuvrqmpICT+GZWNIjtOdhPyqFKDMtQy7tsQ5rErM9L2ZQdy/+T/BKSO1JdTeBhdg9OP+0yfsqoYp2aT6A==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", @@ -10631,6 +10721,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.22.3.tgz", "integrity": "sha512-6MNr6z0PxwfJFs+BKhHcvPNvY+UV1PXgqzTiTM4Z9guml84iVZxv7ZOCSj1dFYTr3Bf1MiOs4hT1yvBFlTfIaQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "fast-equals": "^5.3.3", @@ -10724,6 +10815,7 @@ "https://trpc.io/sponsor" ], "license": "MIT", + "peer": true, "bin": { "intent": "bin/intent.js" }, @@ -10740,6 +10832,7 @@ "https://trpc.io/sponsor" ], "license": "MIT", + "peer": true, "bin": { "intent": "bin/intent.js" }, @@ -11278,6 +11371,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -11287,6 +11381,7 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -11390,6 +11485,7 @@ "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", @@ -11552,6 +11648,7 @@ "integrity": "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.2", @@ -12098,6 +12195,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -12129,6 +12227,7 @@ "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.159.tgz", "integrity": "sha512-S18ozG7Dkm3Ud1tzOtAK5acczD4vygfml80RkpM9VWMFpvAFwAKSHaGYkATvPQHIE+VpD1tJY9zcTXLZ/zR5cw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@ai-sdk/gateway": "3.0.96", "@ai-sdk/provider": "3.0.8", @@ -12637,6 +12736,7 @@ "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.6.3.tgz", "integrity": "sha512-jMsoSYQyO8nNRuLEoCP+OUShLyeIGU8ioPYqra0IteLjnS3WNjHj21YE/COSJ/V/f0H5SInZiF+uXcEEHREDMQ==", "license": "MIT", + "peer": true, "dependencies": { "@better-auth/core": "1.6.3", "@better-auth/drizzle-adapter": "1.6.3", @@ -12742,6 +12842,7 @@ "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.3.5.tgz", "integrity": "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA==", "license": "MIT", + "peer": true, "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", @@ -12940,6 +13041,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -13006,6 +13108,7 @@ "integrity": "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*" } @@ -13295,6 +13398,7 @@ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-12.0.0.tgz", "integrity": "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "12.0.0", "@chevrotain/gast": "12.0.0", @@ -13784,13 +13888,15 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/cytoscape": { "version": "3.33.2", "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.2.tgz", "integrity": "sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10" } @@ -14200,6 +14306,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -14655,7 +14762,8 @@ "version": "0.0.1581282", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/diff": { "version": "8.0.4", @@ -14735,7 +14843,6 @@ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", "license": "(MPL-2.0 OR Apache-2.0)", - "peer": true, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -14798,6 +14905,7 @@ "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", "license": "Apache-2.0", + "peer": true, "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", @@ -14945,6 +15053,12 @@ "node": ">= 0.4" } }, + "node_modules/earcut": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.2.3.tgz", + "integrity": "sha512-vnS4AVwp1KHAF13i1vp1/2D5evWy3k5u/iW/B81QVsUZtV8cv2tU0b2VNFlqvh4kYwrFMDdjPCfAmfyJW9y14Q==", + "license": "ISC" + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -15194,6 +15308,7 @@ "devOptional": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -15241,7 +15356,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -15300,6 +15414,7 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -16803,6 +16918,12 @@ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "license": "MIT" }, + "node_modules/gl-matrix": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", + "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", + "license": "MIT" + }, "node_modules/glob": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", @@ -17357,6 +17478,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -18252,7 +18374,6 @@ "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", "license": "MIT", - "peer": true, "funding": { "type": "GitHub Sponsors ❤", "url": "https://github.com/sponsors/dmonad" @@ -18278,6 +18399,7 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "license": "MIT", + "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -18287,6 +18409,7 @@ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/panva" } @@ -18325,6 +18448,7 @@ "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", @@ -18457,7 +18581,6 @@ "resolved": "https://registry.npmjs.org/json-schema-resolver/-/json-schema-resolver-3.0.0.tgz", "integrity": "sha512-HqMnbz0tz2DaEJ3ntsqtx3ezzZyDE7G56A/pPY/NGmrPu76UzsWquOpHFRAf5beTNXoH2LU5cQePVvRli1nchA==", "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.1.1", "fast-uri": "^3.0.5", @@ -18490,6 +18613,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -18643,6 +18772,12 @@ "node": ">= 12" } }, + "node_modules/kdbush": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz", + "integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==", + "license": "ISC" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -18672,6 +18807,7 @@ "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.16.tgz", "integrity": "sha512-3i5pmOiZvMDj00qhrIVbH0AnioVTx22DMP7Vn5At4yJO46iy+FM8Y/g61ltenLVSo3fiO8h8Q3QOFgf/gQ72ww==", "license": "MIT", + "peer": true, "engines": { "node": ">=20.0.0" } @@ -18730,7 +18866,6 @@ "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz", "integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==", "license": "MIT", - "peer": true, "dependencies": { "isomorphic.js": "^0.2.4" }, @@ -19373,6 +19508,40 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/maplibre-gl": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.24.0.tgz", + "integrity": "sha512-ALyFxgtd5R+65UqZ/++lOqwWcC0SNho9c27fYSyLmG7AfnAul2o46F05aDJGPbFU57wos9dgcIySHs0Xe6ia3A==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/point-geometry": "^1.1.0", + "@mapbox/tiny-sdf": "^2.1.0", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^2.0.4", + "@mapbox/whoots-js": "^3.1.0", + "@maplibre/geojson-vt": "^6.1.0", + "@maplibre/maplibre-gl-style-spec": "^24.8.1", + "@maplibre/mlt": "^1.1.8", + "@maplibre/vt-pbf": "^4.3.0", + "@types/geojson": "^7946.0.16", + "earcut": "^3.0.2", + "gl-matrix": "^3.4.4", + "kdbush": "^4.0.2", + "murmurhash-js": "^1.0.0", + "pbf": "^4.0.1", + "potpack": "^2.1.0", + "quickselect": "^3.0.0", + "tinyqueue": "^3.0.0" + }, + "engines": { + "node": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" + } + }, "node_modules/markdown-it": { "version": "14.1.1", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", @@ -20556,7 +20725,6 @@ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", "license": "MIT", - "peer": true, "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" @@ -20567,7 +20735,6 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -20587,6 +20754,12 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", + "license": "MIT" + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -20628,6 +20801,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": "^20.0.0 || >=22.0.0" } @@ -21624,6 +21798,18 @@ "node": ">= 14.16" } }, + "node_modules/pbf": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.2.tgz", + "integrity": "sha512-J0ajxARhZfpUEebxYs1vhMGMuLSXtBe1e+fFPDrf2uA2hgo+UshKfNUWOz92HJNz6/NFEXseQPddnHkTreWRqg==", + "license": "BSD-3-Clause", + "dependencies": { + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -21635,6 +21821,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", @@ -21910,6 +22097,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -22065,6 +22253,12 @@ "cross-spawn": "^7.0.6" } }, + "node_modules/potpack": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz", + "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", + "license": "ISC" + }, "node_modules/preact": { "version": "10.29.1", "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", @@ -22343,6 +22537,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", "license": "MIT", + "peer": true, "dependencies": { "orderedmap": "^2.0.0" } @@ -22372,6 +22567,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", @@ -22420,6 +22616,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.8.tgz", "integrity": "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", @@ -22450,6 +22647,12 @@ "node": ">=12.0.0" } }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz", + "integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -22580,6 +22783,12 @@ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", "license": "MIT" }, + "node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, "node_modules/radix-ui": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", @@ -22772,6 +22981,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -22828,6 +23038,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -23405,6 +23616,15 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "license": "MIT", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -23814,6 +24034,7 @@ "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.2.tgz", "integrity": "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" } @@ -24156,6 +24377,7 @@ "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.12.tgz", "integrity": "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", @@ -24905,6 +25127,12 @@ "node": "^18.0.0 || >=20.0.0" } }, + "node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, "node_modules/tinyrainbow": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", @@ -25671,6 +25899,7 @@ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -26264,6 +26493,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -26489,6 +26719,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -26722,6 +26953,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -27283,6 +27515,7 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -28041,7 +28274,6 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "license": "ISC", - "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -28163,6 +28395,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" }