mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-04 21:17:30 +00:00
fix(studio): copy absolute paths in agent prompts
This commit is contained in:
@@ -25,6 +25,6 @@ export function registerProjectRoutes(api: Hono, adapter: StudioApiAdapter): voi
|
||||
const project = await adapter.resolveProject(c.req.param("id"));
|
||||
if (!project) return c.json({ error: "not found" }, 404);
|
||||
const files = walkDir(project.dir);
|
||||
return c.json({ id: project.id, files });
|
||||
return c.json({ id: project.id, dir: project.dir, title: project.title, files });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -181,6 +181,23 @@ function toRelativeProjectAssetPath(sourceFile: string, assetPath: string): stri
|
||||
return [...fromParts.map(() => ".."), ...targetParts].join("/") || assetPath;
|
||||
}
|
||||
|
||||
function isAbsoluteFilePath(value: string): boolean {
|
||||
return /^(?:\/|[A-Za-z]:[\\/]|\\\\)/.test(value);
|
||||
}
|
||||
|
||||
function toProjectAbsolutePath(projectDir: string | null, sourceFile: string): string | undefined {
|
||||
const trimmedSource = sourceFile.trim();
|
||||
if (!trimmedSource) return undefined;
|
||||
|
||||
const normalizedSource = trimmedSource.replace(/\\/g, "/");
|
||||
if (isAbsoluteFilePath(normalizedSource)) return normalizedSource;
|
||||
|
||||
const normalizedRoot = projectDir?.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
||||
if (!normalizedRoot) return undefined;
|
||||
|
||||
return `${normalizedRoot}/${normalizedSource.replace(/^\.?\//, "")}`;
|
||||
}
|
||||
|
||||
function ensureImportedFontFace(
|
||||
html: string,
|
||||
asset: ImportedFontAsset,
|
||||
@@ -589,6 +606,7 @@ export function StudioApp() {
|
||||
});
|
||||
|
||||
const [editingFile, setEditingFile] = useState<EditingFile | null>(null);
|
||||
const [projectDir, setProjectDir] = useState<string | null>(null);
|
||||
const [activeCompPath, setActiveCompPath] = useState<string | null>(null);
|
||||
const [fileTree, setFileTree] = useState<string[]>([]);
|
||||
const [compIdToSrc, setCompIdToSrc] = useState<Map<string, string>>(new Map());
|
||||
@@ -1094,10 +1112,13 @@ export function StudioApp() {
|
||||
let cancelled = false;
|
||||
fetch(`/api/projects/${projectId}`)
|
||||
.then((r) => r.json())
|
||||
.then((data: { files?: string[] }) => {
|
||||
.then((data: { files?: string[]; dir?: string }) => {
|
||||
if (!cancelled && data.files) setFileTree(data.files);
|
||||
if (!cancelled) setProjectDir(typeof data.dir === "string" ? data.dir : null);
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => {
|
||||
if (!cancelled) setProjectDir(null);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
@@ -2009,6 +2030,7 @@ export function StudioApp() {
|
||||
currentTime,
|
||||
tagSnippet,
|
||||
userInstruction,
|
||||
sourceFilePath: toProjectAbsolutePath(projectDir, targetPath),
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -2030,7 +2052,7 @@ export function StudioApp() {
|
||||
setCopiedAgentPrompt(true);
|
||||
copiedAgentTimerRef.current = setTimeout(() => setCopiedAgentPrompt(false), 1600);
|
||||
},
|
||||
[activeCompPath, currentTime, domEditSelection],
|
||||
[activeCompPath, currentTime, domEditSelection, projectDir],
|
||||
);
|
||||
|
||||
const handlePreviewIframeRef = useCallback(
|
||||
|
||||
@@ -467,6 +467,43 @@ describe("patch builders and prompt builder", () => {
|
||||
expect(prompt).toContain("Do not modify other elements' data-* attributes or positioning.");
|
||||
});
|
||||
|
||||
it("uses an absolute source path in copied agent prompts when provided", () => {
|
||||
const selection = {
|
||||
element: {} as HTMLElement,
|
||||
id: "editable-card",
|
||||
selector: "#editable-card",
|
||||
selectorIndex: undefined,
|
||||
sourceFile: "index.html",
|
||||
compositionPath: "index.html",
|
||||
compositionSrc: undefined,
|
||||
isCompositionHost: false,
|
||||
label: "Drag me first",
|
||||
tagName: "div",
|
||||
boundingBox: { x: 108, y: 112, width: 380, height: 196 },
|
||||
textContent: "Drag me first",
|
||||
dataAttributes: {},
|
||||
inlineStyles: {},
|
||||
computedStyles: {},
|
||||
textFields: [],
|
||||
capabilities: {
|
||||
canSelect: true,
|
||||
canEditStyles: true,
|
||||
canMove: true,
|
||||
canResize: true,
|
||||
canDetachFromLayout: false,
|
||||
},
|
||||
} satisfies DomEditSelection;
|
||||
|
||||
const prompt = buildElementAgentPrompt({
|
||||
selection,
|
||||
currentTime: 1.25,
|
||||
sourceFilePath: "/tmp/hf-studio-project/index.html",
|
||||
});
|
||||
|
||||
expect(prompt).toContain("Source file: /tmp/hf-studio-project/index.html");
|
||||
expect(prompt).not.toContain("Source file: index.html");
|
||||
});
|
||||
|
||||
it("serializes child text fields back into HTML", () => {
|
||||
expect(
|
||||
serializeDomEditTextFields([
|
||||
|
||||
@@ -697,12 +697,15 @@ export function buildElementAgentPrompt({
|
||||
currentTime,
|
||||
tagSnippet,
|
||||
userInstruction,
|
||||
sourceFilePath,
|
||||
}: {
|
||||
selection: DomEditSelection;
|
||||
currentTime: number;
|
||||
tagSnippet?: string;
|
||||
userInstruction?: string;
|
||||
sourceFilePath?: string;
|
||||
}): string {
|
||||
const displayedSourceFile = sourceFilePath?.trim() || selection.sourceFile;
|
||||
const lines = [
|
||||
"## HyperFrames element edit request v1",
|
||||
"Schema version: 1",
|
||||
@@ -711,7 +714,7 @@ export function buildElementAgentPrompt({
|
||||
"",
|
||||
`Composition: ${selection.compositionPath}`,
|
||||
`Playback time: ${formatTime(currentTime)}`,
|
||||
`Source file: ${selection.sourceFile}`,
|
||||
`Source file: ${displayedSourceFile}`,
|
||||
`DOM id: ${selection.id ?? "(none)"}`,
|
||||
`Selector: ${selection.selector ?? "(none)"}`,
|
||||
`Selector index: ${selection.selectorIndex ?? 0}`,
|
||||
|
||||
Reference in New Issue
Block a user