radiant-docs 0.1.62 → 0.1.64
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 +1 -1
- package/template/astro.config.mjs +38 -27
- package/template/package-lock.json +2857 -1145
- package/template/package.json +16 -20
- package/template/scripts/generate-proxy-allowed-origins.mjs +10 -187
- package/template/scripts/publish-shiki-platform-assets.mjs +32 -6
- package/template/src/components/chat/AssistantEmbedPanel.tsx +133 -1
- package/template/src/components/endpoint/PlaygroundForm.astro +69 -55
- package/template/src/components/endpoint/ResponseDisplay.astro +2 -2
- package/template/src/generated/shiki-platform-assets.json +8 -8
- package/template/src/lib/assistant-panel-config.ts +2 -57
- package/template/src/lib/assistant-shiki-client.ts +16 -0
- package/template/src/lib/client-shiki-config.ts +63 -0
- package/template/src/lib/dev-playground-proxy.mjs +597 -0
- package/template/src/lib/proxy-allowed-origins.mjs +189 -0
- package/template/src/styles/global.css +4 -4
- package/template/src/components/ui/demo/CodeDemo.astro +0 -15
- package/template/src/components/ui/demo/Demo.astro +0 -3
- package/template/src/components/ui/demo/UiDisplay.astro +0 -13
package/template/package.json
CHANGED
|
@@ -10,18 +10,19 @@
|
|
|
10
10
|
"preview": "astro preview",
|
|
11
11
|
"astro": "astro",
|
|
12
12
|
"search:index": "astro build && node scripts/remove-assistant-for-non-pro.mjs && node scripts/generate-og-metadata.mjs && node scripts/generate-og-images.mjs && node scripts/stamp-og-image-versions.mjs && node scripts/stamp-image-versions.mjs && pagefind --site dist && node scripts/stamp-pagefind-runtime-version.mjs && node scripts/generate-proxy-allowed-origins.mjs && node scripts/generate-robots-txt.mjs && cp -r dist/pagefind public/",
|
|
13
|
-
"shiki:
|
|
14
|
-
"shiki:
|
|
15
|
-
"shiki:
|
|
13
|
+
"shiki:assets:update": "node scripts/publish-shiki-platform-assets.mjs --prepare-only --write-current",
|
|
14
|
+
"shiki:assets:upload": "node scripts/publish-shiki-platform-assets.mjs --check-current",
|
|
15
|
+
"shiki:assets:upload:dev": "node scripts/publish-shiki-platform-assets.mjs --env-file .env --check-current"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@alpinejs/collapse": "^3.15.2",
|
|
19
19
|
"@alpinejs/focus": "^3.15.3",
|
|
20
20
|
"@alpinejs/persist": "^3.15.2",
|
|
21
|
-
"@astrojs/alpinejs": "^0.
|
|
22
|
-
"@astrojs/
|
|
23
|
-
"@astrojs/
|
|
24
|
-
"@astrojs/
|
|
21
|
+
"@astrojs/alpinejs": "^1.0.0",
|
|
22
|
+
"@astrojs/markdown-remark": "^7.2.0",
|
|
23
|
+
"@astrojs/mdx": "^7.0.0",
|
|
24
|
+
"@astrojs/preact": "^6.0.0",
|
|
25
|
+
"@astrojs/sitemap": "^3.7.3",
|
|
25
26
|
"@aws-sdk/client-s3": "^3.964.0",
|
|
26
27
|
"@fontsource-variable/geist": "^5.2.9",
|
|
27
28
|
"@fontsource-variable/geist-mono": "^5.2.8",
|
|
@@ -37,22 +38,21 @@
|
|
|
37
38
|
"@iconify-json/lucide": "^1.2.79",
|
|
38
39
|
"@iconify-json/simple-icons": "^1.2.69",
|
|
39
40
|
"@iconify/react": "^6.0.2",
|
|
40
|
-
"@jongwooo/prism-theme-github": "^1.15.1",
|
|
41
41
|
"@paper-design/shaders": "^0.0.76",
|
|
42
42
|
"@preact/preset-vite": "^2.10.3",
|
|
43
43
|
"@readme/oas-to-snippet": "^29.3.0",
|
|
44
44
|
"@resvg/resvg-js": "^2.6.2",
|
|
45
|
-
"@shikijs/core": "
|
|
46
|
-
"@shikijs/engine-javascript": "
|
|
47
|
-
"@shikijs/langs": "
|
|
48
|
-
"@shikijs/themes": "
|
|
45
|
+
"@shikijs/core": "4.2.0",
|
|
46
|
+
"@shikijs/engine-javascript": "4.2.0",
|
|
47
|
+
"@shikijs/langs": "4.2.0",
|
|
48
|
+
"@shikijs/themes": "4.2.0",
|
|
49
49
|
"@stoplight/spectral-core": "^1.20.0",
|
|
50
50
|
"@stoplight/spectral-rulesets": "^1.22.0",
|
|
51
51
|
"@tailwindcss/typography": "^0.5.19",
|
|
52
52
|
"@tailwindcss/vite": "^4.1.17",
|
|
53
53
|
"@xt0rted/expressive-code-file-icons": "^1.0.0",
|
|
54
54
|
"alpinejs": "^3.15.2",
|
|
55
|
-
"astro": "^
|
|
55
|
+
"astro": "^7.0.2",
|
|
56
56
|
"astro-icon": "^1.1.5",
|
|
57
57
|
"fontkit": "^2.0.4",
|
|
58
58
|
"fs-extra": "^11.3.3",
|
|
@@ -62,23 +62,19 @@
|
|
|
62
62
|
"oas": "^28.7.0",
|
|
63
63
|
"openapi-sampler": "^1.6.2",
|
|
64
64
|
"preact": "^10.29.0",
|
|
65
|
-
"
|
|
66
|
-
"prismjs": "^1.30.0",
|
|
67
|
-
"radiant-docs-validator": "^0.1.28",
|
|
65
|
+
"radiant-docs-validator": "^0.1.29",
|
|
68
66
|
"rehype-autolink-headings": "^7.1.0",
|
|
69
67
|
"rehype-slug": "^6.0.0",
|
|
70
68
|
"remark-gfm": "^4.0.1",
|
|
69
|
+
"shiki": "4.2.0",
|
|
71
70
|
"simple-git": "^3.30.0",
|
|
72
|
-
"shiki": "3.23.0",
|
|
73
71
|
"tailwindcss": "^4.2.2",
|
|
74
72
|
"wawoff2": "^2.0.1",
|
|
75
|
-
"yaml": "^2.8.2"
|
|
76
|
-
"zod": "^3.25.76"
|
|
73
|
+
"yaml": "^2.8.2"
|
|
77
74
|
},
|
|
78
75
|
"devDependencies": {
|
|
79
76
|
"@types/fs-extra": "^11.0.4",
|
|
80
77
|
"@types/mime-types": "^3.0.1",
|
|
81
|
-
"@types/prismjs": "^1.26.5",
|
|
82
78
|
"esbuild": "^0.25.12",
|
|
83
79
|
"pagefind": "^1.4.0",
|
|
84
80
|
"tsx": "^4.21.0"
|
|
@@ -1,201 +1,24 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import
|
|
3
|
+
import { buildAllowedOrigins } from "../src/lib/proxy-allowed-origins.mjs";
|
|
4
4
|
|
|
5
5
|
const CWD = process.cwd();
|
|
6
|
-
const DOCS_DIR = path.join(CWD, "src/content/docs");
|
|
7
|
-
const DOCS_CONFIG_PATH = path.join(DOCS_DIR, "docs.json");
|
|
8
6
|
const OUTPUT_DIR = path.join(CWD, ".radiant");
|
|
9
7
|
const OUTPUT_FILE = path.join(OUTPUT_DIR, "proxy-allowed-origins.json");
|
|
10
8
|
|
|
11
|
-
function isRecord(value) {
|
|
12
|
-
return typeof value === "object" && value !== null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function isHttpUrl(value) {
|
|
16
|
-
return /^https?:\/\//i.test(String(value).trim());
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function collectOpenApiSourcesFromNavigation(navigationNode, target) {
|
|
20
|
-
if (!isRecord(navigationNode)) {
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const openApiConfig = navigationNode.openapi;
|
|
25
|
-
if (typeof openApiConfig === "string") {
|
|
26
|
-
const trimmed = openApiConfig.trim();
|
|
27
|
-
if (trimmed) {
|
|
28
|
-
target.add(trimmed);
|
|
29
|
-
}
|
|
30
|
-
} else if (
|
|
31
|
-
isRecord(openApiConfig) &&
|
|
32
|
-
typeof openApiConfig.source === "string"
|
|
33
|
-
) {
|
|
34
|
-
const trimmed = openApiConfig.source.trim();
|
|
35
|
-
if (trimmed) {
|
|
36
|
-
target.add(trimmed);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const menu = navigationNode.menu;
|
|
41
|
-
if (isRecord(menu) && Array.isArray(menu.items)) {
|
|
42
|
-
for (const menuItem of menu.items) {
|
|
43
|
-
if (!isRecord(menuItem)) {
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
collectOpenApiSourcesFromNavigation(menuItem, target);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const tabs = navigationNode.tabs;
|
|
51
|
-
if (isRecord(tabs) && Array.isArray(tabs.items)) {
|
|
52
|
-
for (const tabItem of tabs.items) {
|
|
53
|
-
if (!isRecord(tabItem)) {
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
collectOpenApiSourcesFromNavigation(tabItem, target);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function resolveServerTemplateUrl(rawUrl, rawVariables) {
|
|
62
|
-
let unresolved = false;
|
|
63
|
-
const variables = isRecord(rawVariables) ? rawVariables : null;
|
|
64
|
-
|
|
65
|
-
const resolved = rawUrl.replace(/\{([^}]+)\}/g, (_match, tokenName) => {
|
|
66
|
-
if (!variables) {
|
|
67
|
-
unresolved = true;
|
|
68
|
-
return "";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const variableConfig = variables[tokenName];
|
|
72
|
-
if (
|
|
73
|
-
!isRecord(variableConfig) ||
|
|
74
|
-
typeof variableConfig.default !== "string"
|
|
75
|
-
) {
|
|
76
|
-
unresolved = true;
|
|
77
|
-
return "";
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const defaultValue = variableConfig.default.trim();
|
|
81
|
-
if (!defaultValue) {
|
|
82
|
-
unresolved = true;
|
|
83
|
-
return "";
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return defaultValue;
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
if (unresolved) {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return resolved;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function normalizeAllowedOrigin(rawUrl) {
|
|
97
|
-
let parsed;
|
|
98
|
-
try {
|
|
99
|
-
parsed = new URL(rawUrl);
|
|
100
|
-
} catch {
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (parsed.protocol !== "https:") {
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (!parsed.hostname || parsed.username || parsed.password) {
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return parsed.origin.toLowerCase();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async function loadOpenApiSpec(source) {
|
|
116
|
-
let fileContent;
|
|
117
|
-
|
|
118
|
-
if (isHttpUrl(source)) {
|
|
119
|
-
const response = await fetch(source);
|
|
120
|
-
if (!response.ok) {
|
|
121
|
-
throw new Error(
|
|
122
|
-
`Failed to fetch OpenAPI spec (${response.status} ${response.statusText})`,
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
fileContent = await response.text();
|
|
126
|
-
} else {
|
|
127
|
-
const absolutePath = path.join(DOCS_DIR, source);
|
|
128
|
-
fileContent = await fs.readFile(absolutePath, "utf8");
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const trimmed = fileContent.trim();
|
|
132
|
-
if (trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html")) {
|
|
133
|
-
throw new Error("OpenAPI source returned HTML instead of JSON or YAML");
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
return JSON.parse(fileContent);
|
|
138
|
-
} catch {
|
|
139
|
-
return YAML.parse(fileContent);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async function buildAllowedOrigins() {
|
|
144
|
-
const docsConfigRaw = await fs.readFile(DOCS_CONFIG_PATH, "utf8");
|
|
145
|
-
const docsConfig = JSON.parse(docsConfigRaw);
|
|
146
|
-
|
|
147
|
-
if (!isRecord(docsConfig) || !isRecord(docsConfig.navigation)) {
|
|
148
|
-
return [];
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const sources = new Set();
|
|
152
|
-
collectOpenApiSourcesFromNavigation(docsConfig.navigation, sources);
|
|
153
|
-
if (sources.size === 0) {
|
|
154
|
-
return [];
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const allowedOrigins = new Set();
|
|
158
|
-
|
|
159
|
-
for (const source of sources) {
|
|
160
|
-
try {
|
|
161
|
-
const spec = await loadOpenApiSpec(source);
|
|
162
|
-
const servers =
|
|
163
|
-
isRecord(spec) && Array.isArray(spec.servers) ? spec.servers : [];
|
|
164
|
-
|
|
165
|
-
for (const server of servers) {
|
|
166
|
-
if (!isRecord(server) || typeof server.url !== "string") {
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const resolvedUrl = resolveServerTemplateUrl(
|
|
171
|
-
server.url,
|
|
172
|
-
server.variables,
|
|
173
|
-
);
|
|
174
|
-
if (!resolvedUrl) {
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const normalizedOrigin = normalizeAllowedOrigin(resolvedUrl);
|
|
179
|
-
if (normalizedOrigin) {
|
|
180
|
-
allowedOrigins.add(normalizedOrigin);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
} catch (error) {
|
|
184
|
-
console.warn(
|
|
185
|
-
`⚠️ Failed to extract OpenAPI server origins from "${source}":`,
|
|
186
|
-
error instanceof Error ? error.message : String(error),
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return Array.from(allowedOrigins).sort();
|
|
192
|
-
}
|
|
193
|
-
|
|
194
9
|
async function run() {
|
|
195
10
|
let allowedOrigins = [];
|
|
196
11
|
|
|
197
12
|
try {
|
|
198
|
-
allowedOrigins = await buildAllowedOrigins(
|
|
13
|
+
allowedOrigins = await buildAllowedOrigins({
|
|
14
|
+
cwd: CWD,
|
|
15
|
+
onSourceError(source, error) {
|
|
16
|
+
console.warn(
|
|
17
|
+
`⚠️ Failed to extract OpenAPI server origins from "${source}":`,
|
|
18
|
+
error instanceof Error ? error.message : String(error),
|
|
19
|
+
);
|
|
20
|
+
},
|
|
21
|
+
});
|
|
199
22
|
} catch (error) {
|
|
200
23
|
console.warn(
|
|
201
24
|
"⚠️ Failed to generate proxy allowed origins. Falling back to empty allowlist.",
|
|
@@ -39,7 +39,7 @@ const DEFAULT_R2_REQUEST_TIMEOUT_MS = 30_000;
|
|
|
39
39
|
const IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
40
40
|
const MARKER_CACHE_CONTROL = "no-store";
|
|
41
41
|
const UPLOAD_COMPLETE_MARKER = ".radiant-upload-complete.json";
|
|
42
|
-
const UPLOAD_MARKER_VERSION =
|
|
42
|
+
const UPLOAD_MARKER_VERSION = 2;
|
|
43
43
|
|
|
44
44
|
const LICENSE_PACKAGES = [
|
|
45
45
|
"shiki",
|
|
@@ -658,7 +658,7 @@ async function checkCurrentAssetFile(filePath, descriptor) {
|
|
|
658
658
|
if (!(await pathExists(filePath))) {
|
|
659
659
|
throw new Error(
|
|
660
660
|
`Current Shiki platform asset file is missing: ${filePath}\n` +
|
|
661
|
-
"Run: npm --prefix framework run shiki:
|
|
661
|
+
"Run: npm --prefix framework run shiki:assets:update",
|
|
662
662
|
);
|
|
663
663
|
}
|
|
664
664
|
|
|
@@ -671,7 +671,7 @@ async function checkCurrentAssetFile(filePath, descriptor) {
|
|
|
671
671
|
`Current Shiki platform asset file is out of date: ${filePath}\n` +
|
|
672
672
|
`Expected: ${describeCurrentDescriptor(descriptor)}\n` +
|
|
673
673
|
`Found: ${describeCurrentDescriptor(current)}\n` +
|
|
674
|
-
"Run: npm --prefix framework run shiki:
|
|
674
|
+
"Run: npm --prefix framework run shiki:assets:update",
|
|
675
675
|
);
|
|
676
676
|
}
|
|
677
677
|
|
|
@@ -808,7 +808,24 @@ async function sendR2CommandWithTimeout(s3, command, { label, timeoutMs }) {
|
|
|
808
808
|
}
|
|
809
809
|
}
|
|
810
810
|
|
|
811
|
-
|
|
811
|
+
function normalizeCacheControl(value) {
|
|
812
|
+
return String(value ?? "")
|
|
813
|
+
.trim()
|
|
814
|
+
.toLowerCase()
|
|
815
|
+
.replace(/\s*,\s*/g, ",");
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function cacheControlMatches(actual, expected) {
|
|
819
|
+
return normalizeCacheControl(actual) === normalizeCacheControl(expected);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
async function objectMatchesExpectedAssetMetadata(
|
|
823
|
+
s3,
|
|
824
|
+
bucket,
|
|
825
|
+
key,
|
|
826
|
+
sha256,
|
|
827
|
+
requestTimeoutMs,
|
|
828
|
+
) {
|
|
812
829
|
try {
|
|
813
830
|
const response = await sendR2CommandWithTimeout(
|
|
814
831
|
s3,
|
|
@@ -821,7 +838,10 @@ async function objectHasSameHash(s3, bucket, key, sha256, requestTimeoutMs) {
|
|
|
821
838
|
timeoutMs: requestTimeoutMs,
|
|
822
839
|
},
|
|
823
840
|
);
|
|
824
|
-
return
|
|
841
|
+
return (
|
|
842
|
+
response.Metadata?.["radiant-sha256"] === sha256 &&
|
|
843
|
+
cacheControlMatches(response.CacheControl, IMMUTABLE_CACHE_CONTROL)
|
|
844
|
+
);
|
|
825
845
|
} catch (error) {
|
|
826
846
|
const statusCode = error?.$metadata?.httpStatusCode;
|
|
827
847
|
if (statusCode === 403 || statusCode === 404) return false;
|
|
@@ -856,6 +876,10 @@ async function completionMarkerMatches({
|
|
|
856
876
|
metadata["radiant-shiki-asset-version"] === assetVersion &&
|
|
857
877
|
metadata["radiant-shiki-file-count"] === String(fileCount) &&
|
|
858
878
|
metadata["radiant-shiki-manifest-sha256"] === manifestSha256 &&
|
|
879
|
+
cacheControlMatches(
|
|
880
|
+
metadata["radiant-shiki-cache-control"],
|
|
881
|
+
IMMUTABLE_CACHE_CONTROL,
|
|
882
|
+
) &&
|
|
859
883
|
metadata["radiant-shiki-marker-version"] ===
|
|
860
884
|
String(UPLOAD_MARKER_VERSION)
|
|
861
885
|
);
|
|
@@ -875,6 +899,7 @@ function buildCompletionMarker({
|
|
|
875
899
|
}) {
|
|
876
900
|
return {
|
|
877
901
|
assetVersion,
|
|
902
|
+
assetCacheControl: IMMUTABLE_CACHE_CONTROL,
|
|
878
903
|
completedAt: new Date().toISOString(),
|
|
879
904
|
fileCount,
|
|
880
905
|
manifest: {
|
|
@@ -918,6 +943,7 @@ async function uploadCompletionMarker({
|
|
|
918
943
|
Metadata: {
|
|
919
944
|
"radiant-sha256": hashBuffer(body),
|
|
920
945
|
"radiant-shiki-asset-version": assetVersion,
|
|
946
|
+
"radiant-shiki-cache-control": IMMUTABLE_CACHE_CONTROL,
|
|
921
947
|
"radiant-shiki-file-count": String(fileCount),
|
|
922
948
|
"radiant-shiki-manifest-sha256": manifestSha256,
|
|
923
949
|
"radiant-shiki-marker-version": String(UPLOAD_MARKER_VERSION),
|
|
@@ -986,7 +1012,7 @@ async function uploadAssets({
|
|
|
986
1012
|
const body = await fs.readFile(absolutePath);
|
|
987
1013
|
const sha256 = hashBuffer(body);
|
|
988
1014
|
const key = joinPosix(uploadPrefix, relativePath);
|
|
989
|
-
const shouldSkip = await
|
|
1015
|
+
const shouldSkip = await objectMatchesExpectedAssetMetadata(
|
|
990
1016
|
s3,
|
|
991
1017
|
args.bucket,
|
|
992
1018
|
key,
|
|
@@ -10,6 +10,7 @@ import { getDocsBasePath, withBasePath } from "../../lib/base-path";
|
|
|
10
10
|
import {
|
|
11
11
|
highlightAssistantCodeToHtml,
|
|
12
12
|
normalizeAssistantCodeLanguage,
|
|
13
|
+
warmAssistantShikiRuntime,
|
|
13
14
|
type AssistantShikiRuntimeConfig,
|
|
14
15
|
} from "../../lib/assistant-shiki-client";
|
|
15
16
|
|
|
@@ -56,15 +57,22 @@ type AssistantColorByMode = {
|
|
|
56
57
|
dark: string;
|
|
57
58
|
};
|
|
58
59
|
|
|
60
|
+
type AssistantMessageSource = {
|
|
61
|
+
href: string;
|
|
62
|
+
label: string;
|
|
63
|
+
};
|
|
64
|
+
|
|
59
65
|
type ChatMessage = {
|
|
60
66
|
id: string;
|
|
61
67
|
role: "user" | "assistant";
|
|
62
68
|
content: string;
|
|
69
|
+
sources?: AssistantMessageSource[];
|
|
63
70
|
};
|
|
64
71
|
|
|
65
72
|
type AssistantStreamEvent = {
|
|
66
73
|
type?: string;
|
|
67
74
|
delta?: string;
|
|
75
|
+
sources?: unknown;
|
|
68
76
|
};
|
|
69
77
|
|
|
70
78
|
type ChatInputKeyEvent = JSX.TargetedKeyboardEvent<HTMLTextAreaElement>;
|
|
@@ -351,6 +359,33 @@ function createMessageId(): string {
|
|
|
351
359
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
352
360
|
}
|
|
353
361
|
|
|
362
|
+
function normalizeAssistantMessageSources(
|
|
363
|
+
rawSources: unknown,
|
|
364
|
+
): AssistantMessageSource[] {
|
|
365
|
+
if (!Array.isArray(rawSources)) {
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return rawSources.flatMap((rawSource) => {
|
|
370
|
+
if (!rawSource || typeof rawSource !== "object") {
|
|
371
|
+
return [];
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const source = rawSource as Partial<AssistantMessageSource>;
|
|
375
|
+
const href = typeof source.href === "string" ? source.href.trim() : "";
|
|
376
|
+
const label =
|
|
377
|
+
typeof source.label === "string" && source.label.trim()
|
|
378
|
+
? source.label.trim()
|
|
379
|
+
: href;
|
|
380
|
+
|
|
381
|
+
if (!href || !label) {
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return [{ href, label }];
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
354
389
|
function normalizePersistedMessages(rawMessages: unknown): ChatMessage[] {
|
|
355
390
|
if (!Array.isArray(rawMessages)) {
|
|
356
391
|
return [];
|
|
@@ -372,6 +407,7 @@ function normalizePersistedMessages(rawMessages: unknown): ChatMessage[] {
|
|
|
372
407
|
id: message.id,
|
|
373
408
|
role: message.role,
|
|
374
409
|
content: message.content,
|
|
410
|
+
sources: normalizeAssistantMessageSources(message.sources),
|
|
375
411
|
}));
|
|
376
412
|
}
|
|
377
413
|
|
|
@@ -953,6 +989,43 @@ function AssistantMarkdownBlock({
|
|
|
953
989
|
);
|
|
954
990
|
}
|
|
955
991
|
|
|
992
|
+
function AssistantSourcesBlock({
|
|
993
|
+
linkTarget,
|
|
994
|
+
onClick,
|
|
995
|
+
sources,
|
|
996
|
+
}: {
|
|
997
|
+
linkTarget: AssistantLinkTarget;
|
|
998
|
+
onClick: (event: JSX.TargetedMouseEvent<HTMLDivElement>) => void;
|
|
999
|
+
sources: AssistantMessageSource[] | undefined;
|
|
1000
|
+
}) {
|
|
1001
|
+
const visibleSources = normalizeAssistantMessageSources(sources);
|
|
1002
|
+
if (visibleSources.length === 0) {
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
return (
|
|
1007
|
+
<div
|
|
1008
|
+
className="ask-ai-sources ask-ai-markdown prose-rules mt-3 max-w-full min-w-0 wrap-break-word text-[15px]!"
|
|
1009
|
+
onClick={onClick}
|
|
1010
|
+
>
|
|
1011
|
+
<p>Sources:</p>
|
|
1012
|
+
<ul>
|
|
1013
|
+
{visibleSources.map((source) => (
|
|
1014
|
+
<li key={`${source.href}\0${source.label}`}>
|
|
1015
|
+
<a
|
|
1016
|
+
href={source.href}
|
|
1017
|
+
target={linkTarget === "blank" ? "_blank" : undefined}
|
|
1018
|
+
rel={linkTarget === "blank" ? "noopener noreferrer" : undefined}
|
|
1019
|
+
>
|
|
1020
|
+
{source.label}
|
|
1021
|
+
</a>
|
|
1022
|
+
</li>
|
|
1023
|
+
))}
|
|
1024
|
+
</ul>
|
|
1025
|
+
</div>
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
956
1029
|
function extractErrorMessage(rawBody: string): string {
|
|
957
1030
|
if (!rawBody.trim()) {
|
|
958
1031
|
return "";
|
|
@@ -1110,6 +1183,7 @@ export default function AssistantEmbedPanel({
|
|
|
1110
1183
|
const panelSizeRef = useRef<AssistantPanelSize>(
|
|
1111
1184
|
panelSize ?? initialPanelState.panelSize,
|
|
1112
1185
|
);
|
|
1186
|
+
const hasPrewarmedShikiRef = useRef(false);
|
|
1113
1187
|
const skipNextMessagesPersistRef = useRef(false);
|
|
1114
1188
|
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
|
1115
1189
|
const resolvedApiPathRef = useRef(
|
|
@@ -1799,6 +1873,13 @@ export default function AssistantEmbedPanel({
|
|
|
1799
1873
|
setMessages(nextConversation);
|
|
1800
1874
|
queueThreadScrollToBottom(nextConversation);
|
|
1801
1875
|
|
|
1876
|
+
if (shiki && !hasPrewarmedShikiRef.current) {
|
|
1877
|
+
hasPrewarmedShikiRef.current = true;
|
|
1878
|
+
void warmAssistantShikiRuntime(shiki).catch((error) => {
|
|
1879
|
+
console.warn("Assistant embed Shiki runtime prewarm failed", error);
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1802
1883
|
activeRequestAbortRef.current?.abort();
|
|
1803
1884
|
const abortController = new AbortController();
|
|
1804
1885
|
activeRequestAbortRef.current = abortController;
|
|
@@ -1916,6 +1997,49 @@ export default function AssistantEmbedPanel({
|
|
|
1916
1997
|
: message,
|
|
1917
1998
|
);
|
|
1918
1999
|
});
|
|
2000
|
+
continue;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
if (parsed.type === "sources") {
|
|
2004
|
+
const nextSources = normalizeAssistantMessageSources(
|
|
2005
|
+
parsed.sources,
|
|
2006
|
+
);
|
|
2007
|
+
if (nextSources.length === 0) {
|
|
2008
|
+
continue;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
if (!hasReceivedFirstTextDelta) {
|
|
2012
|
+
hasReceivedFirstTextDelta = true;
|
|
2013
|
+
setSharedAwaitingFirstToken(false);
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
setMessages((previous) => {
|
|
2017
|
+
const existingAssistantMessage = previous.find(
|
|
2018
|
+
(message) => message.id === assistantId,
|
|
2019
|
+
);
|
|
2020
|
+
|
|
2021
|
+
const nextMessages = existingAssistantMessage
|
|
2022
|
+
? previous.map((message) =>
|
|
2023
|
+
message.id === assistantId
|
|
2024
|
+
? {
|
|
2025
|
+
...message,
|
|
2026
|
+
sources: nextSources,
|
|
2027
|
+
}
|
|
2028
|
+
: message,
|
|
2029
|
+
)
|
|
2030
|
+
: [
|
|
2031
|
+
...previous,
|
|
2032
|
+
{
|
|
2033
|
+
id: assistantId,
|
|
2034
|
+
role: "assistant" as const,
|
|
2035
|
+
content: "",
|
|
2036
|
+
sources: nextSources,
|
|
2037
|
+
},
|
|
2038
|
+
];
|
|
2039
|
+
|
|
2040
|
+
queueThreadScrollToBottom(nextMessages);
|
|
2041
|
+
return nextMessages;
|
|
2042
|
+
});
|
|
1919
2043
|
}
|
|
1920
2044
|
}
|
|
1921
2045
|
}
|
|
@@ -2242,7 +2366,8 @@ export default function AssistantEmbedPanel({
|
|
|
2242
2366
|
{messages.map((message) => {
|
|
2243
2367
|
if (
|
|
2244
2368
|
message.role === "assistant" &&
|
|
2245
|
-
message.content.length === 0
|
|
2369
|
+
message.content.length === 0 &&
|
|
2370
|
+
normalizeAssistantMessageSources(message.sources).length === 0
|
|
2246
2371
|
) {
|
|
2247
2372
|
return null;
|
|
2248
2373
|
}
|
|
@@ -2265,6 +2390,13 @@ export default function AssistantEmbedPanel({
|
|
|
2265
2390
|
onRendered={handleMarkdownRendered}
|
|
2266
2391
|
shiki={shiki}
|
|
2267
2392
|
/>
|
|
2393
|
+
{!isUser ? (
|
|
2394
|
+
<AssistantSourcesBlock
|
|
2395
|
+
linkTarget={linkTarget}
|
|
2396
|
+
onClick={handleRenderedMarkdownClick}
|
|
2397
|
+
sources={message.sources}
|
|
2398
|
+
/>
|
|
2399
|
+
) : null}
|
|
2268
2400
|
</div>
|
|
2269
2401
|
);
|
|
2270
2402
|
})}
|