diff --git a/apps/desktop/layer/renderer/package.json b/apps/desktop/layer/renderer/package.json index 69662a8fa..7e8f8eb3c 100644 --- a/apps/desktop/layer/renderer/package.json +++ b/apps/desktop/layer/renderer/package.json @@ -53,6 +53,7 @@ "@yornaath/batshit": "0.11.1", "ai": "5.0.76", "camelcase-keys": "10.0.0", + "chrono-node": "2.9.0", "class-variance-authority": "0.7.1", "clsx": "2.1.1", "cmdk": "1.1.1", diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/dateMentionParsers.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/dateMentionParsers.ts index 9401e1506..4a6e663a4 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/dateMentionParsers.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/dateMentionParsers.ts @@ -1,3 +1,4 @@ +import * as chrono from "chrono-node" import dayjs from "dayjs" import type { IFuseOptions } from "fuse.js" import Fuse from "fuse.js" @@ -129,6 +130,74 @@ const normalizeQuery = (query: string): string => { return trimmed.startsWith("@") ? trimmed.slice(1) : trimmed } +/** + * Get the appropriate chrono parser based on language + */ +const getChronoParser = (language: string) => { + if (language === "zh-CN") { + return chrono.zh.hans + } + if (language === "zh-TW") { + return chrono.zh.hant + } + if (language === "ja") { + return chrono.ja + } + return chrono.en +} + +/** + * Parse natural language dates using chrono-node with language-specific parser + * Returns a DateRange if the input can be parsed, otherwise null + */ +const parseNaturalLanguageDate = (query: string, language: string): DateRange | null => { + if (!query.trim()) return null + + try { + // Get language-specific parser + const parser = getChronoParser(language) + + // Try to parse the date using chrono with language-specific parser first + let parsed = parser.parse(query) + + // If failed and not already using English parser, try English as fallback + if ((!parsed || parsed.length === 0) && parser !== chrono.en) { + parsed = chrono.en.parse(query) + } + + if (!parsed || parsed.length === 0) return null + + const result = parsed[0] + if (!result) return null + + // Get the start date + const startDate = result.start.date() + const start = dayjs(startDate).startOf("day") + + // Check if there's an end date (for ranges) + let end: dayjs.Dayjs + if (result.end) { + const endDate = result.end.date() + end = dayjs(endDate).startOf("day") + } else { + // If no end date, use the start date as a single day + end = start + } + + // Validate the dates are valid + if (!start.isValid() || !end.isValid()) return null + + // Ensure start is before or equal to end + if (start.isAfter(end)) { + return { start: end, end: start } + } + + return { start, end } + } catch { + return null + } +} + export const createDateMentionBuilder = (context: DateMentionBuilderContext) => { const candidates = buildRelativeCandidates(context) const fuse = new Fuse(candidates, FUSE_OPTIONS) @@ -136,10 +205,27 @@ export const createDateMentionBuilder = (context: DateMentionBuilderContext) => return (query: string): MentionData[] => { const normalized = normalizeQuery(query) const today = dayjs().startOf("day") - const bucket = normalized ? fuse.search(normalized).map((result) => result.item) : candidates - const mentions: MentionData[] = [] + // Try to parse as natural language date first + if (normalized) { + const naturalDateRange = parseNaturalLanguageDate(normalized, context.language) + if (naturalDateRange) { + // Successfully parsed a natural language date + const chronoMention = createDateMentionData({ + range: naturalDateRange, + translate: context.t, + locale: context.language, + withRangeKey: RANGE_WITH_LABEL_KEY, + displayName: formatLocalizedRange(naturalDateRange, context.language), + }) + mentions.push(chronoMention) + } + } + + // Add predefined relative date suggestions + const bucket = normalized ? fuse.search(normalized).map((result) => result.item) : candidates + bucket.forEach((candidate) => { const range = candidate.definition.range(today) if (!range) return diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7bb6837f0..2a4073090 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -563,6 +563,9 @@ importers: camelcase-keys: specifier: 10.0.0 version: 10.0.0 + chrono-node: + specifier: 2.9.0 + version: 2.9.0 class-variance-authority: specifier: 0.7.1 version: 0.7.1 @@ -773,7 +776,7 @@ importers: version: link:../../../../packages/internal/utils '@folo-services/ai-tools': specifier: 0.2.43 - version: 0.2.43(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(better-sqlite3@12.4.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(hono@4.9.8)(kysely@0.28.8)(pg@8.16.3) + version: 0.2.43(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(better-sqlite3@12.4.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(kysely@0.28.8)(pg@8.16.3) '@types/node': specifier: 24.8.1 version: 24.8.1 @@ -8415,6 +8418,10 @@ packages: chromium-pickle-js@0.2.0: resolution: {integrity: sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==} + chrono-node@2.9.0: + resolution: {integrity: sha512-glI4YY2Jy6JII5l3d5FN6rcrIbKSQqKPhWsIRYPK2IK8Mm4Q1ZZFdYIaDqglUNf7gNwG+kWIzTn0omzzE0VkvQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + ci-info@2.0.0: resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} @@ -20685,10 +20692,10 @@ snapshots: - svelte - vue - '@folo-services/ai-tools@0.2.43(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(better-sqlite3@12.4.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(hono@4.9.8)(kysely@0.28.8)(pg@8.16.3)': + '@folo-services/ai-tools@0.2.43(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(better-sqlite3@12.4.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(kysely@0.28.8)(pg@8.16.3)': dependencies: '@ai-sdk/openai': 2.0.52(zod@4.1.11) - '@folo-services/drizzle': 0.1.29(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(better-sqlite3@12.4.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(hono@4.9.8)(kysely@0.28.8) + '@folo-services/drizzle': 0.1.29(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(better-sqlite3@12.4.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(kysely@0.28.8) ai: 5.0.48(zod@4.1.11) drizzle-orm: 0.44.6(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(better-sqlite3@12.4.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(kysely@0.28.8)(pg@8.16.3) zod: 4.1.11 @@ -20770,6 +20777,47 @@ snapshots: - sql.js - sqlite3 + '@folo-services/drizzle@0.1.29(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(better-sqlite3@12.4.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(kysely@0.28.8)': + dependencies: + '@folo-services/exceptions': 0.1.16 + '@hono/zod-openapi': 1.1.2(hono@4.9.8)(zod@4.1.11) + drizzle-orm: 0.44.6(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(better-sqlite3@12.4.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(kysely@0.28.8)(pg@8.16.3) + drizzle-zod: 0.8.3(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(better-sqlite3@12.4.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0))(react@19.2.0))(kysely@0.28.8)(pg@8.16.3))(zod@4.1.11) + nanoid: 5.1.6 + pg: 8.16.3 + zod: 4.1.11 + transitivePeerDependencies: + - '@aws-sdk/client-rds-data' + - '@cloudflare/workers-types' + - '@electric-sql/pglite' + - '@libsql/client' + - '@libsql/client-wasm' + - '@neondatabase/serverless' + - '@op-engineering/op-sqlite' + - '@opentelemetry/api' + - '@planetscale/database' + - '@prisma/client' + - '@tidbcloud/serverless' + - '@types/better-sqlite3' + - '@types/pg' + - '@types/sql.js' + - '@upstash/redis' + - '@vercel/postgres' + - '@xata.io/client' + - better-sqlite3 + - bun-types + - expo-sqlite + - gel + - hono + - knex + - kysely + - mysql2 + - pg-native + - postgres + - prisma + - sql.js + - sqlite3 + '@folo-services/drizzle@0.1.29(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(better-sqlite3@12.4.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(utf-8-validate@6.0.5))(react@19.2.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(utf-8-validate@6.0.5))(react@19.2.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(utf-8-validate@6.0.5))(react@19.2.0))(hono@4.9.8)(kysely@0.28.8)': dependencies: '@folo-services/exceptions': 0.1.16 @@ -23256,7 +23304,7 @@ snapshots: debug: 2.6.9 invariant: 2.2.4 metro: 0.82.4(bufferutil@4.0.9)(utf-8-validate@6.0.5) - metro-config: 0.82.4(bufferutil@4.0.9) + metro-config: 0.82.4(bufferutil@4.0.9)(utf-8-validate@6.0.5) metro-core: 0.82.4 semver: 7.7.3 transitivePeerDependencies: @@ -26171,6 +26219,8 @@ snapshots: chromium-pickle-js@0.2.0: {} + chrono-node@2.9.0: {} + ci-info@2.0.0: {} ci-info@3.9.0: {} @@ -31200,6 +31250,22 @@ snapshots: - supports-color - utf-8-validate + metro-config@0.82.4(bufferutil@4.0.9)(utf-8-validate@6.0.5): + dependencies: + connect: 3.7.0 + cosmiconfig: 5.2.1 + flow-enums-runtime: 0.0.6 + jest-validate: 29.7.0 + metro: 0.82.4(bufferutil@4.0.9)(utf-8-validate@6.0.5) + metro-cache: 0.82.4 + metro-core: 0.82.4 + metro-runtime: 0.82.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + optional: true + metro-core@0.82.4: dependencies: flow-enums-runtime: 0.0.6 @@ -31385,7 +31451,7 @@ snapshots: metro-babel-transformer: 0.82.4 metro-cache: 0.82.4 metro-cache-key: 0.82.4 - metro-config: 0.82.4(bufferutil@4.0.9) + metro-config: 0.82.4(bufferutil@4.0.9)(utf-8-validate@6.0.5) metro-core: 0.82.4 metro-file-map: 0.82.4 metro-resolver: 0.82.4