@clidey/dory 1.0.7 → 1.9.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/README.md +12 -1
- package/bin/chat-server.ts +270 -0
- package/bin/dist/analytics-server.d.ts +11 -0
- package/bin/dist/analytics-server.d.ts.map +1 -0
- package/bin/dist/analytics-server.js +121 -0
- package/bin/dist/analytics-server.js.map +1 -0
- package/bin/dist/automation.d.ts +35 -0
- package/bin/dist/automation.d.ts.map +1 -0
- package/bin/dist/automation.js +259 -0
- package/bin/dist/automation.js.map +1 -0
- package/bin/dist/chat-server.d.ts +8 -0
- package/bin/dist/chat-server.d.ts.map +1 -0
- package/bin/dist/chat-server.js +322 -0
- package/bin/dist/chat-server.js.map +1 -0
- package/bin/dist/check.d.ts +16 -0
- package/bin/dist/check.d.ts.map +1 -0
- package/bin/dist/check.js +247 -0
- package/bin/dist/check.js.map +1 -0
- package/bin/dist/dory.js +252 -402
- package/bin/dist/dory.js.map +1 -1
- package/bin/dist/i18n-init.d.ts +6 -0
- package/bin/dist/i18n-init.d.ts.map +1 -0
- package/bin/dist/i18n-init.js +56 -0
- package/bin/dist/i18n-init.js.map +1 -0
- package/bin/dist/mcp-http.d.ts +8 -0
- package/bin/dist/mcp-http.d.ts.map +1 -0
- package/bin/dist/mcp-http.js +72 -0
- package/bin/dist/mcp-http.js.map +1 -0
- package/bin/dist/mcp.d.ts +2 -0
- package/bin/dist/mcp.d.ts.map +1 -0
- package/bin/dist/mcp.js +81 -0
- package/bin/dist/mcp.js.map +1 -0
- package/bin/dist/preview-entry.d.ts +2 -0
- package/bin/dist/preview-entry.d.ts.map +1 -0
- package/bin/dist/preview-entry.js +6 -0
- package/bin/dist/preview-entry.js.map +1 -0
- package/bin/dist/preview-server.d.ts +5 -0
- package/bin/dist/preview-server.d.ts.map +1 -0
- package/bin/dist/preview-server.js +159 -0
- package/bin/dist/preview-server.js.map +1 -0
- package/bin/dist/project-config.d.ts +22 -0
- package/bin/dist/project-config.d.ts.map +1 -0
- package/bin/dist/project-config.js +44 -0
- package/bin/dist/project-config.js.map +1 -0
- package/dist/index.html +57 -0
- package/dist/runtime/dory-runtime.css +2 -0
- package/dist/runtime/dory-runtime.html +14 -0
- package/dist/runtime/dory-runtime.js +5490 -0
- package/docs/api-playground-demo.mdx +119 -0
- package/docs/api-reference/asyncapi.json +324 -0
- package/docs/api-reference/openapi.json +366 -0
- package/docs/api-reference/operations/create.mdx +5 -0
- package/docs/api-reference/operations/delete.mdx +5 -0
- package/docs/api-reference/operations/get.mdx +5 -0
- package/docs/api-reference/operations/update.mdx +5 -0
- package/docs/api-reference/websocket-operations/receive-chat-message.mdx +5 -0
- package/docs/api-reference/websocket-operations/receive-notifications.mdx +5 -0
- package/docs/api-reference/websocket-operations/receive-presence-update.mdx +5 -0
- package/docs/api-reference/websocket-operations/send-chat-message.mdx +5 -0
- package/docs/api-reference/websocket-operations/send-presence-update.mdx +5 -0
- package/docs/assets/dory.jpeg +0 -0
- package/docs/components/accordion.mdx +145 -0
- package/docs/components/callouts.mdx +134 -0
- package/docs/components/cards.mdx +153 -0
- package/docs/components/code-blocks.mdx +281 -0
- package/docs/components/expandable.mdx +189 -0
- package/docs/components/icons.mdx +71 -0
- package/docs/components/images.mdx +82 -0
- package/docs/components/latex.mdx +148 -0
- package/docs/components/layout.mdx +175 -0
- package/docs/components/lists.mdx +134 -0
- package/docs/components/mermaid.mdx +149 -0
- package/docs/components/overview.mdx +81 -0
- package/docs/components/source.mdx +130 -0
- package/docs/components/steps.mdx +122 -0
- package/docs/components/tables.mdx +114 -0
- package/docs/components/tags.mdx +179 -0
- package/docs/components/typography.mdx +185 -0
- package/docs/configuration/analytics.mdx +45 -0
- package/docs/configuration/chat.mdx +59 -0
- package/docs/configuration/dory-json.mdx +278 -0
- package/docs/configuration/embed.mdx +73 -0
- package/docs/configuration/fonts.mdx +86 -0
- package/docs/configuration/internationalization.mdx +61 -0
- package/docs/configuration/navigation.mdx +217 -0
- package/docs/configuration/theming.mdx +166 -0
- package/docs/dory.json +129 -0
- package/docs/embed-example.html +176 -0
- package/docs/favicon.svg +18 -0
- package/docs/features.mdx +440 -0
- package/docs/getting-started/automation.mdx +76 -0
- package/docs/getting-started/configuration.mdx +95 -0
- package/docs/getting-started/installation.mdx +88 -0
- package/docs/getting-started/quick-start.mdx +53 -0
- package/docs/getting-started/whodb-ee-apps.mdx +122 -0
- package/docs/introduction.mdx +50 -0
- package/docs/logo/logo.svg +18 -0
- package/docs/playgrounds/api-playground.mdx +122 -0
- package/docs/playgrounds/websocket-playground.mdx +98 -0
- package/docs/social-card.png +0 -0
- package/docs/social-card.svg +17 -0
- package/docs/websocket-playground-demo.mdx +135 -0
- package/package.json +23 -17
- package/scripts/verify-runtime-parity.mjs +146 -0
- package/src/app.css +19 -1
- package/src/assets/fonts/HankenGrotesk.ttf +0 -0
- package/src/assets/fonts/OFL.txt +94 -0
- package/src/components/chat-assistant.tsx +229 -0
- package/src/components/dropdown.tsx +3 -3
- package/src/components/error-boundary.tsx +2 -2
- package/src/components/hooks.tsx +20 -1
- package/src/components/navigation.tsx +82 -51
- package/src/components/notification.tsx +2 -3
- package/src/components/routes.tsx +17 -19
- package/src/components/search-index.test.ts +15 -0
- package/src/components/search-index.ts +49 -48
- package/src/components/search.tsx +208 -104
- package/src/components/store.ts +27 -9
- package/src/config/mdx-common.ts +11 -0
- package/src/config/mdx.ts +10 -11
- package/src/embed/loader.ts +0 -3
- package/src/embed/types.ts +0 -2
- package/src/embed/widget-iframe.ts +19 -21
- package/src/embedded-runtime/config.ts +138 -0
- package/src/embedded-runtime/index.tsx +680 -0
- package/src/embedded-runtime/runtime.html +14 -0
- package/src/entry-server.tsx +46 -124
- package/src/main.tsx +16 -4
- package/src/mdx/accordion.tsx +3 -3
- package/src/mdx/api-playground.tsx +18 -16
- package/src/mdx/async-api.tsx +2 -2
- package/src/mdx/card.tsx +3 -2
- package/src/mdx/code.tsx +11 -9
- package/src/mdx/fence.tsx +6 -5
- package/src/mdx/icon-grid.tsx +4 -1
- package/src/mdx/icon-names.test.ts +15 -0
- package/src/mdx/icon-names.ts +60 -0
- package/src/mdx/icon-source-variants.ts +102 -0
- package/src/mdx/icon.tsx +12 -73
- package/src/mdx/latex.tsx +3 -2
- package/src/mdx/mdx.tsx +1 -1
- package/src/mdx/mermaid.tsx +12 -6
- package/src/mdx/open-api.tsx +130 -74
- package/src/mdx/preprocess.test.ts +16 -0
- package/src/mdx/preprocess.ts +61 -0
- package/src/mdx/processor.ts +4 -96
- package/src/mdx/source.tsx +5 -4
- package/src/mdx/specimen.tsx +38 -6
- package/src/mdx/tabs.tsx +29 -21
- package/src/mdx/text.tsx +9 -9
- package/src/mdx/try-it-panel.tsx +284 -0
- package/src/mdx/type-scale.tsx +4 -4
- package/src/mdx/websocket-playground.tsx +21 -14
- package/src/plugins/analytics-injector.ts +8 -0
- package/src/plugins/chat-dev-server.ts +32 -0
- package/src/plugins/docs-assets.test.ts +25 -0
- package/src/plugins/docs-assets.ts +6 -2
- package/src/plugins/frontmatter-dev-server.ts +2 -2
- package/src/plugins/frontmatter-generator.ts +1 -1
- package/src/plugins/html-files-middleware.test.ts +21 -1
- package/src/plugins/html-files-middleware.ts +26 -6
- package/src/plugins/html-metadata-injector.ts +24 -4
- package/src/plugins/llm-txt-dev-server.ts +2 -2
- package/src/plugins/llm-txt-generator.ts +21 -4
- package/src/plugins/prerender.ts +47 -20
- package/src/plugins/robots-generator.ts +1 -1
- package/src/plugins/sanitize.ts +1 -1
- package/src/plugins/shared.ts +6 -18
- package/src/plugins/sitemap-generator.ts +1 -1
- package/src/plugins/social-preview.test.ts +71 -0
- package/src/plugins/social-preview.ts +87 -0
- package/src/plugins/unified-mdx-plugin.ts +1 -1
- package/src/theme.scss +630 -207
- package/src/types/config.ts +53 -1
- package/src/ui/breadcrumb.tsx +17 -0
- package/src/ui/docs-header-view.tsx +32 -0
- package/src/ui/docs-navigation-view.tsx +69 -0
- package/src/ui/docs-page-frame.tsx +50 -0
- package/src/ui/dory-mark.tsx +9 -0
- package/src/ui/header.tsx +33 -90
- package/src/ui/layout.tsx +99 -153
- package/src/ui/mobile-navigation.tsx +10 -8
- package/src/ui/page-actions.tsx +67 -0
- package/src/ui/page-feedback.tsx +50 -0
- package/src/ui/page-footer.tsx +23 -0
- package/src/ui/prev-next-link.tsx +15 -69
- package/src/ui/prev-next-view.tsx +27 -0
- package/src/ui/table-of-content-view.tsx +167 -0
- package/src/ui/table-of-content.tsx +25 -183
- package/src/utils/analytics.test.ts +36 -0
- package/src/utils/analytics.ts +35 -0
- package/src/utils/colors.ts +7 -0
- package/src/utils/content-scope.test.ts +26 -0
- package/src/utils/content-scope.ts +39 -0
- package/src/utils/search-content.ts +12 -0
- package/src/utils/theme.ts +1 -7
- package/vite.config.ssr.ts +3 -3
- package/vite.config.ts +21 -15
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clidey/dory",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.0
|
|
4
|
+
"version": "1.9.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A CLI tool for building and previewing documentation sites",
|
|
7
7
|
"keywords": [
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"bin/dist",
|
|
19
|
+
"dist/runtime",
|
|
20
|
+
"bin/chat-server.ts",
|
|
21
|
+
"docs",
|
|
22
|
+
"!docs/dist",
|
|
23
|
+
"!docs/dist-ssr",
|
|
19
24
|
"src",
|
|
20
25
|
"scripts",
|
|
21
26
|
"index.html",
|
|
@@ -36,7 +41,9 @@
|
|
|
36
41
|
},
|
|
37
42
|
"scripts": {
|
|
38
43
|
"dev": "vite",
|
|
39
|
-
"build": "
|
|
44
|
+
"build": "pnpm run build:cli && tsc -b && pnpm run build:docs && pnpm run build:runtime",
|
|
45
|
+
"build:runtime": "vite build -c vite.config.runtime.ts && cp src/embedded-runtime/runtime.html dist/runtime/dory-runtime.html",
|
|
46
|
+
"build:server": "vite build -c vite.config.server.ts",
|
|
40
47
|
"build:cli": "tsc -p tsconfig.bin.json",
|
|
41
48
|
"build:docs": "vite build && vite build --config vite.config.ssr.ts && node scripts/render-routes.mjs",
|
|
42
49
|
"build:embed": "npm run build:embed:loader && npm run build:embed:widget",
|
|
@@ -44,46 +51,45 @@
|
|
|
44
51
|
"build:embed:widget": "vite build -c vite.config.embed-widget.ts",
|
|
45
52
|
"preview": "vite preview",
|
|
46
53
|
"test": "node scripts/check-trigger-children.mjs src && vitest run",
|
|
54
|
+
"test:runtime-parity": "pnpm run build && node scripts/verify-runtime-parity.mjs",
|
|
47
55
|
"lint:triggers": "node scripts/check-trigger-children.mjs src",
|
|
48
56
|
"publish": "npm run build && npm publish --access public --no-git-checks",
|
|
49
57
|
"prepublishOnly": "echo 'Skipping prepublishOnly - build handled by publish script'"
|
|
50
58
|
},
|
|
51
59
|
"dependencies": {
|
|
52
60
|
"@algolia/autocomplete-core": "1.19.12",
|
|
53
|
-
"@clidey/ux": "1.
|
|
54
|
-
"@fortawesome/fontawesome-svg-core": "7.3.1",
|
|
55
|
-
"@fortawesome/free-brands-svg-icons": "7.3.1",
|
|
56
|
-
"@fortawesome/free-regular-svg-icons": "7.3.1",
|
|
57
|
-
"@fortawesome/free-solid-svg-icons": "7.3.1",
|
|
58
|
-
"@fortawesome/react-fontawesome": "3.5.0",
|
|
61
|
+
"@clidey/ux": "1.6.4",
|
|
59
62
|
"@headlessui/react": "2.2.10",
|
|
60
|
-
"@heroicons/react": "2.2.0",
|
|
61
63
|
"@mdx-js/mdx": "3.1.1",
|
|
62
64
|
"@mdx-js/preact": "3.1.1",
|
|
63
65
|
"@mdx-js/rollup": "3.1.1",
|
|
66
|
+
"@modelcontextprotocol/node": "2.1.2",
|
|
67
|
+
"@modelcontextprotocol/server": "2.4.0",
|
|
64
68
|
"@preact/preset-vite": "2.10.6",
|
|
69
|
+
"@resvg/resvg-js": "2.6.2",
|
|
65
70
|
"@tailwindcss/vite": "4.3.3",
|
|
66
71
|
"classnames": "2.5.1",
|
|
67
72
|
"flexsearch": "0.8.212",
|
|
68
73
|
"gray-matter": "4.0.3",
|
|
69
74
|
"katex": "0.19.0",
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"preact": "
|
|
75
|
+
"mermaid": "12.2.0",
|
|
76
|
+
"motion": "14.5.0",
|
|
77
|
+
"playwright-core": "1.64.1",
|
|
78
|
+
"preact": "11.0.1",
|
|
74
79
|
"preact-render-to-string": "6.8.1",
|
|
75
80
|
"prism-react-renderer": "2.4.1",
|
|
76
|
-
"react-highlight-words": "0.
|
|
81
|
+
"react-highlight-words": "1.0.0",
|
|
77
82
|
"rehype-slug": "6.0.0",
|
|
78
83
|
"remark-gfm": "4.0.1",
|
|
79
|
-
"sass-embedded": "1.
|
|
84
|
+
"sass-embedded": "1.106.0",
|
|
80
85
|
"sirv": "3.0.2",
|
|
81
86
|
"tailwindcss": "4.3.3",
|
|
82
87
|
"tsx": "4.23.15",
|
|
83
88
|
"typescript": "7.0.2",
|
|
84
89
|
"unist-util-visit": "5.1.0",
|
|
85
|
-
"vite": "8.3.
|
|
86
|
-
"wouter-preact": "3.13.0"
|
|
90
|
+
"vite": "8.3.5",
|
|
91
|
+
"wouter-preact": "3.13.0",
|
|
92
|
+
"zod": "4.6.7"
|
|
87
93
|
},
|
|
88
94
|
"packageManager": "pnpm@12.13.0+sha512.1e55719bf26e809dce5c5084fdebaa64efb3da9038ae5d68a9f8a68311f1a45841c8c3b76a37298c3b66f6bcc80409a9b7f33c78a7168a3e90fb65a5d8235ae4",
|
|
89
95
|
"devDependencies": {
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join, relative, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { chromium } from 'playwright-core';
|
|
5
|
+
import { preview } from 'vite';
|
|
6
|
+
|
|
7
|
+
const root = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
8
|
+
const docsRoot = join(root, 'docs');
|
|
9
|
+
const pagePath = 'getting-started/whodb-ee-apps';
|
|
10
|
+
|
|
11
|
+
function docsFiles(directory = docsRoot) {
|
|
12
|
+
return readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
|
|
13
|
+
const file = join(directory, entry.name);
|
|
14
|
+
if (entry.isDirectory()) return docsFiles(file);
|
|
15
|
+
return entry.name.endsWith('.mdx')
|
|
16
|
+
? [{ path: `docs/${relative(docsRoot, file).replaceAll('\\', '/')}`, content: readFileSync(file, 'utf8') }]
|
|
17
|
+
: [];
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const config = JSON.parse(readFileSync(join(docsRoot, 'dory.json'), 'utf8'));
|
|
22
|
+
const logo = readFileSync(join(docsRoot, 'logo/logo.svg')).toString('base64');
|
|
23
|
+
config.logo = {
|
|
24
|
+
light: `data:image/svg+xml;base64,${logo}`,
|
|
25
|
+
dark: `data:image/svg+xml;base64,${logo}`,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function visualMetrics() {
|
|
29
|
+
const measure = selector => {
|
|
30
|
+
const element = document.querySelector(selector);
|
|
31
|
+
if (!element) return null;
|
|
32
|
+
const rect = element.getBoundingClientRect();
|
|
33
|
+
const style = getComputedStyle(element);
|
|
34
|
+
return {
|
|
35
|
+
text: element.textContent?.trim().replace(/\s+/g, ' ').slice(0, 120) ?? '',
|
|
36
|
+
x: rect.x, y: rect.y, width: rect.width, height: rect.height,
|
|
37
|
+
font: style.fontFamily, fontSize: style.fontSize, lineHeight: style.lineHeight,
|
|
38
|
+
weight: style.fontWeight, color: style.color, background: style.backgroundColor,
|
|
39
|
+
padding: style.padding,
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
const article = measure('.dory-article');
|
|
43
|
+
const headings = [...document.querySelectorAll('.dory-article h1')].map(element => {
|
|
44
|
+
const rect = element.getBoundingClientRect();
|
|
45
|
+
const style = getComputedStyle(element);
|
|
46
|
+
return { text: element.textContent?.trim().replace(/\s+/g, ' ') ?? '', y: rect.y, fontSize: style.fontSize, lineHeight: style.lineHeight };
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
html: measure('html'), header: measure('.dory-topbar'), main: measure('.dory-main'), article,
|
|
50
|
+
breadcrumb: measure('.dory-breadcrumb'), activeNav: measure('.dory-sidebar-link.active'),
|
|
51
|
+
topNav: measure('header nav[aria-label]'), firstTab: measure('.dory-topbar-tab'),
|
|
52
|
+
headerSearch: measure('.dory-topbar-search'), sidebarSearch: measure('#dory-embedded-search'),
|
|
53
|
+
pageActions: measure('.dory-page-actions'), firstSection: measure('.dory-article h2'),
|
|
54
|
+
firstParagraph: measure('.dory-article p'), toc: measure('.dory-toc-list'), headings,
|
|
55
|
+
topTabs: [...document.querySelectorAll('.dory-topbar-tab')].map(element => element.textContent?.trim()),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const differences = [];
|
|
60
|
+
function equal(label, expected, actual) {
|
|
61
|
+
if (expected !== actual) differences.push(`${label}: full site ${JSON.stringify(expected)}, embed ${JSON.stringify(actual)}`);
|
|
62
|
+
}
|
|
63
|
+
function near(label, expected, actual, tolerance = 3) {
|
|
64
|
+
if (typeof expected !== 'number' || typeof actual !== 'number' || Math.abs(expected - actual) > tolerance)
|
|
65
|
+
differences.push(`${label}: full site ${Math.round(expected ?? NaN)}px, embed ${Math.round(actual ?? NaN)}px`);
|
|
66
|
+
}
|
|
67
|
+
function compare(full, embed, theme) {
|
|
68
|
+
for (const key of ['html', 'header', 'main', 'article', 'firstSection', 'firstParagraph']) {
|
|
69
|
+
for (const field of ['background', 'color', 'font', 'fontSize', 'lineHeight']) {
|
|
70
|
+
equal(`${theme} ${key}.${field}`, full[key]?.[field], embed[key]?.[field]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const key of ['header', 'main', 'article', 'breadcrumb', 'activeNav', 'topNav', 'firstTab', 'firstSection', 'firstParagraph']) {
|
|
74
|
+
for (const field of ['x', 'width']) near(`${theme} ${key}.${field}`, full[key]?.[field], embed[key]?.[field]);
|
|
75
|
+
}
|
|
76
|
+
equal(`${theme} first tab color`, full.firstTab?.color, embed.firstTab?.color);
|
|
77
|
+
equal(`${theme} breadcrumb`, full.breadcrumb?.text, embed.breadcrumb?.text);
|
|
78
|
+
equal(`${theme} top tabs`, full.topTabs.join('|'), embed.topTabs.join('|'));
|
|
79
|
+
equal(`${theme} page actions present`, Boolean(full.pageActions), Boolean(embed.pageActions));
|
|
80
|
+
equal(`${theme} topbar search present`, Boolean(full.headerSearch), Boolean(embed.headerSearch));
|
|
81
|
+
equal(`${theme} sidebar search present`, Boolean(full.sidebarSearch), Boolean(embed.sidebarSearch));
|
|
82
|
+
equal(`${theme} hero count`, full.headings.length, embed.headings.length);
|
|
83
|
+
for (let index = 0; index < full.headings.length; index++) {
|
|
84
|
+
equal(`${theme} hero ${index + 1} text`, full.headings[index]?.text, embed.headings[index]?.text);
|
|
85
|
+
equal(`${theme} hero ${index + 1} font size`, full.headings[index]?.fontSize, embed.headings[index]?.fontSize);
|
|
86
|
+
near(`${theme} hero ${index + 1} offset`, full.headings[index]?.y - full.article.y, embed.headings[index]?.y - embed.article.y);
|
|
87
|
+
}
|
|
88
|
+
for (const key of ['firstSection', 'firstParagraph', 'activeNav'])
|
|
89
|
+
near(`${theme} ${key} vertical offset`, full[key]?.y - full.article.y, embed[key]?.y - embed.article.y);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const server = process.env.DORY_PARITY_ORIGIN ? null : await preview({
|
|
93
|
+
root,
|
|
94
|
+
preview: { host: '127.0.0.1', port: 0 },
|
|
95
|
+
});
|
|
96
|
+
const port = server?.httpServer.address()?.port;
|
|
97
|
+
const origin = process.env.DORY_PARITY_ORIGIN ?? `http://127.0.0.1:${port}`;
|
|
98
|
+
let browser;
|
|
99
|
+
try {
|
|
100
|
+
browser = await chromium.launch({ channel: 'chrome', headless: true });
|
|
101
|
+
for (const theme of ['light', 'dark']) {
|
|
102
|
+
const full = await browser.newPage({ viewport: { width: 2048, height: 1200 } });
|
|
103
|
+
await full.goto(`${origin}/${pagePath}`, { waitUntil: 'networkidle' });
|
|
104
|
+
if (theme === 'dark') await full.locator('html').evaluate(element => { element.classList.remove('light'); element.classList.add('dark'); });
|
|
105
|
+
await full.evaluate(() => document.fonts.ready);
|
|
106
|
+
const fullMetrics = await full.evaluate(visualMetrics);
|
|
107
|
+
|
|
108
|
+
const embed = await browser.newPage({ viewport: { width: 2048, height: 1200 } });
|
|
109
|
+
await embed.goto(`${origin}/runtime/dory-runtime.html`, { waitUntil: 'networkidle' });
|
|
110
|
+
await embed.evaluate(({ files, config, theme, pagePath }) => {
|
|
111
|
+
window.postMessage({ __dory_docs: true, type: 'load', files, path: `docs/${pagePath}.mdx`, theme, locale: 'en_US', config }, '*');
|
|
112
|
+
}, { files: docsFiles(), config, theme, pagePath });
|
|
113
|
+
await embed.getByRole('heading', { name: 'Document WhoDB EE apps' }).waitFor();
|
|
114
|
+
await embed.evaluate(() => document.fonts.ready);
|
|
115
|
+
const embedMetrics = await embed.evaluate(visualMetrics);
|
|
116
|
+
compare(fullMetrics, embedMetrics, theme);
|
|
117
|
+
if (differences.length || process.env.DORY_PARITY_SCREENSHOTS === '1') {
|
|
118
|
+
await full.screenshot({ path: `/tmp/dory-parity-full-${theme}.png` });
|
|
119
|
+
await embed.screenshot({ path: `/tmp/dory-parity-embed-${theme}.png` });
|
|
120
|
+
}
|
|
121
|
+
await embed.evaluate(({ files, config, theme }) => {
|
|
122
|
+
window.postMessage({ __dory_docs: true, type: 'load', files, path: 'docs/components/accordion.mdx', theme, locale: 'en_US', config }, '*');
|
|
123
|
+
}, { files: docsFiles(), config, theme });
|
|
124
|
+
await embed.getByRole('heading', { name: 'Accordion', exact: true }).first().waitFor();
|
|
125
|
+
const specimen = embed.locator('.dory-specimen').first();
|
|
126
|
+
await specimen.getByRole('tab', { name: 'Code' }).click();
|
|
127
|
+
await specimen.locator('pre').first().waitFor();
|
|
128
|
+
await specimen.getByRole('tab', { name: 'Preview' }).click();
|
|
129
|
+
await embed.evaluate(({ files, config, theme }) => {
|
|
130
|
+
window.postMessage({ __dory_docs: true, type: 'load', files, path: 'docs/components/icons.mdx', theme, locale: 'en_US', config }, '*');
|
|
131
|
+
}, { files: docsFiles(), config, theme });
|
|
132
|
+
await embed.locator('button[title^="Copy <Icon"]').first().waitFor();
|
|
133
|
+
await full.close();
|
|
134
|
+
await embed.close();
|
|
135
|
+
}
|
|
136
|
+
if (differences.length) {
|
|
137
|
+
console.error(`Dory embedded runtime differs from the full docs site:\n${differences.map(item => `- ${item}`).join('\n')}`);
|
|
138
|
+
console.error('Screenshots: /tmp/dory-parity-{full,embed}-{light,dark}.png');
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
} else {
|
|
141
|
+
console.log('Dory full site and embedded runtime visual parity passed in light and dark mode.');
|
|
142
|
+
}
|
|
143
|
+
} finally {
|
|
144
|
+
await browser?.close();
|
|
145
|
+
await new Promise(resolve => server?.httpServer.close(resolve) ?? resolve());
|
|
146
|
+
}
|
package/src/app.css
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
/* UX supplies the shared reset. Dory adds its own generated utilities. */
|
|
2
|
+
@layer theme, base, components, utilities;
|
|
3
|
+
@import "tailwindcss/theme.css" layer(theme);
|
|
4
|
+
@import "tailwindcss/utilities.css" layer(utilities);
|
|
2
5
|
|
|
3
6
|
@custom-variant dark (&:where(.dark, .dark *));
|
|
4
7
|
|
|
8
|
+
.mermaid-wrapper .mermaid svg {
|
|
9
|
+
display: block;
|
|
10
|
+
max-width: 100%;
|
|
11
|
+
height: auto;
|
|
12
|
+
margin-inline: auto;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@media (max-width: 480px) {
|
|
16
|
+
/* Keep the UX 404 illustration inside its narrow stage. Its animation sets left inline. */
|
|
17
|
+
.ux-not-found-character {
|
|
18
|
+
left: clamp(80px, 24vw, 100px) !important;
|
|
19
|
+
scale: .6;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
5
23
|
|
|
Binary file
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Copyright 2021 The Hanken Grotesk Project Authors (https://github.com/marcologous/hanken-grotesk)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
5
|
+
This license is copied below, and is also available with a FAQ at:
|
|
6
|
+
http://scripts.sil.org/OFL
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
-----------------------------------------------------------
|
|
10
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
11
|
+
-----------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
PREAMBLE
|
|
14
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
15
|
+
development of collaborative font projects, to support the font creation
|
|
16
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
17
|
+
open framework in which fonts may be shared and improved in partnership
|
|
18
|
+
with others.
|
|
19
|
+
|
|
20
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
21
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
22
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
23
|
+
redistributed and/or sold with any software provided that any reserved
|
|
24
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
25
|
+
however, cannot be released under any other type of license. The
|
|
26
|
+
requirement for fonts to remain under this license does not apply
|
|
27
|
+
to any document created using the fonts or their derivatives.
|
|
28
|
+
|
|
29
|
+
DEFINITIONS
|
|
30
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
31
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
32
|
+
include source files, build scripts and documentation.
|
|
33
|
+
|
|
34
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
35
|
+
copyright statement(s).
|
|
36
|
+
|
|
37
|
+
"Original Version" refers to the collection of Font Software components as
|
|
38
|
+
distributed by the Copyright Holder(s).
|
|
39
|
+
|
|
40
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
41
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
42
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
43
|
+
new environment.
|
|
44
|
+
|
|
45
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
46
|
+
writer or other person who contributed to the Font Software.
|
|
47
|
+
|
|
48
|
+
PERMISSION & CONDITIONS
|
|
49
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
50
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
51
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
52
|
+
Software, subject to the following conditions:
|
|
53
|
+
|
|
54
|
+
1) Neither the Font Software nor any of its individual components,
|
|
55
|
+
in Original or Modified Versions, may be sold by itself.
|
|
56
|
+
|
|
57
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
58
|
+
redistributed and/or sold with any software, provided that each copy
|
|
59
|
+
contains the above copyright notice and this license. These can be
|
|
60
|
+
included either as stand-alone text files, human-readable headers or
|
|
61
|
+
in the appropriate machine-readable metadata fields within text or
|
|
62
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
63
|
+
|
|
64
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
65
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
66
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
67
|
+
presented to the users.
|
|
68
|
+
|
|
69
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
70
|
+
Software shall not be used to promote, endorse or advertise any
|
|
71
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
72
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
73
|
+
permission.
|
|
74
|
+
|
|
75
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
76
|
+
must be distributed entirely under this license, and must not be
|
|
77
|
+
distributed under any other license. The requirement for fonts to
|
|
78
|
+
remain under this license does not apply to any document created
|
|
79
|
+
using the Font Software.
|
|
80
|
+
|
|
81
|
+
TERMINATION
|
|
82
|
+
This license becomes null and void if any of the above conditions are
|
|
83
|
+
not met.
|
|
84
|
+
|
|
85
|
+
DISCLAIMER
|
|
86
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
87
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
88
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
89
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
90
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
91
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
92
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
93
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
94
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
2
|
+
import { withBase } from '../utils/base-path';
|
|
3
|
+
import { captureDocsEvent } from '../utils/analytics';
|
|
4
|
+
|
|
5
|
+
import { CatalogIcon, Button, CopyButton, copyToClipboard } from '@clidey/ux';
|
|
6
|
+
import docsConfig from '../../docs/dory.json' with { type: 'json' };
|
|
7
|
+
import type { DoryConfig } from '../types/config';
|
|
8
|
+
|
|
9
|
+
export type Citation = { id: string; title: string; url: string; excerpt: string };
|
|
10
|
+
export type Message = { role: 'user' | 'assistant'; content: string; citations?: Citation[]; followUps?: string[] };
|
|
11
|
+
export type ChatTransport = {
|
|
12
|
+
status: () => Promise<boolean>;
|
|
13
|
+
ask: (message: string, history: Pick<Message, 'role' | 'content'>[], pathname: string, signal: AbortSignal) => Promise<{ answer?: string; citations?: Citation[]; followUps?: string[]; error?: string }>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
class ChatRequestError extends Error {
|
|
17
|
+
readonly status: number;
|
|
18
|
+
constructor(message: string, status: number) { super(message); this.status = status; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const defaultTransport: ChatTransport = {
|
|
22
|
+
async status() {
|
|
23
|
+
const response = await fetch(withBase('/api/chat/status'), { credentials: 'same-origin' });
|
|
24
|
+
if (!response.headers.get('content-type')?.includes('application/json')) return false;
|
|
25
|
+
const status = await response.json();
|
|
26
|
+
return status?.enabled === true;
|
|
27
|
+
},
|
|
28
|
+
async ask(message, history, pathname, signal) {
|
|
29
|
+
const response = await fetch(withBase('/api/chat'), {
|
|
30
|
+
method: 'POST', credentials: 'same-origin', signal,
|
|
31
|
+
headers: { 'Content-Type': 'application/json' },
|
|
32
|
+
body: JSON.stringify({ message, history, pathname }),
|
|
33
|
+
});
|
|
34
|
+
const result = await response.json() as { answer?: string; citations?: Citation[]; followUps?: string[]; error?: string };
|
|
35
|
+
if (!response.ok) throw new ChatRequestError(result.error || 'The answer is unavailable right now.', response.status);
|
|
36
|
+
return result;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const site = docsConfig as DoryConfig;
|
|
41
|
+
const suggestedQuestions = Array.isArray(site.chat?.suggestedQuestions)
|
|
42
|
+
? site.chat.suggestedQuestions.filter(question => typeof question === 'string' && question.trim().length >= 3 && question.trim().length <= 600).slice(0, 4)
|
|
43
|
+
: [];
|
|
44
|
+
const starters = suggestedQuestions?.length ? suggestedQuestions : [
|
|
45
|
+
`How do I get started with ${site.name}?`,
|
|
46
|
+
`What can I do with ${site.name}?`,
|
|
47
|
+
'Explain this page',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
function renderInline(text: string) {
|
|
51
|
+
return text.split(/(\*\*[^*]+\*\*|`[^`]+`)/g).map((part, index) => {
|
|
52
|
+
if (part.startsWith('**') && part.endsWith('**')) return <strong key={index}>{part.slice(2, -2)}</strong>;
|
|
53
|
+
if (part.startsWith('`') && part.endsWith('`')) return <code key={index} className="rounded bg-[var(--accent)] px-1 py-0.5 font-mono text-[0.9em]">{part.slice(1, -1)}</code>;
|
|
54
|
+
return part;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function AnswerText({ text }: { text: string }) {
|
|
59
|
+
return <div className="space-y-3 text-sm leading-7 text-foreground sm:text-base">{text.split(/(```[^\n]*\n[\s\S]*?```)/g).map((part, index) => {
|
|
60
|
+
if (part.startsWith('```') && part.endsWith('```')) {
|
|
61
|
+
const language = part.slice(3, part.indexOf('\n')).trim();
|
|
62
|
+
const code = part.slice(part.indexOf('\n') + 1, -3).trimEnd();
|
|
63
|
+
return <div key={index} className="overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--accent)]">
|
|
64
|
+
<div className="flex items-center justify-between border-b border-[var(--border)] px-3 py-1 text-xs text-muted-foreground">
|
|
65
|
+
<span>{language || 'Code'}</span>
|
|
66
|
+
<CopyButton text={code} tooltipLabel="Copy code" copiedLabel="Code copied" onCopy={() => captureDocsEvent('docs_chat_code_copied', { path: window.location.pathname })} />
|
|
67
|
+
</div>
|
|
68
|
+
<pre className="overflow-x-auto p-4 text-sm leading-6"><code>{code}</code></pre>
|
|
69
|
+
</div>;
|
|
70
|
+
}
|
|
71
|
+
return part.split(/\n\s*\n/).filter(Boolean).map((paragraph, paragraphIndex) => <p key={`${index}-${paragraphIndex}`} className="whitespace-pre-wrap">{renderInline(paragraph.trim())}</p>);
|
|
72
|
+
})}</div>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function CopyAnswerButton({ text }: { text: string }) {
|
|
76
|
+
const [copied, setCopied] = useState(false);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!copied) return;
|
|
79
|
+
const timeout = window.setTimeout(() => setCopied(false), 2000);
|
|
80
|
+
return () => window.clearTimeout(timeout);
|
|
81
|
+
}, [copied]);
|
|
82
|
+
|
|
83
|
+
return <Button type="button" variant="ghost" size="sm" className="gap-2 text-xs text-muted-foreground" onClick={async () => {
|
|
84
|
+
if (await copyToClipboard(text)) {
|
|
85
|
+
setCopied(true);
|
|
86
|
+
captureDocsEvent('docs_chat_answer_copied', { path: window.location.pathname });
|
|
87
|
+
}
|
|
88
|
+
}}>
|
|
89
|
+
<CatalogIcon name={copied ? 'check' : 'copy'} className="h-4 w-4" />
|
|
90
|
+
{copied ? 'Copied' : 'Copy answer'}
|
|
91
|
+
</Button>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function useChatAssistant(options: { transport?: ChatTransport; initialMessages?: Message[]; initialDraft?: string; onMessagesChange?: (messages: Message[]) => void } = {}) {
|
|
95
|
+
const transport = options.transport ?? defaultTransport;
|
|
96
|
+
const [enabled, setEnabled] = useState(false);
|
|
97
|
+
const [messages, setMessages] = useState<Message[]>(options.initialMessages ?? []);
|
|
98
|
+
const [draft, setDraft] = useState(options.initialDraft ?? '');
|
|
99
|
+
const [busy, setBusy] = useState(false);
|
|
100
|
+
const [error, setError] = useState('');
|
|
101
|
+
const controller = useRef<AbortController | null>(null);
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
transport.status()
|
|
105
|
+
.then(setEnabled)
|
|
106
|
+
.catch(() => setEnabled(false));
|
|
107
|
+
return () => controller.current?.abort();
|
|
108
|
+
}, []);
|
|
109
|
+
useEffect(() => { options.onMessagesChange?.(messages); }, [messages]);
|
|
110
|
+
|
|
111
|
+
function reset() {
|
|
112
|
+
controller.current?.abort();
|
|
113
|
+
controller.current = null;
|
|
114
|
+
setMessages([]);
|
|
115
|
+
setDraft('');
|
|
116
|
+
setError('');
|
|
117
|
+
setBusy(false);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function ask(question = draft) {
|
|
121
|
+
const text = question.trim();
|
|
122
|
+
if (busy || text.length < 3 || text.length > 600) return;
|
|
123
|
+
const previous = messages.slice(-6).map(({ role, content }) => ({ role, content }));
|
|
124
|
+
setMessages(current => [...current, { role: 'user', content: text }]);
|
|
125
|
+
setDraft('');
|
|
126
|
+
setError('');
|
|
127
|
+
setBusy(true);
|
|
128
|
+
captureDocsEvent('docs_chat_question', { path: window.location.pathname, question_length: text.length });
|
|
129
|
+
let errorKind = 'network';
|
|
130
|
+
const request = new AbortController();
|
|
131
|
+
controller.current = request;
|
|
132
|
+
try {
|
|
133
|
+
const result = await transport.ask(text, previous, window.location.pathname, request.signal);
|
|
134
|
+
if (!result.answer) { errorKind = 'invalid_answer'; throw new Error(result.error || 'The answer is unavailable right now.'); }
|
|
135
|
+
if (!request.signal.aborted) {
|
|
136
|
+
setMessages(current => [...current, { role: 'assistant', content: result.answer!, citations: result.citations, followUps: result.followUps }]);
|
|
137
|
+
captureDocsEvent('docs_chat_answer', { path: window.location.pathname, citation_count: result.citations?.length ?? 0 });
|
|
138
|
+
}
|
|
139
|
+
} catch (cause) {
|
|
140
|
+
if (!request.signal.aborted) {
|
|
141
|
+
if (cause instanceof ChatRequestError) errorKind = cause.status === 429 ? 'rate_limited' : 'server_error';
|
|
142
|
+
setError(cause instanceof Error ? cause.message : 'The answer is unavailable right now.');
|
|
143
|
+
captureDocsEvent('docs_chat_error', { path: window.location.pathname, error_kind: errorKind });
|
|
144
|
+
}
|
|
145
|
+
} finally {
|
|
146
|
+
if (controller.current === request) {
|
|
147
|
+
controller.current = null;
|
|
148
|
+
setBusy(false);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { enabled, messages, draft, setDraft, busy, error, ask, reset };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function ChatAssistant({ chat, onNavigate, onSearch, siteName = site.name, questions = starters, intro, onSourceClick }: {
|
|
157
|
+
chat: ReturnType<typeof useChatAssistant>;
|
|
158
|
+
onNavigate: () => void;
|
|
159
|
+
onSearch: () => void;
|
|
160
|
+
siteName?: string;
|
|
161
|
+
questions?: string[];
|
|
162
|
+
intro?: string;
|
|
163
|
+
onSourceClick?: (source: Citation) => void;
|
|
164
|
+
}) {
|
|
165
|
+
const bottom = useRef<HTMLDivElement>(null);
|
|
166
|
+
const input = useRef<HTMLTextAreaElement>(null);
|
|
167
|
+
|
|
168
|
+
useEffect(() => { input.current?.focus(); }, [chat.messages.length === 0]);
|
|
169
|
+
useEffect(() => bottom.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }), [chat.messages, chat.busy]);
|
|
170
|
+
|
|
171
|
+
const composer = (
|
|
172
|
+
<form className="dory-chat-composer border-t border-[var(--border)] p-4 sm:px-6" onSubmit={event => { event.preventDefault(); chat.ask(); }}>
|
|
173
|
+
<label htmlFor="dory-chat-question" className="sr-only">Your documentation question</label>
|
|
174
|
+
<div className="flex items-end gap-2 rounded-xl border border-[var(--border)] bg-[var(--background)] px-3 py-2 focus-within:border-[var(--brand-primary)]">
|
|
175
|
+
<textarea id="dory-chat-question" ref={input} value={chat.draft} onInput={event => chat.setDraft(event.currentTarget.value)} onKeyDown={event => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); chat.ask(); } else if (event.key === 'Tab' && !event.shiftKey) { event.preventDefault(); onSearch(); } }} rows={1} maxLength={600} placeholder={chat.messages.length ? 'Ask a follow-up' : `Ask Dory anything about ${siteName}`} className="max-h-28 min-h-10 flex-1 resize-none bg-transparent px-2 py-2 text-sm outline-none" />
|
|
176
|
+
<button type="button" disabled={chat.busy || chat.draft.trim().length < 3} onClick={() => void chat.ask()} className="rounded-lg bg-[var(--brand-primary)] px-3 py-2 text-sm font-semibold text-white disabled:opacity-40" aria-label="Send question">↑</button>
|
|
177
|
+
</div>
|
|
178
|
+
{chat.messages.length > 0 && <p className="mt-2 text-center text-[11px] text-muted-foreground">Sources are linked when the docs support an answer. Check important details on the page.</p>}
|
|
179
|
+
</form>
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<div className={`dory-search-chat flex flex-col ${chat.messages.length === 0 ? 'h-[min(58vh,480px)]' : 'h-[min(78vh,640px)] sm:h-[min(72vh,620px)]'}`}>
|
|
184
|
+
{chat.messages.length === 0 && composer}
|
|
185
|
+
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-6 sm:px-8" aria-live="polite">
|
|
186
|
+
{chat.messages.length === 0 && (
|
|
187
|
+
<div className="space-y-5">
|
|
188
|
+
<p className="text-sm leading-6 text-muted-foreground">{intro ?? 'Ask in plain language. Dory reads these docs and answers with links to the sections it used.'}</p>
|
|
189
|
+
<div className="grid gap-2">
|
|
190
|
+
{questions.map(prompt => <button key={prompt} type="button" onClick={() => chat.ask(prompt)} className="flex items-center gap-3 rounded-lg px-3 py-2 text-left text-sm hover:bg-[var(--accent)]"><CatalogIcon name="arrow-up-right" className="h-4 w-4 shrink-0 text-[var(--primary)]" /><span>{prompt}</span></button>)}
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
<div className="space-y-6">
|
|
195
|
+
{chat.messages.map((message, index) => (
|
|
196
|
+
<div key={index} className="space-y-3">
|
|
197
|
+
{message.role === 'user' ? (
|
|
198
|
+
<h2 className="text-xl font-semibold leading-snug text-foreground">{message.content}</h2>
|
|
199
|
+
) : (
|
|
200
|
+
<div className="dory-chat-answer space-y-4">
|
|
201
|
+
<AnswerText text={message.content} />
|
|
202
|
+
<CopyAnswerButton text={message.content} />
|
|
203
|
+
{message.citations && message.citations.length > 0 && (
|
|
204
|
+
<div className="space-y-2 pt-2">
|
|
205
|
+
<p className="text-xs font-semibold uppercase tracking-widest text-muted-foreground">Sources</p>
|
|
206
|
+
<div className="flex flex-wrap gap-2">
|
|
207
|
+
{message.citations.map((source, sourceIndex) => <a key={source.id} href={onSourceClick ? '#' : withBase(source.url)} onClick={event => { if (onSourceClick) { event.preventDefault(); onSourceClick(source); } else { captureDocsEvent('docs_chat_source_clicked', { path: window.location.pathname, source: source.url }); onNavigate(); } }} className="min-w-36 rounded-lg border border-[var(--border)] px-3 py-2 hover:bg-[var(--accent)]">
|
|
208
|
+
<span className="block text-sm font-medium text-foreground"><span className="mr-2 text-[var(--brand-primary)]">{sourceIndex + 1}</span>{source.title}</span>
|
|
209
|
+
{source.excerpt && <span className="mt-1 block line-clamp-2 text-xs text-muted-foreground">{source.excerpt}</span>}
|
|
210
|
+
</a>)}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
{message.followUps && message.followUps.length > 0 && (
|
|
215
|
+
<div className="flex flex-wrap gap-2 pt-2">{message.followUps.map(prompt => <button key={prompt} type="button" onClick={() => chat.ask(prompt)} disabled={chat.busy} className="rounded-full border border-[var(--border)] px-3 py-1.5 text-xs hover:bg-[var(--accent)] disabled:opacity-50">→ {prompt}</button>)}</div>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
220
|
+
))}
|
|
221
|
+
{chat.busy && <p className="text-sm text-muted-foreground" role="status">Reading the docs and checking sources…</p>}
|
|
222
|
+
{chat.error && <p className="rounded-lg bg-red-500/10 p-3 text-sm text-red-700 dark:text-red-300" role="alert">{chat.error}</p>}
|
|
223
|
+
<div ref={bottom} />
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
{chat.messages.length > 0 && composer}
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CatalogIcon,
|
|
2
2
|
DropdownMenu,
|
|
3
3
|
DropdownMenuTrigger,
|
|
4
4
|
DropdownMenuContent,
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
DropdownMenuSeparator,
|
|
7
7
|
Button
|
|
8
8
|
} from '@clidey/ux'
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
import type { ComponentChildren } from 'preact'
|
|
11
11
|
import type { ReactNode } from 'preact/compat'
|
|
12
12
|
|
|
@@ -30,7 +30,7 @@ export default function Dropdown({ buttonLabel, items, className = '' }: Dropdow
|
|
|
30
30
|
<DropdownMenuTrigger>
|
|
31
31
|
<Button className={className}>
|
|
32
32
|
{buttonLabel}
|
|
33
|
-
<
|
|
33
|
+
<CatalogIcon name="chevron-down" className="ml-2 h-4 w-4" />
|
|
34
34
|
</Button>
|
|
35
35
|
</DropdownMenuTrigger>
|
|
36
36
|
<DropdownMenuContent>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { CatalogIcon } from '@clidey/ux'
|
|
1
2
|
import { Component, type ComponentChildren } from 'preact';
|
|
2
|
-
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline';
|
|
3
3
|
|
|
4
4
|
interface ErrorBoundaryProps {
|
|
5
5
|
children: ComponentChildren;
|
|
@@ -33,7 +33,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
33
33
|
return (
|
|
34
34
|
<div className="flex flex-col items-center justify-center min-h-[50vh] p-8">
|
|
35
35
|
<div className="w-16 h-16 mb-4 flex items-center justify-center bg-red-100 dark:bg-red-900/30 rounded-full">
|
|
36
|
-
<
|
|
36
|
+
<CatalogIcon name="warning" className="w-8 h-8 text-red-500" />
|
|
37
37
|
</div>
|
|
38
38
|
<h2 className="text-2xl font-semibold text-red-700 dark:text-red-300 mb-2">
|
|
39
39
|
Something went wrong
|