jamdesk 1.1.29 → 1.1.30
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/dist/lib/deps.js +3 -3
- package/package.json +3 -3
- package/vendored/app/[[...slug]]/page.tsx +36 -109
- package/vendored/app/api/assets/[...path]/route.ts +2 -2
- package/vendored/app/api/chat/[project]/route.ts +76 -35
- package/vendored/app/api/isr-health/route.ts +2 -3
- package/vendored/app/layout.tsx +2 -0
- package/vendored/components/JdReadySentinel.tsx +25 -0
- package/vendored/components/navigation/Breadcrumb.tsx +2 -2
- package/vendored/components/navigation/Header.tsx +23 -17
- package/vendored/components/navigation/LanguageSelector.tsx +7 -4
- package/vendored/components/navigation/Sidebar.tsx +28 -37
- package/vendored/components/navigation/TabsNav.tsx +1 -1
- package/vendored/hooks/useChat.ts +113 -60
- package/vendored/hooks/useDelayedNavigationSpinner.ts +94 -0
- package/vendored/hooks/useTextStreamPacer.ts +152 -0
- package/vendored/lib/chat-prompt.ts +5 -3
- package/vendored/lib/docs-types.ts +4 -0
- package/vendored/lib/find-first-nav-page.ts +40 -0
- package/vendored/lib/hedge-strip.ts +29 -0
- package/vendored/lib/middleware-helpers.ts +2 -1
- package/vendored/lib/openapi/lang-spec-path.ts +16 -0
- package/vendored/lib/page-isr-helpers.ts +4 -1
- package/vendored/lib/public-paths-resolver.ts +3 -42
- package/vendored/lib/ui-strings.ts +52 -0
- package/vendored/schema/docs-schema.json +15 -0
- package/vendored/workspace-package-lock.json +18 -18
package/dist/lib/deps.js
CHANGED
|
@@ -73,8 +73,8 @@ export const REQUIRED_DEPS = {
|
|
|
73
73
|
'unified': '^11.0.0',
|
|
74
74
|
'unist-util-visit': '^5.0.0',
|
|
75
75
|
// CSS
|
|
76
|
-
'tailwindcss': '^4.2.
|
|
77
|
-
'@tailwindcss/postcss': '^4.2.
|
|
76
|
+
'tailwindcss': '^4.2.4',
|
|
77
|
+
'@tailwindcss/postcss': '^4.2.4',
|
|
78
78
|
'@tailwindcss/typography': '^0.5.10',
|
|
79
79
|
'postcss': '^8.5.10',
|
|
80
80
|
'autoprefixer': '^10.4.24',
|
|
@@ -83,7 +83,7 @@ export const REQUIRED_DEPS = {
|
|
|
83
83
|
'json5': '^2.2.3',
|
|
84
84
|
'glob': '^13.0.6',
|
|
85
85
|
// TypeScript (needed for Next.js to avoid auto-install breaking symlink)
|
|
86
|
-
'typescript': '^6.0.
|
|
86
|
+
'typescript': '^6.0.3',
|
|
87
87
|
'@types/node': '^25.5.2',
|
|
88
88
|
'@types/react': '^19.2.14',
|
|
89
89
|
'@types/react-dom': '^19.0.0',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jamdesk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.30",
|
|
4
4
|
"description": "CLI for Jamdesk — build, preview, and deploy documentation sites from MDX. Dev server with hot reload, 50+ components, OpenAPI support, AI search, and Mintlify migration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jamdesk",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"json5": "^2.2.3",
|
|
115
115
|
"nspell": "^2.1.5",
|
|
116
116
|
"open": "^11.0.0",
|
|
117
|
-
"ora": "^9.
|
|
117
|
+
"ora": "^9.4.0",
|
|
118
118
|
"tar": "^7.5.9"
|
|
119
119
|
},
|
|
120
120
|
"devDependencies": {
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
"@types/fs-extra": "^11.0.0",
|
|
123
123
|
"@types/node": "^25.5.0",
|
|
124
124
|
"typescript": "^6.0.2",
|
|
125
|
-
"vitest": "^4.1.
|
|
125
|
+
"vitest": "^4.1.5"
|
|
126
126
|
},
|
|
127
127
|
"engines": {
|
|
128
128
|
"node": ">=20.0.0"
|
|
@@ -74,6 +74,9 @@ import {
|
|
|
74
74
|
type CodeExample,
|
|
75
75
|
type AuthMethod,
|
|
76
76
|
} from '@/lib/openapi';
|
|
77
|
+
import { extractLanguageFromPath, isValidLanguageCode } from '@/lib/language-utils';
|
|
78
|
+
import { findFirstNavPage } from '@/lib/find-first-nav-page';
|
|
79
|
+
import { candidateSpecPaths } from '@/lib/openapi/lang-spec-path';
|
|
77
80
|
import { ApiEndpoint } from '@/components/mdx/ApiEndpoint';
|
|
78
81
|
import { ImagePriorityProvider } from '@/components/mdx/ImagePriorityProvider';
|
|
79
82
|
import { AIActionsMenu } from '@/components/AIActionsMenu';
|
|
@@ -182,100 +185,19 @@ function getAllDocPaths(): string[] {
|
|
|
182
185
|
return paths;
|
|
183
186
|
}
|
|
184
187
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// Helper to extract page path from a page entry
|
|
192
|
-
const getPagePath = (page: unknown): string | null => {
|
|
193
|
-
if (typeof page === 'string') return page;
|
|
194
|
-
if (typeof page === 'object' && page && 'page' in page) {
|
|
195
|
-
return (page as { page: string }).page;
|
|
196
|
-
}
|
|
197
|
-
return null;
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
// Helper to extract first page from a group
|
|
201
|
-
const extractFirstFromGroup = (group: { pages?: unknown[] }): string | null => {
|
|
202
|
-
const itemPages = group.pages || [];
|
|
203
|
-
for (const page of itemPages) {
|
|
204
|
-
if (typeof page === 'object' && page && 'group' in page) {
|
|
205
|
-
const result = extractFirstFromGroup(page as { pages?: unknown[] });
|
|
206
|
-
if (result) return result;
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
const pagePath = getPagePath(page);
|
|
210
|
-
if (pagePath) return pagePath;
|
|
211
|
-
}
|
|
212
|
-
return null;
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
// Check languages first (multi-language navigation structure)
|
|
216
|
-
if (navigation.languages && navigation.languages.length > 0) {
|
|
217
|
-
const firstLang = navigation.languages[0];
|
|
218
|
-
if (firstLang.tabs && firstLang.tabs.length > 0) {
|
|
219
|
-
for (const tab of firstLang.tabs) {
|
|
220
|
-
if (tab.groups) {
|
|
221
|
-
for (const group of tab.groups) {
|
|
222
|
-
const result = extractFirstFromGroup(group);
|
|
223
|
-
if (result) return result;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Check tabs first
|
|
231
|
-
if (navigation.tabs && navigation.tabs.length > 0) {
|
|
232
|
-
for (const tab of navigation.tabs) {
|
|
233
|
-
if (tab.groups) {
|
|
234
|
-
for (const group of tab.groups) {
|
|
235
|
-
const result = extractFirstFromGroup(group);
|
|
236
|
-
if (result) return result;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Check top-level groups
|
|
243
|
-
if (navigation.groups && navigation.groups.length > 0) {
|
|
244
|
-
for (const group of navigation.groups) {
|
|
245
|
-
const result = extractFirstFromGroup(group);
|
|
246
|
-
if (result) return result;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Check anchors
|
|
251
|
-
if (navigation.anchors && navigation.anchors.length > 0) {
|
|
252
|
-
for (const anchor of navigation.anchors) {
|
|
253
|
-
if (anchor.groups) {
|
|
254
|
-
for (const group of anchor.groups) {
|
|
255
|
-
const result = extractFirstFromGroup(group);
|
|
256
|
-
if (result) return result;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
188
|
+
function findFirstPage(config: DocsConfig, lang?: string): string {
|
|
189
|
+
const nav = config.navigation;
|
|
190
|
+
const langBlock = lang ? nav.languages?.find((l) => l.language === lang) : undefined;
|
|
191
|
+
const result = (langBlock && findFirstNavPage(langBlock)) || findFirstNavPage(nav);
|
|
192
|
+
return result ? result.replace(/^\//, '') : 'introduction';
|
|
193
|
+
}
|
|
261
194
|
|
|
262
|
-
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const directPath = getPagePath(item);
|
|
267
|
-
if (directPath) return directPath;
|
|
268
|
-
|
|
269
|
-
// Could be a group object { group: "...", pages: [...] }
|
|
270
|
-
if (item && typeof item === 'object' && 'group' in item) {
|
|
271
|
-
const result = extractFirstFromGroup(item as { pages?: unknown[] });
|
|
272
|
-
if (result) return result;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
195
|
+
function resolveSlug(normalizedSlug: string[], config: DocsConfig): string[] {
|
|
196
|
+
if (normalizedSlug.length === 0) return pathToSlug(findFirstPage(config));
|
|
197
|
+
if (normalizedSlug.length === 1 && isValidLanguageCode(normalizedSlug[0])) {
|
|
198
|
+
return pathToSlug(findFirstPage(config, normalizedSlug[0]));
|
|
275
199
|
}
|
|
276
|
-
|
|
277
|
-
// Fallback
|
|
278
|
-
return 'introduction';
|
|
200
|
+
return normalizedSlug;
|
|
279
201
|
}
|
|
280
202
|
|
|
281
203
|
export async function generateStaticParams() {
|
|
@@ -342,17 +264,12 @@ export async function generateMetadata({ params }: PageProps) {
|
|
|
342
264
|
// Normalize slug: strip /docs prefix when hostAtDocs=true.
|
|
343
265
|
// Empty root → resolve to first page (see DocPage for the full rationale).
|
|
344
266
|
const normalizedSlug = normalizeSlugForContent(resolvedParams.slug || [], hostAtDocs);
|
|
267
|
+
const config = await loader.getConfig();
|
|
268
|
+
const slug = resolveSlug(normalizedSlug, config);
|
|
345
269
|
const isRoot = normalizedSlug.length === 0;
|
|
346
|
-
const slug = isRoot
|
|
347
|
-
? pathToSlug(findFirstPage(await loader.getConfig()))
|
|
348
|
-
: normalizedSlug;
|
|
349
270
|
const pagePath = slug.join('/');
|
|
350
271
|
|
|
351
|
-
|
|
352
|
-
const [fileContents, config] = await Promise.all([
|
|
353
|
-
loader.getContent(pagePath).catch(() => null),
|
|
354
|
-
loader.getConfig(),
|
|
355
|
-
]);
|
|
272
|
+
const fileContents = await loader.getContent(pagePath).catch(() => null);
|
|
356
273
|
|
|
357
274
|
if (!fileContents) {
|
|
358
275
|
return {
|
|
@@ -428,14 +345,11 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
428
345
|
// redirect() emits cache-control: private, blocking CDN caching. Canonical
|
|
429
346
|
// + noindex in generateMetadata prevent duplicate indexing.
|
|
430
347
|
const normalizedSlug = normalizeSlugForContent(resolvedParams.slug || [], hostAtDocs);
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
: normalizedSlug;
|
|
348
|
+
const config = await loader.getConfig();
|
|
349
|
+
const slug = resolveSlug(normalizedSlug, config);
|
|
434
350
|
const pagePath = slug.join('/');
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
loader.getConfig(),
|
|
438
|
-
]);
|
|
351
|
+
const currentLang = extractLanguageFromPath(`/${pagePath}`);
|
|
352
|
+
const fileContents = await loader.getContent(pagePath).catch(() => null);
|
|
439
353
|
|
|
440
354
|
// Check if content exists (getContent returns null via catch if not found)
|
|
441
355
|
if (!fileContents) {
|
|
@@ -567,9 +481,13 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
567
481
|
allSpecPaths.length > 0 ? allSpecPaths : undefined
|
|
568
482
|
);
|
|
569
483
|
|
|
570
|
-
const
|
|
484
|
+
const baseSpecs = parsed.isShortFormat && allSpecPaths.length > 1
|
|
571
485
|
? allSpecPaths
|
|
572
486
|
: [parsed.specPath];
|
|
487
|
+
// For each base spec, expand into [<base>.<lang>.ext, <base>.ext] when
|
|
488
|
+
// the page is under a localized URL. Resolver tries each in order; the
|
|
489
|
+
// first that loads wins. Missing lang variant falls back to English.
|
|
490
|
+
const specsToTry = baseSpecs.flatMap(p => candidateSpecPaths(p, currentLang));
|
|
573
491
|
|
|
574
492
|
// Hoist mode-dependent values before the loop
|
|
575
493
|
const useIsr = isIsrMode() && !!projectSlug;
|
|
@@ -580,7 +498,8 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
580
498
|
|
|
581
499
|
let lastError: unknown = null;
|
|
582
500
|
|
|
583
|
-
for (
|
|
501
|
+
for (let i = 0; i < specsToTry.length; i++) {
|
|
502
|
+
const specPath = specsToTry[i];
|
|
584
503
|
try {
|
|
585
504
|
if (resolveSpec && projectSlug) {
|
|
586
505
|
const spec = await resolveSpec(projectSlug, specPath);
|
|
@@ -598,6 +517,14 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
598
517
|
break;
|
|
599
518
|
} catch (err) {
|
|
600
519
|
lastError = err;
|
|
520
|
+
const isLast = i === specsToTry.length - 1;
|
|
521
|
+
if (!isLast) {
|
|
522
|
+
// Lang variant (or intermediate candidate) failed — log so we
|
|
523
|
+
// notice transient R2 errors that silently fall through to English.
|
|
524
|
+
console.warn(
|
|
525
|
+
`[openapi] spec candidate "${specPath}" failed; trying next: ${(err as Error).message}`
|
|
526
|
+
);
|
|
527
|
+
}
|
|
601
528
|
}
|
|
602
529
|
}
|
|
603
530
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { NextRequest, NextResponse } from 'next/server';
|
|
9
9
|
import { fetchAsset } from '@/lib/r2-content';
|
|
10
10
|
import { log } from '@/lib/logger';
|
|
11
|
+
import { isIsrMode } from '@/lib/page-isr-helpers';
|
|
11
12
|
|
|
12
13
|
// Cache durations by content type (in seconds)
|
|
13
14
|
const CACHE_DURATIONS: Record<string, number> = {
|
|
@@ -27,8 +28,7 @@ export async function GET(
|
|
|
27
28
|
request: NextRequest,
|
|
28
29
|
{ params }: { params: Promise<{ path: string[] }> }
|
|
29
30
|
) {
|
|
30
|
-
|
|
31
|
-
if (process.env.ISR_MODE !== 'true') {
|
|
31
|
+
if (!isIsrMode()) {
|
|
32
32
|
return new NextResponse('Asset serving only available in ISR mode', {
|
|
33
33
|
status: 404,
|
|
34
34
|
});
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import { NextRequest } from 'next/server';
|
|
24
24
|
import { querySimilarChunks } from '@/lib/vector-store';
|
|
25
25
|
import { buildSystemPrompt, isCannedIdentityReply, resolveSiteName } from '@/lib/chat-prompt';
|
|
26
|
+
import { stripHedge, isHedgeReply } from '@/lib/hedge-strip';
|
|
26
27
|
import { getDocsPath, getBaseUrl, trackChatAnalytics } from '@/lib/route-helpers';
|
|
27
28
|
import { getAnthropicClient } from '@/lib/anthropic-client';
|
|
28
29
|
import { rewriteQueryForSearch } from '@/lib/query-rewriter';
|
|
@@ -38,6 +39,9 @@ const CHAT_MODEL = 'claude-haiku-4-5-20251001';
|
|
|
38
39
|
const RATE_LIMIT = 10;
|
|
39
40
|
const RATE_WINDOW_SECONDS = 60;
|
|
40
41
|
const MAX_HISTORY = 10;
|
|
42
|
+
/** Messages shorter than this with prior history are treated as follow-ups.
|
|
43
|
+
* Used both for searchQuery enrichment and to skip the rewriter. */
|
|
44
|
+
const SHORT_FOLLOWUP_LEN = 60;
|
|
41
45
|
const VALID_ROLES = new Set<string>(['user', 'assistant']);
|
|
42
46
|
|
|
43
47
|
export async function POST(
|
|
@@ -93,11 +97,12 @@ export async function POST(
|
|
|
93
97
|
})
|
|
94
98
|
.map((h) => ({ role: h.role, content: h.content.slice(0, 4000) }));
|
|
95
99
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
//
|
|
100
|
+
const isShortFollowUp = message.length < SHORT_FOLLOWUP_LEN && history.length > 0;
|
|
101
|
+
|
|
102
|
+
// When a user picks a clarification option (e.g. "Post Analytics"), the
|
|
103
|
+
// message alone is too short for good retrieval — combine with the prior turn.
|
|
99
104
|
let searchQuery = message;
|
|
100
|
-
if (
|
|
105
|
+
if (isShortFollowUp) {
|
|
101
106
|
const prevUserMsg = [...history].reverse().find(
|
|
102
107
|
h => h.role === 'user' && h.content !== message,
|
|
103
108
|
);
|
|
@@ -127,16 +132,21 @@ export async function POST(
|
|
|
127
132
|
// original query still returns results so chat doesn't fail.
|
|
128
133
|
const originalQueryPromise = querySimilarChunks(project, searchQuery, 15);
|
|
129
134
|
|
|
135
|
+
// Skipping the rewriter on short follow-ups avoids 500-2000ms of serial
|
|
136
|
+
// Anthropic latency — `searchQuery` above is already enriched with prior-turn
|
|
137
|
+
// context using the same threshold, so the rewrite has no retrieval benefit.
|
|
130
138
|
const rewrittenPathPromise: Promise<Awaited<ReturnType<typeof querySimilarChunks>>> =
|
|
131
|
-
|
|
132
|
-
.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
isShortFollowUp
|
|
140
|
+
? Promise.resolve([])
|
|
141
|
+
: rewriteQueryForSearch(message, history)
|
|
142
|
+
.catch(() => null)
|
|
143
|
+
.then(rewritten => {
|
|
144
|
+
// Compare against `message` (the rewriter's input), not `searchQuery`
|
|
145
|
+
// (which is enriched with prior-turn context for short follow-ups).
|
|
146
|
+
// The rewriter only sees `message`, so a no-op rewrite equals `message`.
|
|
147
|
+
if (!rewritten || rewritten === message) return [];
|
|
148
|
+
return querySimilarChunks(project, rewritten, 15).catch(() => []);
|
|
149
|
+
});
|
|
140
150
|
|
|
141
151
|
let originalChunks: Awaited<ReturnType<typeof querySimilarChunks>>;
|
|
142
152
|
let rewrittenChunks: Awaited<ReturnType<typeof querySimilarChunks>>;
|
|
@@ -228,23 +238,53 @@ export async function POST(
|
|
|
228
238
|
let emittedMarkdownLength = 0;
|
|
229
239
|
let emittedQuestionLength = 0;
|
|
230
240
|
|
|
231
|
-
|
|
232
|
-
|
|
241
|
+
// Incrementally extract a still-open string field from partial tool_use JSON.
|
|
242
|
+
// The SDK's jsonSnapshot only exposes a field once its string literal closes,
|
|
243
|
+
// so a ~1000-char answer lands in one snapshot update and the client sees it
|
|
244
|
+
// in one flash. Instead, accumulate the raw partial_json and close the string
|
|
245
|
+
// + object ourselves to read the in-progress value.
|
|
246
|
+
let accumulatedJson = '';
|
|
247
|
+
function extractPartialString(field: 'markdown' | 'question'): string | null {
|
|
248
|
+
// A trailing odd number of backslashes means we're mid-escape. Appending
|
|
249
|
+
// `"}` would turn `\` into `\"` — a valid escaped quote — producing a
|
|
250
|
+
// bogus literal `"` in the extracted string that gets streamed as text.
|
|
251
|
+
const trailing = accumulatedJson.match(/\\+$/)?.[0].length ?? 0;
|
|
252
|
+
if (trailing % 2 !== 0) return null;
|
|
253
|
+
try {
|
|
254
|
+
const parsed = JSON.parse(accumulatedJson + '"}') as Record<string, unknown>;
|
|
255
|
+
const value = parsed[field];
|
|
256
|
+
return typeof value === 'string' ? value : null;
|
|
257
|
+
} catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
stream.on('inputJson', (partialJson, jsonSnapshot) => {
|
|
263
|
+
accumulatedJson += typeof partialJson === 'string' ? partialJson : '';
|
|
233
264
|
|
|
234
265
|
// Gate on toolName — emitting before content_block_start resolves
|
|
235
266
|
// would leak clarification JSON into the answer text channel.
|
|
236
267
|
if (toolName === 'answer') {
|
|
237
|
-
const snap = jsonSnapshot as AnswerInput;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
268
|
+
const snap = jsonSnapshot as AnswerInput | null | undefined;
|
|
269
|
+
const md = typeof snap?.markdown === 'string'
|
|
270
|
+
? snap.markdown
|
|
271
|
+
: extractPartialString('markdown');
|
|
272
|
+
if (md !== null) {
|
|
273
|
+
const trimmed = stripHedge(md);
|
|
274
|
+
if (trimmed.length > emittedMarkdownLength) {
|
|
275
|
+
const newContent = trimmed.slice(emittedMarkdownLength);
|
|
276
|
+
emittedMarkdownLength = trimmed.length;
|
|
277
|
+
controller.enqueue(sendEvent({ type: 'text', content: newContent }));
|
|
278
|
+
}
|
|
242
279
|
}
|
|
243
280
|
} else if (toolName === 'ask_clarification') {
|
|
244
|
-
const snap = jsonSnapshot as ClarificationInput;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
281
|
+
const snap = jsonSnapshot as ClarificationInput | null | undefined;
|
|
282
|
+
const q = typeof snap?.question === 'string'
|
|
283
|
+
? snap.question
|
|
284
|
+
: extractPartialString('question');
|
|
285
|
+
if (q !== null && q.length > emittedQuestionLength) {
|
|
286
|
+
const newContent = q.slice(emittedQuestionLength);
|
|
287
|
+
emittedQuestionLength = q.length;
|
|
248
288
|
controller.enqueue(sendEvent({ type: 'text', content: newContent }));
|
|
249
289
|
}
|
|
250
290
|
}
|
|
@@ -275,15 +315,17 @@ export async function POST(
|
|
|
275
315
|
if (toolUse.name === 'answer') {
|
|
276
316
|
const input = toolUse.input as AnswerInput;
|
|
277
317
|
const slugs = Array.isArray(input.cited_page_slugs) ? input.cited_page_slugs : [];
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
|
|
318
|
+
const rawMarkdown = typeof input.markdown === 'string' ? input.markdown : '';
|
|
319
|
+
const markdownText = stripHedge(rawMarkdown);
|
|
320
|
+
// Suppress citations on identity + hedge replies — the top-2 fallback
|
|
321
|
+
// would otherwise attach unrelated docs under "I'm the documentation
|
|
322
|
+
// assistant…" or under "I don't have information…".
|
|
323
|
+
const suppressCitations =
|
|
324
|
+
isCannedIdentityReply(markdownText, siteName) || isHedgeReply(rawMarkdown);
|
|
283
325
|
|
|
284
326
|
const seen = new Set<string>();
|
|
285
327
|
const explicitSources: Citation[] = [];
|
|
286
|
-
if (!
|
|
328
|
+
if (!suppressCitations) {
|
|
287
329
|
for (const slug of slugs) {
|
|
288
330
|
const chunk = chunks.find(c => c.pageSlug === slug);
|
|
289
331
|
if (chunk && !seen.has(chunk.pageSlug)) {
|
|
@@ -296,10 +338,8 @@ export async function POST(
|
|
|
296
338
|
}
|
|
297
339
|
}
|
|
298
340
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
hadExplicitCitations = isIdentityReply || explicitSources.length > 0;
|
|
302
|
-
sources = isIdentityReply
|
|
341
|
+
hadExplicitCitations = suppressCitations || explicitSources.length > 0;
|
|
342
|
+
sources = suppressCitations
|
|
303
343
|
? []
|
|
304
344
|
: explicitSources.length > 0
|
|
305
345
|
? explicitSources
|
|
@@ -354,8 +394,9 @@ export async function POST(
|
|
|
354
394
|
return new Response(readable, {
|
|
355
395
|
headers: {
|
|
356
396
|
'Content-Type': 'text/event-stream',
|
|
357
|
-
'Cache-Control': 'no-cache',
|
|
397
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
358
398
|
'Connection': 'keep-alive',
|
|
399
|
+
'X-Accel-Buffering': 'no',
|
|
359
400
|
},
|
|
360
401
|
});
|
|
361
402
|
}
|
|
@@ -9,6 +9,7 @@ import { NextResponse } from 'next/server';
|
|
|
9
9
|
import { getConfigCacheSize } from '@/lib/r2-content';
|
|
10
10
|
import { getSnippetCacheSize } from '@/lib/snippet-compiler-isr';
|
|
11
11
|
import { getOpenApiCacheSize } from '@/lib/openapi-isr';
|
|
12
|
+
import { isIsrMode } from '@/lib/page-isr-helpers';
|
|
12
13
|
|
|
13
14
|
interface HealthCheck {
|
|
14
15
|
status: 'ok' | 'degraded' | 'unhealthy';
|
|
@@ -31,8 +32,6 @@ interface HealthCheck {
|
|
|
31
32
|
const startTime = Date.now();
|
|
32
33
|
|
|
33
34
|
export async function GET() {
|
|
34
|
-
const isIsrMode = process.env.ISR_MODE === 'true';
|
|
35
|
-
|
|
36
35
|
// Memory usage
|
|
37
36
|
const memUsage = process.memoryUsage();
|
|
38
37
|
const memory = {
|
|
@@ -54,7 +53,7 @@ export async function GET() {
|
|
|
54
53
|
memory,
|
|
55
54
|
uptime: Math.round((Date.now() - startTime) / 1000),
|
|
56
55
|
timestamp: new Date().toISOString(),
|
|
57
|
-
isrMode: isIsrMode,
|
|
56
|
+
isrMode: isIsrMode(),
|
|
58
57
|
};
|
|
59
58
|
|
|
60
59
|
return NextResponse.json(health, {
|
package/vendored/app/layout.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { LayoutWrapper } from '@/components/layout/LayoutWrapper';
|
|
|
7
7
|
import { CodeBlockCopyButton } from '@/components/CodeBlockCopyButton';
|
|
8
8
|
import { HeaderLinkCopy } from '@/components/HeaderLinkCopy';
|
|
9
9
|
import { FontAwesomeLoader } from '@/components/FontAwesomeLoader';
|
|
10
|
+
import { JdReadySentinel } from '@/components/JdReadySentinel';
|
|
10
11
|
import { FA_CSS_HREF } from '@/lib/font-awesome';
|
|
11
12
|
import { getDocsConfig } from '@/lib/docs';
|
|
12
13
|
import { getDocsConfig as getIsrDocsConfig } from '@/lib/docs-isr';
|
|
@@ -580,6 +581,7 @@ export default async function RootLayout({
|
|
|
580
581
|
{config.integrations?.ga4?.measurementId && (
|
|
581
582
|
<ConditionalGA gaId={config.integrations.ga4.measurementId} />
|
|
582
583
|
)}
|
|
584
|
+
<JdReadySentinel />
|
|
583
585
|
</body>
|
|
584
586
|
</html>
|
|
585
587
|
);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Sets document.body.dataset.jdReady = 'true' after window.load so the
|
|
7
|
+
* pdf-service (and any future browser automation) can wait on a reliable
|
|
8
|
+
* signal that the page is fully rendered and resources have settled.
|
|
9
|
+
*/
|
|
10
|
+
export function JdReadySentinel(): null {
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const mark = () => {
|
|
13
|
+
document.body.dataset.jdReady = 'true';
|
|
14
|
+
};
|
|
15
|
+
if (document.readyState === 'complete') {
|
|
16
|
+
mark();
|
|
17
|
+
} else {
|
|
18
|
+
window.addEventListener('load', mark, { once: true });
|
|
19
|
+
}
|
|
20
|
+
// removeEventListener with an unregistered listener is a no-op, so the
|
|
21
|
+
// cleanup is safe whether or not the branch above registered it.
|
|
22
|
+
return () => window.removeEventListener('load', mark);
|
|
23
|
+
}, []);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
@@ -267,7 +267,7 @@ export function Breadcrumb({ slug, config }: BreadcrumbProps) {
|
|
|
267
267
|
{/* Home link */}
|
|
268
268
|
<Link
|
|
269
269
|
href={homeLink}
|
|
270
|
-
prefetch={
|
|
270
|
+
prefetch={true}
|
|
271
271
|
className="text-[var(--color-marker)] hover:text-[var(--color-text-primary)] transition-colors flex items-center"
|
|
272
272
|
aria-label="Home"
|
|
273
273
|
>
|
|
@@ -284,7 +284,7 @@ export function Breadcrumb({ slug, config }: BreadcrumbProps) {
|
|
|
284
284
|
) : item.path ? (
|
|
285
285
|
<Link
|
|
286
286
|
href={`${linkPrefix}/${item.path}`}
|
|
287
|
-
prefetch={
|
|
287
|
+
prefetch={true}
|
|
288
288
|
className="text-[var(--color-marker)] hover:text-[var(--color-text-primary)] transition-colors"
|
|
289
289
|
>
|
|
290
290
|
{item.label}
|