@agent-native/core 0.101.10 → 0.101.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/client.ts +23 -1
- package/corpus/core/src/a2a/handlers.ts +65 -1
- package/corpus/core/src/a2a/server.ts +124 -0
- package/corpus/core/src/a2a/task-store.ts +253 -0
- package/corpus/core/src/a2a/types.ts +26 -0
- package/corpus/core/src/agent/production-agent.ts +5 -2
- package/corpus/core/src/coding-tools/run-code.ts +65 -12
- package/corpus/core/src/extensions/actions.ts +141 -7
- package/corpus/core/src/mcp/builtin-tools.ts +25 -1
- package/corpus/core/src/scripts/call-agent.ts +22 -3
- package/corpus/core/src/server/agent-chat-plugin.ts +86 -1
- package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +59 -0
- package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +5 -2
- package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +13 -4
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +63 -1
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +30 -0
- package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +23 -0
- package/corpus/templates/content/app/components/editor/MathRenderer.tsx +43 -0
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +260 -4
- package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +78 -0
- package/corpus/templates/content/app/global.css +53 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +13 -0
- package/corpus/templates/content/app/i18n-data.ts +163 -0
- package/corpus/templates/content/app/root.tsx +2 -0
- package/corpus/templates/content/changelog/2026-07-14-latex-equations-render-in-documents-public-pages-and-exports.md +6 -0
- package/corpus/templates/content/package.json +1 -0
- package/corpus/templates/content/shared/document-export.ts +296 -25
- package/corpus/templates/content/shared/inline-math.ts +128 -0
- package/corpus/templates/content/shared/math-rendering.ts +50 -0
- package/corpus/templates/content/shared/nfm.ts +17 -14
- package/corpus/templates/content/vite.config.ts +0 -1
- package/corpus/templates/mail/.agents/skills/email-drafts/SKILL.md +17 -0
- package/corpus/templates/mail/AGENTS.md +21 -19
- package/corpus/templates/mail/actions/create-attachment-upload.ts +49 -0
- package/corpus/templates/mail/changelog/2026-07-14-connected-agents-can-now-securely-upload-local-files-for-mai.md +6 -0
- package/corpus/templates/mail/server/handlers/media.ts +84 -61
- package/corpus/templates/mail/server/lib/attachment-upload-ticket.ts +234 -0
- package/corpus/templates/mail/server/lib/mail-connector-catalog.ts +8 -4
- package/corpus/templates/mail/server/lib/media-upload.ts +114 -0
- package/corpus/templates/mail/server/plugins/auth.ts +9 -1
- package/corpus/templates/mail/server/routes/api/media/attachment-upload/[uploadId].put.ts +1 -0
- package/dist/a2a/client.d.ts +5 -1
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +18 -1
- package/dist/a2a/client.js.map +1 -1
- package/dist/a2a/handlers.d.ts.map +1 -1
- package/dist/a2a/handlers.js +57 -8
- package/dist/a2a/handlers.js.map +1 -1
- package/dist/a2a/server.d.ts.map +1 -1
- package/dist/a2a/server.js +87 -1
- package/dist/a2a/server.js.map +1 -1
- package/dist/a2a/task-store.d.ts +27 -0
- package/dist/a2a/task-store.d.ts.map +1 -1
- package/dist/a2a/task-store.js +216 -0
- package/dist/a2a/task-store.js.map +1 -1
- package/dist/a2a/types.d.ts +24 -0
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/a2a/types.js.map +1 -1
- package/dist/agent/production-agent.d.ts +1 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +5 -2
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/coding-tools/run-code.js +64 -11
- package/dist/coding-tools/run-code.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/extensions/actions.d.ts.map +1 -1
- package/dist/extensions/actions.js +129 -7
- package/dist/extensions/actions.js.map +1 -1
- package/dist/mcp/builtin-tools.d.ts.map +1 -1
- package/dist/mcp/builtin-tools.js +22 -3
- package/dist/mcp/builtin-tools.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +5 -5
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/scripts/call-agent.d.ts +1 -1
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +19 -1
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +71 -2
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
|
@@ -16,23 +16,32 @@ export const INITIAL_TOOL_NAMES = [
|
|
|
16
16
|
"list-analyses",
|
|
17
17
|
"get-analysis",
|
|
18
18
|
"save-analysis",
|
|
19
|
+
// Dashboard/extension INSPECTION stays on the initial surface so a
|
|
20
|
+
// template-clone request can resolve and inspect the source on the first
|
|
21
|
+
// turn. The MUTATING writers (update-dashboard, mutate-dashboard,
|
|
22
|
+
// create-extension, update-extension) are intentionally left off: the
|
|
23
|
+
// dashboard-construction final-response guard retries with
|
|
24
|
+
// `expandToolSurface: true` (see server/plugins/agent-chat.ts), which opens
|
|
25
|
+
// the full run registry exactly when a save is needed, and tool-search can
|
|
26
|
+
// surface them otherwise. This keeps the first-request surface under the
|
|
27
|
+
// 40-tool ceiling enforced by scripts/guard-agent-chat-context.ts.
|
|
19
28
|
"get-sql-dashboard",
|
|
20
|
-
"
|
|
29
|
+
"list-sql-dashboards",
|
|
30
|
+
"list-dashboard-templates",
|
|
31
|
+
"list-extensions",
|
|
32
|
+
"get-extension",
|
|
21
33
|
"generate-chart",
|
|
22
34
|
"query-agent-native-analytics",
|
|
23
35
|
"bigquery",
|
|
24
36
|
"search-bigquery-schema",
|
|
25
|
-
"bigquery-table-info",
|
|
26
37
|
"provider-api-catalog",
|
|
27
38
|
"provider-api-docs",
|
|
28
39
|
"provider-api-request",
|
|
29
40
|
"run-code",
|
|
30
41
|
"get-code-execution",
|
|
31
42
|
"provider-corpus-job",
|
|
32
|
-
"provider-corpus-jobs",
|
|
33
43
|
"query-staged-dataset",
|
|
34
44
|
"list-staged-datasets",
|
|
35
|
-
"delete-staged-dataset",
|
|
36
45
|
"account-deep-dive",
|
|
37
46
|
"hubspot-deals",
|
|
38
47
|
"hubspot-records",
|
|
@@ -57,6 +57,25 @@ export const CORPUS_SOURCE_ACTIONS = new Set([
|
|
|
57
57
|
|
|
58
58
|
export const CORPUS_REDUCTION_ACTIONS = new Set(["run-code"]);
|
|
59
59
|
|
|
60
|
+
// Inspecting or cloning an existing dashboard/extension template is
|
|
61
|
+
// construction progress, not a metric query. These do not satisfy
|
|
62
|
+
// hasDataQueryAttempt, but they should stop the guard from steering a
|
|
63
|
+
// template-clone turn into "connect a missing source". Deliberately limited
|
|
64
|
+
// to read/inspection actions: update-dashboard/mutate-dashboard/
|
|
65
|
+
// compose-dashboard/install-dashboard-template/create-extension/
|
|
66
|
+
// update-extension can all author brand-new SQL or extension content, so
|
|
67
|
+
// calling one of those alone is not proof the turn actually inspected a
|
|
68
|
+
// template rather than inventing it from scratch. If the tool run also
|
|
69
|
+
// includes one of these read actions, the bypass still applies even when an
|
|
70
|
+
// authoring/save action ran alongside it.
|
|
71
|
+
export const DASHBOARD_CONSTRUCTION_ACTIONS = new Set([
|
|
72
|
+
"get-sql-dashboard",
|
|
73
|
+
"list-sql-dashboards",
|
|
74
|
+
"list-dashboard-templates",
|
|
75
|
+
"list-extensions",
|
|
76
|
+
"get-extension",
|
|
77
|
+
]);
|
|
78
|
+
|
|
60
79
|
const RUN_CODE_BRIDGE_TOOLS_USED = /^bridgeToolsUsed:\s*(.+)$/im;
|
|
61
80
|
|
|
62
81
|
const MCP_DATA_SOURCE_TOKENS = [
|
|
@@ -97,6 +116,41 @@ function isDataQueryActionName(name: string): boolean {
|
|
|
97
116
|
return DATA_QUERY_ACTIONS.has(normalizeActionToolName(name));
|
|
98
117
|
}
|
|
99
118
|
|
|
119
|
+
function isDashboardConstructionActionName(name: string): boolean {
|
|
120
|
+
return DASHBOARD_CONSTRUCTION_ACTIONS.has(normalizeActionToolName(name));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// "Build/clone/template" language targeting a dashboard/extension/panel is
|
|
124
|
+
// dashboard construction, distinct from an analytics-result question. Turns
|
|
125
|
+
// like this may inspect and clone a template without running a metric query.
|
|
126
|
+
const DASHBOARD_CONSTRUCTION_INTENT_TERMS =
|
|
127
|
+
/\b(build|create|make|clone|copy|duplicate|adapt|template|based (?:off|on)|using .{1,80}? as a template)\b/i;
|
|
128
|
+
|
|
129
|
+
const DASHBOARD_CONSTRUCTION_TARGET_TERMS =
|
|
130
|
+
/\b(dashboard|extension|panel|widget)\b/i;
|
|
131
|
+
|
|
132
|
+
export function looksLikeDashboardConstructionRequest(text: string): boolean {
|
|
133
|
+
const requestText = stripInjectedAnalyticsGuardContext(text);
|
|
134
|
+
const lower = requestText.toLowerCase();
|
|
135
|
+
if (!lower) return false;
|
|
136
|
+
const wantsBuild = DASHBOARD_CONSTRUCTION_INTENT_TERMS.test(lower);
|
|
137
|
+
const targetsDashboard =
|
|
138
|
+
DASHBOARD_CONSTRUCTION_TARGET_TERMS.test(lower) ||
|
|
139
|
+
lower.includes(REAL_DATA_REQUIRED_MARKER.toLowerCase());
|
|
140
|
+
return wantsBuild && targetsDashboard;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function hasDashboardConstructionAttempt(
|
|
144
|
+
toolResults:
|
|
145
|
+
| Array<{ name?: string; isError?: boolean; content?: string }>
|
|
146
|
+
| undefined,
|
|
147
|
+
): boolean {
|
|
148
|
+
return (toolResults ?? []).some((result) => {
|
|
149
|
+
if (result.isError) return false;
|
|
150
|
+
return isDashboardConstructionActionName(String(result.name ?? ""));
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
100
154
|
function isCorpusSourceActionName(name: string): boolean {
|
|
101
155
|
return CORPUS_SOURCE_ACTIONS.has(normalizeActionToolName(name));
|
|
102
156
|
}
|
|
@@ -253,7 +307,15 @@ export function looksLikeAnalyticsDataRequest(text: string): boolean {
|
|
|
253
307
|
}
|
|
254
308
|
|
|
255
309
|
const UNSUPPORTED_RESULT_CLAIM =
|
|
256
|
-
/(?:\b\d[\d,.]*(?:\.\d+)?\s*(?:%|percent|users?|customers?|accounts?|sessions?|events?|deals?|tickets?|issues?|calls?|messages?|signups?|pageviews?)\b|\$\s*\d|\b(?:data|query|results?)\s+(?:shows?|showed|indicates?|returned|found)\b|\b(?:i found|the top|the bottom|highest|lowest|increased|decreased|grew|declined|converted|churned|retained|averaged|total(?:ed)?|count(?:ed)?)\b)/i;
|
|
310
|
+
/(?:\b\d[\d,.]*(?:\.\d+)?\s*(?:%|percent|users?|customers?|accounts?|sessions?|events?|deals?|tickets?|issues?|calls?|messages?|signups?|pageviews?)\b|\$\s*\d|\b(?:zero|no|none)\s+(?:users?|customers?|accounts?|sessions?|events?|deals?|tickets?|issues?|calls?|messages?|signups?|pageviews?)\b|\b(?:data|query|results?)\s+(?:shows?|showed|indicates?|returned|found)\b|\b(?:i found|the top|the bottom|highest|lowest|increased|decreased|grew|declined|converted|churned|retained|averaged|total(?:ed)?|count(?:ed)?)\b)/i;
|
|
311
|
+
|
|
312
|
+
// Reuse the same broad unsupported-result-claim vocabulary that gates
|
|
313
|
+
// isSafeNoDataAnalyticsResponse so a dashboard-construction turn cannot
|
|
314
|
+
// bypass the no-query fallback just by avoiding the narrower set of units a
|
|
315
|
+
// dashboard-specific regex would otherwise miss (e.g. "signups", "accounts").
|
|
316
|
+
export function draftClaimsAnalyticsMetrics(text: string): boolean {
|
|
317
|
+
return UNSUPPORTED_RESULT_CLAIM.test(String(text ?? "").trim());
|
|
318
|
+
}
|
|
257
319
|
|
|
258
320
|
export const GENERIC_NO_DATA_FALLBACK_MESSAGE =
|
|
259
321
|
"I can't provide a grounded analytics result yet because no real data-source query ran successfully. Tell me which source to use or connect the missing source, and I'll run it before giving numbers or source-record conclusions.";
|
|
@@ -14,7 +14,9 @@ import {
|
|
|
14
14
|
import { ANALYTICS_CONNECTOR_CATALOG } from "../lib/analytics-connector-catalog";
|
|
15
15
|
import { credentialProviderConfigs } from "../lib/credential-keys";
|
|
16
16
|
import {
|
|
17
|
+
draftClaimsAnalyticsMetrics,
|
|
17
18
|
failedDataQueryAttemptMessage,
|
|
19
|
+
hasDashboardConstructionAttempt,
|
|
18
20
|
hasExplicitPartialDisclosure,
|
|
19
21
|
hasFailedCorpusWorkflowEvidence,
|
|
20
22
|
hasDataQueryAttempt,
|
|
@@ -23,6 +25,7 @@ import {
|
|
|
23
25
|
isSafeNoDataAnalyticsResponse,
|
|
24
26
|
hasOverstatedCoverageConfidenceClaim,
|
|
25
27
|
looksLikeCoverageSensitiveAnalyticsRequest,
|
|
28
|
+
looksLikeDashboardConstructionRequest,
|
|
26
29
|
looksLikeStrongCoverageClaim,
|
|
27
30
|
looksLikeAnalyticsDataRequest,
|
|
28
31
|
needsCorpusWorkflowForCoverageSensitiveRequest,
|
|
@@ -522,6 +525,33 @@ export function realDataFinalGuard(
|
|
|
522
525
|
"I can't make a confident exhaustive analytics claim yet because part of the source evidence was aborted, truncated, or still paginated. I need to recover the missing coverage or state the answer as partial with the inspected sample size.",
|
|
523
526
|
};
|
|
524
527
|
}
|
|
528
|
+
// Dashboard construction/template-clone turns may inspect and clone an
|
|
529
|
+
// existing dashboard/extension without running a metric query, as long as
|
|
530
|
+
// the draft does not invent numbers. Check this before the generic
|
|
531
|
+
// "no data query ran" fallback so a template-based extension clone is not
|
|
532
|
+
// treated the same as an unanswerable analytics-result question.
|
|
533
|
+
if (
|
|
534
|
+
looksLikeDashboardConstructionRequest(userText) &&
|
|
535
|
+
!draftClaimsAnalyticsMetrics(context.text)
|
|
536
|
+
) {
|
|
537
|
+
if (
|
|
538
|
+
hasDashboardConstructionAttempt(context.toolResults) ||
|
|
539
|
+
isSafeNoDataAnalyticsResponse(context.text)
|
|
540
|
+
) {
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
retryMessage:
|
|
545
|
+
'This is a dashboard construction/template-clone request. Resolve the named template\'s id (use `list-sql-dashboards` if you only have a title) and call `get-sql-dashboard` with `includeConfig: true` first. If its panels are `chartType: "extension"`, use `get-extension` then `create-extension` to clone/adapt it, then `update-dashboard` to save the new dashboard. Do not invent SQL panels for an extension-backed template. Ask one clarifying filter question if needed. Only run a data-source query before presenting numbers or authoring invented SQL.',
|
|
546
|
+
fallbackMessage:
|
|
547
|
+
"I need to inspect the template dashboard (and its extension, if it uses one) before creating the new one. Tell me the template dashboard name, or confirm the org/account filter, and I'll clone it without inventing metrics.",
|
|
548
|
+
// list-sql-dashboards/list-dashboard-templates are on the initial
|
|
549
|
+
// surface, but expand anyway so a corrective retry can always reach
|
|
550
|
+
// the lookup/inspection tools this message asks for.
|
|
551
|
+
expandToolSurface: true,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
525
555
|
if (dataQueryAttempted) return null;
|
|
526
556
|
if (isSafeNoDataAnalyticsResponse(context.text)) {
|
|
527
557
|
if (firstPartySourceShouldBeTried) {
|
|
@@ -145,6 +145,29 @@ Always run this after any document modification to update the sidebar.
|
|
|
145
145
|
|
|
146
146
|
Documents use **markdown** for content. The editor renders markdown in real time.
|
|
147
147
|
|
|
148
|
+
### Math
|
|
149
|
+
|
|
150
|
+
Content renders LaTeX with KaTeX while preserving the source in NFM. Use the
|
|
151
|
+
canonical Content delimiters when an agent creates or edits math:
|
|
152
|
+
|
|
153
|
+
```md
|
|
154
|
+
Inline math: The relationship is $`E = mc^2`$ in this example.
|
|
155
|
+
|
|
156
|
+
Block math:
|
|
157
|
+
|
|
158
|
+
$$
|
|
159
|
+
\int_0^1 x^2 dx = \frac{1}{3}
|
|
160
|
+
$$
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- Inline math uses ``$`...`$``. Do not substitute ordinary `$...$`; dollar
|
|
164
|
+
amounts in prose are intentionally not interpreted as equations.
|
|
165
|
+
- Block equations use `$$` on their own lines before and after the LaTeX.
|
|
166
|
+
- Keep the raw LaTeX intact when editing around an equation. Invalid or
|
|
167
|
+
unsupported expressions remain visible as source so they can be repaired.
|
|
168
|
+
- Markdown exports preserve these delimiters. HTML and PDF-ready exports render
|
|
169
|
+
the equation.
|
|
170
|
+
|
|
148
171
|
## Parent-Child Hierarchy
|
|
149
172
|
|
|
150
173
|
Documents form a tree via `parent_id`:
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { renderMathToHtml } from "@shared/math-rendering";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
|
|
4
|
+
interface MathRendererProps {
|
|
5
|
+
latex: string;
|
|
6
|
+
displayMode: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function MathRenderer({ latex, displayMode }: MathRendererProps) {
|
|
10
|
+
const rendered = useMemo(
|
|
11
|
+
() => renderMathToHtml(latex, displayMode),
|
|
12
|
+
[displayMode, latex],
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
if (!rendered.ok) {
|
|
16
|
+
return (
|
|
17
|
+
<code
|
|
18
|
+
className={
|
|
19
|
+
displayMode
|
|
20
|
+
? "content-math-error content-math-error--block"
|
|
21
|
+
: "content-math-error content-math-error--inline"
|
|
22
|
+
}
|
|
23
|
+
title={rendered.error}
|
|
24
|
+
>
|
|
25
|
+
{latex || "Empty equation"}
|
|
26
|
+
</code>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return displayMode ? (
|
|
31
|
+
<span
|
|
32
|
+
className="content-math content-math--block"
|
|
33
|
+
contentEditable={false}
|
|
34
|
+
dangerouslySetInnerHTML={{ __html: rendered.html }}
|
|
35
|
+
/>
|
|
36
|
+
) : (
|
|
37
|
+
<span
|
|
38
|
+
className="content-math content-math--inline"
|
|
39
|
+
contentEditable={false}
|
|
40
|
+
dangerouslySetInnerHTML={{ __html: rendered.html }}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { useSendToAgentChat, useT } from "@agent-native/core/client";
|
|
2
2
|
import type { CreateInlineDatabaseResponse } from "@shared/api";
|
|
3
|
+
import { renderMathToHtml } from "@shared/math-rendering";
|
|
3
4
|
import { collapseExactRepeatedNfm, docToNfm } from "@shared/nfm";
|
|
4
5
|
import { serializeRegistryBlockToMdx } from "@shared/nfm-registry";
|
|
5
6
|
import {
|
|
7
|
+
IconCheck,
|
|
6
8
|
IconTypography,
|
|
7
9
|
IconH1,
|
|
8
10
|
IconH2,
|
|
@@ -23,6 +25,8 @@ import {
|
|
|
23
25
|
IconFileText,
|
|
24
26
|
IconDatabase,
|
|
25
27
|
IconVideo,
|
|
28
|
+
IconMathFunction,
|
|
29
|
+
IconSquareRoot2,
|
|
26
30
|
} from "@tabler/icons-react";
|
|
27
31
|
import { Editor } from "@tiptap/react";
|
|
28
32
|
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
|
@@ -30,6 +34,7 @@ import { useNavigate } from "react-router";
|
|
|
30
34
|
import { toast } from "sonner";
|
|
31
35
|
|
|
32
36
|
import { contentBlockRegistry } from "@/blocks/contentBlockRegistry";
|
|
37
|
+
import { Button } from "@/components/ui/button";
|
|
33
38
|
import {
|
|
34
39
|
Popover,
|
|
35
40
|
PopoverContent,
|
|
@@ -42,6 +47,7 @@ import { localContentComponents } from "@/local-components";
|
|
|
42
47
|
|
|
43
48
|
import { focusMostRecentEmptyToggleSummary } from "./extensions/NotionExtensions";
|
|
44
49
|
import { buildLocalComponentSlashItems } from "./localComponentSlashItems";
|
|
50
|
+
import { MathRenderer } from "./MathRenderer";
|
|
45
51
|
import { buildRegistrySlashItems } from "./registrySlashItems";
|
|
46
52
|
|
|
47
53
|
interface SlashCommandMenuProps {
|
|
@@ -64,6 +70,13 @@ interface EditorMenuPosition {
|
|
|
64
70
|
left: number;
|
|
65
71
|
}
|
|
66
72
|
|
|
73
|
+
interface EquationDraft {
|
|
74
|
+
displayMode: boolean;
|
|
75
|
+
insertionRange: { from: number; to: number };
|
|
76
|
+
slashRange: { from: number; to: number };
|
|
77
|
+
position: EditorMenuPosition;
|
|
78
|
+
}
|
|
79
|
+
|
|
67
80
|
interface CommandItem {
|
|
68
81
|
title: string;
|
|
69
82
|
description: string;
|
|
@@ -178,6 +191,47 @@ export function insertInlineDatabaseBlock(
|
|
|
178
191
|
: chain.insertContent(content).run();
|
|
179
192
|
}
|
|
180
193
|
|
|
194
|
+
export function equationNodeContent(latex: string, displayMode: boolean) {
|
|
195
|
+
return displayMode
|
|
196
|
+
? {
|
|
197
|
+
type: "notionBlockAtom",
|
|
198
|
+
attrs: { tagName: "equation", attrsJson: "{}", label: latex },
|
|
199
|
+
}
|
|
200
|
+
: {
|
|
201
|
+
type: "notionInlineAtom",
|
|
202
|
+
attrs: { tagName: "math", attrsJson: "{}", label: latex },
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function insertEquation(
|
|
207
|
+
editor: Editor,
|
|
208
|
+
latex: string,
|
|
209
|
+
displayMode: boolean,
|
|
210
|
+
range: { from: number; to: number },
|
|
211
|
+
) {
|
|
212
|
+
const content = equationNodeContent(latex, displayMode);
|
|
213
|
+
return editor
|
|
214
|
+
.chain()
|
|
215
|
+
.focus()
|
|
216
|
+
.insertContentAt(
|
|
217
|
+
range,
|
|
218
|
+
displayMode ? [content, { type: "paragraph" }] : content,
|
|
219
|
+
)
|
|
220
|
+
.run();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function getEquationInsertionRange(
|
|
224
|
+
editor: Editor,
|
|
225
|
+
slashRange: { from: number; to: number },
|
|
226
|
+
displayMode: boolean,
|
|
227
|
+
) {
|
|
228
|
+
if (!displayMode) return slashRange;
|
|
229
|
+
const resolved = editor.state.doc.resolve(slashRange.from);
|
|
230
|
+
return resolved.parent.isTextblock
|
|
231
|
+
? { from: resolved.before(), to: resolved.after() }
|
|
232
|
+
: slashRange;
|
|
233
|
+
}
|
|
234
|
+
|
|
181
235
|
const commands: CommandTemplate[] = [
|
|
182
236
|
{
|
|
183
237
|
titleKey: "editor.slash.text",
|
|
@@ -460,6 +514,16 @@ export function SlashCommandMenu({
|
|
|
460
514
|
);
|
|
461
515
|
const generateTextareaRef = useRef<HTMLTextAreaElement>(null);
|
|
462
516
|
|
|
517
|
+
const [equationDraft, setEquationDraft] = useState<EquationDraft | null>(
|
|
518
|
+
null,
|
|
519
|
+
);
|
|
520
|
+
const [equationLatex, setEquationLatex] = useState("");
|
|
521
|
+
const equationInputRef = useRef<HTMLTextAreaElement>(null);
|
|
522
|
+
const equationResult = useMemo(
|
|
523
|
+
() => renderMathToHtml(equationLatex, equationDraft?.displayMode ?? false),
|
|
524
|
+
[equationDraft?.displayMode, equationLatex],
|
|
525
|
+
);
|
|
526
|
+
|
|
463
527
|
const submitGeneratePrompt = useCallback(
|
|
464
528
|
(prompt: string) => {
|
|
465
529
|
const trimmed = prompt.trim();
|
|
@@ -684,6 +748,80 @@ export function SlashCommandMenu({
|
|
|
684
748
|
},
|
|
685
749
|
};
|
|
686
750
|
|
|
751
|
+
const openEquationComposer = useCallback(
|
|
752
|
+
(displayMode: boolean, slashRange: { from: number; to: number } | null) => {
|
|
753
|
+
const menuPosition = position ?? getSelectionMenuPosition();
|
|
754
|
+
if (!slashRange || !menuPosition) return false;
|
|
755
|
+
setEquationLatex("");
|
|
756
|
+
setEquationDraft({
|
|
757
|
+
displayMode,
|
|
758
|
+
slashRange,
|
|
759
|
+
insertionRange: getEquationInsertionRange(
|
|
760
|
+
editor,
|
|
761
|
+
slashRange,
|
|
762
|
+
displayMode,
|
|
763
|
+
),
|
|
764
|
+
position: menuPosition,
|
|
765
|
+
});
|
|
766
|
+
setTimeout(() => equationInputRef.current?.focus(), 0);
|
|
767
|
+
return true;
|
|
768
|
+
},
|
|
769
|
+
[editor, getSelectionMenuPosition, position],
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
const cancelEquation = useCallback(() => {
|
|
773
|
+
const draft = equationDraft;
|
|
774
|
+
setEquationDraft(null);
|
|
775
|
+
setEquationLatex("");
|
|
776
|
+
if (draft) {
|
|
777
|
+
editor.chain().focus().deleteRange(draft.slashRange).run();
|
|
778
|
+
}
|
|
779
|
+
}, [editor, equationDraft]);
|
|
780
|
+
|
|
781
|
+
const submitEquation = useCallback(() => {
|
|
782
|
+
if (!equationDraft || !equationResult.ok) return;
|
|
783
|
+
const latex = equationLatex.trim();
|
|
784
|
+
const { displayMode, insertionRange } = equationDraft;
|
|
785
|
+
setEquationDraft(null);
|
|
786
|
+
setEquationLatex("");
|
|
787
|
+
const inserted = insertEquation(editor, latex, displayMode, insertionRange);
|
|
788
|
+
if (!inserted) {
|
|
789
|
+
toast.error(t("editor.slash.equationInsertFailed"));
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
void onDraftCommitted?.();
|
|
793
|
+
}, [
|
|
794
|
+
editor,
|
|
795
|
+
equationDraft,
|
|
796
|
+
equationLatex,
|
|
797
|
+
equationResult.ok,
|
|
798
|
+
onDraftCommitted,
|
|
799
|
+
t,
|
|
800
|
+
]);
|
|
801
|
+
|
|
802
|
+
const equationCommands: CommandItem[] = isTurnInto
|
|
803
|
+
? []
|
|
804
|
+
: [
|
|
805
|
+
{
|
|
806
|
+
title: t("editor.slash.blockEquation"),
|
|
807
|
+
description: t("editor.slash.blockEquationDescription"),
|
|
808
|
+
searchText: "latex katex math formula",
|
|
809
|
+
icon: IconMathFunction,
|
|
810
|
+
preserveSlashRange: true,
|
|
811
|
+
action: (_editor, { slashRange }) =>
|
|
812
|
+
openEquationComposer(true, slashRange),
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
title: t("editor.slash.inlineEquation"),
|
|
816
|
+
description: t("editor.slash.inlineEquationDescription"),
|
|
817
|
+
searchText: "latex katex math formula",
|
|
818
|
+
icon: IconSquareRoot2,
|
|
819
|
+
preserveSlashRange: true,
|
|
820
|
+
action: (_editor, { slashRange }) =>
|
|
821
|
+
openEquationComposer(false, slashRange),
|
|
822
|
+
},
|
|
823
|
+
];
|
|
824
|
+
|
|
687
825
|
// Registry-derived block items (the shared dev-doc / OpenAPI / structured
|
|
688
826
|
// library). Filtered to Notion-compatible specs when the document is linked to
|
|
689
827
|
// a Notion page. "Turn into" only converts the current text block, so these
|
|
@@ -713,9 +851,10 @@ export function SlashCommandMenu({
|
|
|
713
851
|
title: t(cmd.titleKey),
|
|
714
852
|
description: t(cmd.descriptionKey),
|
|
715
853
|
});
|
|
716
|
-
const blockCommands =
|
|
717
|
-
localizeCommand,
|
|
718
|
-
|
|
854
|
+
const blockCommands = [
|
|
855
|
+
...(isTurnInto ? turnIntoCommands : commands).map(localizeCommand),
|
|
856
|
+
...equationCommands,
|
|
857
|
+
];
|
|
719
858
|
const pageCommands = isTurnInto ? [] : [pageCommand, databaseCommand];
|
|
720
859
|
const mediaCommands = isTurnInto
|
|
721
860
|
? []
|
|
@@ -1042,7 +1181,7 @@ export function SlashCommandMenu({
|
|
|
1042
1181
|
align="start"
|
|
1043
1182
|
side="bottom"
|
|
1044
1183
|
className="w-[calc(100vw-2rem)] max-w-80 rounded-xl p-0"
|
|
1045
|
-
onOpenAutoFocus={(e) => {
|
|
1184
|
+
onOpenAutoFocus={(e: Event) => {
|
|
1046
1185
|
e.preventDefault();
|
|
1047
1186
|
generateTextareaRef.current?.focus();
|
|
1048
1187
|
}}
|
|
@@ -1086,6 +1225,123 @@ export function SlashCommandMenu({
|
|
|
1086
1225
|
</PopoverContent>
|
|
1087
1226
|
</Popover>
|
|
1088
1227
|
)}
|
|
1228
|
+
|
|
1229
|
+
{equationDraft && (
|
|
1230
|
+
<Popover
|
|
1231
|
+
open
|
|
1232
|
+
onOpenChange={(open: boolean) => {
|
|
1233
|
+
if (!open) cancelEquation();
|
|
1234
|
+
}}
|
|
1235
|
+
>
|
|
1236
|
+
<PopoverTrigger asChild>
|
|
1237
|
+
<span
|
|
1238
|
+
className="pointer-events-none absolute size-0"
|
|
1239
|
+
style={{
|
|
1240
|
+
top: equationDraft.position.top,
|
|
1241
|
+
left: Math.min(equationDraft.position.left, 16),
|
|
1242
|
+
}}
|
|
1243
|
+
/>
|
|
1244
|
+
</PopoverTrigger>
|
|
1245
|
+
<PopoverContent
|
|
1246
|
+
align="start"
|
|
1247
|
+
side="bottom"
|
|
1248
|
+
className="w-[calc(100vw-2rem)] max-w-md rounded-xl p-0"
|
|
1249
|
+
onOpenAutoFocus={(event: Event) => {
|
|
1250
|
+
event.preventDefault();
|
|
1251
|
+
equationInputRef.current?.focus();
|
|
1252
|
+
}}
|
|
1253
|
+
>
|
|
1254
|
+
<div className="p-4">
|
|
1255
|
+
<div className="flex items-center gap-2 text-sm font-semibold">
|
|
1256
|
+
{equationDraft.displayMode ? (
|
|
1257
|
+
<IconMathFunction
|
|
1258
|
+
className="text-muted-foreground"
|
|
1259
|
+
size={16}
|
|
1260
|
+
/>
|
|
1261
|
+
) : (
|
|
1262
|
+
<IconSquareRoot2
|
|
1263
|
+
className="text-muted-foreground"
|
|
1264
|
+
size={16}
|
|
1265
|
+
/>
|
|
1266
|
+
)}
|
|
1267
|
+
{equationDraft.displayMode
|
|
1268
|
+
? t("editor.slash.blockEquation")
|
|
1269
|
+
: t("editor.slash.inlineEquation")}
|
|
1270
|
+
</div>
|
|
1271
|
+
<textarea
|
|
1272
|
+
ref={equationInputRef}
|
|
1273
|
+
value={equationLatex}
|
|
1274
|
+
onChange={(event) => setEquationLatex(event.target.value)}
|
|
1275
|
+
onKeyDown={(event) => {
|
|
1276
|
+
if (
|
|
1277
|
+
event.key === "Enter" &&
|
|
1278
|
+
(event.metaKey || event.ctrlKey) &&
|
|
1279
|
+
equationResult.ok
|
|
1280
|
+
) {
|
|
1281
|
+
event.preventDefault();
|
|
1282
|
+
submitEquation();
|
|
1283
|
+
}
|
|
1284
|
+
if (event.key === "Escape") {
|
|
1285
|
+
event.preventDefault();
|
|
1286
|
+
cancelEquation();
|
|
1287
|
+
}
|
|
1288
|
+
}}
|
|
1289
|
+
rows={equationDraft.displayMode ? 3 : 2}
|
|
1290
|
+
placeholder={t("editor.slash.equationPlaceholder")}
|
|
1291
|
+
aria-label={t("editor.slash.equationInputLabel")}
|
|
1292
|
+
aria-invalid={equationLatex.length > 0 && !equationResult.ok}
|
|
1293
|
+
aria-describedby="equation-preview-status"
|
|
1294
|
+
className="mt-3 w-full resize-y rounded-lg border border-input bg-background px-3 py-2 font-mono text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
1295
|
+
/>
|
|
1296
|
+
<div className="mt-3 min-h-20 rounded-lg border border-border bg-muted/30 p-3">
|
|
1297
|
+
<div className="mb-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
1298
|
+
{t("editor.slash.equationPreview")}
|
|
1299
|
+
</div>
|
|
1300
|
+
<div className="flex min-h-9 items-center justify-center overflow-x-auto text-foreground">
|
|
1301
|
+
{equationResult.ok ? (
|
|
1302
|
+
<MathRenderer
|
|
1303
|
+
latex={equationLatex}
|
|
1304
|
+
displayMode={equationDraft.displayMode}
|
|
1305
|
+
/>
|
|
1306
|
+
) : (
|
|
1307
|
+
<span className="text-sm text-muted-foreground">
|
|
1308
|
+
{equationLatex
|
|
1309
|
+
? t("editor.slash.equationNeedsRepair")
|
|
1310
|
+
: t("editor.slash.equationPreviewEmpty")}
|
|
1311
|
+
</span>
|
|
1312
|
+
)}
|
|
1313
|
+
</div>
|
|
1314
|
+
</div>
|
|
1315
|
+
<p
|
|
1316
|
+
id="equation-preview-status"
|
|
1317
|
+
className={cn(
|
|
1318
|
+
"mt-2 min-h-5 text-xs",
|
|
1319
|
+
equationLatex && !equationResult.ok
|
|
1320
|
+
? "text-destructive"
|
|
1321
|
+
: "text-muted-foreground",
|
|
1322
|
+
)}
|
|
1323
|
+
>
|
|
1324
|
+
{equationLatex && !equationResult.ok
|
|
1325
|
+
? equationResult.error
|
|
1326
|
+
: t("editor.slash.equationSubmitHint")}
|
|
1327
|
+
</p>
|
|
1328
|
+
</div>
|
|
1329
|
+
<div className="flex items-center justify-end gap-2 border-t border-border px-4 py-3">
|
|
1330
|
+
<Button variant="ghost" size="sm" onClick={cancelEquation}>
|
|
1331
|
+
{t("editor.slash.cancelEquation")}
|
|
1332
|
+
</Button>
|
|
1333
|
+
<Button
|
|
1334
|
+
size="sm"
|
|
1335
|
+
onClick={submitEquation}
|
|
1336
|
+
disabled={!equationResult.ok}
|
|
1337
|
+
>
|
|
1338
|
+
<IconCheck />
|
|
1339
|
+
{t("editor.slash.insertEquation")}
|
|
1340
|
+
</Button>
|
|
1341
|
+
</div>
|
|
1342
|
+
</PopoverContent>
|
|
1343
|
+
</Popover>
|
|
1344
|
+
)}
|
|
1089
1345
|
</>
|
|
1090
1346
|
);
|
|
1091
1347
|
}
|