mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
fix(web): support legacy html artifact rewrites
This commit is contained in:
parent
c48aee0aa6
commit
361c18a414
3 changed files with 113 additions and 18 deletions
|
|
@ -84,38 +84,61 @@ function buildLatestHtmlFileIndex(
|
|||
projectFiles: readonly HtmlLinkProjectFile[],
|
||||
options: HtmlLinkRewriteOptions,
|
||||
): Map<string, string> {
|
||||
const latest = new Map<string, HtmlLinkProjectFile>();
|
||||
const groupedLatest = new Map<string, HtmlLinkProjectFile>();
|
||||
const legacyLatest = new Map<string, HtmlLinkProjectFile>();
|
||||
const groupedFamilies = new Set<string>();
|
||||
const artifactGroupIdentifier = normalizeOptionalIdentifier(options.artifactGroupIdentifier);
|
||||
for (const file of projectFiles) {
|
||||
if (!isHtmlProjectFile(file)) continue;
|
||||
const key = htmlFileFamilyKey(file.name);
|
||||
if (!key) continue;
|
||||
if (!htmlFileManifestMatchesFamily(file, key, artifactGroupIdentifier)) continue;
|
||||
const current = latest.get(key);
|
||||
if (!current || file.mtime > current.mtime) latest.set(key, file);
|
||||
const manifestMatch = htmlFileManifestFamilyMatch(file, key);
|
||||
if (!manifestMatch) continue;
|
||||
|
||||
if (manifestMatch.artifactGroupIdentifier !== null) {
|
||||
groupedFamilies.add(key);
|
||||
}
|
||||
|
||||
if (
|
||||
artifactGroupIdentifier !== null &&
|
||||
manifestMatch.artifactGroupIdentifier === artifactGroupIdentifier
|
||||
) {
|
||||
const current = groupedLatest.get(key);
|
||||
if (!current || file.mtime > current.mtime) groupedLatest.set(key, file);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (manifestMatch.artifactGroupIdentifier === null) {
|
||||
const current = legacyLatest.get(key);
|
||||
if (!current || file.mtime > current.mtime) legacyLatest.set(key, file);
|
||||
}
|
||||
}
|
||||
|
||||
const latest = new Map<string, HtmlLinkProjectFile>();
|
||||
for (const [key, file] of legacyLatest) {
|
||||
if (!groupedFamilies.has(key)) latest.set(key, file);
|
||||
}
|
||||
for (const [key, file] of groupedLatest) {
|
||||
latest.set(key, file);
|
||||
}
|
||||
return new Map(Array.from(latest, ([key, file]) => [key, file.name]));
|
||||
}
|
||||
|
||||
function htmlFileManifestMatchesFamily(
|
||||
function htmlFileManifestFamilyMatch(
|
||||
file: HtmlLinkProjectFile,
|
||||
familyKey: string,
|
||||
artifactGroupIdentifier: string | null,
|
||||
): boolean {
|
||||
): { artifactGroupIdentifier: string | null } | null {
|
||||
const identifier = file.artifactManifest?.metadata?.identifier;
|
||||
if (typeof identifier !== 'string') return false;
|
||||
if (typeof identifier !== 'string') return null;
|
||||
const normalizedIdentifier = normalizeArtifactIdentifier(identifier);
|
||||
if (!normalizedIdentifier) return false;
|
||||
if (normalizedIdentifier !== htmlFileFamilyIdentifier(familyKey)) return false;
|
||||
if (!normalizedIdentifier) return null;
|
||||
if (normalizedIdentifier !== htmlFileFamilyIdentifier(familyKey)) return null;
|
||||
|
||||
const fileGroupIdentifier = normalizeOptionalIdentifier(
|
||||
file.artifactManifest?.metadata?.artifactGroupIdentifier,
|
||||
);
|
||||
return (
|
||||
artifactGroupIdentifier !== null &&
|
||||
fileGroupIdentifier !== null &&
|
||||
artifactGroupIdentifier === fileGroupIdentifier
|
||||
);
|
||||
return {
|
||||
artifactGroupIdentifier: normalizeOptionalIdentifier(
|
||||
file.artifactManifest?.metadata?.artifactGroupIdentifier,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function isHtmlProjectFile(file: HtmlLinkProjectFile): boolean {
|
||||
|
|
|
|||
|
|
@ -146,6 +146,40 @@ describe('rewriteHtmlLinksToCurrentProjectFiles', () => {
|
|||
expect(out).toContain('href="about.html"');
|
||||
expect(out).not.toContain('href="about-2.html"');
|
||||
});
|
||||
|
||||
it('falls back to legacy same-identifier files when no group-tagged candidates exist', () => {
|
||||
const html =
|
||||
'<!doctype html><html><body>' +
|
||||
'<a href="about.html">About</a>' +
|
||||
'</body></html>';
|
||||
|
||||
const out = rewriteHtmlLinksToCurrentProjectFiles(html, [
|
||||
htmlFile('about.html', 10, { artifactIdentifier: 'about' }),
|
||||
htmlFile('about-2.html', 40, { artifactIdentifier: 'about' }),
|
||||
], { artifactGroupIdentifier: 'site-a' });
|
||||
|
||||
expect(out).toContain('href="about-2.html"');
|
||||
});
|
||||
|
||||
it('does not fall back to legacy files when a group-tagged candidate family exists', () => {
|
||||
const html =
|
||||
'<!doctype html><html><body>' +
|
||||
'<a href="about.html">About</a>' +
|
||||
'</body></html>';
|
||||
|
||||
const out = rewriteHtmlLinksToCurrentProjectFiles(html, [
|
||||
htmlFile('about.html', 10, { artifactIdentifier: 'about' }),
|
||||
htmlFile('about-2.html', 20, { artifactIdentifier: 'about' }),
|
||||
htmlFile('about-3.html', 40, {
|
||||
artifactIdentifier: 'about',
|
||||
artifactGroupIdentifier: 'site-b',
|
||||
}),
|
||||
], { artifactGroupIdentifier: 'site-a' });
|
||||
|
||||
expect(out).toContain('href="about.html"');
|
||||
expect(out).not.toContain('href="about-2.html"');
|
||||
expect(out).not.toContain('href="about-3.html"');
|
||||
});
|
||||
});
|
||||
|
||||
function htmlFile(
|
||||
|
|
|
|||
|
|
@ -643,6 +643,44 @@ describe('ProjectView API empty response handling', () => {
|
|||
expect(content).toContain('href="about-2.html"');
|
||||
});
|
||||
|
||||
it('rewrites child links for legacy multipage artifacts without group metadata', async () => {
|
||||
mockedFetchProjectFiles.mockResolvedValue([
|
||||
htmlProjectFile('index.html', 10, { artifactIdentifier: 'index' }),
|
||||
htmlProjectFile('about.html', 20, { artifactIdentifier: 'about' }),
|
||||
htmlProjectFile('about-2.html', 40, { artifactIdentifier: 'about' }),
|
||||
] as never);
|
||||
mockedWriteProjectTextFile.mockResolvedValue(htmlProjectFile('index.html', 50) as never);
|
||||
const artifact =
|
||||
'<artifact identifier="index" type="text/html" title="Multipage Site">' +
|
||||
'<!doctype html><html><head><title>Home</title></head><body>' +
|
||||
'<main><h1>Home</h1><a href="about.html">About</a></main>' +
|
||||
'</body></html>' +
|
||||
'</artifact>';
|
||||
mockedStreamMessage.mockImplementation(async (
|
||||
_cfg: AppConfig,
|
||||
_system: string,
|
||||
_history: ChatMessage[],
|
||||
_signal: AbortSignal,
|
||||
handlers: StreamHandlers,
|
||||
) => {
|
||||
handlers.onDelta(artifact);
|
||||
handlers.onDone('');
|
||||
});
|
||||
renderProjectView();
|
||||
|
||||
await sendTestPrompt();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedWriteProjectTextFile).toHaveBeenCalled();
|
||||
});
|
||||
const [, fileName, content, options] = mockedWriteProjectTextFile.mock.calls.at(-1) ?? [];
|
||||
expect(fileName).toBe('index.html');
|
||||
expect(content).toContain('href="about-2.html"');
|
||||
expect(options?.artifactManifest?.metadata?.artifactGroupIdentifier).toBe(
|
||||
'html-artifact:index.html',
|
||||
);
|
||||
});
|
||||
|
||||
it('injects ElevenLabs voice options into API-mode audio project prompts', async () => {
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
|
|
|
|||
Loading…
Reference in a new issue