@k2b/cloud 0.40.0 → 0.41.0
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/package.json +6 -3
- package/src/_internal/capability-sentences.ts +2 -1
- package/src/_internal/define-app.ts +13 -3
- package/src/_internal/heartbeat.ts +203 -14
- package/src/_internal/registry.ts +28 -1
- package/src/_internal/setup-lock.ts +103 -0
- package/src/ai/chat/action-card.ts +69 -3
- package/src/ai/chat/action-question.tsx +12 -1
- package/src/ai/chat/action-text.ts +288 -0
- package/src/ai/chat/tool-action-words.ts +31 -0
- package/src/ai/chat/tool-action.tsx +1 -26
- package/src/ai/executor.ts +10 -2
- package/src/ai/firecrawl-tools.ts +15 -2
- package/src/ai/index.ts +1 -0
- package/src/ai/migrate.ts +15 -0
- package/src/ai/open-questions.ts +172 -0
- package/src/ai/question-text.ts +8 -0
- package/src/ai/routes.ts +51 -5
- package/src/ai/store.ts +18 -0
- package/src/ai/testing.ts +36 -0
- package/src/ai/tool-sentences.ts +152 -34
- package/src/ai/tool-wording.ts +12 -6
- package/src/ai/tools.ts +8 -3
- package/src/ai/types.ts +16 -2
- package/src/api/app-approval-actions.ts +31 -7
- package/src/api/auth/schemas.ts +3 -0
- package/src/api/auth.ts +54 -18
- package/src/api/mcp.ts +27 -3
- package/src/api/me.ts +79 -3
- package/src/approvals/Approvals.tsx +25 -63
- package/src/approvals/approval-card.ts +68 -0
- package/src/contracts/app-approval.ts +3 -3
- package/src/contracts/capabilities.ts +1 -1
- package/src/contracts/shared.ts +3 -0
- package/src/server/middleware/auth.ts +4 -2
- package/src/services/accounts/app.ts +23 -3
- package/src/services/accounts/users.ts +12 -1
- package/src/services/approvals/device.ts +49 -33
- package/src/services/approvals/index.ts +1 -0
- package/src/services/approvals/list.ts +1 -1
- package/src/services/approvals/question.ts +68 -0
- package/src/services/audit/index.ts +1 -0
- package/src/services/auth-flows/ipa.ts +39 -2
- package/src/services/auth-flows/password-reset.ts +32 -6
- package/src/services/index.ts +2 -0
- package/src/services/pdf/markdown.ts +160 -2
- package/src/services/session/index.ts +203 -29
- package/src/ssr/HelpPanelArticle.island.tsx +308 -0
- package/src/ssr/HelpPanelBack.island.tsx +32 -0
- package/src/ssr/HelpPanelMenu.island.tsx +133 -0
- package/src/ssr/HelpPanelSearch.island.tsx +123 -0
- package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
- package/src/ssr/HelpPanelView.island.tsx +27 -0
- package/src/ssr/LayoutHelp.tsx +182 -540
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +6 -7
- package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
- package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
- package/src/ssr/help.ts +4 -3
- package/src/ssr/index.ts +1 -0
- package/src/ssr/islands/index.ts +3 -4
- package/src/ssr/layout-help-panel-controls.tsx +43 -0
- package/src/ssr/layout-help-panel-document.ts +67 -0
- package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
- package/src/ssr/layout-help-panel-menu.tsx +113 -0
- package/src/ssr/layout-help-panel-modules.ts +7 -0
- package/src/ssr/layout-help-panel-reader.tsx +159 -0
- package/src/ssr/layout-help-panel-search.tsx +69 -0
- package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
- package/src/ssr/layout-help-panel-state.ts +141 -0
- package/src/ssr/layout-help-panel-types.ts +19 -0
- package/src/ssr/layout-help-views.tsx +484 -0
- package/src/ssr/panel-idle.ts +9 -0
- package/src/styles/approvals.css +6 -0
- package/src/styles/effects.css +15 -0
|
@@ -7,7 +7,8 @@ import type { User } from "../contracts/shared";
|
|
|
7
7
|
import type { HelpDocumentManifest, HelpDocumentPayload } from "../shared/help";
|
|
8
8
|
import { appAppearanceStyle } from "./app-appearance";
|
|
9
9
|
import Layout from "./Layout";
|
|
10
|
-
import { LayoutHelpPage, type LayoutHelpPageProps
|
|
10
|
+
import { LayoutHelpPage, type LayoutHelpPageProps } from "./LayoutHelp";
|
|
11
|
+
import { LayoutHelpPanel, type LayoutHelpPanelProps } from "./LayoutHelpPanel.server";
|
|
11
12
|
import { helpArticlePartsServer } from "./layout-help-article.server";
|
|
12
13
|
import PanelLayout from "./PanelLayout";
|
|
13
14
|
import { readPanelContext } from "./panel-context";
|
|
@@ -34,7 +35,8 @@ export const fixtureDocument: HelpDocumentPayload = {
|
|
|
34
35
|
// A lead, the facts chips, and a section: everything the article head reads from the article.
|
|
35
36
|
html: '<p>Share a Space so other people can see and edit its tasks.</p><h2 id="share">Share</h2><div class="help-steps"><ol><li>Open Space settings.</li><li>Add a person.</li></ol></div>',
|
|
36
37
|
};
|
|
37
|
-
const
|
|
38
|
+
const { html, ...payload } = fixtureDocument;
|
|
39
|
+
const initialArticle = { payload, parts: helpArticlePartsServer(html) };
|
|
38
40
|
const appearance: AppAppearance = { accent: "#16a34a", background: { from: "#16a34a", to: "#0284c7" } };
|
|
39
41
|
const apps: CloudRuntime["apps"] = [
|
|
40
42
|
{
|
|
@@ -65,7 +67,7 @@ const app = defineApp({
|
|
|
65
67
|
routes: ["/"],
|
|
66
68
|
});
|
|
67
69
|
type Environment = { Variables: { runtime: CloudRuntime; user: User } };
|
|
68
|
-
const PropsSnapshot = (props: { value:
|
|
70
|
+
const PropsSnapshot = (props: { value: LayoutHelpPageProps }) => (
|
|
69
71
|
<script id="help-fixture-props" type="application/json" innerHTML={JSON.stringify(props.value).replace(/</g, "\\u003c")} />
|
|
70
72
|
);
|
|
71
73
|
export const server = new Hono<Environment>()
|
|
@@ -98,10 +100,7 @@ export const server = new Hono<Environment>()
|
|
|
98
100
|
};
|
|
99
101
|
return () => (
|
|
100
102
|
<PanelLayout c={c} title="Help">
|
|
101
|
-
<
|
|
102
|
-
<LayoutHelpPanel {...props} />
|
|
103
|
-
</div>
|
|
104
|
-
<PropsSnapshot value={props} />
|
|
103
|
+
<LayoutHelpPanel {...props} />
|
|
105
104
|
</PanelLayout>
|
|
106
105
|
);
|
|
107
106
|
}),
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { basename, resolve } from "node:path";
|
|
2
|
+
import tailwind from "bun-plugin-tailwind";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import { toJSON } from "seroval";
|
|
5
|
+
import { createPanelRenderer, panelFixtureProps, panelIslandEntries } from "../../../../scripts/fixtures/help-panel";
|
|
6
|
+
import { buildIslandClosure } from "../../../../scripts/fixtures/island-closure";
|
|
7
|
+
import { islandIdFromFile } from "../../../ssr/src/island-id";
|
|
8
|
+
import { helpArticlePartsServer } from "./layout-help-article.server";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build the real islands together, with the production mount runtime and shared ESM chunks. One module script imports
|
|
12
|
+
* every island at once, as @k2b/ssr's loader does, so a held island does not hold the others.
|
|
13
|
+
*/
|
|
14
|
+
export async function helpPanelServerBrowserFixture() {
|
|
15
|
+
const root = resolve(import.meta.dir, "../../../..");
|
|
16
|
+
const renderer = await createPanelRenderer();
|
|
17
|
+
const files: Record<string, string> = {};
|
|
18
|
+
const entrypoints = panelIslandEntries.map((entry) => {
|
|
19
|
+
const id = islandIdFromFile(entry, root);
|
|
20
|
+
const virtual = resolve(import.meta.dir, `${id}.ts`);
|
|
21
|
+
files[virtual] =
|
|
22
|
+
`import { mount } from ${JSON.stringify(resolve(root, "packages/ssr/src/mount.ts"))}; import C from ${JSON.stringify(entry)}; mount(C, 'solid-island[data-id="${id}"]'); document.documentElement.dataset[${JSON.stringify(`mounted${id}`)}] = "true";`;
|
|
23
|
+
return virtual;
|
|
24
|
+
});
|
|
25
|
+
try {
|
|
26
|
+
const build = await buildIslandClosure(entrypoints, files);
|
|
27
|
+
if (!build.metafile) throw new Error("Missing browser fixture metafile");
|
|
28
|
+
const outputMeta = new Map(Object.entries(build.metafile.outputs).map(([path, output]) => [basename(path), output]));
|
|
29
|
+
const eager = new Set<string>();
|
|
30
|
+
const visit = (name: string) => {
|
|
31
|
+
if (eager.has(name)) return;
|
|
32
|
+
eager.add(name);
|
|
33
|
+
for (const edge of outputMeta.get(name)?.imports ?? []) if (edge.kind !== "dynamic-import") visit(basename(edge.path));
|
|
34
|
+
};
|
|
35
|
+
const scripts = entrypoints.map((entry) => {
|
|
36
|
+
const output = [...outputMeta].find(([, meta]) => meta.entryPoint && resolve(meta.entryPoint) === entry);
|
|
37
|
+
if (!output) throw new Error(`Missing browser island entry ${entry}`);
|
|
38
|
+
visit(output[0]);
|
|
39
|
+
return output[0];
|
|
40
|
+
});
|
|
41
|
+
const outputs = new Map(await Promise.all(build.outputs.map(async (output) => [basename(output.path), await output.text()] as const)));
|
|
42
|
+
const dynamicChunks = [...outputMeta.keys()].filter((name) => !eager.has(name));
|
|
43
|
+
const chunkFor = (suffix: string) => {
|
|
44
|
+
const found = [...outputMeta]
|
|
45
|
+
.filter(([, meta]) => Object.entries(meta.inputs).some(([input, details]) => input.endsWith(suffix) && details.bytesInOutput > 0))
|
|
46
|
+
.map(([name]) => name);
|
|
47
|
+
if (!found.length) throw new Error(`Missing chunk for ${suffix}`);
|
|
48
|
+
return found;
|
|
49
|
+
};
|
|
50
|
+
const styles = await Bun.build({ entrypoints: [resolve(root, "styles.css")], plugins: [tailwind] });
|
|
51
|
+
if (!styles.success) throw new AggregateError(styles.logs, "Could not compile panel styles");
|
|
52
|
+
const css = await styles.outputs[0]!.text();
|
|
53
|
+
const initialArticle = {
|
|
54
|
+
payload: {
|
|
55
|
+
id: "share",
|
|
56
|
+
title: "Share a Space",
|
|
57
|
+
locale: "en",
|
|
58
|
+
kind: "task" as const,
|
|
59
|
+
needs: "edit" as const,
|
|
60
|
+
markdown: "Share a Space.\n\n## Invite\n\nInvite people.",
|
|
61
|
+
related: [],
|
|
62
|
+
},
|
|
63
|
+
parts: helpArticlePartsServer(
|
|
64
|
+
'<p>Share a Space.</p><h2 id="invite">Invite</h2><p>Invite people.</p><a href="/help/apps/spaces/roles">Roles</a>',
|
|
65
|
+
),
|
|
66
|
+
};
|
|
67
|
+
const runtimeId = islandIdFromFile(panelIslandEntries.at(-1)!, root);
|
|
68
|
+
const runtimeProps = JSON.stringify(toJSON({ provider: "fixture", scope: "/panel" }))
|
|
69
|
+
.replaceAll("&", "&")
|
|
70
|
+
.replaceAll('"', """);
|
|
71
|
+
const app = new Hono()
|
|
72
|
+
// Split chunks import each other by relative paths from the build's output layout; serve every file by name.
|
|
73
|
+
.get("/:path{.+\\.js}", (c) => {
|
|
74
|
+
const content = outputs.get(basename(c.req.path));
|
|
75
|
+
return content === undefined ? c.notFound() : c.body(content, 200, { "content-type": "text/javascript" });
|
|
76
|
+
})
|
|
77
|
+
.get("/", (c) =>
|
|
78
|
+
c.html(
|
|
79
|
+
`<!doctype html><html lang="en"${themeClass(c.req.query("theme"))}><head><style>${css}</style></head><body class="k2b-ui" style="margin:0"><div class="cloud-app-canvas" data-panel="open" data-panel-sheet="open" style="position:relative;display:flex;height:100dvh"><main data-cloud-app-area style="flex:1">Host app</main><aside class="cloud-panel" data-state="ready"><div class="cloud-panel__card"><header class="cloud-panel__head">Help</header><div class="cloud-panel__body"><iframe class="cloud-panel__frame" title="Help" src="/panel?${new URLSearchParams({ ...(c.req.query("topic") ? { topic: "share" } : {}), theme: c.req.query("theme") ?? "light" })}"></iframe></div></div></aside></div></body></html>`,
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
.get("/panel", (c) =>
|
|
83
|
+
c.html(
|
|
84
|
+
`<!doctype html><html lang="en"${themeClass(c.req.query("theme"))}><head><style>${css}</style><style>html,body{margin:0;height:100%}*,*::before,*::after{animation:none!important;transition:none!important}</style></head><body class="k2b-ui"><div class="cloud-panel-page" data-cloud-panel-page><script id="cloud-panel-context" type="application/json">{"v":1,"seq":0,"pageId":"22222222-2222-4222-8222-222222222222","app":"spaces","help":["share"]}</script><solid-island data-id="${runtimeId}" data-props="${runtimeProps}"></solid-island>${renderer.render({ ...panelFixtureProps, ...(c.req.query("topic") ? { initialTopic: "share", initialArticle } : {}) })}</div><script>document.documentElement.dataset.serverGeometry=JSON.stringify((${panelGeometry.toString()})());</script><script type="module">${JSON.stringify(scripts)}.forEach((file)=>import("/assets/"+file))</script></body></html>`,
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
.get("/api/help/search", (c) => c.json({ ids: ["share"] }))
|
|
88
|
+
.get("/api/help/:id", (c) =>
|
|
89
|
+
c.json({
|
|
90
|
+
...initialArticle.payload,
|
|
91
|
+
id: c.req.param("id"),
|
|
92
|
+
html: '<p>Fixture.</p><h2 id="invite">Invite</h2><p>Invite people.</p>',
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
const server = Bun.serve({ port: 0, fetch: app.fetch });
|
|
96
|
+
return {
|
|
97
|
+
origin: `http://localhost:${server.port}`,
|
|
98
|
+
dynamicChunks,
|
|
99
|
+
searchChunks: chunkFor("/layout-help-panel-search.tsx"),
|
|
100
|
+
menuChunks: chunkFor("/layout-help-panel-menu.tsx"),
|
|
101
|
+
islandChunks: new Map(panelIslandEntries.map((entry, index) => [basename(entry), scripts[index]!])),
|
|
102
|
+
ids: panelIslandEntries.map((entry) => islandIdFromFile(entry, root)),
|
|
103
|
+
cleanup: () => {
|
|
104
|
+
server.stop(true);
|
|
105
|
+
renderer.cleanup();
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
renderer.cleanup();
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Cloud's theme script sets `dark` on the root; the fixture renders that class on the server for both documents. */
|
|
115
|
+
const themeClass = (theme: string | undefined) => (theme === "dark" ? ' class="dark"' : "");
|
|
116
|
+
|
|
117
|
+
/** First-frame geometry is the assertion subject, so callers also take it while action chunks are held. */
|
|
118
|
+
export function panelGeometry() {
|
|
119
|
+
return [
|
|
120
|
+
"[data-help-head]",
|
|
121
|
+
"[data-help-search]",
|
|
122
|
+
"[data-help-results]",
|
|
123
|
+
"[data-help-tiles] button",
|
|
124
|
+
"[data-help-server-article] [data-help-article-head]",
|
|
125
|
+
].map((selector) => {
|
|
126
|
+
const element = document.querySelector<HTMLElement>(selector);
|
|
127
|
+
if (!element || element.closest("[hidden]")) return null;
|
|
128
|
+
const rect = element.getBoundingClientRect();
|
|
129
|
+
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
130
|
+
});
|
|
131
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { MarkdownView, ScrollArea, TableOfContents, useLocale } from "@k2b/ui";
|
|
2
|
+
import { Show } from "solid-js";
|
|
3
|
+
import HelpPanelArticle from "./HelpPanelArticle.island";
|
|
4
|
+
import HelpPanelBack from "./HelpPanelBack.island";
|
|
5
|
+
import HelpPanelMenu from "./HelpPanelMenu.island";
|
|
6
|
+
import HelpPanelSearch from "./HelpPanelSearch.island";
|
|
7
|
+
import HelpPanelShortcuts from "./HelpPanelShortcuts.island";
|
|
8
|
+
import HelpPanelView from "./HelpPanelView.island";
|
|
9
|
+
import { helpMessages } from "./help-messages";
|
|
10
|
+
import { HelpPanelSectionTrigger } from "./layout-help-panel-controls";
|
|
11
|
+
import type { HelpPanelData } from "./layout-help-panel-state";
|
|
12
|
+
import type { LayoutHelpPanelProps } from "./layout-help-panel-types";
|
|
13
|
+
import { helpStart } from "./layout-help-start";
|
|
14
|
+
import { HelpArticleView, HelpHead, HelpNotAvailableView, HelpStartView } from "./layout-help-views";
|
|
15
|
+
|
|
16
|
+
export type { LayoutHelpPanelProps } from "./layout-help-panel-types";
|
|
17
|
+
|
|
18
|
+
/** Static provider content survives island mounting; only the controls own browser state. */
|
|
19
|
+
export function LayoutHelpPanel(props: LayoutHelpPanelProps) {
|
|
20
|
+
const locale = useLocale();
|
|
21
|
+
const t = () => helpMessages.resolve([locale()]).t;
|
|
22
|
+
const topic = props.documents.find((topic) => topic.id === props.initialTopic);
|
|
23
|
+
const article = topic && props.initialArticle?.payload.id === topic.id ? props.initialArticle : undefined;
|
|
24
|
+
const start = helpStart(props.documents, props.view);
|
|
25
|
+
const data: HelpPanelData = {
|
|
26
|
+
// The legacy reader searched and copied its topics in start order; keep that order for every manifest.
|
|
27
|
+
documents: [...props.documents]
|
|
28
|
+
.sort((a, b) => (a.order ?? 100) - (b.order ?? 100) || a.title.localeCompare(b.title))
|
|
29
|
+
.map(({ id, title, description, icon, kind, order, url, searchUrl, locale }) => ({
|
|
30
|
+
id,
|
|
31
|
+
title,
|
|
32
|
+
description,
|
|
33
|
+
icon,
|
|
34
|
+
kind,
|
|
35
|
+
order,
|
|
36
|
+
url,
|
|
37
|
+
searchUrl,
|
|
38
|
+
locale,
|
|
39
|
+
})),
|
|
40
|
+
appId: props.app?.id,
|
|
41
|
+
pageBase: props.pageBase,
|
|
42
|
+
article: article?.payload,
|
|
43
|
+
};
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
class="help-shell app-accent-scope flex h-full min-h-0 flex-col"
|
|
47
|
+
data-surface="panel"
|
|
48
|
+
style={props.appearance}
|
|
49
|
+
data-help-initial-topic={topic?.id}
|
|
50
|
+
>
|
|
51
|
+
<Show when={props.state !== "unavailable"}>
|
|
52
|
+
<HelpHead
|
|
53
|
+
row
|
|
54
|
+
back={!!topic}
|
|
55
|
+
tile={props.app?.icon ?? "ti ti-help-circle"}
|
|
56
|
+
title={props.app?.name ?? t().help}
|
|
57
|
+
lead={<HelpPanelBack back={!!topic} tile={props.app?.icon ?? "ti ti-help-circle"} />}
|
|
58
|
+
menu={<HelpPanelMenu />}
|
|
59
|
+
closable={false}
|
|
60
|
+
onBack={() => {}}
|
|
61
|
+
onClose={() => {}}
|
|
62
|
+
query=""
|
|
63
|
+
onQuery={() => {}}
|
|
64
|
+
searchRef={() => {}}
|
|
65
|
+
/>
|
|
66
|
+
</Show>
|
|
67
|
+
<ScrollArea class="min-h-0 flex-1 p-4 sm:p-5">
|
|
68
|
+
<Show when={props.state !== "unavailable"} fallback={<HelpNotAvailableView />}>
|
|
69
|
+
<div class="help-start help-measure" data-help-results="" hidden={!!topic}>
|
|
70
|
+
<div data-help-start-content="">
|
|
71
|
+
<HelpPanelView topics={start.view} />
|
|
72
|
+
<HelpStartView
|
|
73
|
+
start={{ ...start, view: [] }}
|
|
74
|
+
empty={props.documents.length === 0}
|
|
75
|
+
failed={props.state === "failed"}
|
|
76
|
+
onOpen={() => {}}
|
|
77
|
+
onReload={() => {}}
|
|
78
|
+
shortcuts={<HelpPanelShortcuts appName={props.app?.name} platform={props.platform} />}
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
<HelpPanelSearch />
|
|
82
|
+
</div>
|
|
83
|
+
<Show when={article && topic}>
|
|
84
|
+
{article && topic && (
|
|
85
|
+
<article class="help-article" data-surface="panel" data-help-server-article="">
|
|
86
|
+
<HelpArticleView
|
|
87
|
+
surface="panel"
|
|
88
|
+
topic={topic}
|
|
89
|
+
appName={props.app?.name}
|
|
90
|
+
appId={props.app?.id}
|
|
91
|
+
parts={article.parts}
|
|
92
|
+
document={{ ...article.payload, html: "" }}
|
|
93
|
+
Markdown={MarkdownView}
|
|
94
|
+
showTools={article.parts.sections.length > 0}
|
|
95
|
+
sectionMenu={article.parts.sections.length ? <HelpPanelSectionTrigger label={t().onThisPage} /> : null}
|
|
96
|
+
copyButton={null}
|
|
97
|
+
loading={null}
|
|
98
|
+
failure={null}
|
|
99
|
+
toc={<TableOfContents items={article.parts.sections} title={t().onThisPage} />}
|
|
100
|
+
copyAction={null}
|
|
101
|
+
headingRef={() => {}}
|
|
102
|
+
contentRef={() => {}}
|
|
103
|
+
onBack={() => {}}
|
|
104
|
+
/>
|
|
105
|
+
</article>
|
|
106
|
+
)}
|
|
107
|
+
</Show>
|
|
108
|
+
<HelpPanelArticle topic={article ? undefined : topic} />
|
|
109
|
+
</Show>
|
|
110
|
+
<span class="sr-only" aria-live="polite" data-help-announcement="" />
|
|
111
|
+
</ScrollArea>
|
|
112
|
+
<script type="application/json" data-help-panel-data="" innerHTML={JSON.stringify(data).replace(/</g, "\\u003c")} />
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|