diff --git a/.env.example b/.env.example index 28bb55f02..9e2e35d76 100644 --- a/.env.example +++ b/.env.example @@ -167,3 +167,5 @@ POSTHOG_DISABLED=false # (used to opt-out of tracking) # BETA_AUTOMATIONS_ENABLED=false # Set to true to enable the Context Recommendations feature. # BETA_CONTEXT_RECOMMENDATIONS_ENABLED=false +# Set to true to enable interactive story filters. +# BETA_STORY_FILTERS_ENABLED=false diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index c92f28920..2db623ac5 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -218,6 +218,7 @@ jobs: -e "ENABLE_USER_SIGNUP=false" \ -e "BETA_AUTOMATIONS_ENABLED=true" \ -e "BETA_CONTEXT_RECOMMENDATIONS_ENABLED=true" \ + -e "BETA_STORY_FILTERS_ENABLED=true" \ -e "SMTP_PASSWORD=${{ secrets.SMTP_PASSWORD }}" \ -e "SMTP_HOST=${{ secrets.SMTP_HOST }}" \ -e "SMTP_PORT=${{ secrets.SMTP_PORT }}" \ @@ -271,6 +272,7 @@ jobs: -e "ENABLE_USER_SIGNUP=true" \ -e "BETA_AUTOMATIONS_ENABLED=true" \ -e "BETA_CONTEXT_RECOMMENDATIONS_ENABLED=true" \ + -e "BETA_STORY_FILTERS_ENABLED=true" \ -e "GOOGLE_CLIENT_ID=${{ secrets.NAOCLOUD_GOOGLE_CLIENT_ID }}" \ -e "GOOGLE_CLIENT_SECRET=${{ secrets.NAOCLOUD_GOOGLE_CLIENT_SECRET }}" \ getnao/nao:${{ needs.merge.outputs.image-tag }} diff --git a/apps/backend/src/agents/tools/execute-sql.ts b/apps/backend/src/agents/tools/execute-sql.ts index 04c658d72..87b77472d 100644 --- a/apps/backend/src/agents/tools/execute-sql.ts +++ b/apps/backend/src/agents/tools/execute-sql.ts @@ -1,19 +1,26 @@ +import { sqlIncludesFilterTemplate, stripSqlFilterBlocks, validateSqlFilterTemplate } from '@nao/shared/sql-template'; import type { executeSql } from '@nao/shared/tools'; import { executeSql as schemas } from '@nao/shared/tools'; import { ExecuteSqlOutput, renderToModelOutput } from '../../components/tool-outputs'; import { env } from '../../env'; +import { getExecuteSqlPartByQueryIdInChat, updateExecuteSqlPart } from '../../queries/execute-sql.queries'; import { ToolContext } from '../../types/tools'; import { detectQueryRowLimit, isReadOnlySqlQuery } from '../../utils/sql-filter'; import { createTool } from '../../utils/tools'; import { queryAppDb } from './query-app-db'; export async function executeQuery( - { sql_query, database_id }: executeSql.Input, + { sql_query, database_id, query_id }: executeSql.Input, context: ToolContext, ): Promise { + const templateWarnings = env.BETA_STORY_FILTERS_ENABLED ? validateSqlFilterTemplate(sql_query) : []; + const effectiveSql = stripSqlFilterBlocks(sql_query); + if (templateWarnings.length > 0 && sqlIncludesFilterTemplate(effectiveSql)) { + throw new Error(`Invalid story filter SQL template: ${templateWarnings.join(' ')}`); + } const writePermEnabled = context.agentSettings?.sql?.dangerouslyWritePermEnabled ?? false; - if (!writePermEnabled && !(await isReadOnlySqlQuery(sql_query))) { + if (!writePermEnabled && !(await isReadOnlySqlQuery(effectiveSql))) { throw new Error( 'Write SQL operations are disabled. Only SELECT queries are allowed. ' + 'Enable "Dangerous write permissions" in the admin panel to allow INSERT, UPDATE, DELETE and DDL queries.', @@ -21,7 +28,7 @@ export async function executeQuery( } if (context.adminMode) { - return executeAppDbQuery(sql_query, context); + return withTemplateWarnings(await executeAppDbQuery(effectiveSql, context, query_id), templateWarnings); } const naoProjectFolder = context.projectFolder; @@ -32,7 +39,7 @@ export async function executeQuery( 'Content-Type': 'application/json', }, body: JSON.stringify({ - sql: sql_query, + sql: effectiveSql, nao_project_folder: naoProjectFolder, ...(database_id && { database_id }), ...(Object.keys(envVars).length > 0 && { env_vars: envVars }), @@ -46,23 +53,30 @@ export async function executeQuery( } const data = await response.json(); - const id = `query_${crypto.randomUUID().slice(0, 8)}` as const; + const id = query_id ?? (`query_${crypto.randomUUID().slice(0, 8)}` as const); context.queryResults.set(id, { columns: data.columns, data: data.data }); - const appliedLimit = detectQueryRowLimit(sql_query); + const appliedLimit = detectQueryRowLimit(effectiveSql); - return { - _version: '1', - ...data, - id, - ...(appliedLimit !== null && { applied_limit: appliedLimit }), - }; + return withTemplateWarnings( + { + _version: '1', + ...data, + id, + ...(appliedLimit !== null && { applied_limit: appliedLimit }), + }, + templateWarnings, + ); } -async function executeAppDbQuery(sqlQuery: string, context: ToolContext): Promise { +async function executeAppDbQuery( + sqlQuery: string, + context: ToolContext, + queryId?: `query_${string}`, +): Promise { const { columns, rows } = await queryAppDb(context.projectId, sqlQuery); - const id = `query_${crypto.randomUUID().slice(0, 8)}` as const; + const id = queryId ?? (`query_${crypto.randomUUID().slice(0, 8)}` as const); context.queryResults.set(id, { columns, data: rows }); const appliedLimit = detectQueryRowLimit(sqlQuery); return { @@ -75,11 +89,60 @@ async function executeAppDbQuery(sqlQuery: string, context: ToolContext): Promis }; } -export default createTool({ - description: +function withTemplateWarnings(output: executeSql.Output, templateWarnings: string[]): executeSql.Output { + if (templateWarnings.length === 0) { + return output; + } + return { ...output, template_warnings: templateWarnings }; +} + +async function updateExistingQuery( + input: executeSql.Input & { query_id: `query_${string}` }, + context: ToolContext, +): Promise { + const existing = await getExecuteSqlPartByQueryIdInChat(context.chatId, input.query_id); + if (!existing) { + throw new Error( + `Query ${input.query_id} not found in this chat. Use execute_sql without query_id to create a new query.`, + ); + } + + const nextInput: executeSql.Input = { + sql_query: input.sql_query, + database_id: input.database_id ?? existing.toolInput.database_id, + name: input.name ?? existing.toolInput.name, + }; + + const output = await executeQuery({ ...nextInput, query_id: input.query_id }, context); + await updateExecuteSqlPart(existing.toolCallId, nextInput, output); + return output; +} + +function buildExecuteSqlToolDescription() { + return [ 'Execute a SQL query against the connected database and return the results. If multiple databases are configured, specify the database_id.', + ...(env.BETA_STORY_FILTERS_ENABLED + ? [ + 'Story filters may be embedded as SQL template blocks that are stripped when this tool runs in chat.', + 'Correct syntax: WHERE 1 = 1 {% filter country %} AND country IN ({{ filters.country.sql }}) {% endfilter %}.', + "For date_range filters, {{ filters..sql }} already expands to 'start' AND 'end', so write: {% filter period %} AND order_date BETWEEN {{ filters.period.sql }} {% endfilter %}.", + 'Never use filters..start, .end, .value, or placeholders outside {% filter %} blocks — placeholders outside blocks are rejected; other invalid templates return template_warnings.', + 'Prefer query_id when adding story filter templates so existing / tags keep working.', + ] + : []), + 'To edit a previous query in-place (keep the same query_id for charts/stories), pass query_id from an earlier execute_sql result.', + ].join(' '); +} + +export default createTool({ + description: buildExecuteSqlToolDescription(), inputSchema: schemas.InputSchema, outputSchema: schemas.OutputSchema, - execute: executeQuery, + execute: async (input, context) => { + if (input.query_id) { + return updateExistingQuery({ ...input, query_id: input.query_id }, context); + } + return executeQuery(input, context); + }, toModelOutput: ({ output }) => renderToModelOutput(ExecuteSqlOutput({ output }), output), }); diff --git a/apps/backend/src/agents/tools/story.ts b/apps/backend/src/agents/tools/story.ts index bb531d0e4..d971c5069 100644 --- a/apps/backend/src/agents/tools/story.ts +++ b/apps/backend/src/agents/tools/story.ts @@ -3,20 +3,34 @@ import { story } from '@nao/shared/tools'; import { renderToModelOutput, StoryOutput } from '../../components/tool-outputs'; import { db } from '../../db/db'; +import { env } from '../../env'; import { getDisplayChartTableFormatsForChat } from '../../queries/chart-image'; import * as storyQueries from '../../queries/story.queries'; import * as storyFolderQueries from '../../queries/story-folder.queries'; +import { getStoryTemplateWarnings } from '../../services/story-template-validation'; import type { ToolContext } from '../../types/tools'; import { createTool } from '../../utils/tools'; -export default createTool({ - description: [ +const STORY_FILTER_DESCRIPTION = [ + 'Story-level filters are declared via .', + 'For select/multi_select, provide either table+column (options from SELECT DISTINCT) or options=\'["a","b"]\' (hardcoded values).', + 'When using table+column and multiple databases are configured, set database_id on the filter so option loading targets the correct database.', + 'Matching SQL must use template blocks that reference the same filter id, e.g. WHERE 1 = 1 {% filter country %} AND country IN ({{ filters.country.sql }}) {% endfilter %}.', + "For date_range, {{ filters..sql }} already expands to 'start' AND 'end' — write {% filter period %} AND order_date BETWEEN {{ filters.period.sql }} {% endfilter %}. Never use .start/.end/.value.", + 'Chat and live refresh strip unset filter blocks so the query still runs; active story filter selections re-render and re-execute SQL.', + 'Invalid filter templates are reported as template_warnings in the tool result — fix them before finishing.', + 'When adding filters to existing charts, prefer execute_sql with query_id set to the existing query so chart/table tags keep the same query_id.', +].join(' '); + +function buildStoryToolDescription() { + return [ 'Create or modify a nao Story — an interactive document combining markdown text and chart visualizations.', 'Use "create" to initialize a new story, "update" to search-and-replace within it (producing a new version),', 'or "replace" to overwrite the entire content (producing a new version).', 'Charts are embedded via .', 'For kpi_card charts you may add comparison_mode="percentage|variation|absolute" to show a period-over-period change pill; this requires the query to return at least two time-ordered rows (one per period) for the metric, and kpi_card does not need x_axis_key.', 'SQL result tables are embedded via
.', + ...(env.BETA_STORY_FILTERS_ENABLED ? [STORY_FILTER_DESCRIPTION] : []), 'Use ... to place 2–4 charts/tables side by side; its direct /
blocks are the columns.', 'For unequal columns add widths="w1,w2,..." to the — one positive integer per column giving its relative width (e.g. widths="2,1" makes the first column twice as wide as the second). The number of values must equal the number of columns; omit widths for equal columns. Choose widths that fit the content, e.g. a wide time-series next to a narrow KPI or pie.', 'Use consecutive ... blocks to organize a story into top-level tabs.', @@ -24,7 +38,11 @@ export default createTool({ 'A story can also be refered as a "canva", an "artifact" or a "report".', 'Users may edit stories directly; the tool result always reflects the latest version, including user edits.', 'Unless explicitly stated, dont use the stories to display a chart, but the display_chart tool.', - ].join(' '), + ].join(' '); +} + +export default createTool({ + description: buildStoryToolDescription(), inputSchema: story.InputSchema, outputSchema: story.OutputSchema, @@ -77,6 +95,7 @@ export default createTool({ version: version.version, code: version.code, title: version.title, + ...(await storyTemplateWarnings(chatId, version.code)), }; } @@ -115,6 +134,7 @@ export default createTool({ version: version.version, code: version.code, title: version.title, + ...(await storyTemplateWarnings(chatId, version.code)), }; } @@ -141,6 +161,7 @@ export default createTool({ version: version.version, code: version.code, title: version.title, + ...(await storyTemplateWarnings(chatId, version.code)), }; }, @@ -152,6 +173,17 @@ async function carryOverTableFormatting(code: string, chatId: string): Promise { + try { + const warnings = await getStoryTemplateWarnings(chatId, code); + return warnings.length > 0 ? { template_warnings: warnings } : {}; + } catch (error) { + console.error('Failed to compute story template warnings', error); + return {}; + } +} + function rememberStoryArtifact(context: ToolContext, id: string, title: string): void { const existing = context.generatedArtifacts.stories.find((story) => story.id === id); if (existing) { diff --git a/apps/backend/src/components/tool-outputs/execute-sql.tsx b/apps/backend/src/components/tool-outputs/execute-sql.tsx index fb8b9f62a..7254a4c47 100644 --- a/apps/backend/src/components/tool-outputs/execute-sql.tsx +++ b/apps/backend/src/components/tool-outputs/execute-sql.tsx @@ -1,14 +1,35 @@ import { pluralize } from '@nao/shared'; import type { executeSql } from '@nao/shared/tools'; -import { Block, ListItem, Span, Title, TitledList } from '../../lib/markdown'; +import { Block, List, ListItem, Span, Title, TitledList } from '../../lib/markdown'; import { QueryRows } from './query-rows'; const MAX_ROWS = 40; export const ExecuteSqlOutput = ({ output, maxRows = MAX_ROWS }: { output: executeSql.Output; maxRows?: number }) => { + const templateWarnings = output.template_warnings ?? []; + + if (output.superseded) { + return ( + + Stale result: query {output.id} was re-run later in this conversation. Refer to the latest execute_sql + result with this query id (or call read_query_result) for the current SQL and rows. + + ); + } + if (output.data.length === 0) { - return The query was successfully executed and returned no rows.; + return ( + + The query was successfully executed and returned no rows. + {templateWarnings.length > 0 && ( + <> + Query ID: {output.id} + + + )} + + ); } const isTruncated = output.data.length > maxRows; @@ -23,8 +44,8 @@ export const ExecuteSqlOutput = ({ output, maxRows = MAX_ROWS }: { output: execu Query ID: {output.id} - {output.columns.map((column) => ( - {column} + {output.columns.map((column, index) => ( + {column} ))} @@ -41,6 +62,8 @@ export const ExecuteSqlOutput = ({ output, maxRows = MAX_ROWS }: { output: execu )} + {templateWarnings.length > 0 && } + {remainingRows > 0 && ( @@ -52,3 +75,19 @@ export const ExecuteSqlOutput = ({ output, maxRows = MAX_ROWS }: { output: execu ); }; + +function TemplateWarnings({ warnings }: { warnings: string[] }) { + return ( + + + Story filter template warnings — the baseline query ran (filter blocks stripped), but story filters will + not apply correctly until you fix the SQL with execute_sql (prefer query_id): + + + {warnings.map((warning) => ( + {warning} + ))} + + + ); +} diff --git a/apps/backend/src/components/tool-outputs/story.tsx b/apps/backend/src/components/tool-outputs/story.tsx index 7332da9ff..81af2934f 100644 --- a/apps/backend/src/components/tool-outputs/story.tsx +++ b/apps/backend/src/components/tool-outputs/story.tsx @@ -1,6 +1,6 @@ import type { story } from '@nao/shared/tools'; -import { Block } from '../../lib/markdown'; +import { Block, List, ListItem, Span } from '../../lib/markdown'; export type StoryModelOutput = story.Output & { _stale?: boolean; @@ -21,6 +21,8 @@ export function StoryOutput({ output }: { output: StoryModelOutput }) { ); } + const templateWarnings = output.template_warnings ?? []; + return ( Story "{output.title}" (v{output.version}) — {output.id} @@ -30,6 +32,19 @@ export function StoryOutput({ output }: { output: StoryModelOutput }) { current version. Base any further changes on this content. )} + {templateWarnings.length > 0 && ( + + + Story filter template warnings — fix the referenced SQL with execute_sql (prefer query_id) + and/or the story filter tags before considering this story complete: + + + {templateWarnings.map((warning) => ( + {warning} + ))} + + + )} {output.code} ); diff --git a/apps/backend/src/env.ts b/apps/backend/src/env.ts index 0610f1ae9..4b024c0b6 100644 --- a/apps/backend/src/env.ts +++ b/apps/backend/src/env.ts @@ -132,6 +132,12 @@ const envSchema = z.object({ .optional() .default('false') .transform((val) => val === 'true'), + + BETA_STORY_FILTERS_ENABLED: z + .enum(['true', 'false']) + .optional() + .default('false') + .transform((val) => val === 'true'), }); const result = envSchema.safeParse(process.env); diff --git a/apps/backend/src/queries/chart-image.ts b/apps/backend/src/queries/chart-image.ts index 066bea5ab..baf2d2ddd 100644 --- a/apps/backend/src/queries/chart-image.ts +++ b/apps/backend/src/queries/chart-image.ts @@ -1,11 +1,12 @@ import { displayChart, executeSql } from '@nao/shared/tools'; -import { and, asc, eq, isNull, sql } from 'drizzle-orm'; +import { and, asc, eq, isNull } from 'drizzle-orm'; import s from '../db/abstractSchema'; import { db } from '../db/db'; -import dbConfig, { Dialect } from '../db/dbConfig'; +import { HandlerError } from '../utils/error'; import { takeFirstOrThrow } from '../utils/queries'; import { selectLatestDisplayChartTableFormats } from './chart-image.utils'; +import { getLatestExecuteSqlByQueryId } from './execute-sql.queries'; const DISPLAY_CHART_TOOL_TYPE = 'tool-display_chart'; @@ -34,22 +35,13 @@ export const getDisplayConfigByToolCallId = async (toolCallId: string): Promise< export const getExecuteSqlPartByQueryId = async ( queryId: string, ): Promise<{ toolInput: executeSql.Input; toolOutput: executeSql.Output }> => { - const jsonIdFilter = - dbConfig.dialect === Dialect.Postgres - ? sql`${s.messagePart.toolOutput}->>'id' = ${queryId}` - : sql`json_extract(${s.messagePart.toolOutput}, '$.id') = ${queryId}`; - - const result = await takeFirstOrThrow( - db - .select({ toolInput: s.messagePart.toolInput, toolOutput: s.messagePart.toolOutput }) - .from(s.messagePart) - .where(jsonIdFilter) - .execute(), - ); - + const result = await getLatestExecuteSqlByQueryId(queryId); + if (!result) { + throw new HandlerError('NOT_FOUND', `Query ${queryId} not found.`); + } return { - toolInput: executeSql.InputSchema.parse(result.toolInput), - toolOutput: executeSql.OutputSchema.parse(result.toolOutput), + toolInput: result.toolInput, + toolOutput: result.toolOutput, }; }; diff --git a/apps/backend/src/queries/chat.queries.ts b/apps/backend/src/queries/chat.queries.ts index 7396b7e1f..13ca7d2e7 100644 --- a/apps/backend/src/queries/chat.queries.ts +++ b/apps/backend/src/queries/chat.queries.ts @@ -29,6 +29,7 @@ import { import { applyChatFilters, buildChatGroups, type EnrichedChat, type SourcePlatform } from '../utils/chat-list'; import { convertDBPartToUIPart, mapUIPartsToDBParts } from '../utils/chat-message-part-mappings'; import { getErrorMessage } from '../utils/utils'; +import * as executeSqlQueries from './execute-sql.queries'; const chatCreatedAtMs = dbConfig.dialect === Dialect.Postgres @@ -853,20 +854,8 @@ export const getChatProjectId = async (chatId: string): Promise => { - const jsonIdFilter = - dbConfig.dialect === Dialect.Postgres - ? sql`${s.messagePart.toolOutput}->>'id' = ${queryId}` - : sql`json_extract(${s.messagePart.toolOutput}, '$.id') = ${queryId}`; - - const [result] = await db - .select({ projectId: s.chat.projectId }) - .from(s.messagePart) - .innerJoin(s.chatMessage, eq(s.messagePart.messageId, s.chatMessage.id)) - .innerJoin(s.chat, eq(s.chatMessage.chatId, s.chat.id)) - .where(jsonIdFilter) - .execute(); - - return result?.projectId; + const owner = await executeSqlQueries.getExecuteSqlOwnerByQueryId(queryId); + return owner?.projectId; }; /** @@ -878,24 +867,8 @@ export const getQueryResultByQueryId = async ( chatId: string, queryId: string, ): Promise<{ columns: string[]; data: Record[] } | null> => { - const jsonIdFilter = buildQueryIdJsonFilter(queryId); - - const [result] = await db - .select({ toolOutput: s.messagePart.toolOutput }) - .from(s.messagePart) - .innerJoin(s.chatMessage, eq(s.messagePart.messageId, s.chatMessage.id)) - .where( - and( - eq(s.chatMessage.chatId, chatId), - isNull(s.chatMessage.supersededAt), - eq(s.messagePart.toolName, 'execute_sql'), - jsonIdFilter, - ), - ) - .limit(1) - .execute(); - - return extractQueryResultFromToolOutput(result?.toolOutput); + const part = await executeSqlQueries.getExecuteSqlPartByQueryIdInChat(chatId, queryId); + return part ? extractQueryResultFromToolOutput(part.toolOutput) : null; }; export const getQueryResultByQueryIdInProject = async ( @@ -903,8 +876,6 @@ export const getQueryResultByQueryIdInProject = async ( userId: string, queryId: string, ): Promise<{ columns: string[]; data: Record[]; chatId: string } | null> => { - const jsonIdFilter = buildQueryIdJsonFilter(queryId); - const [result] = await db .select({ toolOutput: s.messagePart.toolOutput, chatId: s.chat.id }) .from(s.messagePart) @@ -915,10 +886,11 @@ export const getQueryResultByQueryIdInProject = async ( eq(s.chat.projectId, projectId), eq(s.chat.userId, userId), isNull(s.chatMessage.supersededAt), - eq(s.messagePart.toolName, 'execute_sql'), - jsonIdFilter, + eq(s.messagePart.toolName, executeSqlQueries.EXECUTE_SQL_TOOL_NAME), + executeSqlQueries.messagePartToolOutputIdEquals(queryId), ), ) + .orderBy(desc(s.chatMessage.createdAt), desc(s.messagePart.order)) .limit(1) .execute(); @@ -929,12 +901,6 @@ export const getQueryResultByQueryIdInProject = async ( return { ...extracted, chatId: result.chatId }; }; -function buildQueryIdJsonFilter(queryId: string) { - return dbConfig.dialect === Dialect.Postgres - ? sql`${s.messagePart.toolOutput}->>'id' = ${queryId}` - : sql`json_extract(${s.messagePart.toolOutput}, '$.id') = ${queryId}`; -} - function extractQueryResultFromToolOutput( toolOutput: unknown, ): { columns: string[]; data: Record[] } | null { diff --git a/apps/backend/src/queries/execute-sql.queries.ts b/apps/backend/src/queries/execute-sql.queries.ts new file mode 100644 index 000000000..1b75614ea --- /dev/null +++ b/apps/backend/src/queries/execute-sql.queries.ts @@ -0,0 +1,209 @@ +import { executeSql } from '@nao/shared/tools'; +import { and, asc, desc, eq, isNull, or, sql } from 'drizzle-orm'; + +import s from '../db/abstractSchema'; +import { db } from '../db/db'; +import dbConfig, { Dialect } from '../db/dbConfig'; +import { takeFirstOrThrow } from '../utils/queries'; + +export const EXECUTE_SQL_TOOL_NAME = 'execute_sql'; + +export function messagePartToolOutputIdEquals(queryId: string) { + return dbConfig.dialect === Dialect.Postgres + ? sql`${s.messagePart.toolOutput}->>'id' = ${queryId}` + : sql`json_extract(${s.messagePart.toolOutput}, '$.id') = ${queryId}`; +} + +function messagePartToolOutputIdIn(queryIds: Set) { + return or(...[...queryIds].map(messagePartToolOutputIdEquals)); +} + +export type LatestExecuteSqlRow = { + projectId: string; + userId: string; + chatId: string; + toolCallId: string; + toolInput: executeSql.Input; + toolOutput: executeSql.Output; + adminMode: boolean; +}; + +/** Latest non-superseded execute_sql part for a query id (global). */ +export async function getLatestExecuteSqlByQueryId(queryId: string): Promise { + const [row] = await db + .select({ + projectId: s.chat.projectId, + userId: s.chat.userId, + chatId: s.chat.id, + toolCallId: s.messagePart.toolCallId, + toolInput: s.messagePart.toolInput, + toolOutput: s.messagePart.toolOutput, + messageSource: s.chatMessage.source, + }) + .from(s.messagePart) + .innerJoin(s.chatMessage, eq(s.messagePart.messageId, s.chatMessage.id)) + .innerJoin(s.chat, eq(s.chatMessage.chatId, s.chat.id)) + .where( + and( + eq(s.messagePart.toolName, EXECUTE_SQL_TOOL_NAME), + isNull(s.chatMessage.supersededAt), + messagePartToolOutputIdEquals(queryId), + ), + ) + .orderBy(desc(s.chatMessage.createdAt), desc(s.messagePart.createdAt), desc(s.messagePart.order)) + .limit(1) + .execute(); + + if (!row?.toolCallId || !row.toolInput || !row.toolOutput) { + return null; + } + + return { + projectId: row.projectId, + userId: row.userId, + chatId: row.chatId, + toolCallId: row.toolCallId, + toolInput: executeSql.InputSchema.parse(row.toolInput), + toolOutput: executeSql.OutputSchema.parse(row.toolOutput), + adminMode: row.messageSource === 'admin', + }; +} + +export async function getExecuteSqlOwnerByQueryId( + queryId: string, +): Promise<{ projectId: string; userId: string; chatId: string; toolCallId: string } | null> { + const row = await getLatestExecuteSqlByQueryId(queryId); + if (!row) { + return null; + } + return { + projectId: row.projectId, + userId: row.userId, + chatId: row.chatId, + toolCallId: row.toolCallId, + }; +} + +/** Latest non-superseded execute_sql part for a query id within a chat. */ +export async function getExecuteSqlPartByQueryIdInChat( + chatId: string, + queryId: string, +): Promise<{ toolCallId: string; toolInput: executeSql.Input; toolOutput: executeSql.Output } | null> { + const [row] = await db + .select({ + toolCallId: s.messagePart.toolCallId, + toolInput: s.messagePart.toolInput, + toolOutput: s.messagePart.toolOutput, + }) + .from(s.messagePart) + .innerJoin(s.chatMessage, eq(s.messagePart.messageId, s.chatMessage.id)) + .where( + and( + eq(s.chatMessage.chatId, chatId), + isNull(s.chatMessage.supersededAt), + eq(s.messagePart.toolName, EXECUTE_SQL_TOOL_NAME), + messagePartToolOutputIdEquals(queryId), + ), + ) + .orderBy(desc(s.chatMessage.createdAt), desc(s.messagePart.createdAt), desc(s.messagePart.order)) + .limit(1) + .execute(); + + if (!row?.toolCallId || !row.toolInput || !row.toolOutput) { + return null; + } + + return { + toolCallId: row.toolCallId, + toolInput: executeSql.InputSchema.parse(row.toolInput), + toolOutput: executeSql.OutputSchema.parse(row.toolOutput), + }; +} + +export async function updateExecuteSqlPart( + toolCallId: string, + toolInput: executeSql.Input, + toolOutput: executeSql.Output, +): Promise { + await takeFirstOrThrow( + db + .update(s.messagePart) + .set({ toolInput, toolOutput }) + .where(and(eq(s.messagePart.toolCallId, toolCallId), eq(s.messagePart.toolName, EXECUTE_SQL_TOOL_NAME))) + .returning({ toolCallId: s.messagePart.toolCallId }) + .execute(), + ); +} + +async function loadLatestExecuteSqlParts(chatId: string, queryIds: Set) { + return db + .select({ + toolInput: s.messagePart.toolInput, + toolOutput: s.messagePart.toolOutput, + }) + .from(s.messagePart) + .innerJoin(s.chatMessage, eq(s.messagePart.messageId, s.chatMessage.id)) + .where( + and( + eq(s.chatMessage.chatId, chatId), + isNull(s.chatMessage.supersededAt), + eq(s.messagePart.toolName, EXECUTE_SQL_TOOL_NAME), + messagePartToolOutputIdIn(queryIds), + ), + ) + .orderBy(asc(s.chatMessage.createdAt), asc(s.messagePart.createdAt), asc(s.messagePart.order)) + .execute(); +} + +/** + * Load SQL templates for the given query ids from a chat. + * When duplicates exist, the latest non-superseded part wins. + */ +export async function getLatestSqlQueriesByIds( + chatId: string, + queryIds: Set, +): Promise> { + if (queryIds.size === 0) { + return {}; + } + + const parts = await loadLatestExecuteSqlParts(chatId, queryIds); + const queries: Record = {}; + for (const part of parts) { + const output = part.toolOutput as { id?: string } | null; + const input = part.toolInput as { sql_query?: string; database_id?: string } | null; + if (output?.id && queryIds.has(output.id) && input?.sql_query) { + queries[output.id] = { + sqlQuery: input.sql_query, + ...(input.database_id && { databaseId: input.database_id }), + }; + } + } + return queries; +} + +/** + * Load cached query result rows for the given query ids from a chat. + * When duplicates exist, the latest non-superseded part wins. + */ +export async function getLatestSqlQueryDataByIds( + chatId: string, + queryIds: Set, +): Promise> { + if (queryIds.size === 0) { + return {}; + } + + const parts = await loadLatestExecuteSqlParts(chatId, queryIds); + const data: Record = {}; + for (const part of parts) { + const output = part.toolOutput as { id?: string; data?: unknown[]; columns?: string[] } | null; + if (output?.id && queryIds.has(output.id)) { + data[output.id] = { + data: output.data ?? [], + columns: output.columns ?? [], + }; + } + } + return data; +} diff --git a/apps/backend/src/queries/shared-story.queries.ts b/apps/backend/src/queries/shared-story.queries.ts index 390a97cde..e17bac2e7 100644 --- a/apps/backend/src/queries/shared-story.queries.ts +++ b/apps/backend/src/queries/shared-story.queries.ts @@ -3,6 +3,7 @@ import { and, count, desc, eq, isNull, max, or, type SQL, sql } from 'drizzle-or import s, { type DBSharedStory } from '../db/abstractSchema'; import { db } from '../db/db'; +import * as executeSqlQueries from './execute-sql.queries'; export type SharedStoryWithLatest = DBSharedStory & { updatedAt: Date; @@ -136,24 +137,7 @@ export async function getQueryDataFromCode( return null; } - const parts = await db - .select({ toolOutput: s.messagePart.toolOutput }) - .from(s.messagePart) - .innerJoin(s.chatMessage, eq(s.messagePart.messageId, s.chatMessage.id)) - .where(and(eq(s.chatMessage.chatId, chatId), eq(s.messagePart.toolName, 'execute_sql'))) - .execute(); - - const data: Record = {}; - for (const part of parts) { - const output = part.toolOutput as { id?: string; data?: unknown[]; columns?: string[] } | null; - if (output?.id && queryIds.has(output.id)) { - data[output.id] = { - data: output.data ?? [], - columns: output.columns ?? [], - }; - } - } - + const data = await executeSqlQueries.getLatestSqlQueryDataByIds(chatId, queryIds); return Object.keys(data).length > 0 ? data : null; } diff --git a/apps/backend/src/queries/story.queries.ts b/apps/backend/src/queries/story.queries.ts index 0e7f597a4..cbd029c59 100644 --- a/apps/backend/src/queries/story.queries.ts +++ b/apps/backend/src/queries/story.queries.ts @@ -4,6 +4,7 @@ import { and, asc, desc, eq, inArray, isNull, max, or, type SQL, sql } from 'dri import s, { type DBStory, type DBStoryDataCache, type DBStoryVersion } from '../db/abstractSchema'; import { db, type DBExecutor } from '../db/db'; +import * as executeSqlQueries from './execute-sql.queries'; export type UserStoryRow = Pick< DBStory, @@ -563,15 +564,21 @@ export async function getSqlQueriesFromCode( return {}; } - return getSqlQueriesByIds(chatId, queryIds); + return executeSqlQueries.getLatestSqlQueriesByIds(chatId, queryIds); } export async function getSqlQueryById( chatId: string, queryId: string, ): Promise<{ sqlQuery: string; databaseId?: string } | null> { - const result = await getSqlQueriesByIds(chatId, new Set([queryId])); - return result[queryId] ?? null; + const part = await executeSqlQueries.getExecuteSqlPartByQueryIdInChat(chatId, queryId); + if (!part) { + return null; + } + return { + sqlQuery: part.toolInput.sql_query, + ...(part.toolInput.database_id && { databaseId: part.toolInput.database_id }), + }; } async function queryStoriesWithLatestVersion( @@ -660,32 +667,6 @@ async function getOrCreateStandaloneStory(data: { return row; } -async function getSqlQueriesByIds( - chatId: string, - queryIds: Set, -): Promise> { - const parts = await db - .select({ toolInput: s.messagePart.toolInput, toolOutput: s.messagePart.toolOutput }) - .from(s.messagePart) - .innerJoin(s.chatMessage, eq(s.messagePart.messageId, s.chatMessage.id)) - .where(and(eq(s.chatMessage.chatId, chatId), eq(s.messagePart.toolName, 'execute_sql'))) - .execute(); - - const queries: Record = {}; - for (const part of parts) { - const output = part.toolOutput as { id?: string } | null; - const input = part.toolInput as { sql_query?: string; database_id?: string } | null; - if (output?.id && queryIds.has(output.id) && input?.sql_query) { - queries[output.id] = { - sqlQuery: input.sql_query, - ...(input.database_id && { databaseId: input.database_id }), - }; - } - } - - return queries; -} - async function getStoryVersion( whereCondition: SQL, options?: { latest?: boolean }, diff --git a/apps/backend/src/services/agent.ts b/apps/backend/src/services/agent.ts index 8532ad17d..d84718e51 100644 --- a/apps/backend/src/services/agent.ts +++ b/apps/backend/src/services/agent.ts @@ -1,3 +1,4 @@ +import { markSupersededExecuteSqlParts } from '@nao/shared/execute-sql-parts'; import { story } from '@nao/shared/tools'; import type { LlmProvider, LlmSelectedModel } from '@nao/shared/types'; import { @@ -72,6 +73,7 @@ import { mcpService } from './mcp'; import { memoryService } from './memory'; import { getAzureAccessTokenForUser } from './microsoft-auth.service'; import { skillService } from './skill'; +import { getStoryTemplateWarnings } from './story-template-validation'; export interface AgentRunResult { text: string; @@ -511,6 +513,7 @@ class AgentManager { await chatQueries.upsertMessage({ ...settledMessage, chatId: this.chat.id, + source: this._toolContext.adminMode ? 'admin' : settledMessage.source, stopReason, error, tokenUsage, @@ -535,7 +538,8 @@ class AgentManager { chatUrl?: string, ): Promise { const settledUiMessages = settleInterruptedToolParts(uiMessages); - const uiMessagesWithStories = await this._syncStoryToolOutputs(settledUiMessages); + const uiMessagesWithoutStaleQueries = markSupersededExecuteSqlParts(settledUiMessages); + const uiMessagesWithStories = await this._syncStoryToolOutputs(uiMessagesWithoutStaleQueries); const uiMessagesWithStoryMode = this._addStoryMode(uiMessagesWithStories, mentions); const uiMessagesWithSkills = this._addSkills(uiMessagesWithStoryMode, mentions); const uiMessagesWithCitation = this._addCitationContext(uiMessagesWithSkills); @@ -621,16 +625,23 @@ class AgentManager { } try { - const latestVersions = new Map< + const latestStories = new Map< string, - Awaited> + { + version: NonNullable>>; + templateWarnings: string[]; + } >(); await Promise.all( [...lastToolCallByStory.keys()].map(async (storyId) => { - latestVersions.set( - storyId, - await storyQueries.getLatestVersionByChatAndSlug(this.chat.id, storyId), - ); + const version = await storyQueries.getLatestVersionByChatAndSlug(this.chat.id, storyId); + if (!version) { + return; + } + latestStories.set(storyId, { + version, + templateWarnings: await getStoryTemplateWarnings(this.chat.id, version.code), + }); }), ); @@ -647,10 +658,11 @@ class AgentManager { return { ...part, output: { ...part.output, _stale: true, code: '' } }; } - const latest = latestVersions.get(storyId); - if (!latest) { + const latestStory = latestStories.get(storyId); + if (!latestStory) { return part; } + const { version: latest, templateWarnings } = latestStory; return { ...part, @@ -660,6 +672,7 @@ class AgentManager { code: latest.code, title: latest.title, _editedByUser: latest.source === 'user', + template_warnings: templateWarnings.length > 0 ? templateWarnings : undefined, }, }; }), diff --git a/apps/backend/src/services/live-story.ts b/apps/backend/src/services/live-story.ts index e7b9deeec..6ffe5420b 100644 --- a/apps/backend/src/services/live-story.ts +++ b/apps/backend/src/services/live-story.ts @@ -1,3 +1,4 @@ +import { stripSqlFilterBlocks } from '@nao/shared/sql-template'; import { TAG_ATTRS } from '@nao/shared/story-segments'; import { generateText, Output } from 'ai'; import { CronExpressionParser } from 'cron-parser'; @@ -38,7 +39,7 @@ export async function executeLiveQuery( } const envVars = await projectQueries.getEnvVars(projectId); - return executeRawSql(query.sqlQuery, project.path, query.databaseId, envVars); + return executeRawSql(stripSqlFilterBlocks(query.sqlQuery), project.path, query.databaseId, envVars); } export interface RefreshResult { @@ -71,7 +72,12 @@ export async function refreshStoryData(chatId: string, slug: string): Promise { const projectEnvVars = await projectQueries.getEnvVars(projectId); - const result = await executeRawSql(sqlQuery, project.path!, databaseId, projectEnvVars); + const result = await executeRawSql( + stripSqlFilterBlocks(sqlQuery), + project.path!, + databaseId, + projectEnvVars, + ); queryData[queryId] = result; }), ); @@ -131,7 +137,7 @@ async function resolveFromCache(chatId: string, code: string, cache: DBStoryData return { queryData, cachedAt: cache.cachedAt }; } -async function executeRawSql( +export async function executeRawSql( sqlQuery: string, projectFolder: string, databaseId?: string, @@ -306,7 +312,7 @@ function preservesStoryStructure(originalCode: string, candidateCode: string): b function extractStructureTokens(code: string): string[] { const tokenRegex = new RegExp( - String.raw`|<\/grid>||`, + String.raw`|<\/grid>|||`, 'g', ); return code.match(tokenRegex) ?? []; diff --git a/apps/backend/src/services/ping.ts b/apps/backend/src/services/ping.ts index b3a10e046..93c91d5d4 100644 --- a/apps/backend/src/services/ping.ts +++ b/apps/backend/src/services/ping.ts @@ -43,6 +43,7 @@ interface StartupAdditionalInfo { userCount: number | null; betaAutomationsEnabled: boolean; betaContextRecommendationsEnabled: boolean; + betaStoryFiltersEnabled: boolean; smtpConfigured: boolean; loginModesConfigured: { google: boolean; @@ -64,6 +65,7 @@ async function startupAdditionalInfo(): Promise { userCount, betaAutomationsEnabled: env.BETA_AUTOMATIONS_ENABLED, betaContextRecommendationsEnabled: env.BETA_CONTEXT_RECOMMENDATIONS_ENABLED, + betaStoryFiltersEnabled: env.BETA_STORY_FILTERS_ENABLED, smtpConfigured: isSmtpConfigured(), loginModesConfigured: { google: googleConfigured, diff --git a/apps/backend/src/services/story-filters.ts b/apps/backend/src/services/story-filters.ts new file mode 100644 index 000000000..1206a4f14 --- /dev/null +++ b/apps/backend/src/services/story-filters.ts @@ -0,0 +1,138 @@ +import { + renderSqlTemplate, + type StoryFilterSelections, + type StoryFilterTypeById, + stripSqlFilterBlocks, +} from '@nao/shared/sql-template'; +import { getStoryFiltersFromCode } from '@nao/shared/story-segments'; +import { TRPCError } from '@trpc/server'; + +import { env } from '../env'; +import * as chatQueries from '../queries/chat.queries'; +import * as projectQueries from '../queries/project.queries'; +import * as storyQueries from '../queries/story.queries'; +import { assertSafeSqlIdentifier } from '../utils/sql-identifiers'; +import { executeRawSql } from './live-story'; + +const FILTER_OPTIONS_LIMIT = 100; + +export function assertStoryFiltersEnabled() { + if (!env.BETA_STORY_FILTERS_ENABLED) { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Story filters are disabled on this instance.' }); + } +} + +export async function getStoryFilterOptions( + chatId: string, + storySlug: string, + filterId: string, +): Promise<{ options: string[] }> { + const { code, projectPath, envVars, databaseId } = await loadStoryExecutionContext(chatId, storySlug); + const filter = getStoryFiltersFromCode(code).find((candidate) => candidate.id === filterId); + if (!filter) { + throw new TRPCError({ code: 'NOT_FOUND', message: `Filter "${filterId}" not found in story.` }); + } + + if (filter.options?.length) { + return { options: [...new Set(filter.options)] }; + } + + if (!filter.table || !filter.column) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: `Filter "${filterId}" has no hardcoded options and is missing table/column.`, + }); + } + + const table = assertSafeSqlIdentifier(filter.table, 'table'); + const column = assertSafeSqlIdentifier(filter.column, 'column'); + const sql = `SELECT DISTINCT ${column} AS value FROM ${table} WHERE ${column} IS NOT NULL ORDER BY ${column} LIMIT ${FILTER_OPTIONS_LIMIT}`; + const result = await executeRawSql(sql, projectPath, filter.databaseId ?? databaseId, envVars); + const options = result.data + .map((row) => { + if (!row || typeof row !== 'object') { + return null; + } + const value = (row as Record).value; + return value === null || value === undefined ? null : String(value); + }) + .filter((value): value is string => value !== null && value.trim() !== ''); + + return { options: [...new Set(options)].sort((a, b) => a.localeCompare(b)) }; +} + +export async function getFilteredStoryQueryData( + chatId: string, + storySlug: string, + selections: StoryFilterSelections, +): Promise> { + const { code, projectPath, envVars, sqlQueries } = await loadStoryExecutionContext(chatId, storySlug); + const types = filterTypesFromCode(code); + const queryData: Record = {}; + + await Promise.all( + Object.entries(sqlQueries).map(async ([queryId, { sqlQuery, databaseId }]) => { + const renderedSql = renderStorySql(sqlQuery, selections, types); + queryData[queryId] = await executeRawSql(renderedSql, projectPath, databaseId, envVars); + }), + ); + + return queryData; +} + +export async function getStoryQuerySql( + chatId: string, + storySlug: string, + queryId: string, + selections: StoryFilterSelections = {}, +): Promise<{ sqlQuery: string; renderedSql: string }> { + const { code, sqlQueries } = await loadStoryExecutionContext(chatId, storySlug); + const query = sqlQueries[queryId]; + if (!query) { + throw new TRPCError({ code: 'NOT_FOUND', message: `Query "${queryId}" not found.` }); + } + + return { + sqlQuery: query.sqlQuery, + renderedSql: renderStorySql(query.sqlQuery, selections, filterTypesFromCode(code)), + }; +} + +function filterTypesFromCode(code: string): StoryFilterTypeById { + return Object.fromEntries(getStoryFiltersFromCode(code).map((filter) => [filter.id, filter.filterType])); +} + +function renderStorySql(sqlQuery: string, selections: StoryFilterSelections, types: StoryFilterTypeById): string { + return Object.keys(selections).length === 0 + ? stripSqlFilterBlocks(sqlQuery) + : renderSqlTemplate(sqlQuery, selections, types); +} + +async function loadStoryExecutionContext(chatId: string, storySlug: string) { + const version = await storyQueries.getLatestVersionByChatAndSlug(chatId, storySlug); + if (!version) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Story not found.' }); + } + + const projectId = await chatQueries.getChatProjectId(chatId); + if (!projectId) { + throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Chat project not found.' }); + } + + const project = await projectQueries.retrieveProjectById(projectId); + if (!project.path) { + throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Project path not configured.' }); + } + + const envVars = await projectQueries.getEnvVars(projectId); + const sqlQueries = await storyQueries.getSqlQueriesFromCode(chatId, version.code); + const databaseId = Object.values(sqlQueries).find((query) => query.databaseId)?.databaseId; + + return { + code: version.code, + projectPath: project.path, + envVars, + databaseId, + sqlQueries, + }; +} diff --git a/apps/backend/src/services/story-template-validation.ts b/apps/backend/src/services/story-template-validation.ts new file mode 100644 index 000000000..b73334358 --- /dev/null +++ b/apps/backend/src/services/story-template-validation.ts @@ -0,0 +1,51 @@ +import { findUnreferencedStoryFilters, validateSqlFilterTemplate } from '@nao/shared/sql-template'; +import { getStoryFiltersFromCode } from '@nao/shared/story-segments'; + +import { env } from '../env'; +import * as storyQueries from '../queries/story.queries'; + +export async function getStoryTemplateWarnings(chatId: string, code: string): Promise { + if (!env.BETA_STORY_FILTERS_ENABLED) { + return []; + } + + const filters = getStoryFiltersFromCode(code); + const knownFilterIds = filters.map((filter) => filter.id); + const sqlQueries = await storyQueries.getSqlQueriesFromCode(chatId, code); + const warnings: string[] = []; + + for (const duplicateId of findDuplicateFilterIds(knownFilterIds)) { + warnings.push( + `Story declares multiple tags with id "${duplicateId}". Filter ids must be unique — rename or remove the duplicates so selections and SQL rendering use the same definition.`, + ); + } + + for (const [queryId, { sqlQuery }] of Object.entries(sqlQueries)) { + for (const issue of validateSqlFilterTemplate(sqlQuery, { knownFilterIds })) { + warnings.push(`[${queryId}] ${issue}`); + } + } + + if (knownFilterIds.length > 0) { + const sqlList = Object.values(sqlQueries).map((query) => query.sqlQuery); + for (const filterId of findUnreferencedStoryFilters(knownFilterIds, sqlList)) { + warnings.push( + `Story filter "${filterId}" is declared but not referenced in any chart/table SQL. Add {% filter ${filterId} %} ... {{ filters.${filterId}.sql }} ... {% endfilter %} to the relevant queries, or remove the unused tag.`, + ); + } + } + + return warnings; +} + +function findDuplicateFilterIds(filterIds: string[]): string[] { + const seen = new Set(); + const duplicates = new Set(); + for (const filterId of filterIds) { + if (seen.has(filterId)) { + duplicates.add(filterId); + } + seen.add(filterId); + } + return [...duplicates]; +} diff --git a/apps/backend/src/services/update-sql.ts b/apps/backend/src/services/update-sql.ts new file mode 100644 index 000000000..ead6b630b --- /dev/null +++ b/apps/backend/src/services/update-sql.ts @@ -0,0 +1,76 @@ +import { stripSqlFilterBlocks } from '@nao/shared/sql-template'; +import type { executeSql } from '@nao/shared/tools'; +import { TRPCError } from '@trpc/server'; + +import { executeQuery } from '../agents/tools/execute-sql'; +import { getLatestExecuteSqlByQueryId, updateExecuteSqlPart } from '../queries/execute-sql.queries'; +import { buildToolContext } from './agent'; + +export async function updateSqlQueryInChat(opts: { + queryId: string; + sqlQuery: string; + databaseId?: string; + name?: string; + userId: string; +}): Promise<{ input: executeSql.Input; output: executeSql.Output; toolCallId: string; chatId: string }> { + const { existing, context, input } = await prepareSqlEditContext(opts); + const output = await executeQuery({ ...input, query_id: opts.queryId as `query_${string}` }, context); + await updateExecuteSqlPart(existing.toolCallId, input, output); + + return { input, output, toolCallId: existing.toolCallId, chatId: existing.chatId }; +} + +export async function previewSqlQueryInChat(opts: { + queryId: string; + sqlQuery: string; + databaseId?: string; + userId: string; +}): Promise { + const { context, input } = await prepareSqlEditContext(opts); + return executeQuery(input, context); +} + +async function prepareSqlEditContext(opts: { + queryId: string; + sqlQuery: string; + databaseId?: string; + name?: string; + userId: string; +}) { + assertSqlQueryEditable(opts.sqlQuery); + + const existing = await getLatestExecuteSqlByQueryId(opts.queryId); + if (!existing) { + throw new TRPCError({ code: 'NOT_FOUND', message: `Query ${opts.queryId} not found.` }); + } + if (existing.userId !== opts.userId) { + throw new TRPCError({ code: 'FORBIDDEN', message: 'You are not authorized to edit this query.' }); + } + + const context = await buildToolContext({ + projectId: existing.projectId, + userId: opts.userId, + chatId: existing.chatId, + adminMode: existing.adminMode, + }); + + const input: executeSql.Input = { + sql_query: opts.sqlQuery, + database_id: opts.databaseId ?? existing.toolInput.database_id ?? undefined, + name: opts.name ?? existing.toolInput.name ?? undefined, + }; + + return { existing, context, input }; +} + +export function assertSqlQueryEditable(sqlQuery: string): void { + if (!sqlQuery.trim()) { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'SQL query cannot be empty.' }); + } + if (stripSqlFilterBlocks(sqlQuery).trim() === '') { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'SQL query cannot be empty after removing filter blocks.', + }); + } +} diff --git a/apps/backend/src/trpc/router.ts b/apps/backend/src/trpc/router.ts index e1d1e98b7..e33871654 100644 --- a/apps/backend/src/trpc/router.ts +++ b/apps/backend/src/trpc/router.ts @@ -27,6 +27,7 @@ import { projectRoutes } from './project.routes'; import { sharedChatRoutes } from './shared-chat.routes'; import { sharedStoryRoutes } from './shared-story.routes'; import { skillRoutes } from './skill.routes'; +import { sqlRoutes } from './sql.routes'; import { storyRoutes } from './story.routes'; import { storyFolderRoutes } from './story-folder.routes'; import { systemRoutes } from './system.routes'; @@ -42,6 +43,7 @@ export const trpcRouter = router({ embed: embedRoutes, chart: chartRoutes, chat: chatRoutes, + sql: sqlRoutes, sharedChat: sharedChatRoutes, automation: automationRoutes, chatFork: chatForkRoutes, diff --git a/apps/backend/src/trpc/shared-story.routes.ts b/apps/backend/src/trpc/shared-story.routes.ts index ca2439d20..49f58db04 100644 --- a/apps/backend/src/trpc/shared-story.routes.ts +++ b/apps/backend/src/trpc/shared-story.routes.ts @@ -10,6 +10,12 @@ import * as storyQueries from '../queries/story.queries'; import * as storyFolderQueries from '../queries/story-folder.queries'; import { logActivity } from '../services/activity'; import { executeLiveQuery, getStoryQueryData, refreshStoryData } from '../services/live-story'; +import { + assertStoryFiltersEnabled, + getFilteredStoryQueryData, + getStoryFilterOptions, + getStoryQuerySql, +} from '../services/story-filters'; import { logAnalyticsEvent } from '../utils/analytics-event'; import { notifySharedItemRecipients } from '../utils/email'; import { buildDownloadResponse } from '../utils/story-download'; @@ -166,6 +172,50 @@ export const sharedStoryRoutes = { return executeLiveQuery(input.chatId, input.queryId); }), + getFilterOptions: shareAccessProcedure + .input(z.object({ shareId: z.string(), filterId: z.string() })) + .query(async ({ input, ctx }) => { + assertStoryFiltersEnabled(); + const shared = ctx.resource; + if (!shared.chatId) { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Shared story has no chat.' }); + } + return getStoryFilterOptions(shared.chatId, shared.slug, input.filterId); + }), + + getFilteredQueryData: shareAccessProcedure + .input( + z.object({ + shareId: z.string(), + selections: z.record(z.string(), z.union([z.string(), z.array(z.string())])), + }), + ) + .query(async ({ input, ctx }) => { + assertStoryFiltersEnabled(); + const shared = ctx.resource; + if (!shared.chatId) { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Shared story has no chat.' }); + } + return getFilteredStoryQueryData(shared.chatId, shared.slug, input.selections); + }), + + getQuerySql: shareAccessProcedure + .input( + z.object({ + shareId: z.string(), + queryId: z.string(), + selections: z.record(z.string(), z.union([z.string(), z.array(z.string())])).default({}), + }), + ) + .query(async ({ input, ctx }) => { + assertStoryFiltersEnabled(); + const shared = ctx.resource; + if (!shared.chatId) { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Shared story has no chat.' }); + } + return getStoryQuerySql(shared.chatId, shared.slug, input.queryId, input.selections); + }), + refreshData: shareAccessProcedure.input(z.object({ shareId: z.string() })).mutation(async ({ ctx }) => { const shared = ctx.resource; const story = await storyQueries.getStoryByChatAndSlug(shared.chatId!, shared.slug); diff --git a/apps/backend/src/trpc/sql.routes.ts b/apps/backend/src/trpc/sql.routes.ts new file mode 100644 index 000000000..b1709ab07 --- /dev/null +++ b/apps/backend/src/trpc/sql.routes.ts @@ -0,0 +1,32 @@ +import { z } from 'zod/v4'; + +import { previewSqlQueryInChat, updateSqlQueryInChat } from '../services/update-sql'; +import { protectedProcedure } from './trpc'; + +const sqlEditInput = z.object({ + queryId: z.string().regex(/^query_.+$/), + sql_query: z.string().min(1), + database_id: z.string().nullish(), + name: z.string().nullish(), +}); + +export const sqlRoutes = { + previewQuery: protectedProcedure.input(sqlEditInput).mutation(async ({ input, ctx }) => { + return previewSqlQueryInChat({ + queryId: input.queryId, + sqlQuery: input.sql_query, + databaseId: input.database_id ?? undefined, + userId: ctx.user.id, + }); + }), + + updateQuery: protectedProcedure.input(sqlEditInput).mutation(async ({ input, ctx }) => { + return updateSqlQueryInChat({ + queryId: input.queryId, + sqlQuery: input.sql_query, + databaseId: input.database_id ?? undefined, + name: input.name ?? undefined, + userId: ctx.user.id, + }); + }), +}; diff --git a/apps/backend/src/trpc/story.routes.ts b/apps/backend/src/trpc/story.routes.ts index c46df87ea..a6d3f11f2 100644 --- a/apps/backend/src/trpc/story.routes.ts +++ b/apps/backend/src/trpc/story.routes.ts @@ -15,6 +15,12 @@ import * as storyFolderQueries from '../queries/story-folder.queries'; import { naturalLanguageToCron } from '../services/cron-nlp'; import { executeLiveQuery, getStoryQueryData, refreshStoryData } from '../services/live-story'; import { nextCronTick } from '../services/scheduler.service'; +import { + assertStoryFiltersEnabled, + getFilteredStoryQueryData, + getStoryFilterOptions, + getStoryQuerySql, +} from '../services/story-filters'; import { logAnalyticsEvent } from '../utils/analytics-event'; import { buildDownloadResponse } from '../utils/story-download'; import { backfillMissingQueryData } from '../utils/story-query-data'; @@ -293,6 +299,40 @@ export const storyRoutes = { return executeLiveQuery(input.chatId, input.queryId); }), + getFilterOptions: chatOwnerProcedure + .input(z.object({ chatId: z.string(), storySlug: z.string(), filterId: z.string() })) + .query(async ({ input }) => { + assertStoryFiltersEnabled(); + return getStoryFilterOptions(input.chatId, input.storySlug, input.filterId); + }), + + getFilteredQueryData: chatOwnerProcedure + .input( + z.object({ + chatId: z.string(), + storySlug: z.string(), + selections: z.record(z.string(), z.union([z.string(), z.array(z.string())])), + }), + ) + .query(async ({ input }) => { + assertStoryFiltersEnabled(); + return getFilteredStoryQueryData(input.chatId, input.storySlug, input.selections); + }), + + getQuerySql: chatOwnerProcedure + .input( + z.object({ + chatId: z.string(), + storySlug: z.string(), + queryId: z.string(), + selections: z.record(z.string(), z.union([z.string(), z.array(z.string())])).default({}), + }), + ) + .query(async ({ input }) => { + assertStoryFiltersEnabled(); + return getStoryQuerySql(input.chatId, input.storySlug, input.queryId, input.selections); + }), + parseCronFromText: projectProtectedProcedure .input(z.object({ text: z.string().min(1) })) .mutation(async ({ input, ctx }) => { diff --git a/apps/backend/src/trpc/system.routes.ts b/apps/backend/src/trpc/system.routes.ts index e66801bab..cff588510 100644 --- a/apps/backend/src/trpc/system.routes.ts +++ b/apps/backend/src/trpc/system.routes.ts @@ -10,6 +10,7 @@ export const systemRoutes = { enableUserSignup: await isUserSignupAvailable(), betaAutomationsEnabled: env.BETA_AUTOMATIONS_ENABLED, betaContextRecommendationsEnabled: env.BETA_CONTEXT_RECOMMENDATIONS_ENABLED, + betaStoryFiltersEnabled: env.BETA_STORY_FILTERS_ENABLED, })), version: adminProtectedProcedure.query(() => ({ diff --git a/apps/backend/src/utils/chat-context-usage.ts b/apps/backend/src/utils/chat-context-usage.ts index b87febce6..404741468 100644 --- a/apps/backend/src/utils/chat-context-usage.ts +++ b/apps/backend/src/utils/chat-context-usage.ts @@ -1,3 +1,4 @@ +import { markSupersededExecuteSqlParts } from '@nao/shared/execute-sql-parts'; import type { LlmProvider } from '@nao/shared/types'; import { convertToModelMessages, type ModelMessage, type Tool } from 'ai'; @@ -35,7 +36,7 @@ export async function getChatAsModelMessages(opts: { projectId: string; tools: Record; }): Promise { - const uiMessages = await chatQueries.getChatMessages(opts.chatId); + const uiMessages = markSupersededExecuteSqlParts(await chatQueries.getChatMessages(opts.chatId)); const uiMessagesWithCompaction = compactionService.useLastCompaction(uiMessages); const memories = await memoryService.safeGetUserMemories(opts.userId, opts.projectId, opts.chatId); const systemPrompt = renderToMarkdown(SystemPrompt({ memories })); diff --git a/apps/backend/src/utils/sql-identifiers.ts b/apps/backend/src/utils/sql-identifiers.ts new file mode 100644 index 000000000..745f97753 --- /dev/null +++ b/apps/backend/src/utils/sql-identifiers.ts @@ -0,0 +1,14 @@ +import { TRPCError } from '@trpc/server'; + +export function assertSafeSqlIdentifier(value: string, kind: 'table' | 'column'): string { + const unquoted = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/; + const doubleQuoted = /^"[^"]+"(\."[^"]+")*$/; + const backtickQuoted = /^`[^`]+`(\.`[^`]+`)*$/; + if (!unquoted.test(value) && !doubleQuoted.test(value) && !backtickQuoted.test(value)) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: `Invalid filter ${kind} identifier "${value}". Use unquoted names, "double quotes", or \`backticks\` (required for hyphens).`, + }); + } + return value; +} diff --git a/apps/backend/src/utils/story-html.tsx b/apps/backend/src/utils/story-html.tsx index 0082eed17..83eafbc9a 100644 --- a/apps/backend/src/utils/story-html.tsx +++ b/apps/backend/src/utils/story-html.tsx @@ -95,6 +95,8 @@ function StorySegment({ segment, queryData }: { segment: Segment; queryData: Que return ; case 'table': return ; + case 'filter': + return null; case 'grid': return ; } diff --git a/apps/backend/tests/license.test.ts b/apps/backend/tests/license.test.ts index 7f8e8f30f..113ee267f 100644 --- a/apps/backend/tests/license.test.ts +++ b/apps/backend/tests/license.test.ts @@ -177,6 +177,7 @@ describe('license.service', () => { process.env.NAO_MODE = 'self-hosted'; process.env.BETA_AUTOMATIONS_ENABLED = 'false'; process.env.BETA_CONTEXT_RECOMMENDATIONS_ENABLED = 'true'; + process.env.BETA_STORY_FILTERS_ENABLED = 'true'; process.env.SMTP_HOST = 'smtp.example.com'; process.env.SMTP_MAIL_FROM = 'hello@example.com'; process.env.SMTP_PASSWORD = 'secret'; @@ -202,6 +203,7 @@ describe('license.service', () => { additionalInfo: expect.objectContaining({ betaAutomationsEnabled: false, betaContextRecommendationsEnabled: true, + betaStoryFiltersEnabled: true, smtpConfigured: true, loginModesConfigured: { google: false, diff --git a/apps/backend/tests/story-filters.test.ts b/apps/backend/tests/story-filters.test.ts new file mode 100644 index 000000000..40ed3bf01 --- /dev/null +++ b/apps/backend/tests/story-filters.test.ts @@ -0,0 +1,51 @@ +import { renderSqlTemplate, stripSqlFilterBlocks } from '@nao/shared/sql-template'; +import { describe, expect, it } from 'vitest'; + +import { assertSafeSqlIdentifier } from '../src/utils/sql-identifiers'; + +describe('story filter SQL templates', () => { + it('strips filter blocks for chat / live baseline execution', () => { + const sql = ` +SELECT SUM(revenue) AS revenue +FROM orders +WHERE 1 = 1 +{% filter country %} AND country IN ({{ filters.country.sql }}) {% endfilter %} +`.trim(); + + expect(stripSqlFilterBlocks(sql)).toContain('WHERE 1 = 1'); + expect(stripSqlFilterBlocks(sql)).not.toContain('{% filter'); + expect(stripSqlFilterBlocks(sql)).not.toContain('country IN'); + }); + + it('renders active filter selections into executable SQL', () => { + const sql = ` +SELECT SUM(revenue) AS revenue +FROM orders +WHERE 1 = 1 +{% filter country %} AND country IN ({{ filters.country.sql }}) {% endfilter %} +`.trim(); + + expect(renderSqlTemplate(sql, { country: ['US', 'FR'] }, { country: 'multi_select' })).toContain( + "AND country IN ('US', 'FR')", + ); + }); +}); + +describe('assertSafeSqlIdentifier', () => { + it('accepts dotted and quoted identifiers', () => { + expect(assertSafeSqlIdentifier('orders', 'table')).toBe('orders'); + expect(assertSafeSqlIdentifier('public.orders', 'table')).toBe('public.orders'); + expect(assertSafeSqlIdentifier('"Order Status"', 'column')).toBe('"Order Status"'); + expect(assertSafeSqlIdentifier('`nao-production`.`prod_silver`.`dim_products`', 'table')).toBe( + '`nao-production`.`prod_silver`.`dim_products`', + ); + }); + + it('rejects unsafe identifiers', () => { + expect(() => assertSafeSqlIdentifier('orders; DROP TABLE x', 'table')).toThrow(/Invalid filter table/); + expect(() => assertSafeSqlIdentifier('col-name', 'column')).toThrow(/Invalid filter column/); + expect(() => assertSafeSqlIdentifier('nao-production.prod_silver.dim_products', 'table')).toThrow( + /Invalid filter table/, + ); + }); +}); diff --git a/apps/backend/tests/tool-outputs/execute-sql.test.tsx b/apps/backend/tests/tool-outputs/execute-sql.test.tsx index 0fbebe76f..50c260502 100644 --- a/apps/backend/tests/tool-outputs/execute-sql.test.tsx +++ b/apps/backend/tests/tool-outputs/execute-sql.test.tsx @@ -78,6 +78,23 @@ created_at: 2025-03-10T09:15:00Z expect(result).toBe('The query was successfully executed and returned no rows.'); }); + it('includes the query id when an empty result has template warnings', () => { + const result = renderToMarkdown( + , + ); + + expect(result).toContain('Query ID: query_warning'); + expect(result).toContain('Invalid filter template'); + }); + it('warns when the row count equals the applied LIMIT', () => { const result = renderToMarkdown( groupMessages(messages), [messages]); + const messageGroups = useMemo(() => groupMessages(filterSupersededExecuteSqlParts(messages)), [messages]); const citation = useMemo(() => { if (!forkMetadata?.selectionText || forkMetadata.selectionStart == null || forkMetadata.selectionEnd == null) { diff --git a/apps/frontend/src/components/chat-messages/chat-messages.tsx b/apps/frontend/src/components/chat-messages/chat-messages.tsx index 078a21e97..6fa12cafd 100644 --- a/apps/frontend/src/components/chat-messages/chat-messages.tsx +++ b/apps/frontend/src/components/chat-messages/chat-messages.tsx @@ -1,6 +1,7 @@ import { memo, useMemo, useRef } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useParams, useRouterState } from '@tanstack/react-router'; +import { filterSupersededExecuteSqlParts } from '@nao/shared/execute-sql-parts'; import { TextShimmer } from '../ui/text-shimmer'; import { ChatError } from './chat-error'; import { FollowUpSuggestions } from './follow-up-suggestions'; @@ -74,7 +75,10 @@ export const ChatMessagesContent = memo(() => { const followUpSuggestionsToolCall = useMemo(() => getLastFollowUpSuggestionsToolCall(messages), [messages]); const extraComponentsRef = useRef(null); const extraComponentsHeight = useHeight(extraComponentsRef); - const visibleMessages = useMemo(() => messages.filter((m) => !m.isForked), [messages]); + const visibleMessages = useMemo( + () => filterSupersededExecuteSqlParts(messages.filter((m) => !m.isForked)), + [messages], + ); const messageGroups = useMemo(() => groupMessages(visibleMessages), [visibleMessages]); const forkMetadata = useQuery({ diff --git a/apps/frontend/src/components/fix-in-chat-button.tsx b/apps/frontend/src/components/fix-in-chat-button.tsx new file mode 100644 index 000000000..7b77986ea --- /dev/null +++ b/apps/frontend/src/components/fix-in-chat-button.tsx @@ -0,0 +1,26 @@ +import { MessageSquare } from 'lucide-react'; + +import { useOptionalAgentContext } from '@/contexts/agent.provider'; +import { Button } from '@/components/ui/button'; + +export function FixInChatButton({ message, className }: { message: string; className?: string }) { + const agent = useOptionalAgentContext(); + if (!agent || agent.isReadonly || !message.trim()) { + return null; + } + + return ( + + ); +} diff --git a/apps/frontend/src/components/mcp-app/story-app-view.tsx b/apps/frontend/src/components/mcp-app/story-app-view.tsx index 64b79714b..7ed27d337 100644 --- a/apps/frontend/src/components/mcp-app/story-app-view.tsx +++ b/apps/frontend/src/components/mcp-app/story-app-view.tsx @@ -38,16 +38,58 @@ export function StoryAppView({ title, code, queryData, naoUrl, onDownload }: Sto function StoryBody({ code, queryData }: { code: string; queryData: QueryDataMap | null }) { const renderChart = useCallback( - (chart: ParsedChartBlock) => , - [queryData], + ( + chart: ParsedChartBlock, + { + queryData: data, + baselineQueryData, + hasActiveFilters, + isRefreshing, + }: { + queryData: QueryDataMap | null; + baselineQueryData: QueryDataMap | null; + hasActiveFilters: boolean; + isRefreshing: boolean; + }, + ) => ( + + ), + [], ); const renderTable = useCallback( - (table: ParsedTableBlock) => , - [queryData], + ( + table: ParsedTableBlock, + { + queryData: data, + hasActiveFilters, + isRefreshing, + }: { queryData: QueryDataMap | null; hasActiveFilters: boolean; isRefreshing: boolean }, + ) => ( + + ), + [], ); - return ; + return ( + + ); } function StoryDownloadButton({ onDownload }: { onDownload: (format: DownloadFormat) => Promise }) { diff --git a/apps/frontend/src/components/side-panel/sql-editor.tsx b/apps/frontend/src/components/side-panel/sql-editor.tsx index beef9cb4b..85854d2c0 100644 --- a/apps/frontend/src/components/side-panel/sql-editor.tsx +++ b/apps/frontend/src/components/side-panel/sql-editor.tsx @@ -1,32 +1,276 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Editor } from '@monaco-editor/react'; +import { validateSqlFilterTemplate } from '@nao/shared/sql-template'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { AlertTriangle, Loader2, Play, Save } from 'lucide-react'; +import { usePanelRef } from 'react-resizable-panels'; import { ResizableSeparator, ResizablePanel, ResizablePanelGroup } from '../ui/resizable'; +import type { Monaco } from '@monaco-editor/react'; +import type { UIMessage } from '@nao/backend/chat'; import type { executeSql } from '@nao/shared/tools'; +import type { editor } from 'monaco-editor'; + +import { useOptionalAgentContext } from '@/contexts/agent.provider'; +import { FixInChatButton } from '@/components/fix-in-chat-button'; import { SidePanelHeader } from '@/components/side-panel/side-panel-header'; import { TableDisplay } from '@/components/tool-calls/display-table'; +import { Button } from '@/components/ui/button'; +import { applyExecuteSqlResultToMessages } from '@/lib/execute-sql-messages'; import { formatSQL } from '@/lib/sql-formatter'; +import { trpc } from '@/main'; + +const RESULTS_MIN_HEIGHT = 240; +const SQL_MIN_HEIGHT = 100; +const SEPARATOR_HEIGHT = 12; +const SQL_CONTENT_PADDING = 8; + +export const SidePanelContent = ({ + input, + output, + editable = false, +}: { + input: executeSql.Input; + output: executeSql.Output; + editable?: boolean; +}) => { + const queryClient = useQueryClient(); + const agent = useOptionalAgentContext(); + const agentRef = useRef(agent); + agentRef.current = agent; + const queryId = output.id; + const [sqlQuery, setSqlQuery] = useState(input.sql_query); + const [savedSql, setSavedSql] = useState(input.sql_query); + const [previewOutput, setPreviewOutput] = useState(output); + const [error, setError] = useState(null); + const rootRef = useRef(null); + const editorRef = useRef(null); + const sqlPanelRef = usePanelRef(); + const groupElementRef = useRef(null); + const sqlQueryRef = useRef(sqlQuery); + sqlQueryRef.current = sqlQuery; + + useEffect(() => { + setSqlQuery(input.sql_query); + setSavedSql(input.sql_query); + setPreviewOutput(output); + setError(null); + }, [queryId]); // eslint-disable-line react-hooks/exhaustive-deps -- reset only when switching queries + + const canEdit = Boolean(editable && queryId && agent && !agent.isReadonly); + const isDirty = sqlQuery !== savedSql; + const validationIssues = useMemo(() => (canEdit ? validateSqlFilterTemplate(sqlQuery) : []), [canEdit, sqlQuery]); + + const previewMutation = useMutation(trpc.sql.previewQuery.mutationOptions()); + const saveMutation = useMutation(trpc.sql.updateQuery.mutationOptions()); + const isBusy = previewMutation.isPending || saveMutation.isPending; + const isBusyRef = useRef(isBusy); + isBusyRef.current = isBusy; + + const buildMutationInput = useCallback( + (sql: string) => ({ + queryId: queryId!, + sql_query: sql, + database_id: input.database_id ?? undefined, + name: input.name ?? undefined, + }), + [queryId, input.database_id, input.name], + ); + + const beginMutation = useCallback(() => { + const sql = editorRef.current?.getValue() ?? sqlQueryRef.current; + if (!queryId || !sql.trim() || isBusyRef.current) { + return null; + } + setSqlQuery(sql); + setError(null); + return sql; + }, [queryId]); + + const handleRun = useCallback(() => { + const sql = beginMutation(); + if (!sql) { + return; + } + previewMutation.mutate(buildMutationInput(sql), { + onSuccess: (result) => { + setPreviewOutput(result); + setError(null); + }, + onError: (err) => setError(getMutationErrorMessage(err)), + }); + }, [beginMutation, buildMutationInput, previewMutation]); + + const handleSaveAndRun = useCallback(() => { + const sql = beginMutation(); + if (!sql) { + return; + } + saveMutation.mutate(buildMutationInput(sql), { + onSuccess: (result) => { + setPreviewOutput(result.output); + setSqlQuery(result.input.sql_query); + setSavedSql(result.input.sql_query); + setError(null); + + const applyUpdate = (messages: UIMessage[]) => + applyExecuteSqlResultToMessages(messages, result.output.id, result.input, result.output); + + const currentAgent = agentRef.current; + if (currentAgent) { + currentAgent.setMessages((messages) => applyUpdate(messages)); + if (currentAgent.chatId) { + queryClient.setQueryData(trpc.chat.get.queryKey({ chatId: currentAgent.chatId }), (previous) => + previous ? { ...previous, messages: applyUpdate(previous.messages) } : previous, + ); + } + } + + void queryClient.invalidateQueries(trpc.story.getQuerySql.pathFilter()); + void queryClient.invalidateQueries(trpc.story.getFilteredQueryData.pathFilter()); + void queryClient.invalidateQueries(trpc.storyShare.getQuerySql.pathFilter()); + void queryClient.invalidateQueries(trpc.storyShare.getFilteredQueryData.pathFilter()); + }, + onError: (err) => setError(getMutationErrorMessage(err)), + }); + }, [beginMutation, buildMutationInput, saveMutation, queryClient]); + + useEffect(() => { + if (!canEdit) { + return; + } + + const isFocusInsidePanel = (target: EventTarget | null) => + target instanceof Node && Boolean(rootRef.current?.contains(target)); + + const onKeyDown = (event: KeyboardEvent) => { + if (!isFocusInsidePanel(event.target)) { + return; + } + const withModifier = (event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey; + if (withModifier && event.key.toLowerCase() === 's') { + event.preventDefault(); + event.stopPropagation(); + handleSaveAndRun(); + return; + } + if (withModifier && event.key === 'Enter') { + event.preventDefault(); + event.stopPropagation(); + handleRun(); + } + }; + + document.addEventListener('keydown', onKeyDown, true); + return () => document.removeEventListener('keydown', onKeyDown, true); + }, [canEdit, handleSaveAndRun, handleRun]); + + const fitSqlPanelToContent = useCallback( + (instance: editor.IStandaloneCodeEditor) => { + const groupHeight = groupElementRef.current?.clientHeight ?? 0; + const panel = sqlPanelRef.current; + if (!panel || groupHeight <= 0) { + return false; + } + + instance.layout(); + const contentHeight = instance.getContentHeight() + SQL_CONTENT_PADDING; + const maxSqlHeight = Math.max(SQL_MIN_HEIGHT, groupHeight - RESULTS_MIN_HEIGHT - SEPARATOR_HEIGHT); + const sqlHeight = Math.min(Math.max(contentHeight, SQL_MIN_HEIGHT), maxSqlHeight); + panel.resize(sqlHeight); + return true; + }, + [sqlPanelRef], + ); + + const handleEditorMount = useCallback( + (instance: editor.IStandaloneCodeEditor, _monaco: Monaco) => { + editorRef.current = instance; + + let fitted = false; + const tryFit = () => { + if (fitted || !fitSqlPanelToContent(instance)) { + return; + } + fitted = true; + disposable.dispose(); + }; + const disposable = instance.onDidContentSizeChange(tryFit); + requestAnimationFrame(tryFit); + }, + [fitSqlPanelToContent], + ); -export const SidePanelContent = ({ input, output }: { input: executeSql.Input; output: executeSql.Output }) => { return ( -
- - - - +
+ + + {canEdit && ( +
+
+ {isDirty ? 'Unsaved changes' : 'Edit SQL, then run or save'} +
+
+ + +
+
+ )} + + {error && ( +
{error}
+ )} + {validationIssues.length > 0 && } + + +
setSqlQuery(value ?? '') : undefined} + onMount={handleEditorMount} language='sql' theme='light' options={{ + readOnly: !canEdit, minimap: { enabled: false, }, folding: false, - lineNumbers: 'off', + lineNumbers: canEdit ? 'on' : 'off', scrollbar: { horizontal: 'hidden', vertical: 'hidden', @@ -44,10 +288,10 @@ export const SidePanelContent = ({ input, output }: { input: executeSql.Input; o - + []} - columns={output.columns} + data={previewOutput.data as Record[]} + columns={previewOutput.columns} className='h-full' tableContainerClassName='flex-1 rounded-none border-0' emptyLabel='No rows returned' @@ -58,3 +302,47 @@ export const SidePanelContent = ({ input, output }: { input: executeSql.Input; o
); }; + +function SqlValidationBanner({ queryId, issues }: { queryId: string | undefined; issues: string[] }) { + const fixMessage = [ + `I'm seeing SQL template validation warnings${queryId ? ` for query ${queryId}` : ''}:`, + ...issues.map((issue) => `- ${issue}`), + '', + 'Please fix the SQL filter template while preserving the query id.', + ].join('\n'); + + return ( +
+
+
+ + + {issues.length} SQL template {issues.length === 1 ? 'warning' : 'warnings'} + +
+
    + {issues.slice(0, 5).map((issue) => ( +
  • + {issue} +
  • + ))} + {issues.length > 5 &&
  • and {issues.length - 5} more...
  • } +
+
+ +
+ ); +} + +function getMutationErrorMessage(err: unknown): string { + if (err instanceof Error && err.message) { + return err.message; + } + if (err && typeof err === 'object' && 'shape' in err) { + const shape = (err as { shape?: { message?: string } }).shape; + if (shape?.message) { + return shape.message; + } + } + return 'Failed to run query'; +} diff --git a/apps/frontend/src/components/side-panel/story-chart-embed.tsx b/apps/frontend/src/components/side-panel/story-chart-embed.tsx index 17d241f8a..ef10867bc 100644 --- a/apps/frontend/src/components/side-panel/story-chart-embed.tsx +++ b/apps/frontend/src/components/side-panel/story-chart-embed.tsx @@ -1,17 +1,20 @@ -import { Pencil } from 'lucide-react'; +import { Code, Pencil } from 'lucide-react'; import { memo, useMemo, useState } from 'react'; import { StoryEmbedFallback } from './story-embed-fallback'; -import type { UIMessage } from '@nao/backend/chat'; import type { ParsedChartBlock } from '@nao/shared/story-segments'; import type { displayChart } from '@nao/shared/tools'; +import { StoryChartQueryView } from '@/components/side-panel/story-chart-query'; import { ChartDisplay } from '@/components/tool-calls/display-chart'; import { ChartConfigEditDialog } from '@/components/tool-calls/display-chart-edit-dialog'; import { Button } from '@/components/ui/button'; import { useOptionalAgentContext } from '@/contexts/agent.provider'; import { useStoryChartEdit } from '@/contexts/story-chart-edit'; import { useStoryEmbedData } from '@/contexts/story-embed-data'; +import { useStoryQuerySql } from '@/contexts/story-query-sql'; import { sortByDateKey } from '@/lib/charts.utils'; +import { findLatestExecuteSqlInMessages } from '@/lib/execute-sql-messages'; +import { cn } from '@/lib/utils'; const STORY_CHART_HEIGHT_CLASS = 'h-72'; @@ -33,18 +36,7 @@ export const StoryChartEmbed = memo(function StoryChartEmbed({ return fromEmbedData; } - const findInMessages = (messages: UIMessage[]) => { - for (const message of messages) { - for (const part of message.parts) { - if (part.type === 'tool-execute_sql' && part.output?.id === chart.queryId) { - return part.output; - } - } - } - return null; - }; - - return findInMessages(agent?.messages ?? []); + return findLatestExecuteSqlInMessages(agent?.messages ?? [], chart.queryId)?.output ?? null; }, [embedData, agent?.messages, chart.queryId]); const data = useMemo( @@ -118,8 +110,11 @@ export function StoryChartEmbedShell({ children, }: StoryChartEmbedShellProps) { const edit = useStoryChartEdit(); + const querySqlSource = useStoryQuerySql(); const [isEditOpen, setIsEditOpen] = useState(false); + const [showQuery, setShowQuery] = useState(false); const canEdit = Boolean(edit && chart.rawTag); + const canViewQuery = Boolean(querySqlSource); const config = useMemo( () => ({ @@ -161,10 +156,21 @@ export function StoryChartEmbedShell({ ) : null; + const queryButton = canViewQuery ? ( + + ) : null; return (
- {!isKpi && (canEdit || dragHandle != null || chart.title) && ( + {!isKpi && (canEdit || canViewQuery || dragHandle != null || chart.title) && (
{chart.title ? ( @@ -175,15 +181,21 @@ export function StoryChartEmbedShell({ )}
{dragHandle} + {queryButton} {editButton}
)} -
- {children} - {isKpi && (dragHandle != null || editButton) && ( -
+
+ {showQuery && querySqlSource ? ( + + ) : ( + children + )} + {isKpi && (dragHandle != null || canViewQuery || canEdit) && ( +
{dragHandle} + {queryButton} {editButton}
)} diff --git a/apps/frontend/src/components/side-panel/story-chart-query.tsx b/apps/frontend/src/components/side-panel/story-chart-query.tsx new file mode 100644 index 000000000..a32507316 --- /dev/null +++ b/apps/frontend/src/components/side-panel/story-chart-query.tsx @@ -0,0 +1,75 @@ +import { useQuery } from '@tanstack/react-query'; +import { Loader2 } from 'lucide-react'; +import type { StoryQuerySqlSource } from '@/contexts/story-query-sql'; + +import { SqlQueryDisplay } from '@/components/tool-calls/sql-query-display'; +import { trpc } from '@/main'; + +export function StoryChartQueryView({ queryId, source }: { queryId: string; source: StoryQuerySqlSource }) { + const ownedQuery = useQuery({ + ...trpc.story.getQuerySql.queryOptions({ + chatId: source.api.kind === 'owned' ? source.api.chatId : '', + storySlug: source.api.kind === 'owned' ? source.api.storySlug : '', + queryId, + selections: source.selections, + }), + enabled: source.api.kind === 'owned', + }); + + const sharedQuery = useQuery({ + ...trpc.storyShare.getQuerySql.queryOptions({ + shareId: source.api.kind === 'shared' ? source.api.shareId : '', + queryId, + selections: source.selections, + }), + enabled: source.api.kind === 'shared', + }); + + const query = source.api.kind === 'shared' ? sharedQuery : ownedQuery; + const data = query.data; + + if (query.isLoading) { + return ( +
+ + Loading query… +
+ ); + } + + if (query.isError || !data) { + return ( +
+ {query.error?.message ?? 'Query unavailable'} +
+ ); + } + + const showBoth = data.sqlQuery.trim() !== data.renderedSql.trim(); + + return ( +
+ {showBoth ? ( + <> +
+

+ Template query +

+ +
+
+

+ Rendered query +

+ +
+ + ) : ( +
+

Query

+ +
+ )} +
+ ); +} diff --git a/apps/frontend/src/components/side-panel/story-code-view.tsx b/apps/frontend/src/components/side-panel/story-code-view.tsx index 6e1eb8a8f..06c402ad1 100644 --- a/apps/frontend/src/components/side-panel/story-code-view.tsx +++ b/apps/frontend/src/components/side-panel/story-code-view.tsx @@ -5,6 +5,7 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { StoryValidationError } from '@nao/shared/story-validation'; import type { Monaco } from '@monaco-editor/react'; import type { editor } from 'monaco-editor'; +import { FixInChatButton } from '@/components/fix-in-chat-button'; import { useEditorTheme } from '@/hooks/use-editor-theme'; const MONACO_OPTIONS = { @@ -154,22 +155,33 @@ export const StoryCodeView = memo(function StoryCodeView({ }); function ValidationErrorBanner({ errors }: { errors: StoryValidationError[] }) { + const fixMessage = [ + "I'm seeing validation errors in the story code:", + ...errors.slice(0, 10).map((error) => `- L${error.line}: ${error.message}`), + ...(errors.length > 10 ? [`- and ${errors.length - 10} more...`] : []), + '', + 'Please fix these errors in the story.', + ].join('\n'); + return ( -
-
- - - {errors.length} validation {errors.length === 1 ? 'error' : 'errors'} - +
+
+
+ + + {errors.length} validation {errors.length === 1 ? 'error' : 'errors'} + +
+
    + {errors.slice(0, 5).map((error, i) => ( +
  • + L{error.line}: {error.message} +
  • + ))} + {errors.length > 5 &&
  • and {errors.length - 5} more...
  • } +
-
    - {errors.slice(0, 5).map((error, i) => ( -
  • - L{error.line}: {error.message} -
  • - ))} - {errors.length > 5 &&
  • and {errors.length - 5} more...
  • } -
+
); } diff --git a/apps/frontend/src/components/side-panel/story-preview.tsx b/apps/frontend/src/components/side-panel/story-preview.tsx index b3f31ec93..bec8e706d 100644 --- a/apps/frontend/src/components/side-panel/story-preview.tsx +++ b/apps/frontend/src/components/side-panel/story-preview.tsx @@ -5,74 +5,119 @@ import type { ParsedChartBlock, ParsedTableBlock } from '@nao/shared/story-segme import type { QueryDataMap } from '@/components/story-embeds'; import { StoryChartEmbed as LiveChartEmbed, StoryTableEmbed as LiveTableEmbed } from '@/components/story-embeds'; +import { StoryFilterBar } from '@/components/story-filter-bar'; import { SegmentList } from '@/components/story-rendering'; import { StoryChartEmbed as StaticChartEmbed } from '@/components/side-panel/story-chart-embed'; import { StoryTableEmbed as StaticTableEmbed } from '@/components/side-panel/story-table-embed'; +import { StoryQuerySqlProvider } from '@/contexts/story-query-sql'; +import { useStoryFilters } from '@/hooks/use-story-filters'; import { trpc } from '@/main'; interface StoryPreviewProps { code: string; + fullCode?: string; cacheSchedule: string | null; queryData: QueryDataMap | null; chatId: string; + storySlug: string; versionKey?: string | number; + filtersEnabled?: boolean; } export const StoryPreview = memo(function StoryPreview({ code, + fullCode, cacheSchedule, queryData, chatId, + storySlug, versionKey, + filtersEnabled = true, }: StoryPreviewProps) { const segments = useMemo(() => splitCodeIntoSegments(code), [code]); const isNoCacheMode = cacheSchedule === NO_CACHE_SCHEDULE; + const filterApi = useMemo(() => ({ kind: 'owned' as const, chatId, storySlug }), [chatId, storySlug]); + const storyFilters = useStoryFilters({ + code: fullCode ?? code, + baselineQueryData: queryData, + api: filterApi, + enabled: filtersEnabled, + }); const noCacheQuery = useMemo( () => (isNoCacheMode ? { queryOptions: trpc.story.getLiveQueryData.queryOptions, chatId } : undefined), [isNoCacheMode, chatId], ); + const effectiveQueryData = storyFilters.queryData; + + const useLiveUnfiltered = isNoCacheMode && !storyFilters.hasActiveFilters; + const querySqlSource = useMemo( + () => (storyFilters.filtersEnabled ? { api: filterApi, selections: storyFilters.debouncedSelections } : null), + [filterApi, storyFilters.filtersEnabled, storyFilters.debouncedSelections], + ); + const renderChart = useCallback( (chart: ParsedChartBlock) => { - if (!queryData && !isNoCacheMode) { + if (!effectiveQueryData && !useLiveUnfiltered) { return ; } return ( ); }, - [queryData, isNoCacheMode, noCacheQuery], + [ + effectiveQueryData, + useLiveUnfiltered, + noCacheQuery, + storyFilters.hasActiveFilters, + storyFilters.isFiltering, + queryData, + ], ); const renderTable = useCallback( (table: ParsedTableBlock) => { - if (!queryData && !isNoCacheMode) { + if (!effectiveQueryData && !useLiveUnfiltered) { return ; } return ( ); }, - [queryData, isNoCacheMode, noCacheQuery], + [effectiveQueryData, useLiveUnfiltered, noCacheQuery, storyFilters.hasActiveFilters, storyFilters.isFiltering], ); return ( -
- -
+ +
+ + +
+
); }); diff --git a/apps/frontend/src/components/side-panel/story-table-embed.tsx b/apps/frontend/src/components/side-panel/story-table-embed.tsx index e00edac2b..e8bf1ddfb 100644 --- a/apps/frontend/src/components/side-panel/story-table-embed.tsx +++ b/apps/frontend/src/components/side-panel/story-table-embed.tsx @@ -2,7 +2,6 @@ import { sanitizeConditionalFormats } from '@nao/shared/conditional-formatting'; import { Pencil } from 'lucide-react'; import { memo, useMemo, useState } from 'react'; import { StoryEmbedFallback } from './story-embed-fallback'; -import type { UIMessage } from '@nao/backend/chat'; import type { ParsedTableBlock } from '@nao/shared/story-segments'; import { DataTableCard } from '@/components/data-table-card'; @@ -11,6 +10,7 @@ import { Button } from '@/components/ui/button'; import { useOptionalAgentContext } from '@/contexts/agent.provider'; import { useStoryEmbedData } from '@/contexts/story-embed-data'; import { useStoryTableEdit } from '@/contexts/story-table-edit'; +import { findLatestExecuteSqlInMessages } from '@/lib/execute-sql-messages'; export const StoryTableEmbed = memo(function StoryTableEmbed({ table, @@ -28,18 +28,7 @@ export const StoryTableEmbed = memo(function StoryTableEmbed({ return fromEmbedData; } - const findInMessages = (messages: UIMessage[]) => { - for (const message of messages) { - for (const part of message.parts) { - if (part.type === 'tool-execute_sql' && part.output?.id === table.queryId) { - return part.output; - } - } - } - return null; - }; - - return findInMessages(agent?.messages ?? []); + return findLatestExecuteSqlInMessages(agent?.messages ?? [], table.queryId)?.output ?? null; }, [embedData, agent?.messages, table.queryId]); if (!sourceData?.data || !Array.isArray(sourceData.data)) { diff --git a/apps/frontend/src/components/side-panel/story-viewer.tsx b/apps/frontend/src/components/side-panel/story-viewer.tsx index efa9c30d2..8c41da124 100644 --- a/apps/frontend/src/components/side-panel/story-viewer.tsx +++ b/apps/frontend/src/components/side-panel/story-viewer.tsx @@ -272,10 +272,13 @@ export function StoryViewer({ chatId, storySlug, isReadonlyMode: readonlyProp, i code={ isTabbedStory && tabs ? tabs[activeTab].innerCode : stripStoryTabsMarkup(storyCode) } + fullCode={storyCode} cacheSchedule={cacheSchedule} queryData={queryData ?? null} chatId={chatId} + storySlug={resolvedStorySlug} versionKey={`${currentVersionNumber}-${cachedAt ?? ''}`} + filtersEnabled={isViewingLatest && !isAgentRunning} /> ) ) : viewMode === 'edit' ? ( diff --git a/apps/frontend/src/components/story-embeds.tsx b/apps/frontend/src/components/story-embeds.tsx index 6a3ec97c3..77451b59a 100644 --- a/apps/frontend/src/components/story-embeds.tsx +++ b/apps/frontend/src/components/story-embeds.tsx @@ -1,13 +1,15 @@ import { Loader2 } from 'lucide-react'; -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; +import { displayChart } from '@nao/shared/tools'; import type { ParsedChartBlock, ParsedTableBlock } from '@nao/shared/story-segments'; -import type { displayChart } from '@nao/shared/tools'; import { StoryChartEmbedShell } from '@/components/side-panel/story-chart-embed'; import { StoryTableEditControls } from '@/components/side-panel/story-table-embed'; import { ChartDisplay } from '@/components/tool-calls/display-chart'; import { DataTableCard } from '@/components/data-table-card'; +import { alignChartDataToBaselineX } from '@/lib/charts.utils'; +import { cn } from '@/lib/utils'; export type QueryDataMap = Record[]; columns: string[] }>; @@ -35,6 +37,19 @@ function EmbedLoading() { ); } +function EmbedRefreshing({ children, isRefreshing }: { children: React.ReactNode; isRefreshing: boolean }) { + return ( +
+
{children}
+ {isRefreshing && ( +
+ +
+ )} +
+ ); +} + function useLiveQueryData(queryId: string, liveQuery?: LiveQueryConfig) { return useQuery({ queryKey: ['noop', queryId], @@ -48,52 +63,88 @@ function useLiveQueryData(queryId: string, liveQuery?: LiveQueryConfig) { export const StoryChartEmbed = memo(function StoryChartEmbed({ chart, queryData, + baselineQueryData, liveQuery, + hasActiveFilters = false, + isRefreshing = false, }: { chart: ParsedChartBlock; queryData?: QueryDataMap | null; + baselineQueryData?: QueryDataMap | null; liveQuery?: LiveQueryConfig; + hasActiveFilters?: boolean; + isRefreshing?: boolean; }) { const noCacheFetch = useLiveQueryData(chart.queryId, liveQuery); const resolved = liveQuery ? (noCacheFetch.data as { data: Record[]; columns: string[] } | undefined) : queryData?.[chart.queryId]; + const baseline = baselineQueryData?.[chart.queryId]; const resolvedData = resolved?.data; - const resolvedColumns = resolved?.columns ?? []; + const resolvedColumns = resolved?.columns ?? baseline?.columns ?? []; + const keepsXDomain = + hasActiveFilters && + Boolean(baseline?.data?.length) && + !displayChart.isPieChart(chart.chartType as displayChart.ChartType) && + chart.chartType !== 'kpi_card'; + const showRefreshing = isRefreshing || Boolean(liveQuery && noCacheFetch.isFetching); + + const displayData = useMemo(() => { + if (!keepsXDomain || !baseline?.data) { + return resolvedData ?? []; + } + return alignChartDataToBaselineX( + baseline.data, + resolvedData ?? [], + chart.xAxisKey, + chart.series.map((series) => series.data_key), + ); + }, [keepsXDomain, baseline?.data, resolvedData, chart.xAxisKey, chart.series]); if (liveQuery && noCacheFetch.isLoading) { return ; } - if (!resolvedData || resolvedData.length === 0) { + if (!resolved && !keepsXDomain) { return Chart data unavailable; } + if (displayData.length === 0) { + return ( + + {hasActiveFilters ? 'No results match the selected filters' : 'No data to display'} + + ); + } + if (chart.series.length === 0) { return No series configured for chart; } return ( - - + + + + ); }); @@ -102,33 +153,48 @@ export const StoryTableEmbed = memo(function StoryTableEmbed({ table, queryData, liveQuery, + hasActiveFilters = false, + isRefreshing = false, }: { table: ParsedTableBlock; queryData?: QueryDataMap | null; liveQuery?: LiveQueryConfig; + hasActiveFilters?: boolean; + isRefreshing?: boolean; }) { const noCacheFetch = useLiveQueryData(table.queryId, liveQuery); const resolvedResult = liveQuery ? (noCacheFetch.data as { data: Record[]; columns: string[] } | undefined) : queryData?.[table.queryId]; + const showRefreshing = isRefreshing || Boolean(liveQuery && noCacheFetch.isFetching); if (liveQuery && noCacheFetch.isLoading) { return ; } - if (!resolvedResult?.data) { + if (!resolvedResult) { return Table data unavailable; } + if (!resolvedResult.data || resolvedResult.data.length === 0) { + return ( + + {hasActiveFilters ? 'No results match the selected filters' : 'No data to display'} + + ); + } + const columns = resolvedResult.columns ?? []; return ( - } - /> + + } + /> + ); }); diff --git a/apps/frontend/src/components/story-filter-bar.tsx b/apps/frontend/src/components/story-filter-bar.tsx new file mode 100644 index 000000000..bc5fd3898 --- /dev/null +++ b/apps/frontend/src/components/story-filter-bar.tsx @@ -0,0 +1,385 @@ +import { AlertTriangle, CalendarIcon, FilterX, Loader2, X } from 'lucide-react'; +import { format } from 'date-fns'; +import { useCallback, useEffect, useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { isFilterSelectionActive } from '@nao/shared/sql-template'; +import type { StoryFilterSelection } from '@nao/shared/sql-template'; +import type { DateRange } from 'react-day-picker'; +import type { ParsedFilterBlock } from '@nao/shared/story-segments'; + +import type { StoryFilterApi } from '@/hooks/use-story-filters'; +import { FixInChatButton } from '@/components/fix-in-chat-button'; +import { Button } from '@/components/ui/button'; +import { Calendar } from '@/components/ui/calendar'; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Input } from '@/components/ui/input'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { cn } from '@/lib/utils'; +import { trpc } from '@/main'; + +const CLEAR_SELECT_VALUE = '__nao_clear_filter__'; +const SELECT_VALUE_PREFIX = '__nao_filter_value__:'; + +export function StoryFilterBar({ + filters, + selections, + onSelectionChange, + onClear, + api, +}: { + filters: ParsedFilterBlock[]; + selections: Record; + onSelectionChange: (filterId: string, selection: StoryFilterSelection) => void; + onClear: () => void; + api?: StoryFilterApi | null; +}) { + const [optionErrors, setOptionErrors] = useState>({}); + + useEffect(() => { + const filterIds = new Set(filters.map((filter) => filter.id)); + setOptionErrors((current) => { + const next = Object.fromEntries(Object.entries(current).filter(([id]) => filterIds.has(id))); + return Object.keys(next).length === Object.keys(current).length ? current : next; + }); + }, [filters]); + + const reportOptionError = useCallback((filterId: string, error: string | null) => { + setOptionErrors((current) => { + if (error) { + if (current[filterId] === error) { + return current; + } + return { ...current, [filterId]: error }; + } + if (!(filterId in current)) { + return current; + } + const next = { ...current }; + delete next[filterId]; + return next; + }); + }, []); + + if (filters.length === 0) { + return null; + } + + const hasActiveFilters = Object.values(selections).some((selection) => + typeof selection === 'string' ? Boolean(selection) : selection.some(Boolean), + ); + + return ( +
+
+ {filters.map((filter) => ( + onSelectionChange(filter.id, selection)} + onOptionError={reportOptionError} + api={api} + /> + ))} + {hasActiveFilters && ( + + )} +
+ +
+ ); +} + +function StoryFilterControl({ + filter, + selection, + onChange, + onOptionError, + api, +}: { + filter: ParsedFilterBlock; + selection: StoryFilterSelection | undefined; + onChange: (selection: StoryFilterSelection) => void; + onOptionError: (filterId: string, error: string | null) => void; + api?: StoryFilterApi | null; +}) { + const { options, error, isLoading } = useFilterOptions(filter, api); + + useEffect(() => { + onOptionError(filter.id, error); + }, [filter.id, error, onOptionError]); + + const isActive = isFilterSelectionActive(filter.filterType, selection); + + return ( +
+
+ {filter.label} + {isActive && ( + + )} +
+ {filter.filterType === 'select' ? ( + isLoading ? ( + + ) : ( + + ) + ) : filter.filterType === 'multi_select' ? ( + isLoading ? ( + + ) : ( + + ) + ) : filter.filterType === 'date_range' ? ( + + ) : ( + onChange(event.target.value)} + /> + )} +
+ ); +} + +function emptySelection(filterType: ParsedFilterBlock['filterType']): StoryFilterSelection { + return filterType === 'select' || filterType === 'search' ? '' : []; +} + +function FilterOptionsLoading() { + return ( +
+ + Loading… +
+ ); +} + +function MultiSelectFilter({ + options, + value, + onChange, +}: { + options: string[]; + value: string[]; + onChange: (value: string[]) => void; +}) { + return ( + + + + + + {options.map((option) => ( + event.preventDefault()} + onCheckedChange={(checked) => + onChange(checked ? [...value, option] : value.filter((candidate) => candidate !== option)) + } + > + {option} + + ))} + + + ); +} + +function RangeFilter({ value, onChange }: { value: string[]; onChange: (value: string[]) => void }) { + const selected = toDateRange(value); + + return ( + + + + + + onChange(fromDateRange(range))} + defaultMonth={selected?.from} + navLayout='after' + numberOfMonths={1} + /> + + + ); +} + +function toDateRange(value: string[]): DateRange | undefined { + const from = parseDateString(value[0]); + const to = parseDateString(value[1]); + if (!from && !to) { + return undefined; + } + return { from, to }; +} + +function fromDateRange(range: DateRange | undefined): string[] { + return [range?.from ? format(range.from, 'yyyy-MM-dd') : '', range?.to ? format(range.to, 'yyyy-MM-dd') : '']; +} + +function parseDateString(value: string | undefined): Date | undefined { + if (!value) { + return undefined; + } + const [year, month, day] = value.split('-').map(Number); + if (!year || !month || !day) { + return undefined; + } + const date = new Date(year, month - 1, day); + const isSameCalendarDate = date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day; + return isSameCalendarDate ? date : undefined; +} + +function formatDateRangeLabel(range: DateRange | undefined): string { + if (!range?.from) { + return 'Pick a date range'; + } + if (!range.to) { + return format(range.from, 'LLL dd, y'); + } + return `${format(range.from, 'LLL dd, y')} – ${format(range.to, 'LLL dd, y')}`; +} + +function FilterOptionsErrorBanner({ + filters, + errors, +}: { + filters: ParsedFilterBlock[]; + errors: Record; +}) { + const errorItems = filters + .map((filter) => { + const message = errors[filter.id]; + return message ? { id: filter.id, label: filter.label, message } : null; + }) + .filter((item): item is { id: string; label: string; message: string } => item !== null); + + if (errorItems.length === 0) { + return null; + } + + const fixMessage = [ + "I'm seeing errors loading story filter options:", + ...errorItems.map((error) => `- Filter "${error.label}" (id: ${error.id}): ${error.message}`), + '', + 'Please fix the filter definition. For table+column select filters with multiple databases, set database_id on the tag. Use a valid table/column identifier for the warehouse dialect (backticks for BigQuery hyphens).', + ].join('\n'); + + return ( +
+
+
+ + Failed to load filter options +
+
    + {errorItems.map((error) => ( +
  • + {error.label}: {error.message} +
  • + ))} +
+
+ +
+ ); +} + +function useFilterOptions( + filter: ParsedFilterBlock, + api?: StoryFilterApi | null, +): { options: string[]; error: string | null; isLoading: boolean } { + const usesHardcodedOptions = Boolean(filter.options?.length); + const needsRemoteOptions = + !usesHardcodedOptions && (filter.filterType === 'select' || filter.filterType === 'multi_select'); + + const ownedQuery = useQuery({ + ...trpc.story.getFilterOptions.queryOptions({ + chatId: api?.kind === 'owned' ? api.chatId : '', + storySlug: api?.kind === 'owned' ? api.storySlug : '', + filterId: filter.id, + }), + enabled: Boolean(api?.kind === 'owned' && needsRemoteOptions), + }); + + const sharedQuery = useQuery({ + ...trpc.storyShare.getFilterOptions.queryOptions({ + shareId: api?.kind === 'shared' ? api.shareId : '', + filterId: filter.id, + }), + enabled: Boolean(api?.kind === 'shared' && needsRemoteOptions), + }); + + if (usesHardcodedOptions) { + return { options: filter.options ?? [], error: null, isLoading: false }; + } + + const query = api?.kind === 'shared' ? sharedQuery : ownedQuery; + return { + options: query.data?.options ?? [], + error: query.error?.message ?? null, + isLoading: needsRemoteOptions && Boolean(api) && query.isPending, + }; +} diff --git a/apps/frontend/src/components/story-rendering.tsx b/apps/frontend/src/components/story-rendering.tsx index b80d5bbbb..05b152841 100644 --- a/apps/frontend/src/components/story-rendering.tsx +++ b/apps/frontend/src/components/story-rendering.tsx @@ -43,6 +43,8 @@ export const SegmentList = memo(function SegmentList({ return {renderChart(segment.chart, i)}; case 'table': return {renderTable(segment.table, i)}; + case 'filter': + return null; case 'grid': return ( React.ReactNode; - renderTable: (table: ParsedTableBlock, key: number) => React.ReactNode; + renderChart: (chart: ParsedChartBlock, options: StoryEmbedRenderOptions) => React.ReactNode; + renderTable: (table: ParsedTableBlock, options: StoryEmbedRenderOptions) => React.ReactNode; contentClassName?: string; + baselineQueryData?: QueryDataMap | null; + filterApi?: StoryFilterApi | null; } export function StoryTabbedContent({ @@ -18,6 +33,8 @@ export function StoryTabbedContent({ renderChart, renderTable, contentClassName = 'max-w-5xl mx-auto p-4 md:p-8 flex flex-col gap-4', + baselineQueryData, + filterApi, }: StoryTabbedContentProps) { const tabs = useMemo(() => parseStoryTabs(code), [code]); const [activeIndex, setActiveIndex] = useState(0); @@ -25,22 +42,64 @@ export function StoryTabbedContent({ const activeTabIndex = tabs?.length ? Math.min(activeIndex, tabs.length - 1) : 0; const activeCode = isTabbed && tabs ? tabs[activeTabIndex].innerCode : stripStoryTabsMarkup(code); const segments = useMemo(() => splitCodeIntoSegments(activeCode), [activeCode]); + const storyFilters = useStoryFilters({ + code, + baselineQueryData, + api: filterApi, + enabled: Boolean(filterApi), + }); + const querySqlSource = useMemo( + () => + filterApi && storyFilters.filtersEnabled + ? { api: filterApi, selections: storyFilters.debouncedSelections } + : null, + [filterApi, storyFilters.filtersEnabled, storyFilters.debouncedSelections], + ); return ( -
- {isTabbed && tabs && ( - ({ title: tab.title }))} - activeIndex={activeTabIndex} - onSelect={setActiveIndex} - contentClassName='mx-auto w-full max-w-5xl px-4 md:px-8' - /> - )} -
-
- + +
+ {isTabbed && tabs && ( + ({ title: tab.title }))} + activeIndex={activeTabIndex} + onSelect={setActiveIndex} + contentClassName='mx-auto w-full max-w-5xl px-4 md:px-8' + /> + )} +
+
+ + + renderChart(chart, { + queryData: storyFilters.queryData, + baselineQueryData: baselineQueryData ?? null, + hasActiveFilters: storyFilters.hasActiveFilters, + isRefreshing: storyFilters.isFiltering, + key, + }) + } + renderTable={(table, key) => + renderTable(table, { + queryData: storyFilters.queryData, + baselineQueryData: baselineQueryData ?? null, + hasActiveFilters: storyFilters.hasActiveFilters, + isRefreshing: storyFilters.isFiltering, + key, + }) + } + /> +
-
+ ); } diff --git a/apps/frontend/src/components/tool-calls/display-chart-table.tsx b/apps/frontend/src/components/tool-calls/display-chart-table.tsx index 7f50435b2..58a8e367e 100644 --- a/apps/frontend/src/components/tool-calls/display-chart-table.tsx +++ b/apps/frontend/src/components/tool-calls/display-chart-table.tsx @@ -15,6 +15,7 @@ import { useOptionalAgentContext } from '@/contexts/agent.provider'; import { useChatId } from '@/hooks/use-chat-id'; import { useSidePanel } from '@/contexts/side-panel'; import { StoryViewer } from '@/components/side-panel/story-viewer'; +import { findLatestExecuteSqlInMessages } from '@/lib/execute-sql-messages'; import { findStoryIds } from '@/lib/story.utils'; import { trpc } from '@/main'; @@ -42,14 +43,7 @@ export function DisplayChartTable({ config, outputError, toolCallId }: DisplayCh if (!config?.query_id) { return null; } - for (const message of messages) { - for (const part of message.parts) { - if (part.type === 'tool-execute_sql' && part.output && part.output.id === config.query_id) { - return part.output; - } - } - } - return null; + return findLatestExecuteSqlInMessages(messages, config.query_id)?.output ?? null; }, [messages, config?.query_id]); const updateMutation = useMutation( diff --git a/apps/frontend/src/components/tool-calls/display-chart.tsx b/apps/frontend/src/components/tool-calls/display-chart.tsx index af1fdc62f..7e778b949 100644 --- a/apps/frontend/src/components/tool-calls/display-chart.tsx +++ b/apps/frontend/src/components/tool-calls/display-chart.tsx @@ -49,6 +49,7 @@ import { useSidePanel } from '@/contexts/side-panel'; import { useToolCallContext } from '@/contexts/tool-call'; import { StoryViewer } from '@/components/side-panel/story-viewer'; import { cn } from '@/lib/utils'; +import { findLatestExecuteSqlInMessages } from '@/lib/execute-sql-messages'; import { ExportDataMenu } from '@/components/export-data-menu'; const Colors = ['var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)', 'var(--chart-5)']; @@ -107,15 +108,7 @@ export const DisplayChartToolCall = ({ if (!chartConfig?.query_id) { return null; } - - for (const message of messages) { - for (const part of message.parts) { - if (part.type === 'tool-execute_sql' && part.output && part.output.id === chartConfig.query_id) { - return { input: part.input, output: part.output }; - } - } - } - return null; + return findLatestExecuteSqlInMessages(messages, chartConfig.query_id); }, [messages, chartConfig?.query_id]); const sourceData = sourceQuery?.output ?? null; @@ -439,6 +432,7 @@ export interface ChartDisplayProps { yAxisRightMax?: number; yAxisRightLabel?: string; showDataLabels?: boolean; + animate?: boolean; comparisonMode?: displayChart.ComparisonMode; className?: string; chartContainerClassName?: string; @@ -467,6 +461,7 @@ export const ChartDisplay = memo(function ChartDisplay({ yAxisRightMax, yAxisRightLabel, showDataLabels, + animate = false, comparisonMode, className, chartContainerClassName, @@ -608,6 +603,7 @@ export const ChartDisplay = memo(function ChartDisplay({ xAxisMaxLabelChars, showGrid, showDataLabels, + animate, comparisonMode, gradientIdPrefix, margin: { top: 0, right: 0, bottom: 0, left: 0 }, @@ -677,6 +673,7 @@ export const ChartDisplay = memo(function ChartDisplay({ yAxisRightLabel, isDualAxis, showDataLabels, + animate, comparisonMode, gradientIdPrefix, hideTotal, diff --git a/apps/frontend/src/components/tool-calls/execute-sql.tsx b/apps/frontend/src/components/tool-calls/execute-sql.tsx index 7c50cfb4e..4222a121b 100644 --- a/apps/frontend/src/components/tool-calls/execute-sql.tsx +++ b/apps/frontend/src/components/tool-calls/execute-sql.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { useMutation } from '@tanstack/react-query'; -import { ArrowUpRight, Code, Copy, Download, Palette, Table as TableIcon } from 'lucide-react'; +import { Code, Copy, Download, Palette, Pencil, Table as TableIcon } from 'lucide-react'; import { ToolCallWrapper } from './tool-call-wrapper'; import { TableFormatEditDialog } from './display-table-edit-dialog'; import { SqlQueryDisplay } from './sql-query-display'; @@ -25,9 +25,18 @@ export const ExecuteSqlToolCall = ({ const [isFormatOpen, setIsFormatOpen] = useState(false); const { isSettled } = useToolCallContext(); const { open: openSidePanel } = useSidePanel(); - const chatId = useOptionalAgentContext()?.chatId; + const agent = useOptionalAgentContext(); + const chatId = agent?.chatId; + const isEditable = Boolean(agent && !agent.isReadonly && !agent.isRunning && output?.id && input?.sql_query); const logDownload = useMutation(trpc.analyticsEvent.logChatDownload.mutationOptions()); + const openEditor = (editable: boolean) => { + if (state === 'input-streaming' || !output || !input) { + return; + } + openSidePanel(); + }; + const handleExport = (format: DataExportFormat) => { if (chatId) { logDownload.mutate({ chatId, format, queryId: toolCallId, title: input?.name }); @@ -87,15 +96,10 @@ export const ExecuteSqlToolCall = ({ ] : []), { - id: 'expand', - label: , - onClick: () => { - if (state === 'input-streaming' || !output || !input) { - return; - } - openSidePanel(); - }, - title: 'Open in side panel', + id: 'edit', + label: , + onClick: () => openEditor(isEditable), + title: isEditable ? 'Edit in side panel' : 'Open in side panel', }, ]; diff --git a/apps/frontend/src/components/ui/button.tsx b/apps/frontend/src/components/ui/button.tsx index ebf728424..5093ef23e 100644 --- a/apps/frontend/src/components/ui/button.tsx +++ b/apps/frontend/src/components/ui/button.tsx @@ -167,4 +167,4 @@ function AuthSocialButton({ ); } -export { Button, ButtonConnection, ChatSendButton as ChatButton, MicButton, AuthSocialButton }; +export { Button, ButtonConnection, ChatSendButton as ChatButton, MicButton, AuthSocialButton, buttonVariants }; diff --git a/apps/frontend/src/components/ui/calendar.tsx b/apps/frontend/src/components/ui/calendar.tsx new file mode 100644 index 000000000..c6832301f --- /dev/null +++ b/apps/frontend/src/components/ui/calendar.tsx @@ -0,0 +1,164 @@ +import * as React from 'react'; +import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; +import { DayPicker, getDefaultClassNames } from 'react-day-picker'; +import type { DayButton } from 'react-day-picker'; + +import { cn } from '@/lib/utils'; +import { Button, buttonVariants } from '@/components/ui/button'; + +function Calendar({ + className, + classNames, + showOutsideDays = true, + captionLayout = 'label', + buttonVariant = 'ghost', + formatters, + components, + ...props +}: React.ComponentProps & { + buttonVariant?: React.ComponentProps['variant']; +}) { + const defaultClassNames = getDefaultClassNames(); + + return ( + svg]:rotate-180`, + String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, + className, + )} + captionLayout={captionLayout} + formatters={{ + formatMonthDropdown: (date) => date.toLocaleString('default', { month: 'short' }), + ...formatters, + }} + classNames={{ + root: cn('w-fit', defaultClassNames.root), + months: cn('relative flex flex-col gap-4 md:flex-row', defaultClassNames.months), + month: cn('flex w-full flex-col gap-4', defaultClassNames.month), + nav: cn( + 'absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1 pointer-events-none', + defaultClassNames.nav, + ), + button_previous: cn( + buttonVariants({ variant: buttonVariant }), + 'size-(--cell-size) p-0 select-none aria-disabled:opacity-50 pointer-events-auto', + defaultClassNames.button_previous, + ), + button_next: cn( + buttonVariants({ variant: buttonVariant }), + 'size-(--cell-size) p-0 select-none aria-disabled:opacity-50 pointer-events-auto', + defaultClassNames.button_next, + ), + month_caption: cn( + 'flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)', + defaultClassNames.month_caption, + ), + dropdowns: cn( + 'flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium', + defaultClassNames.dropdowns, + ), + dropdown_root: cn( + 'relative rounded-md border border-input shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50', + defaultClassNames.dropdown_root, + ), + dropdown: cn('absolute inset-0 opacity-0', defaultClassNames.dropdown), + caption_label: cn( + 'font-medium select-none', + captionLayout === 'label' + ? 'text-sm' + : 'flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground', + defaultClassNames.caption_label, + ), + month_grid: cn('w-full border-collapse', defaultClassNames.month_grid), + weekdays: cn('flex', defaultClassNames.weekdays), + weekday: cn( + 'flex-1 rounded-md text-[0.8rem] font-normal text-muted-foreground select-none', + defaultClassNames.weekday, + ), + week: cn('mt-2 flex w-full', defaultClassNames.week), + week_number_header: cn('w-(--cell-size) select-none', defaultClassNames.week_number_header), + week_number: cn('text-[0.8rem] text-muted-foreground select-none', defaultClassNames.week_number), + day: cn( + 'group/day relative aspect-square h-full w-full p-0 text-center select-none [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md', + defaultClassNames.day, + ), + range_start: cn('rounded-l-md bg-accent', defaultClassNames.range_start), + range_middle: cn('rounded-none', defaultClassNames.range_middle), + range_end: cn('rounded-r-md bg-accent', defaultClassNames.range_end), + today: cn( + 'rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none', + defaultClassNames.today, + ), + outside: cn('text-muted-foreground aria-selected:text-muted-foreground', defaultClassNames.outside), + disabled: cn('text-muted-foreground opacity-50', defaultClassNames.disabled), + hidden: cn('invisible', defaultClassNames.hidden), + ...classNames, + }} + components={{ + Root: ({ className: rootClassName, rootRef, ...rootProps }) => { + return
; + }, + Chevron: ({ className: chevronClassName, orientation, ...chevronProps }) => { + if (orientation === 'left') { + return ; + } + + if (orientation === 'right') { + return ; + } + + return ; + }, + DayButton: CalendarDayButton, + WeekNumber: ({ children, ...weekNumberProps }) => { + return ( +
+ ); + }, + ...components, + }} + {...props} + /> + ); +} + +function CalendarDayButton({ className, day, modifiers, ...props }: React.ComponentProps) { + const defaultClassNames = getDefaultClassNames(); + + const ref = React.useRef(null); + React.useEffect(() => { + if (modifiers.focused) { + ref.current?.focus(); + } + }, [modifiers.focused]); + + return ( +
+
+ {children} +
+
`; } + +export interface StoryFilterBlockInput { + id: string; + column?: string; + label?: string; + type: StoryFilterType; + table?: string; + database_id?: string; + options?: string[]; +} + +export function buildStoryFilterBlock(input: StoryFilterBlockInput): string { + const labelAttr = input.label ? ` label="${escapeDoubleQuotedStoryAttr(input.label)}"` : ''; + const columnAttr = input.column ? ` column="${escapeDoubleQuotedStoryAttr(input.column)}"` : ''; + const tableAttr = input.table ? ` table="${escapeDoubleQuotedStoryAttr(input.table)}"` : ''; + const databaseIdAttr = input.database_id ? ` database_id="${escapeDoubleQuotedStoryAttr(input.database_id)}"` : ''; + const optionsAttr = input.options ? ` options='${escapeSingleQuotedStoryAttr(JSON.stringify(input.options))}'` : ''; + return ``; +} diff --git a/apps/shared/src/chart-builder.tsx b/apps/shared/src/chart-builder.tsx index 22d55e913..ffaa1cb35 100644 --- a/apps/shared/src/chart-builder.tsx +++ b/apps/shared/src/chart-builder.tsx @@ -280,10 +280,14 @@ export interface BuildChartProps { /** Prefix for SVG gradient ids so multiple charts on one page (and drag clones) don't collide. */ gradientIdPrefix?: string; showDataLabels?: boolean; + /** When true, Recharts animates series as data changes (e.g. story filters). */ + animate?: boolean; comparisonMode?: displayChart.ComparisonMode; idPrefix?: string; } +const CHART_ANIMATION_DURATION_MS = 400; + /** * Builds a Recharts element tree from a display_chart tool config. * @@ -677,7 +681,8 @@ function buildBarChart(props: ResolvedProps) { stackId={isStacked ? 'stack' : undefined} radius={isStacked ? undefined : [4, 4, 4, 4]} shape={isStacked ? renderStackedBarShape(seriesKeys, s.data_key, separatorColor) : undefined} - isAnimationActive={false} + isAnimationActive={Boolean(props.animate)} + animationDuration={CHART_ANIMATION_DURATION_MS} > {showDataLabels && !isStacked && ( @@ -805,7 +810,8 @@ function buildAreaChart(props: ResolvedProps) { stroke={colorFor(s.data_key, i)} fill={`url(#${gradientIdFor(i)})`} stackId={isStacked ? 'stack' : undefined} - isAnimationActive={false} + isAnimationActive={Boolean(props.animate)} + animationDuration={CHART_ANIMATION_DURATION_MS} > {showDataLabels && !isStacked && } {stackTotalLabel && i === stackTotalLabelIndex && } @@ -1056,7 +1062,8 @@ function buildScatterChart(props: ResolvedProps) { key={s.data_key} dataKey={s.data_key} fill={colorFor(s.data_key, i)} - isAnimationActive={false} + isAnimationActive={Boolean(props.animate)} + animationDuration={CHART_ANIMATION_DURATION_MS} /> ))} @@ -1079,7 +1086,8 @@ function buildRadarChart(props: ResolvedProps) { stroke={colorFor(s.data_key, i)} fill={colorFor(s.data_key, i)} fillOpacity={0.3} - isAnimationActive={false} + isAnimationActive={Boolean(props.animate)} + animationDuration={CHART_ANIMATION_DURATION_MS} /> ))} @@ -1111,7 +1119,8 @@ function buildPieChart(props: ResolvedProps) { labelLine={false} stroke={backgroundColor} strokeWidth={1} - isAnimationActive={false} + isAnimationActive={Boolean(props.animate)} + animationDuration={CHART_ANIMATION_DURATION_MS} /> {children} diff --git a/apps/shared/src/execute-sql-parts.ts b/apps/shared/src/execute-sql-parts.ts new file mode 100644 index 000000000..762820a2a --- /dev/null +++ b/apps/shared/src/execute-sql-parts.ts @@ -0,0 +1,85 @@ +type MessageWithParts = { + parts: TPart[]; +}; + +/** + * Parts whose query id was re-run later in the conversation (in-place edits). + * Identity is by object reference — call before cloning parts. + */ +export function findSupersededExecuteSqlParts(messages: Array>): Set { + const latestByQueryId = new Map(); + for (const message of messages) { + for (const part of message.parts) { + const queryId = getExecuteSqlQueryId(part); + if (queryId) { + latestByQueryId.set(queryId, part); + } + } + } + + const superseded = new Set(); + for (const message of messages) { + for (const part of message.parts) { + const queryId = getExecuteSqlQueryId(part); + if (queryId && latestByQueryId.get(queryId) !== part) { + superseded.add(part); + } + } + } + return superseded; +} + +/** Drop superseded execute_sql parts (UI). */ +export function filterSupersededExecuteSqlParts>( + messages: TMessage[], +): TMessage[] { + const superseded = findSupersededExecuteSqlParts(messages); + if (superseded.size === 0) { + return messages; + } + + return messages.map((message) => { + const parts = message.parts.filter((part) => !superseded.has(part)); + return parts.length === message.parts.length ? message : { ...message, parts }; + }); +} + +/** Flag superseded execute_sql parts so the model sees a stub (agent context). */ +export function markSupersededExecuteSqlParts>( + messages: TMessage[], +): TMessage[] { + const superseded = findSupersededExecuteSqlParts(messages); + if (superseded.size === 0) { + return messages; + } + + return messages.map((message) => { + let changed = false; + const parts = message.parts.map((part) => { + if (!superseded.has(part)) { + return part; + } + changed = true; + const output = + part && typeof part === 'object' && 'output' in part && part.output && typeof part.output === 'object' + ? part.output + : {}; + return { + ...part, + output: { ...output, superseded: true }, + }; + }); + return changed ? { ...message, parts } : message; + }); +} + +function getExecuteSqlQueryId(part: unknown): string | null { + if (!part || typeof part !== 'object' || !('type' in part) || part.type !== 'tool-execute_sql') { + return null; + } + if (!('output' in part) || !part.output || typeof part.output !== 'object' || !('id' in part.output)) { + return null; + } + const id = part.output.id; + return typeof id === 'string' ? id : null; +} diff --git a/apps/shared/src/index.ts b/apps/shared/src/index.ts index 8760ed94e..85e17bb77 100644 --- a/apps/shared/src/index.ts +++ b/apps/shared/src/index.ts @@ -3,9 +3,11 @@ export * from './chart-builder'; export * from './chart-domain'; export * from './citation'; export * from './date'; +export * from './execute-sql-parts'; export * from './mcp'; export * from './mcp-embed'; export * from './mentions'; export * from './pluralize'; +export * from './sql-template'; export * from './types'; export * from './utils'; diff --git a/apps/shared/src/sql-template.ts b/apps/shared/src/sql-template.ts new file mode 100644 index 000000000..c98b171f0 --- /dev/null +++ b/apps/shared/src/sql-template.ts @@ -0,0 +1,365 @@ +export const STORY_FILTER_TYPES = ['select', 'multi_select', 'search', 'date_range'] as const; + +export type StoryFilterType = (typeof STORY_FILTER_TYPES)[number]; + +export type StoryFilterSelection = string | string[]; + +export type StoryFilterSelections = Record; + +export type StoryFilterTypeById = Record; + +export const STORY_FILTER_ID_REGEX = /^[A-Za-z_][A-Za-z0-9_]*$/; + +const FILTER_BLOCK_REGEX = /\{%\s*filter\s+([A-Za-z_][A-Za-z0-9_]*)\s*%\}([\s\S]*?)\{%\s*endfilter\s*%\}/g; +const FILTER_PLACEHOLDER_REGEX = /\{\{\s*filters\.([A-Za-z_][A-Za-z0-9_]*)\.sql\s*\}\}/g; +const ANY_FILTER_PLACEHOLDER_REGEX = /\{\{\s*filters\.([^\s.{}]+)(?:\.([^\s{}]*))?\s*\}\}/g; +const FILTER_OPEN_REGEX = /\{%\s*filter\b[^%}]*%\}/g; +const FILTER_CLOSE_REGEX = /\{%\s*endfilter\s*%\}/g; +const FILTER_DELIMITER_REGEX = /\{%\s*(?:filter\b([^%}]*)|(endfilter))\s*%\}/g; + +const INVALID_PLACEHOLDER_HINTS: Record = { + start: "date_range exposes a single {{ filters..sql }} value that already expands to 'start' AND 'end' — use AND col BETWEEN {{ filters..sql }}", + end: "date_range exposes a single {{ filters..sql }} value that already expands to 'start' AND 'end' — use AND col BETWEEN {{ filters..sql }}", + from: "date_range exposes a single {{ filters..sql }} value that already expands to 'start' AND 'end' — use AND col BETWEEN {{ filters..sql }}", + to: "date_range exposes a single {{ filters..sql }} value that already expands to 'start' AND 'end' — use AND col BETWEEN {{ filters..sql }}", + value: 'use {{ filters..sql }} (the only supported placeholder property)', +}; + +export function stripSqlFilterBlocks(sql: string): string { + const ranges: Array<{ start: number; end: number }> = []; + const openers: Array<{ start: number }> = []; + + for (const delimiter of getFilterDelimiters(sql)) { + if (delimiter.kind === 'open') { + openers.push(delimiter); + continue; + } + + const opener = openers.pop(); + if (!opener) { + ranges.push({ start: delimiter.start, end: delimiter.end }); + continue; + } + if (openers.length === 0) { + ranges.push({ start: opener.start, end: delimiter.end }); + } + } + + for (const opener of openers) { + ranges.push({ start: opener.start, end: sql.length }); + } + + return removeRanges(sql, ranges); +} + +export function extractSqlFilterIds(sql: string): string[] { + const ids: string[] = []; + const seen = new Set(); + const regex = new RegExp(FILTER_BLOCK_REGEX.source, 'g'); + let match: RegExpExecArray | null; + while ((match = regex.exec(sql)) !== null) { + const id = match[1]; + if (!seen.has(id)) { + seen.add(id); + ids.push(id); + } + } + return ids; +} + +/** + * Validates story-filter SQL template syntax. + * Returns human-readable issues the agent can use to self-heal incorrect templates. + */ +export function validateSqlFilterTemplate(sql: string, options?: { knownFilterIds?: Iterable }): string[] { + if (!sqlIncludesFilterTemplate(sql)) { + return []; + } + + const issues: string[] = []; + const knownFilterIds = options?.knownFilterIds ? new Set(options.knownFilterIds) : null; + + const openCount = countMatches(sql, FILTER_OPEN_REGEX); + const closeCount = countMatches(sql, FILTER_CLOSE_REGEX); + if (openCount !== closeCount) { + issues.push( + `Mismatched filter blocks: found ${openCount} "{% filter %}" opener(s) and ${closeCount} "{% endfilter %}" closer(s).`, + ); + } + issues.push(...validateFilterDelimiterOrder(sql)); + + const blockRegex = new RegExp(FILTER_BLOCK_REGEX.source, 'g'); + const coveredRanges: Array<{ start: number; end: number }> = []; + let blockMatch: RegExpExecArray | null; + while ((blockMatch = blockRegex.exec(sql)) !== null) { + const filterId = blockMatch[1]; + const inner = blockMatch[2]; + const blockStart = blockMatch.index; + const blockEnd = blockMatch.index + blockMatch[0].length; + coveredRanges.push({ start: blockStart, end: blockEnd }); + + if (knownFilterIds && !knownFilterIds.has(filterId)) { + issues.push( + `SQL references undeclared filter "${filterId}". Add to the story, or fix the filter id in the SQL template.`, + ); + } + + const placeholders = [...inner.matchAll(new RegExp(ANY_FILTER_PLACEHOLDER_REGEX.source, 'g'))]; + if (placeholders.length === 0) { + issues.push( + `Filter block "${filterId}" is missing {{ filters.${filterId}.sql }}. Example: {% filter ${filterId} %} AND col = {{ filters.${filterId}.sql }} {% endfilter %}.`, + ); + continue; + } + + for (const placeholder of placeholders) { + const placeholderId = placeholder[1]; + const property = placeholder[2]; + issues.push(...describePlaceholderIssues(placeholder[0], placeholderId, property, filterId)); + } + } + + const outsidePlaceholderRegex = new RegExp(ANY_FILTER_PLACEHOLDER_REGEX.source, 'g'); + let placeholderMatch: RegExpExecArray | null; + while ((placeholderMatch = outsidePlaceholderRegex.exec(sql)) !== null) { + const start = placeholderMatch.index; + const end = start + placeholderMatch[0].length; + if (coveredRanges.some((range) => start >= range.start && end <= range.end)) { + continue; + } + + const placeholderId = placeholderMatch[1]; + const property = placeholderMatch[2]; + issues.push( + `Placeholder ${placeholderMatch[0]} must appear inside {% filter ${placeholderId} %} ... {% endfilter %}.`, + ); + issues.push(...describePlaceholderIssues(placeholderMatch[0], placeholderId, property, placeholderId)); + } + + return uniqueIssues(issues); +} + +/** Returns declared filter ids that never appear in any of the provided SQL templates. */ +export function findUnreferencedStoryFilters( + declaredFilterIds: Iterable, + sqlQueries: Iterable, +): string[] { + const referenced = new Set(); + for (const sql of sqlQueries) { + for (const filterId of extractSqlFilterIds(sql)) { + referenced.add(filterId); + } + const placeholderRegex = new RegExp(ANY_FILTER_PLACEHOLDER_REGEX.source, 'g'); + let match: RegExpExecArray | null; + while ((match = placeholderRegex.exec(sql)) !== null) { + referenced.add(match[1]); + } + } + + return [...declaredFilterIds].filter((filterId) => !referenced.has(filterId)); +} + +export function sqlIncludesFilterTemplate(sql: string): boolean { + return /\{%\s*(?:filter|endfilter)\b/.test(sql) || /\{\{\s*filters\./.test(sql); +} + +function describePlaceholderIssues( + raw: string, + placeholderId: string, + property: string | undefined, + expectedFilterId: string, +): string[] { + const issues: string[] = []; + + if (placeholderId !== expectedFilterId) { + issues.push( + `Filter block "${expectedFilterId}" contains placeholder for unrelated filter "${placeholderId}". Use {{ filters.${expectedFilterId}.sql }}.`, + ); + } + + if (!property) { + issues.push( + `Invalid placeholder ${raw}. Use {{ filters.${placeholderId}.sql }} (property ".sql" is required).`, + ); + return issues; + } + + if (property === 'sql') { + return issues; + } + + const hint = INVALID_PLACEHOLDER_HINTS[property]; + issues.push( + hint + ? `Invalid placeholder ${raw}: ${hint.replaceAll('', placeholderId)}.` + : `Invalid placeholder ${raw}. Only {{ filters.${placeholderId}.sql }} is supported.`, + ); + return issues; +} + +function countMatches(sql: string, regex: RegExp): number { + return [...sql.matchAll(new RegExp(regex.source, 'g'))].length; +} + +function uniqueIssues(issues: string[]): string[] { + return [...new Set(issues)]; +} + +export function renderSqlTemplate(sql: string, selections: StoryFilterSelections, types: StoryFilterTypeById): string { + const templateIssues = validateSqlFilterTemplate(sql, { knownFilterIds: Object.keys(types) }); + if (templateIssues.length > 0) { + throw new Error(templateIssues.join(' ')); + } + + const rendered = sql.replace(FILTER_BLOCK_REGEX, (_full, filterId: string, inner: string) => { + const filterType = types[filterId]; + if (!filterType) { + throw new Error(`Unknown story filter "${filterId}" referenced in SQL template.`); + } + + const selection = selections[filterId]; + const sqlValue = renderFilterSqlValue(filterType, selection); + if (sqlValue === null) { + return ''; + } + + return inner.replace(FILTER_PLACEHOLDER_REGEX, (_placeholder, placeholderId: string) => { + if (placeholderId !== filterId) { + throw new Error( + `Filter block "${filterId}" contains placeholder for unrelated filter "${placeholderId}".`, + ); + } + return sqlValue; + }); + }); + + const leftover = rendered.match(/\{\{\s*filters\.([A-Za-z_][A-Za-z0-9_]*)\.sql\s*\}\}/); + if (leftover) { + throw new Error( + `SQL template placeholder {{ filters.${leftover[1]}.sql }} must appear inside a {% filter %} block.`, + ); + } + + return rendered; +} + +export function renderFilterSqlValue( + type: StoryFilterType, + selection: StoryFilterSelection | undefined, +): string | null { + if (!isFilterSelectionActive(type, selection)) { + return null; + } + + switch (type) { + case 'select': + return quoteSqlString(selection as string); + case 'multi_select': + return (selection as string[]) + .filter((value) => value.trim() !== '') + .map(quoteSqlString) + .join(', '); + case 'search': + return quoteSqlString(`%${escapeLikePattern(selection as string)}%`); + case 'date_range': { + const [from, to] = selection as string[]; + return `${quoteSqlString(from)} AND ${quoteSqlString(to)}`; + } + } +} + +export function isFilterSelectionActive(type: StoryFilterType, selection: StoryFilterSelection | undefined): boolean { + if (selection === undefined) { + return false; + } + + switch (type) { + case 'select': + case 'search': + return typeof selection === 'string' && selection.trim() !== ''; + case 'multi_select': + return Array.isArray(selection) && selection.some((value) => value.trim() !== ''); + case 'date_range': + return ( + Array.isArray(selection) && + selection.length >= 2 && + Boolean(selection[0]?.trim()) && + Boolean(selection[1]?.trim()) + ); + } +} + +function quoteSqlString(value: string): string { + if (value.includes('\0')) { + throw new Error('Filter values cannot contain null bytes.'); + } + return `'${value.replace(/'/g, "''")}'`; +} + +function escapeLikePattern(value: string): string { + return value.replace(/([%_\\])/g, '\\$1'); +} + +function validateFilterDelimiterOrder(sql: string): string[] { + const issues: string[] = []; + const openers: Array<{ id: string }> = []; + + for (const delimiter of getFilterDelimiters(sql)) { + if (delimiter.kind === 'open') { + if (!STORY_FILTER_ID_REGEX.test(delimiter.id)) { + issues.push( + `Invalid filter id "${delimiter.id}" in SQL template. Use letters, numbers, and underscores, starting with a letter or underscore.`, + ); + } + if (openers.length > 0) { + issues.push('Nested story filter blocks are not supported.'); + } + openers.push(delimiter); + continue; + } + + if (!openers.pop()) { + issues.push('Unexpected "{% endfilter %}" without a preceding "{% filter %}" opener.'); + } + } + + return issues; +} + +function getFilterDelimiters( + sql: string, +): Array<{ kind: 'open'; id: string; start: number; end: number } | { kind: 'close'; start: number; end: number }> { + const delimiters: Array< + { kind: 'open'; id: string; start: number; end: number } | { kind: 'close'; start: number; end: number } + > = []; + const regex = new RegExp(FILTER_DELIMITER_REGEX.source, 'g'); + let match: RegExpExecArray | null; + + while ((match = regex.exec(sql)) !== null) { + delimiters.push( + match[2] !== 'endfilter' + ? { kind: 'open', id: match[1].trim(), start: match.index, end: regex.lastIndex } + : { kind: 'close', start: match.index, end: regex.lastIndex }, + ); + } + + return delimiters; +} + +function removeRanges(sql: string, ranges: Array<{ start: number; end: number }>): string { + const sortedRanges = ranges.sort((a, b) => a.start - b.start); + let result = ''; + let cursor = 0; + + for (const range of sortedRanges) { + if (range.end <= cursor) { + continue; + } + if (range.start > cursor) { + result += sql.slice(cursor, range.start); + } + cursor = range.end; + } + + return result + sql.slice(cursor); +} diff --git a/apps/shared/src/story-segments.ts b/apps/shared/src/story-segments.ts index 2007846ff..8ae71b398 100644 --- a/apps/shared/src/story-segments.ts +++ b/apps/shared/src/story-segments.ts @@ -1,5 +1,6 @@ import { buildStoryTableBlock } from './chart-block'; import { type ColumnConditionalFormats, sanitizeConditionalFormats } from './conditional-formatting'; +import { STORY_FILTER_ID_REGEX, STORY_FILTER_TYPES, type StoryFilterType } from './sql-template'; import type { SeriesConfig } from './tools/display-chart'; export type ParsedChartSeries = SeriesConfig; @@ -35,10 +36,25 @@ export interface ParsedTableBlock { rawTag?: string; } +export interface ParsedFilterBlock { + id: string; + column?: string; + label: string; + filterType: StoryFilterType; + table?: string; + /** Database to use when loading options via SELECT DISTINCT on `table.column`. */ + databaseId?: string; + /** Hardcoded dropdown values; when set, options are not loaded from `table.column`. */ + options?: string[]; + /** The original `` tag this block was parsed from, when available. */ + rawTag?: string; +} + export type Segment = | { type: 'markdown'; content: string } | { type: 'chart'; chart: ParsedChartBlock } | { type: 'table'; table: ParsedTableBlock } + | { type: 'filter'; filter: ParsedFilterBlock } | { type: 'grid'; cols: number; widths: number[] | null; children: Segment[] }; export const TAG_ATTRS = String.raw`(?:[^>"']|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')*?`; @@ -53,7 +69,7 @@ export function tableTagRegex(flags = ''): RegExp { export function storyBlockRegex(): RegExp { return new RegExp( - String.raw`([\s\S]*?)<\/grid>||`, + String.raw`([\s\S]*?)<\/grid>|||`, 'g', ); } @@ -130,6 +146,63 @@ export function parseTableBlock(attrString: string): ParsedTableBlock | null { }; } +export function parseFilterBlock(attrString: string): ParsedFilterBlock | null { + const attrs = parseChartAttributes(attrString); + if ( + !attrs.id || + !STORY_FILTER_ID_REGEX.test(attrs.id) || + !STORY_FILTER_TYPES.includes(attrs.type as StoryFilterType) + ) { + return null; + } + + const filterType = attrs.type as StoryFilterType; + const options = parseStringArrayAttribute(attrs.options); + const needsOptionsSource = filterType === 'select' || filterType === 'multi_select'; + const hasHardcodedOptions = Boolean(options?.length); + const hasTableSource = Boolean(attrs.table && attrs.column); + if (needsOptionsSource && !hasHardcodedOptions && !hasTableSource) { + return null; + } + + return { + id: attrs.id, + ...(attrs.column && { column: attrs.column }), + label: attrs.label || attrs.column || attrs.id, + filterType, + ...(attrs.table && { table: attrs.table }), + ...(attrs.database_id && { databaseId: attrs.database_id }), + ...(hasHardcodedOptions && { options }), + }; +} + +export function parseStringArrayAttribute(value: string | undefined): string[] | undefined { + if (!value) { + return undefined; + } + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) && parsed.every((item) => typeof item === 'string') ? parsed : undefined; + } catch { + return undefined; + } +} + +export function getStoryFiltersFromCode(code: string): ParsedFilterBlock[] { + const filters: ParsedFilterBlock[] = []; + const filterRegex = new RegExp(String.raw``, 'g'); + let match: RegExpExecArray | null; + + while ((match = filterRegex.exec(code)) !== null) { + const filter = parseFilterBlock(match[1]); + if (filter) { + filters.push({ ...filter, rawTag: match[0] }); + } + } + + return filters; +} + function parseConditionalFormats(value: string | undefined): ColumnConditionalFormats | undefined { if (!value) { return undefined; @@ -541,6 +614,11 @@ export function splitCodeIntoSegments(code: string): Segment[] { if (table) { segments.push({ type: 'table', table: { ...table, rawTag: match[0] } }); } + } else if (match[5] !== undefined) { + const filter = parseFilterBlock(match[5]); + if (filter) { + segments.push({ type: 'filter', filter: { ...filter, rawTag: match[0] } }); + } } lastIndex = match.index + match[0].length; diff --git a/apps/shared/src/story-validation.ts b/apps/shared/src/story-validation.ts index 0b5a96b6c..e9273cf73 100644 --- a/apps/shared/src/story-validation.ts +++ b/apps/shared/src/story-validation.ts @@ -1,4 +1,11 @@ -import { parseChartAttributes, parseGridColumns, parseSeriesJsonArray, TAG_ATTRS } from './story-segments'; +import { STORY_FILTER_ID_REGEX, STORY_FILTER_TYPES } from './sql-template'; +import { + parseChartAttributes, + parseGridColumns, + parseSeriesJsonArray, + parseStringArrayAttribute, + TAG_ATTRS, +} from './story-segments'; import { ChartTypeEnum, SeriesTypeEnum, XAxisTypeEnum, YAxisSideEnum } from './tools/display-chart'; export interface StoryValidationError { @@ -11,6 +18,7 @@ export interface StoryValidationError { const REQUIRED_CHART_ATTRS = ['query_id', 'chart_type', 'x_axis_key'] as const; const CHART_TYPES_WITHOUT_X_AXIS_KEY = new Set(['kpi_card']); const REQUIRED_TABLE_ATTRS = ['query_id'] as const; +const REQUIRED_FILTER_ATTRS = ['id', 'type'] as const; const VALID_CHART_TYPES = new Set(ChartTypeEnum.options); @@ -33,6 +41,7 @@ export function validateStoryCode(code: string): StoryValidationError[] { errors.push(...validateGridBlocks(code)); errors.push(...validateChartBlocks(code)); errors.push(...validateTableBlocks(code)); + errors.push(...validateFilterBlocks(code)); errors.push(...validateTabsBlocks(code)); errors.push(...validateUnterminatedTags(code)); @@ -272,6 +281,95 @@ function validateTableBlocks(code: string): StoryValidationError[] { return errors; } +function validateFilterBlocks(code: string): StoryValidationError[] { + const errors: StoryValidationError[] = []; + const filterRegex = new RegExp(String.raw``, 'g'); + const filterIds = new Set(); + let match: RegExpExecArray | null; + + while ((match = filterRegex.exec(code)) !== null) { + const [fullMatch, attrString, slash] = match; + const position = getPosition(code, match.index); + const attrs = parseChartAttributes(attrString ?? ''); + + if (slash !== '/') { + errors.push({ + message: ' tag must be self-closing — use "/>" instead of ">".', + line: position.line, + column: position.column, + length: fullMatch.length, + }); + } + + const missing = REQUIRED_FILTER_ATTRS.filter((attr) => !attrs[attr]); + if (missing.length > 0) { + errors.push({ + message: `Filter is missing required attribute${missing.length === 1 ? '' : 's'}: ${missing.join(', ')}.`, + line: position.line, + column: position.column, + length: fullMatch.length, + }); + } + + if (attrs.type && !STORY_FILTER_TYPES.includes(attrs.type as (typeof STORY_FILTER_TYPES)[number])) { + errors.push({ + message: `Invalid filter type "${attrs.type}". Valid types: ${STORY_FILTER_TYPES.join(', ')}.`, + line: position.line, + column: position.column, + length: fullMatch.length, + }); + } + + if (attrs.id && !STORY_FILTER_ID_REGEX.test(attrs.id)) { + errors.push({ + message: `Invalid filter id "${attrs.id}". Use letters, numbers, and underscores, starting with a letter or underscore.`, + line: position.line, + column: position.column, + length: fullMatch.length, + }); + } + + const options = parseStringArrayAttribute(attrs.options); + if (attrs.options !== undefined && options === undefined) { + errors.push({ + message: 'Filter `options` attribute must be a valid JSON array of strings.', + line: position.line, + column: position.column, + length: fullMatch.length, + }); + } + + const needsOptionsSource = attrs.type === 'select' || attrs.type === 'multi_select'; + if (needsOptionsSource) { + const hasHardcodedOptions = Boolean(options?.length); + const hasTableSource = Boolean(attrs.table && attrs.column); + if (!hasHardcodedOptions && !hasTableSource) { + errors.push({ + message: + 'Select filters require either `options=\'["a","b"]\'` or both `table` and `column` attributes.', + line: position.line, + column: position.column, + length: fullMatch.length, + }); + } + } + + if (attrs.id && filterIds.has(attrs.id)) { + errors.push({ + message: `Filter id "${attrs.id}" must be unique within the story.`, + line: position.line, + column: position.column, + length: fullMatch.length, + }); + } + if (attrs.id) { + filterIds.add(attrs.id); + } + } + + return errors; +} + function isMarkdownTable(code: string, index: number): boolean { const lineStart = code.lastIndexOf('\n', index - 1) + 1; const linePrefix = code.slice(lineStart, index); @@ -374,7 +472,7 @@ function findMatchingClose(code: string, startIndex: number): number { function validateUnterminatedTags(code: string): StoryValidationError[] { const errors: StoryValidationError[] = []; - const tagRegex = /<(chart|table)\b[^>]*$/gm; + const tagRegex = /<(chart|table|filter)\b[^>]*$/gm; let match: RegExpExecArray | null; while ((match = tagRegex.exec(code)) !== null) { diff --git a/apps/shared/src/tools/execute-sql.ts b/apps/shared/src/tools/execute-sql.ts index ab96a7e2c..5c1fc8078 100644 --- a/apps/shared/src/tools/execute-sql.ts +++ b/apps/shared/src/tools/execute-sql.ts @@ -9,6 +9,9 @@ export const InputSchema = z.object({ .optional() .describe('The database name/id to use. Required if multiple databases are configured.'), name: z.string().optional().describe('A descriptive name for the query that will be used to show in the UI.'), + query_id: QueryIdSchema.optional().describe( + 'When set, replace the SQL of this existing query in-place (same query_id) instead of creating a new one. Prefer this when adding story filter templates so chart/table tags keep working.', + ), }); export const OutputSchema = z.object({ @@ -25,6 +28,16 @@ export const OutputSchema = z.object({ * represent the total number of matching rows. */ applied_limit: z.number().optional(), + /** + * Story-filter SQL template syntax issues. The stripped query may still execute, + * but filters will not apply correctly until these are fixed via execute_sql with query_id. + */ + template_warnings: z.array(z.string()).optional(), + /** + * Set in-memory (never persisted) when a later execute_sql part in the chat re-ran + * the same query id. The model then sees a short stub instead of repeated rows. + */ + superseded: z.boolean().optional(), }); export type Input = z.infer; diff --git a/apps/shared/src/tools/story.ts b/apps/shared/src/tools/story.ts index eaea60f06..df790f8c2 100644 --- a/apps/shared/src/tools/story.ts +++ b/apps/shared/src/tools/story.ts @@ -33,6 +33,7 @@ export const OutputSchema = z.object({ code: z.string().describe('The full story code after the operation.'), title: z.string(), error: z.string().optional(), + template_warnings: z.array(z.string()).optional(), }); export type Input = z.infer; diff --git a/apps/shared/tests/execute-sql-parts.test.ts b/apps/shared/tests/execute-sql-parts.test.ts new file mode 100644 index 000000000..8072746e6 --- /dev/null +++ b/apps/shared/tests/execute-sql-parts.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, it } from 'vitest'; + +import { filterSupersededExecuteSqlParts, markSupersededExecuteSqlParts } from '../src/execute-sql-parts'; + +type TestPart = { + type: string; + toolCallId?: string; + output?: { id?: string; superseded?: boolean }; +}; +type TestMessage = { id: string; parts: TestPart[] }; + +function executeSqlPart(queryId: string, rowCount: number): TestPart { + return { + type: 'tool-execute_sql', + toolCallId: `call_${queryId}_${rowCount}`, + output: { id: queryId }, + }; +} + +function message(id: string, parts: TestPart[]): TestMessage { + return { id, parts }; +} + +describe('markSupersededExecuteSqlParts', () => { + it('flags all but the last occurrence of a duplicated query id', () => { + const messages = [ + message('m1', [executeSqlPart('query_abc', 1)]), + message('m2', [executeSqlPart('query_other', 5)]), + message('m3', [executeSqlPart('query_abc', 2)]), + ]; + + const result = markSupersededExecuteSqlParts(messages); + + expect(result[0].parts[0].output?.superseded).toBe(true); + expect(result[1].parts[0].output?.superseded).toBeUndefined(); + expect(result[2].parts[0].output?.superseded).toBeUndefined(); + }); + + it('returns the same message objects when there are no duplicates', () => { + const messages = [message('m1', [executeSqlPart('query_a', 1)]), message('m2', [executeSqlPart('query_b', 1)])]; + + const result = markSupersededExecuteSqlParts(messages); + + expect(result[0]).toBe(messages[0]); + expect(result[1]).toBe(messages[1]); + }); + + it('handles duplicates within the same message', () => { + const messages = [message('m1', [executeSqlPart('query_a', 1), executeSqlPart('query_a', 2)])]; + + const result = markSupersededExecuteSqlParts(messages); + + expect(result[0].parts[0].output?.superseded).toBe(true); + expect(result[0].parts[1].output?.superseded).toBeUndefined(); + }); + + it('ignores execute_sql parts without output', () => { + const pending: TestPart = { type: 'tool-execute_sql', toolCallId: 'call_pending' }; + const messages = [message('m1', [pending, executeSqlPart('query_a', 1)])]; + + const result = markSupersededExecuteSqlParts(messages); + + expect(result[0]).toBe(messages[0]); + }); +}); + +describe('filterSupersededExecuteSqlParts', () => { + it('removes all but the last occurrence of a duplicated query id', () => { + const first = executeSqlPart('query_abc', 1); + const other = executeSqlPart('query_other', 5); + const last = executeSqlPart('query_abc', 2); + const messages = [message('m1', [first]), message('m2', [other]), message('m3', [last])]; + + const result = filterSupersededExecuteSqlParts(messages); + + expect(result[0].parts).toEqual([]); + expect(result[1].parts).toEqual([other]); + expect(result[2].parts).toEqual([last]); + }); + + it('returns the same message objects when there are no duplicates', () => { + const messages = [message('m1', [executeSqlPart('query_a', 1)]), message('m2', [executeSqlPart('query_b', 1)])]; + + const result = filterSupersededExecuteSqlParts(messages); + + expect(result[0]).toBe(messages[0]); + expect(result[1]).toBe(messages[1]); + }); +}); diff --git a/apps/shared/tests/sql-template.test.ts b/apps/shared/tests/sql-template.test.ts new file mode 100644 index 000000000..6636268b8 --- /dev/null +++ b/apps/shared/tests/sql-template.test.ts @@ -0,0 +1,171 @@ +import { describe, expect, it } from 'vitest'; + +import { + extractSqlFilterIds, + findUnreferencedStoryFilters, + renderFilterSqlValue, + renderSqlTemplate, + stripSqlFilterBlocks, + validateSqlFilterTemplate, +} from '../src/sql-template'; + +const SAMPLE_SQL = ` +SELECT SUM(revenue) AS revenue, country +FROM orders +WHERE 1 = 1 +{% filter country %} AND country IN ({{ filters.country.sql }}) {% endfilter %} +{% filter q %} AND customer_name ILIKE {{ filters.q.sql }} {% endfilter %} +{% filter period %} AND order_date BETWEEN {{ filters.period.sql }} {% endfilter %} +GROUP BY country +`.trim(); + +describe('stripSqlFilterBlocks', () => { + it('removes all filter blocks so SQL runs without story filters', () => { + expect(stripSqlFilterBlocks(SAMPLE_SQL)).toBe( + ` +SELECT SUM(revenue) AS revenue, country +FROM orders +WHERE 1 = 1 + + + +GROUP BY country +`.trim(), + ); + }); + + it('preserves untouched whitespace and strips malformed blocks safely', () => { + const sql = "SELECT 'line 1\n\nline 2 '\n{% endfilter %}\n{% filter country %} AND country = 'US'"; + expect(stripSqlFilterBlocks(sql)).toBe("SELECT 'line 1\n\nline 2 '\n\n"); + }); +}); + +describe('extractSqlFilterIds', () => { + it('returns unique filter ids in order of appearance', () => { + expect(extractSqlFilterIds(SAMPLE_SQL)).toEqual(['country', 'q', 'period']); + }); +}); + +describe('renderFilterSqlValue', () => { + it('renders select / multi_select / search / date_range fragments', () => { + expect(renderFilterSqlValue('select', 'US')).toBe("'US'"); + expect(renderFilterSqlValue('multi_select', ['US', "O'Reilly"])).toBe("'US', 'O''Reilly'"); + expect(renderFilterSqlValue('search', 'acme%')).toBe("'%acme\\%%'"); + expect(renderFilterSqlValue('date_range', ['2024-01-01', '2024-12-31'])).toBe("'2024-01-01' AND '2024-12-31'"); + }); + + it('returns null for inactive selections', () => { + expect(renderFilterSqlValue('select', '')).toBeNull(); + expect(renderFilterSqlValue('multi_select', [])).toBeNull(); + expect(renderFilterSqlValue('date_range', ['2024-01-01', ''])).toBeNull(); + }); + + it('omits whitespace-only multi-select values', () => { + expect(renderFilterSqlValue('multi_select', ['US', ' '])).toBe("'US'"); + }); +}); + +describe('renderSqlTemplate', () => { + const types = { + country: 'multi_select', + q: 'search', + period: 'date_range', + } as const; + + it('keeps only active filter blocks and interpolates .sql placeholders', () => { + const rendered = renderSqlTemplate( + SAMPLE_SQL, + { + country: ['US', 'FR'], + q: 'acme', + }, + types, + ); + + expect(rendered).toContain("AND country IN ('US', 'FR')"); + expect(rendered).toContain("AND customer_name ILIKE '%acme%'"); + expect(rendered).not.toContain('order_date BETWEEN'); + expect(rendered).not.toContain('{% filter'); + expect(rendered).not.toContain('{{ filters'); + }); + + it('strips all blocks when no selections are active', () => { + expect(renderSqlTemplate(SAMPLE_SQL, {}, types)).toBe(stripSqlFilterBlocks(SAMPLE_SQL)); + }); + + it('rejects unknown filter ids and misplaced placeholders', () => { + expect(() => renderSqlTemplate(SAMPLE_SQL, { country: ['US'] }, {})).toThrow(/undeclared filter/); + expect(() => + renderSqlTemplate('SELECT 1 WHERE x = {{ filters.country.sql }}', { country: 'US' }, { country: 'select' }), + ).toThrow(/must appear inside/); + expect(() => + renderSqlTemplate( + '{% filter country %} AND country = {{ filters.country.sql.extra }} {% endfilter %}', + { country: 'US' }, + { country: 'select' }, + ), + ).toThrow(/Only \{\{ filters\.country\.sql \}\} is supported/); + }); +}); + +describe('validateSqlFilterTemplate', () => { + it('accepts correct templates', () => { + expect(validateSqlFilterTemplate(SAMPLE_SQL)).toEqual([]); + expect(validateSqlFilterTemplate(SAMPLE_SQL, { knownFilterIds: ['country', 'q', 'period'] })).toEqual([]); + }); + + it('flags date_range start/end/value placeholders', () => { + const sql = ` +SELECT 1 FROM orders WHERE 1 = 1 +{% filter period %} AND order_date BETWEEN {{ filters.period.start }} AND {{ filters.period.end }} {% endfilter %} +`.trim(); + const issues = validateSqlFilterTemplate(sql); + expect(issues.some((issue) => /filters\.period\.start/.test(issue))).toBe(true); + expect(issues.some((issue) => /BETWEEN \{\{ filters\.period\.sql \}\}/.test(issue))).toBe(true); + }); + + it('flags placeholders outside filter blocks', () => { + const issues = validateSqlFilterTemplate('SELECT 1 WHERE country = {{ filters.country.sql }}'); + expect(issues.some((issue) => /must appear inside/.test(issue))).toBe(true); + }); + + it('flags undeclared filter ids when known filters are provided', () => { + const sql = '{% filter country %} AND country = {{ filters.country.sql }} {% endfilter %}'; + const issues = validateSqlFilterTemplate(sql, { knownFilterIds: ['period'] }); + expect(issues.some((issue) => /undeclared filter "country"/.test(issue))).toBe(true); + }); + + it('flags filter blocks missing .sql placeholders', () => { + const sql = "{% filter country %} AND country = 'US' {% endfilter %}"; + const issues = validateSqlFilterTemplate(sql); + expect(issues.some((issue) => /missing \{\{ filters\.country\.sql \}\}/.test(issue))).toBe(true); + }); + + it('flags malformed placeholders and misordered delimiters', () => { + const placeholderIssues = validateSqlFilterTemplate('SELECT 1 WHERE country = {{ filters.country.sql.extra }}'); + expect(placeholderIssues.some((issue) => /Only \{\{ filters\.country\.sql \}\} is supported/.test(issue))).toBe( + true, + ); + + const delimiterIssues = validateSqlFilterTemplate( + '{% endfilter %} SELECT 1 {% filter country %} AND country = {{ filters.country.sql }}', + ); + expect(delimiterIssues.some((issue) => /Unexpected "\{% endfilter %\}"/.test(issue))).toBe(true); + }); + + it('flags placeholders with a trailing dot and no property', () => { + const issues = validateSqlFilterTemplate('SELECT 1 WHERE country = {{ filters.country. }}'); + expect(issues.some((issue) => /property "\.sql" is required/.test(issue))).toBe(true); + }); +}); + +describe('findUnreferencedStoryFilters', () => { + it('returns declared filters unused by any SQL template', () => { + expect( + findUnreferencedStoryFilters( + ['country', 'period'], + ['{% filter country %} AND country = {{ filters.country.sql }} {% endfilter %}'], + ), + ).toEqual(['period']); + }); +}); diff --git a/apps/shared/tests/story-segments.test.ts b/apps/shared/tests/story-segments.test.ts index 3e7280abd..837cc6288 100644 --- a/apps/shared/tests/story-segments.test.ts +++ b/apps/shared/tests/story-segments.test.ts @@ -1,8 +1,9 @@ import { describe, expect, it } from 'vitest'; -import { buildStoryChartBlock } from '../src/chart-block'; +import { buildStoryChartBlock, buildStoryFilterBlock } from '../src/chart-block'; import { getGridTemplateColumns, + getStoryFiltersFromCode, groupBlocksIntoGrid, insertGridColumn, parseChartBlock, @@ -583,3 +584,72 @@ describe('splitCodeIntoSegments slash-in-attribute handling', () => { expect(chart?.series).toEqual([{ data_key: 'rev', color: 'var(--chart-1)', label: undefined }]); }); }); + +describe('story filter tags', () => { + it('parses filter tags from code and as segments', () => { + const code = [ + buildStoryFilterBlock({ + id: 'country', + column: 'country', + label: 'Country', + type: 'multi_select', + table: 'orders', + }), + '', + ].join('\n'); + + expect(getStoryFiltersFromCode(code)).toEqual([ + { + id: 'country', + column: 'country', + label: 'Country', + filterType: 'multi_select', + table: 'orders', + rawTag: expect.any(String), + }, + ]); + expect(splitCodeIntoSegments(code).some((segment) => segment.type === 'filter')).toBe(true); + }); + + it('parses hardcoded options without table/column', () => { + const code = buildStoryFilterBlock({ + id: 'country', + label: 'Country', + type: 'select', + options: ['US', 'FR'], + }); + + expect(getStoryFiltersFromCode(code)).toEqual([ + { + id: 'country', + label: 'Country', + filterType: 'select', + options: ['US', 'FR'], + rawTag: expect.any(String), + }, + ]); + }); + + it('parses database_id for table-backed filters', () => { + const code = buildStoryFilterBlock({ + id: 'product', + column: 'product_name', + label: 'Product', + type: 'select', + table: '`nao-production`.`prod_silver`.`dim_products`', + database_id: 'bigquery', + }); + + expect(getStoryFiltersFromCode(code)).toEqual([ + { + id: 'product', + column: 'product_name', + label: 'Product', + filterType: 'select', + table: '`nao-production`.`prod_silver`.`dim_products`', + databaseId: 'bigquery', + rawTag: expect.any(String), + }, + ]); + }); +}); diff --git a/apps/shared/tests/story-validation.test.ts b/apps/shared/tests/story-validation.test.ts index eb242fc16..94cf47b4e 100644 --- a/apps/shared/tests/story-validation.test.ts +++ b/apps/shared/tests/story-validation.test.ts @@ -266,6 +266,47 @@ describe('validateStoryCode', () => { }); }); + describe('filter validation', () => { + it('accepts a well-formed filter tag with table source', () => { + const code = ''; + expect(validateStoryCode(code)).toEqual([]); + }); + + it('accepts a filter with hardcoded options', () => { + const code = ``; + expect(validateStoryCode(code)).toEqual([]); + }); + + it('flags select filters missing both options and table/column', () => { + expect( + validateStoryCode('').some((e) => + /require either `options=/.test(e.message), + ), + ).toBe(true); + }); + + it('flags invalid filter types', () => { + expect( + validateStoryCode('').some( + (e) => /Invalid filter type/.test(e.message), + ), + ).toBe(true); + }); + + it('flags filter ids that cannot be used in SQL templates', () => { + const errors = validateStoryCode(''); + expect(errors.some((error) => error.message.includes('Invalid filter id "order-status"'))).toBe(true); + }); + + it('flags duplicate filter ids', () => { + const code = [ + '', + '', + ].join('\n'); + expect(validateStoryCode(code).some((e) => /must be unique/.test(e.message))).toBe(true); + }); + }); + it('reports line and column for errors', () => { const code = ['# intro', '', 'some text', '', ''].join('\n'); const errors = validateStoryCode(code); diff --git a/bun.lock b/bun.lock index 1a904118d..0cae41599 100644 --- a/bun.lock +++ b/bun.lock @@ -9,6 +9,7 @@ "@emnapi/core": "^1.9.2", "@emnapi/runtime": "^1.9.2", "@pydantic/monty": "^0.0.7", + "@streamdown/code": "^1.1.1", "depd": "^2.0.0", }, "devDependencies": { @@ -43,11 +44,14 @@ "@fastify/formbody": "^8.0.2", "@fastify/multipart": "^10.0.0", "@fastify/static": "^8.1.0", + "@langfuse/otel": "^5.9.1", + "@langfuse/tracing": "^5.9.1", "@microsoft/microsoft-graph-client": "^3.0.7", "@modelcontextprotocol/ext-apps": "^1.7.1", "@modelcontextprotocol/sdk": "^1.29.0", "@nao/shared": "*", "@openrouter/ai-sdk-provider": "^2.10.0", + "@opentelemetry/sdk-trace-node": "^2.9.0", "@pydantic/monty": "^0.0.7", "@resvg/resvg-js": "^2.6.2", "@slack/socket-mode": "^2.0.6", @@ -57,6 +61,7 @@ "@vscode/ripgrep": "^1.17.0", "ai": "^6.0.97", "ai-sdk-ollama": "^3.7.1", + "ajv": "^8.20.0", "better-auth": "^1.6.3", "better-sqlite3": "^12.5.0", "chat": "^4.15.0", @@ -148,13 +153,15 @@ "date-fns": "^4.1.0", "fuse.js": "^7.3.0", "jszip": "^3.10.1", + "katex": "^0.16.45", "lucide-react": "^0.562.0", "posthog-js": "^1.336.4", - "prompt-mentions": "^0.0.34", + "prompt-mentions": "^0.0.37", "proxy-from-env": "^1.1.0", "radix-ui": "^1.4.3", "react": "^19.2.0", "react-datepicker": "^9.1.0", + "react-day-picker": "^10.0.1", "react-dom": "^19.2.0", "react-resizable-panels": "^4", "react-syntax-highlighter": "^16.1.0", @@ -431,6 +438,8 @@ "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="], + "@date-fns/tz": ["@date-fns/tz@1.5.0", "", {}, "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg=="], + "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="], "@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="], @@ -605,6 +614,12 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@langfuse/core": ["@langfuse/core@5.9.1", "", { "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-KvyAskAO+2ixJwr9wy148ttR/Zn3oapvOJ2Br4Xcp6zhDpjSIIGB4jW/jkp53/KU9MyEHj0RSFPK/yKoxLC4KA=="], + + "@langfuse/otel": ["@langfuse/otel@5.9.1", "", { "dependencies": { "@langfuse/core": "^5.9.1" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^2.0.1", "@opentelemetry/exporter-trace-otlp-http": ">=0.202.0 <1.0.0", "@opentelemetry/sdk-trace-base": "^2.0.1" } }, "sha512-viM5Qq/AIPZPXfO7YdSmDHxEDSrNU/MyGzuE9zKA6hGBII1iLowU+qL99O+TjnXqxoADqlNibhY2pg1/WTIPcw=="], + + "@langfuse/tracing": ["@langfuse/tracing@5.9.1", "", { "dependencies": { "@langfuse/core": "^5.9.1" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-tJRyVAv1JkuOPh4Uz5eWUNH8U4jcJVtK2F5QNy5cZUzXCSrXobCSHusPbxY6VFZcLcFgtpDtSaxL7ev1tV2JNQ=="], + "@libsql/client": ["@libsql/client@0.15.15", "", { "dependencies": { "@libsql/core": "^0.15.14", "@libsql/hrana-client": "^0.7.0", "js-base64": "^3.7.5", "libsql": "^0.5.22", "promise-limit": "^2.7.0" } }, "sha512-twC0hQxPNHPKfeOv3sNT6u2pturQjLcI+CnpTM0SjRpocEGgfiZ7DWKXLNnsothjyJmDqEsBQJ5ztq9Wlu470w=="], "@libsql/core": ["@libsql/core@0.15.15", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-C88Z6UKl+OyuKKPwz224riz02ih/zHYI3Ho/LAcVOgjsunIRZoBw7fjRfaH9oPMmSNeQfhGklSG2il1URoOIsA=="], @@ -669,21 +684,29 @@ "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg=="], - "@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.10.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-bvyMcgLEkozzSzpEEEo1OMoeQ97bxj6Qs2uN3mPrSdDvObMI1myffD/BPqcLlzZO9//d1SqQA/WPw7Cz2AiqhA=="], + + "@opentelemetry/core": ["@opentelemetry/core@2.10.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ=="], "@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0", "@opentelemetry/sdk-logs": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg=="], - "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.208.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA=="], + "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.221.0", "", { "dependencies": { "@opentelemetry/otlp-exporter-base": "0.221.0", "@opentelemetry/otlp-transformer": "0.221.0", "@opentelemetry/sdk-trace": "2.10.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-AySXiKoC+meiWm6zdVj5T2LnPDZuatveBby1cMOeQteIWsYXAUxs8Sru13G2pVSPrUXz6vF+og7QVBX6GdC/oQ=="], + + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.221.0", "", { "dependencies": { "@opentelemetry/core": "2.10.0", "@opentelemetry/otlp-transformer": "0.221.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-UFPIq80OH3Ns/oPFHRj14d4DTOxUo+MUFU8hUiCq5jTqFhdeJnfVSANHT+xp92409cA+oxzvlZCe6NM1wvCuBA=="], - "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.208.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ=="], + "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.221.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.221.0", "@opentelemetry/core": "2.10.0", "@opentelemetry/resources": "2.10.0", "@opentelemetry/sdk-logs": "0.221.0", "@opentelemetry/sdk-metrics": "2.10.0", "@opentelemetry/sdk-trace": "2.10.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lg6lkOU08Az23jVcn/0Els9HP+V8PnR4Km6p0KgpTggS0n/WuhnmY64rSh83Of9iR9nD+dpWr6adlcX8KzAwjg=="], "@opentelemetry/resources": ["@opentelemetry/resources@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g=="], "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA=="], - "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], + "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.10.0", "", { "dependencies": { "@opentelemetry/core": "2.10.0", "@opentelemetry/resources": "2.10.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-t6r1VSvXNtSDnPXU1FbZeetJb7yyovHmgu0wRSoftxtE0g2rSNhQZQUy69sRUCL+iioJpX8SN/S6wq6ZtvLySQ=="], - "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + "@opentelemetry/sdk-trace": ["@opentelemetry/sdk-trace@2.10.0", "", { "dependencies": { "@opentelemetry/core": "2.10.0", "@opentelemetry/resources": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ=="], + + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.10.0", "", { "dependencies": { "@opentelemetry/core": "2.10.0", "@opentelemetry/resources": "2.10.0", "@opentelemetry/sdk-trace": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ=="], + + "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.10.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.10.0", "@opentelemetry/core": "2.10.0", "@opentelemetry/sdk-trace-base": "2.10.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-GZK/G6oZyBLGlH1pUgeDch7D91KoHd2uotUGIkWCPi9GI5T9X0p4L7nNAMDR1BQjkRYoDqo+ddfVx9t5Uhys+Q=="], "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.39.0", "", {}, "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg=="], @@ -963,6 +986,20 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg=="], + "@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], + + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], + + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], + + "@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], + + "@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], + + "@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@slack/logger": ["@slack/logger@4.0.1", "", { "dependencies": { "@types/node": ">=18" } }, "sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ=="], "@slack/socket-mode": ["@slack/socket-mode@2.0.7", "", { "dependencies": { "@slack/logger": "^4.0.1", "@slack/web-api": "^7.15.0", "@types/node": ">=18", "@types/ws": "^8", "eventemitter3": "^5", "ws": "^8" } }, "sha512-qYy07je71WnEHgRwmw12DlAnZLi5HXmdlI2WUzUK2LH/rYXQpP6uEg462S5CwfE8FoCKUdIigHtYnOOfzZH1lQ=="], @@ -1067,6 +1104,8 @@ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@streamdown/code": ["@streamdown/code@1.1.1", "", { "dependencies": { "shiki": "^3.19.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-i7HTNuDgZWb+VdrNVOam9gQhIc5MSSDXKWXgbUrn/4vSRaSMM+Rtl10MQj4wLWPNpF7p80waJsAqFP8HZfb0Jg=="], + "@streamdown/math": ["@streamdown/math@1.0.2", "", { "dependencies": { "katex": "^0.16.27", "rehype-katex": "^7.0.1", "remark-math": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-r8Ur9/lBuFnzZAFdEWrLUF2s/gRwRRRwruqltdZibyjbCBnuW7SJbFm26nXqvpJPW/gzpBUMrBVBzd88z05D5g=="], "@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.6.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.0", "@typescript-eslint/types": "^8.47.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-JCs+MqoXfXrRPGbGmho/zGS/jMcn3ieKl/A8YImqib76C8kjgZwq5uUFzc30lJkMvcchuRn6/v8IApLxli3Jyw=="], @@ -1499,7 +1538,7 @@ "ai-sdk-ollama": ["ai-sdk-ollama@3.8.0", "", { "dependencies": { "@ai-sdk/provider": "^3.0.8", "@ai-sdk/provider-utils": "^4.0.15", "jsonrepair": "^3.13.2", "ollama": "^0.6.3" }, "peerDependencies": { "ai": "^6.0.89" } }, "sha512-Nlla8FpK8QFMNh9m8sPCZoNqnr+n+Ud0QTqpXNds4j/b/lbVZGaji13ZcRuuFvBwPwd4xnFkNrijJzi70Ih1Tg=="], - "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], @@ -2207,6 +2246,8 @@ "hast-util-sanitize": ["hast-util-sanitize@5.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "unist-util-position": "^5.0.0" } }, "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg=="], + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], @@ -2401,7 +2442,7 @@ "json-schema-resolver": ["json-schema-resolver@3.0.0", "", { "dependencies": { "debug": "^4.1.1", "fast-uri": "^3.0.5", "rfdc": "^1.1.4" } }, "sha512-HqMnbz0tz2DaEJ3ntsqtx3ezzZyDE7G56A/pPY/NGmrPu76UzsWquOpHFRAf5beTNXoH2LU5cQePVvRli1nchA=="], - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], @@ -2423,7 +2464,7 @@ "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], - "katex": ["katex@0.16.27", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw=="], + "katex": ["katex@0.16.47", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg=="], "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], @@ -2733,6 +2774,10 @@ "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], + + "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], + "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], @@ -2967,6 +3012,8 @@ "react-datepicker": ["react-datepicker@9.1.0", "", { "dependencies": { "@floating-ui/react": "^0.27.15", "clsx": "^2.1.1", "date-fns": "^4.1.0" }, "peerDependencies": { "date-fns-tz": "^3.0.0", "react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" }, "optionalPeers": ["date-fns-tz"] }, "sha512-lOp+m5bc+ttgtB5MHEjwiVu4nlp4CvJLS/PG1OiOe5pmg9kV73pEqO8H0Geqvg2E8gjqTaL9eRhSe+ZpeKP3nA=="], + "react-day-picker": ["react-day-picker@10.0.1", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0" }, "peerDependencies": { "@types/react": ">=16.8.0", "react": ">=16.8.0" }, "optionalPeers": ["@types/react"] }, "sha512-eNh6BlwcYInWaJtRv18mXQ06Ys/H6rdTZAnTaSdOYJuTpwP1JMCHNd1FDRadA+gbeinq+psdULN5Xnowy9mV8w=="], + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], @@ -3007,6 +3054,12 @@ "refractor": ["refractor@5.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/prismjs": "^1.0.0", "hastscript": "^9.0.0", "parse-entities": "^4.0.0" } }, "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw=="], + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + + "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + + "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], "rehype-harden": ["rehype-harden@1.1.8", "", { "dependencies": { "unist-util-visit": "^5.0.0" } }, "sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw=="], @@ -3121,6 +3174,8 @@ "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], + "shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], @@ -3557,6 +3612,8 @@ "@eslint/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "@eslint/eslintrc/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], "@eslint/eslintrc/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -3579,7 +3636,7 @@ "@nao/frontend/@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="], - "@nao/frontend/prompt-mentions": ["prompt-mentions@0.0.34", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-kIV79vsxdTeGZKDuH8/lgSN40pGvB+hJdg0aE6ugIWbVXW1F3Vu1hX1ks5Lw76KIvjAzeFzoNkrmvI16i2ZZNQ=="], + "@nao/frontend/prompt-mentions": ["prompt-mentions@0.0.37", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-tVXx/OZ4g7G3YgzV4vNuIt+W4sVAyPnMg//GH/NTtuTbYsUZi/eHy0OtIcAhCk5GqqFBrVo9/aw0zNk1683jzQ=="], "@nao/frontend/vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@vitest/browser", "@vitest/ui", "happy-dom"], "bin": "vitest.mjs" }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], @@ -3589,15 +3646,29 @@ "@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], - "@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.208.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA=="], + + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.208.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ=="], + + "@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.221.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-OlanaW1vv7ufTqQ3/fPLI4arGt5ZoM+P8abOMki6uEYnpRazepSWDwDnnw+la7kE26SHVC18//SMccrDvLKOXQ=="], + + "@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.10.0", "", { "dependencies": { "@opentelemetry/core": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA=="], + + "@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.221.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.221.0", "@opentelemetry/core": "2.10.0", "@opentelemetry/resources": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-FaDcazjyMp7TZZZAsqbo4IkovP0UegoCu0EBkiNt+qCqvUf7FPAsfcrZ3+ZEkKgXZ/jHafop+JoGPDk3A0SmLg=="], "@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.5.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ=="], + "@opentelemetry/sdk-logs/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], - "@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], + "@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.10.0", "", { "dependencies": { "@opentelemetry/core": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA=="], + + "@opentelemetry/sdk-trace/@opentelemetry/resources": ["@opentelemetry/resources@2.10.0", "", { "dependencies": { "@opentelemetry/core": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA=="], - "@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], + "@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.10.0", "", { "dependencies": { "@opentelemetry/core": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA=="], "@puppeteer/browsers/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], @@ -3627,6 +3698,8 @@ "@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg=="], + "@streamdown/math/katex": ["katex@0.16.27", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw=="], + "@tanstack/devtools/@tanstack/devtools-client": ["@tanstack/devtools-client@0.0.3", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.3.3" } }, "sha512-kl0r6N5iIL3t9gGDRAv55VRM3UIyMKVH83esRGq7xBjYsRLe/BeCIN2HqrlJkObUXQMKhy7i8ejuGOn+bDqDBw=="], "@tanstack/devtools-vite/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -3739,6 +3812,8 @@ "escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "eslint/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "eslint/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "eslint-plugin-n/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], @@ -3781,10 +3856,14 @@ "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "mermaid/katex": ["katex@0.16.27", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw=="], + "mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], "mermaid/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "micromark-extension-math/katex": ["katex@0.16.27", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw=="], + "monaco-editor/dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="], "monaco-editor/marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], @@ -3835,6 +3914,8 @@ "recharts/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + "rehype-katex/katex": ["katex@0.16.27", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw=="], + "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.57", "", {}, "sha512-aQNelgx14tGA+n2tNSa9x6/jeoCL9fkDeCei7nOKnHx0fEFRRMu5ReiITo+zZD5TzWDGGRjbSYCs93IfRIyTuQ=="], "safe-array-concat/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], @@ -3939,14 +4020,12 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], - "@fastify/ajv-compiler/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "@floating-ui/react/@floating-ui/react-dom/@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "@nao/frontend/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "@nao/frontend/vitest/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], @@ -3967,8 +4046,16 @@ "@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], + + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], + + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + "@puppeteer/browsers/tar-fs/tar-stream": ["tar-stream@3.2.0", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg=="], + "@streamdown/math/katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + "@tanstack/devtools/@tanstack/devtools-client/@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.3.5", "", {}, "sha512-RL1f5ZlfZMpghrCIdzl6mLOFLTuhqmPNblZgBaeKfdtk5rfbjykurv+VfYydOFXj0vxVIoA2d/zT7xfD7Ph8fw=="], "@tanstack/router-plugin/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -3987,8 +4074,6 @@ "@unrs/resolver-binding-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], - "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "bl/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "botbuilder/@azure/msal-node/@azure/msal-common": ["@azure/msal-common@14.16.1", "", {}, "sha512-nyxsA6NA4SVKh5YyRpbSXiMr7oQbwark7JU9LMeg6tJYTSPyAGkdx61wPT4gyxZfxlSxMMEyAsWaubBlNyIa1w=="], @@ -4009,7 +4094,7 @@ "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], - "fast-json-stringify/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], @@ -4021,6 +4106,10 @@ "mcporter/@modelcontextprotocol/sdk/jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + "mermaid/katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + + "micromark-extension-math/katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + "node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], "node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], @@ -4037,6 +4126,8 @@ "npm-run-all/cross-spawn/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": "bin/which" }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], + "rehype-katex/katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + "tar-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "tsup/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], @@ -4215,8 +4306,6 @@ "cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - "mcporter/@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "npm-run-all/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], "npm-run-all/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="], diff --git a/package-lock.json b/package-lock.json index b814fb3e2..0615b374d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -120,7 +120,6 @@ "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.19.0" } @@ -262,6 +261,7 @@ "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-menubar": "^1.1.16", + "@radix-ui/react-popover": "^1.1.21", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tooltip": "^1.2.8", @@ -297,6 +297,7 @@ "radix-ui": "^1.4.3", "react": "^19.2.0", "react-datepicker": "^9.1.0", + "react-day-picker": "^10.0.1", "react-dom": "^19.2.0", "react-resizable-panels": "^4", "react-syntax-highlighter": "^16.1.0", @@ -331,6 +332,400 @@ "web-vitals": "^5.1.0" } }, + "apps/frontend/node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", + "license": "MIT" + }, + "apps/frontend/node_modules/@radix-ui/react-arrow": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.13.tgz", + "integrity": "sha512-0Q310knIY0K+mkmncU9FxLggfW7V49Ok2oY+iu27KkERTsQXxYCIC8XW5QoK4w7Jp1z00vT5xj3oxTcn7QlcTg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.8" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "apps/frontend/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.4.tgz", + "integrity": "sha512-pWJo6lQAfR6uy1n7ii7PaCc9dLPwTXDYbQpORZU5B548Aqvl2pP1SM1vJGKyxIFqZMHRopRO4CQYX2iXAIB5jA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "apps/frontend/node_modules/@radix-ui/react-context": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.1.tgz", + "integrity": "sha512-EraVbFjiIjibpLr6EjvEDmSCYJU2SlKDMiO+qEK/D9GOWnQoAQlpQo2occGYC1UM9MBeEx5Bek3UtW/Qi57vAg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "apps/frontend/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.17.tgz", + "integrity": "sha512-QAXwa38pG0xNAYh1pjdSaf86NrkqsMoDNmget/Y7X8O8E/C3Iqlj9GAPE4DfX9BPLXc7WH2TWSzMRnIoCdcjzQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.4", + "@radix-ui/react-primitive": "2.1.8", + "@radix-ui/react-use-callback-ref": "1.1.3", + "@radix-ui/react-use-effect-event": "0.0.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "apps/frontend/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.5.tgz", + "integrity": "sha512-UQvlB7L/BYh3P8MLvwZnQkH521EDos40Rwnbt5+Qpg4Vbk0z3xJjRUmR6+aka4aT1IQQXFdO5bNPoE7cvFl5xQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "apps/frontend/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.14.tgz", + "integrity": "sha512-/x4htnJfmW53MplkrePaDpf1o/rN1C++g88WpVobULXbSyC19NtLkXmewuJ/HCaceSmfKDNL5gOXcBGnuAvnvQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.4", + "@radix-ui/react-primitive": "2.1.8", + "@radix-ui/react-use-callback-ref": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "apps/frontend/node_modules/@radix-ui/react-id": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.3.tgz", + "integrity": "sha512-f/Wxm0ctyMymUJK0fqTSQlm85rbzdAkoNbPXJQ5+6caowVO8Yx+NWGjGz/oGhs/D+WIbbQpOrU0hU2Li2/42xQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "apps/frontend/node_modules/@radix-ui/react-popover": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.21.tgz", + "integrity": "sha512-+AJ7P5oRcFqFtv7MaCD8fOuK3l5qQBAMdWEOnUOYd3JfHOeIUe7TnHHSSP15CtWwH4KrotyDCTzodOIoONh8vw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.4", + "@radix-ui/react-context": "1.2.1", + "@radix-ui/react-dismissable-layer": "1.1.17", + "@radix-ui/react-focus-guards": "1.1.5", + "@radix-ui/react-focus-scope": "1.1.14", + "@radix-ui/react-id": "1.1.3", + "@radix-ui/react-popper": "1.3.5", + "@radix-ui/react-portal": "1.1.15", + "@radix-ui/react-presence": "1.1.9", + "@radix-ui/react-primitive": "2.1.8", + "@radix-ui/react-slot": "1.3.1", + "@radix-ui/react-use-controllable-state": "1.2.5", + "@radix-ui/react-use-layout-effect": "1.1.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "apps/frontend/node_modules/@radix-ui/react-popper": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.5.tgz", + "integrity": "sha512-6hLng2Rs45IZcvZ31BNvMeaFEMMEbtsog4xPcb8LZrGfyoV9fdAXqB9UKhLpTHtL0+E2DhSr6VW54Tehd6ksAQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.13", + "@radix-ui/react-compose-refs": "1.1.4", + "@radix-ui/react-context": "1.2.1", + "@radix-ui/react-primitive": "2.1.8", + "@radix-ui/react-use-callback-ref": "1.1.3", + "@radix-ui/react-use-layout-effect": "1.1.3", + "@radix-ui/react-use-rect": "1.1.3", + "@radix-ui/react-use-size": "1.1.3", + "@radix-ui/rect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "apps/frontend/node_modules/@radix-ui/react-portal": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.15.tgz", + "integrity": "sha512-kAfBVJUKNNKZuyGQXXG6rKolAV2KAmxxVkPXJgoq9dEFTl39286RufHQFNTL8rzha4vP8159BJ6hMGpB+bqv7A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.8", + "@radix-ui/react-use-layout-effect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "apps/frontend/node_modules/@radix-ui/react-presence": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.9.tgz", + "integrity": "sha512-LTi1v05bprIb8/GSY/GWusI0jfsYjQ3CD3Nin8o7jVxnpHzVQfzjOQJoJTQkE9bdmOnsS7SFdhkXiBv8PrYnxw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "apps/frontend/node_modules/@radix-ui/react-primitive": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.8.tgz", + "integrity": "sha512-DOlK1BdcIeYYUcFkSYFka4v1h95XTov93b0jCgW1EEiZuIhdwHY2NlE1teLIh+p0uBsuZI5A+voay+iVWpprfA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "apps/frontend/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.3.tgz", + "integrity": "sha512-AUS7HoBBAncIsGMLNG+CcpLuJ+JIBbZzmyM8Qdb1eIThX0AlhSSC6wn40xfBlPE+ypx/vSSiRWnklUAjy3U3UA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "apps/frontend/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.5.tgz", + "integrity": "sha512-UB1dXpxvHjR48poyKdKdTm7jT0kp3elkUKdKQiOkirlbYumqXinSJtrjDsr9maXNPvL12bKI4CDSmydms/9Aeg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.4", + "@radix-ui/react-use-layout-effect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "apps/frontend/node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.4.tgz", + "integrity": "sha512-XYcfa6wlXDCwQtePuEiPmXLSAhGL4DWtedSyRgGbG3y10mw+OnrLp6SyeY1gJFMiYF0Dx0nMAX9InylKbLEFQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "apps/frontend/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.3.tgz", + "integrity": "sha512-rDiah9wvtqihWtWz02XreeRKIxt2EJF8y5D9rtY9l5A2zxePAtcPiOMpDugNRw5bFHz+1/8viVoc7ZVKiJknCw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "apps/frontend/node_modules/@radix-ui/react-use-rect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.3.tgz", + "integrity": "sha512-W0GSYZFKEfi6raMiMEfJSngvVbFDIyxtW5JuVg5NoQBY59l0dVQVGLbhog/tOdwq0qtZ+TXuX1ikSNan4/IZXA==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "apps/frontend/node_modules/@radix-ui/react-use-size": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.3.tgz", + "integrity": "sha512-jJq6tQQLvO/z4uWbztjwPV+1a/+H4rCaWypasLB/ac8DEdd6+p7dIm7o3F6P2KZPGkPMqD4s5424EdAdoU+GLA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "apps/frontend/node_modules/@radix-ui/rect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.3.tgz", + "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", + "license": "MIT" + }, "apps/shared": { "name": "@nao/shared", "dependencies": { @@ -1603,7 +1998,6 @@ "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", @@ -1885,7 +2279,6 @@ "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", @@ -2018,7 +2411,6 @@ "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" } @@ -2026,8 +2418,7 @@ "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==", - "peer": true + "integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==" }, "node_modules/@boxlite-ai/boxlite": { "version": "0.3.0", @@ -2087,12 +2478,6 @@ "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", @@ -2297,7 +2682,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -2346,11 +2730,16 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } }, + "node_modules/@date-fns/tz": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.5.0.tgz", + "integrity": "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==", + "license": "MIT" + }, "node_modules/@dnd-kit/accessibility": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", @@ -2368,7 +2757,6 @@ "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", @@ -2518,7 +2906,6 @@ "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" @@ -2529,7 +2916,6 @@ "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" } @@ -2567,6 +2953,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=12" } @@ -2583,6 +2970,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=12" } @@ -2599,6 +2987,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=12" } @@ -2615,6 +3004,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=12" } @@ -2631,6 +3021,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=12" } @@ -2647,6 +3038,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=12" } @@ -2663,6 +3055,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=12" } @@ -2679,6 +3072,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -2695,6 +3089,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -2711,6 +3106,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -2727,6 +3123,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -2743,6 +3140,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -2759,6 +3157,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -2775,6 +3174,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -2791,6 +3191,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -2807,6 +3208,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -2823,6 +3225,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=12" } @@ -2839,6 +3242,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=12" } @@ -2855,6 +3259,7 @@ "os": [ "sunos" ], + "peer": true, "engines": { "node": ">=12" } @@ -2871,6 +3276,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=12" } @@ -2887,6 +3293,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=12" } @@ -2903,6 +3310,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=12" } @@ -2969,6 +3377,7 @@ "os": [ "aix" ], + "peer": true, "engines": { "node": ">=18" } @@ -2985,6 +3394,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -3001,6 +3411,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -3017,6 +3428,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -3033,6 +3445,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -3049,6 +3462,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -3065,6 +3479,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -3081,6 +3496,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -3097,6 +3513,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -3113,6 +3530,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -3129,6 +3547,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -3145,6 +3564,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -3161,6 +3581,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -3177,6 +3598,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -3193,6 +3615,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -3209,6 +3632,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -3241,6 +3665,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -3257,6 +3682,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -3273,6 +3699,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -3289,6 +3716,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -3305,6 +3733,7 @@ "os": [ "openharmony" ], + "peer": true, "engines": { "node": ">=18" } @@ -3321,6 +3750,7 @@ "os": [ "sunos" ], + "peer": true, "engines": { "node": ">=18" } @@ -3337,6 +3767,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -3353,6 +3784,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -3369,6 +3801,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -3909,7 +4342,6 @@ "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" @@ -4313,7 +4745,6 @@ "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", @@ -4466,7 +4897,6 @@ "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" } @@ -4500,7 +4930,6 @@ "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" }, @@ -4535,6 +4964,7 @@ "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", @@ -4554,6 +4984,7 @@ "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" }, @@ -4566,6 +4997,7 @@ "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" }, @@ -4581,6 +5013,7 @@ "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" @@ -4597,6 +5030,7 @@ "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", @@ -4617,6 +5051,7 @@ "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", @@ -4635,6 +5070,7 @@ "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" @@ -4817,7 +5253,6 @@ "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", @@ -6404,13 +6839,35 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", - "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.1.tgz", + "integrity": "sha512-Bu/aAQHFFh6/QAvXAeUMurJ9fbW0JUIqlojU/yBXZ7cAVqy75Y7JYYyuCr9zLNF0p4WWoJYV54CTUIf4l7FzTw==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot/node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.4.tgz", + "integrity": "sha512-pWJo6lQAfR6uy1n7ii7PaCc9dLPwTXDYbQpORZU5B548Aqvl2pP1SM1vJGKyxIFqZMHRopRO4CQYX2iXAIB5jA==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -6849,7 +7306,6 @@ "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" }, @@ -9344,7 +9800,6 @@ "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" }, @@ -9361,7 +9816,6 @@ "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", @@ -9450,7 +9904,6 @@ "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", @@ -9659,7 +10112,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -9707,7 +10159,6 @@ "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" @@ -9985,7 +10436,6 @@ "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" @@ -10120,7 +10570,6 @@ "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" @@ -10152,7 +10601,6 @@ "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", @@ -10183,7 +10631,6 @@ "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", @@ -10277,7 +10724,6 @@ "https://trpc.io/sponsor" ], "license": "MIT", - "peer": true, "bin": { "intent": "bin/intent.js" }, @@ -10294,7 +10740,6 @@ "https://trpc.io/sponsor" ], "license": "MIT", - "peer": true, "bin": { "intent": "bin/intent.js" }, @@ -10833,7 +11278,6 @@ "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" } @@ -10843,7 +11287,6 @@ "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" } @@ -10947,7 +11390,6 @@ "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", @@ -11110,7 +11552,6 @@ "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", @@ -11657,7 +12098,6 @@ "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" }, @@ -11689,7 +12129,6 @@ "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", @@ -12198,7 +12637,6 @@ "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", @@ -12304,7 +12742,6 @@ "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", @@ -12503,7 +12940,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -12570,7 +13006,6 @@ "integrity": "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*" } @@ -12860,7 +13295,6 @@ "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", @@ -13350,15 +13784,13 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "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" } @@ -13768,7 +14200,6 @@ "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" } @@ -14224,8 +14655,7 @@ "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", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/diff": { "version": "8.0.4", @@ -14305,6 +14735,7 @@ "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" } @@ -14367,7 +14798,6 @@ "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", @@ -14764,7 +15194,6 @@ "devOptional": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -14812,6 +15241,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -14870,7 +15300,6 @@ "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", @@ -16928,7 +17357,6 @@ "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" } @@ -17824,6 +18252,7 @@ "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" @@ -17849,7 +18278,6 @@ "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" } @@ -17859,7 +18287,6 @@ "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" } @@ -17898,7 +18325,6 @@ "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", @@ -18031,6 +18457,7 @@ "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", @@ -18245,7 +18672,6 @@ "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" } @@ -18304,6 +18730,7 @@ "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" }, @@ -20129,6 +20556,7 @@ "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" @@ -20139,6 +20567,7 @@ "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" }, @@ -20199,7 +20628,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": "^20.0.0 || >=22.0.0" } @@ -21207,7 +21635,6 @@ "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", @@ -21483,7 +21910,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -21917,7 +22343,6 @@ "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" } @@ -21947,7 +22372,6 @@ "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", @@ -21996,7 +22420,6 @@ "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", @@ -22349,7 +22772,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -22375,12 +22797,37 @@ } } }, + "node_modules/react-day-picker": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-10.0.1.tgz", + "integrity": "sha512-eNh6BlwcYInWaJtRv18mXQ06Ys/H6rdTZAnTaSdOYJuTpwP1JMCHNd1FDRadA+gbeinq+psdULN5Xnowy9mV8w==", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "^1.4.1", + "date-fns": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "@types/react": ">=16.8.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-dom": { "version": "19.2.5", "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" }, @@ -23367,7 +23814,6 @@ "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.2.tgz", "integrity": "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" } @@ -23710,7 +24156,6 @@ "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", @@ -25226,7 +25671,6 @@ "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" @@ -25820,7 +26264,6 @@ "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" @@ -26046,7 +26489,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -26280,7 +26722,6 @@ "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", @@ -26842,7 +27283,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -27601,6 +28041,7 @@ "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" }, @@ -27722,7 +28163,6 @@ "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" }