Skip to content

Commit 73f116b

Browse files
committed
add graph export for full content analysis
1 parent 429aaaa commit 73f116b

7 files changed

Lines changed: 607 additions & 83 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { App, Modal, Setting } from "obsidian";
2+
3+
export interface ExportGraphChoice {
4+
graphName: string;
5+
saveToManifest: boolean;
6+
}
7+
8+
/**
9+
* Confirmation dialog shown before exporting the whole analyzed graph /
10+
* text to InfraNodus: lets the user edit the graph name and — when the
11+
* vault has an `infranodus/manifest.json` — choose whether to register the
12+
* exported graph there.
13+
*/
14+
export class ExportGraphModal extends Modal {
15+
private graphName: string;
16+
private saveToManifest: boolean;
17+
private showManifestOption: boolean;
18+
private onSubmit: (result: ExportGraphChoice | null) => void;
19+
private submitted = false;
20+
21+
constructor(
22+
app: App,
23+
initialGraphName: string,
24+
showManifestOption: boolean,
25+
onSubmit: (result: ExportGraphChoice | null) => void
26+
) {
27+
super(app);
28+
this.graphName = initialGraphName;
29+
this.showManifestOption = showManifestOption;
30+
this.saveToManifest = showManifestOption;
31+
this.onSubmit = onSubmit;
32+
}
33+
34+
onOpen() {
35+
const { contentEl } = this;
36+
37+
contentEl.createEl("h2", { text: "Export the Data to InfraNodus" });
38+
39+
new Setting(contentEl)
40+
.setName("Confirm the InfraNodus graph name")
41+
.addText((text) =>
42+
text.setValue(this.graphName).onChange((value) => {
43+
this.graphName = value;
44+
})
45+
);
46+
47+
if (this.showManifestOption) {
48+
new Setting(contentEl)
49+
.setName("Register in infranodus/manifest.json")
50+
.setDesc(
51+
"Add this graph to the vault's InfraNodus manifest so AI agents can find and query it."
52+
)
53+
.addToggle((toggle) =>
54+
toggle.setValue(this.saveToManifest).onChange((value) => {
55+
this.saveToManifest = value;
56+
})
57+
);
58+
}
59+
60+
new Setting(contentEl)
61+
.addButton((btn) =>
62+
btn
63+
.setButtonText("Export")
64+
.setCta()
65+
.onClick(() => {
66+
this.submitted = true;
67+
this.close();
68+
this.onSubmit(
69+
this.graphName.trim()
70+
? {
71+
graphName: this.graphName.trim(),
72+
saveToManifest:
73+
this.showManifestOption &&
74+
this.saveToManifest,
75+
}
76+
: null
77+
);
78+
})
79+
)
80+
.addButton((btn) =>
81+
btn.setButtonText("Cancel").onClick(() => {
82+
this.submitted = true;
83+
this.close();
84+
this.onSubmit(null);
85+
})
86+
);
87+
}
88+
89+
onClose() {
90+
const { contentEl } = this;
91+
contentEl.empty();
92+
// Closing with Esc / click-outside must still resolve the promise
93+
if (!this.submitted) this.onSubmit(null);
94+
}
95+
}

src/graph_view/GraphView.tsx

Lines changed: 11 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
} from "./lib/jumpToStatement";
3535
import { INTERNAL_SETTINGS, SETTINGS } from "src/settings";
3636
import { generateTextForContext } from "./lib/generateTextForContext";
37+
import { exportGraphToInfraNodus } from "./lib/exportGraphToInfraNodus";
3738
import { unObserveElementAttributes } from "src/utils/observer";
3839
import { GraphViewOverlaySettings } from "./components/GraphViewOverlaySettings";
3940
import { InfoTooltip } from "src/components/InfoTootip";
@@ -59,7 +60,6 @@ import { TopicsObject } from "src/types/general";
5960

6061
import { jwtDecode } from "jwt-decode";
6162

62-
import { GraphNameModal } from "../components/GraphNameModal";
6363

6464
import { GraphPanel } from "./types";
6565

@@ -155,11 +155,6 @@ const GraphView = (params: {
155155

156156
const auth_token = SETTINGS.INFRANODUS_API_KEY;
157157

158-
const exportToInfraNodus = {
159-
type: SETTINGS.EXPORT_TYPE,
160-
graphName: SETTINGS.EXPORT_GRAPH,
161-
};
162-
163158
// Seeding from the cache keeps the iframe src stable from the first
164159
// render — an src change mid-load reboots the graph viewer
165160
const cachedUserId = InfraNodus.getCachedUserId();
@@ -1201,11 +1196,16 @@ const GraphView = (params: {
12011196
.map((statement) => statement.content)
12021197
.join("\n");
12031198

1204-
goToInfraNodus({
1205-
textToShow: contentToCopy,
1206-
contextName: filePath,
1207-
exportToInfraNodus,
1208-
vaultName,
1199+
exportGraphToInfraNodus({
1200+
app,
1201+
text: contentToCopy,
1202+
defaultGraphName:
1203+
encodeInfraNodusGraphName(
1204+
filePath || "",
1205+
SETTINGS.EXPORT_GRAPH,
1206+
vaultName
1207+
),
1208+
sourceFile: filePath,
12091209
});
12101210

12111211
setTimeout(
@@ -1448,59 +1448,3 @@ function convertGraphToText(params: {
14481448
}
14491449
return graphText;
14501450
}
1451-
1452-
async function goToInfraNodus({
1453-
textToShow = "",
1454-
contextName = "",
1455-
exportToInfraNodus = { type: "manual", graphName: "" },
1456-
vaultName = "",
1457-
}) {
1458-
if (!textToShow || textToShow === "ai generating...") {
1459-
// console.log("no data to send to InfraNodus");
1460-
return;
1461-
}
1462-
1463-
const encodedText = encodeURIComponent(textToShow);
1464-
const encodedContext = encodeInfraNodusGraphName(
1465-
contextName,
1466-
SETTINGS.EXPORT_GRAPH,
1467-
vaultName
1468-
);
1469-
1470-
const linkToOpen = `${SETTINGS.INFRANODUS_API_URL}/import/editor?text=${encodedText}&context=${encodedContext}`;
1471-
1472-
if (exportToInfraNodus && exportToInfraNodus.type === "auto") {
1473-
// Change the context name to just be the page title?
1474-
const graphTags = [`context: ${encodedContext}`];
1475-
let graphName = encodedContext;
1476-
1477-
// Show dialog to confirm/edit graph name
1478-
graphName =
1479-
(await new Promise<string | null>((resolve) => {
1480-
new GraphNameModal(app, graphName, resolve).open();
1481-
})) || "";
1482-
1483-
if (!graphName) {
1484-
return;
1485-
}
1486-
1487-
const dataToSave = {
1488-
contextName: graphName,
1489-
text: textToShow,
1490-
tags: graphTags,
1491-
};
1492-
1493-
const exportStatus = await InfraNodus.exportText(dataToSave);
1494-
1495-
// console.log("InfraNodus export status", exportStatus);
1496-
if (exportStatus.error) {
1497-
alert(
1498-
`There was an error saving to the ${graphName} graph in InfraNodus. Reload the page and try again or change your extension setting.`
1499-
);
1500-
} else {
1501-
alert(`Saved to the ${graphName} graph in InfraNodus`);
1502-
}
1503-
} else {
1504-
window.open(linkToOpen, "_blank");
1505-
}
1506-
}

src/graph_view/components/GraphViewOverlay.tsx

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
} from "../lib/jumpToStatement";
4747
import { editStatementsOfFile } from "src/utils/editFile";
4848
import { addLinksToStatementsForFilePath } from "../lib/addLinksToStatement";
49+
import { exportGraphToInfraNodus } from "../lib/exportGraphToInfraNodus";
4950

5051
import { InfraNodus } from "../../infranodus";
5152

@@ -528,14 +529,31 @@ const GraphViewOverlay = (params: {
528529
<span
529530
className="flex flex-row items-center gap-1 ml-4 text-sm font-bold cursor-pointer"
530531
onClick={() => {
531-
goToInfraNodus({
532-
textToShow,
533-
contextName: filePath,
534-
exportToInfraNodus,
535-
adviceMode,
536-
vaultName,
537-
app,
538-
});
532+
if (adviceMode === "context") {
533+
// Whole analyzed text: direct
534+
// API export (URL import breaks
535+
// on big texts)
536+
exportGraphToInfraNodus({
537+
app,
538+
text: textToShow,
539+
defaultGraphName:
540+
encodeInfraNodusGraphName(
541+
filePath || "",
542+
SETTINGS.EXPORT_GRAPH,
543+
vaultName
544+
),
545+
sourceFile: filePath,
546+
});
547+
} else {
548+
goToInfraNodus({
549+
textToShow,
550+
contextName: filePath,
551+
exportToInfraNodus,
552+
adviceMode,
553+
vaultName,
554+
app,
555+
});
556+
}
539557
}}
540558
>
541559
<CrossReferenceIcon size={16} />
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { App, Notice } from "obsidian";
2+
import { InfraNodus } from "src/infranodus";
3+
import { SETTINGS } from "src/settings";
4+
import {
5+
ExportGraphModal,
6+
ExportGraphChoice,
7+
} from "src/components/ExportGraphModal";
8+
import {
9+
registerExportInManifest,
10+
vaultHasManifest,
11+
} from "src/utils/manifest";
12+
13+
/**
14+
* Export the whole analyzed graph / text to InfraNodus with a direct API
15+
* POST (works for big texts, unlike the /import/editor URL which hits the
16+
* URL length limit). Confirms the graph name with the user first and — if
17+
* the vault has an `infranodus/manifest.json` — offers to register the
18+
* exported graph there so agents can query it.
19+
*/
20+
export async function exportGraphToInfraNodus(params: {
21+
app: App;
22+
text: string;
23+
defaultGraphName: string;
24+
/** Vault-relative path of the analyzed page/folder, for provenance */
25+
sourceFile?: string;
26+
}): Promise<void> {
27+
const { app, text, defaultGraphName, sourceFile } = params;
28+
29+
if (!text || text === "ai generating...") return;
30+
31+
if (!SETTINGS.INFRANODUS_API_KEY) {
32+
new Notice(
33+
"InfraNodus: add your API key in the plugin settings to export graphs."
34+
);
35+
return;
36+
}
37+
38+
const hasManifest = await vaultHasManifest(app);
39+
40+
const choice = await new Promise<ExportGraphChoice | null>((resolve) => {
41+
new ExportGraphModal(app, defaultGraphName, hasManifest, resolve).open();
42+
});
43+
44+
if (!choice) return;
45+
46+
const { graphName, saveToManifest } = choice;
47+
48+
const progressNotice = new Notice(
49+
`Exporting to the "${graphName}" graph in InfraNodus...`,
50+
0
51+
);
52+
53+
const exportStatus = await InfraNodus.exportText({
54+
contextName: graphName,
55+
text,
56+
tags: [`context: ${graphName}`],
57+
extendedSummary: saveToManifest,
58+
});
59+
60+
progressNotice.hide();
61+
62+
if (exportStatus.error) {
63+
new Notice(
64+
`There was an error saving to the "${graphName}" graph in InfraNodus. Check your API key and try again.`,
65+
8000
66+
);
67+
return;
68+
}
69+
70+
// Best-effort: never let manifest bookkeeping fail an export that
71+
// already succeeded server-side
72+
const registered = saveToManifest
73+
? await registerExportInManifest({
74+
app,
75+
graphName,
76+
responseData: exportStatus.data,
77+
sourceFile,
78+
})
79+
: false;
80+
81+
new Notice(
82+
registered
83+
? `Saved to the "${graphName}" graph in InfraNodus and registered in infranodus/manifest.json`
84+
: `Saved to the "${graphName}" graph in InfraNodus`
85+
);
86+
}

src/infranodus/index.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -882,21 +882,41 @@ class InfraNodus {
882882
contextName: string;
883883
text: string;
884884
tags: string[];
885+
// Ask the API for topics, content gaps and a structural overview in
886+
// the same request — needed when registering the exported graph in
887+
// the vault's infranodus/manifest.json (no extra round-trip)
888+
extendedSummary?: boolean;
885889
}) {
886890
try {
891+
const summaryQuery = params.extendedSummary
892+
? "&extendedGraphSummary=true&includeGraphSummary=true"
893+
: "";
894+
const body: any = {
895+
name: params.contextName,
896+
text: params.text,
897+
categories: params.tags,
898+
};
899+
if (params.extendedSummary) body.aiTopics = true;
900+
887901
const postResult = await this.genericPost(
888-
"api/v1/graphAndStatements?doNotSave=false&addStats=true",
889-
{
890-
name: params.contextName,
891-
text: params.text,
892-
categories: params.tags,
893-
},
902+
`api/v1/graphAndStatements?doNotSave=false&addStats=true${summaryQuery}&contextName=${encodeURIComponent(
903+
params.contextName
904+
)}`,
905+
body,
894906
{ credentials: "include" }
895907
);
896908

897909
// console.log("infranodus post result", postResult);
898910

899-
return { success: true };
911+
if (postResult?.data?.error) {
912+
console.error(
913+
"InfraNodus export returned an error",
914+
postResult.data.error
915+
);
916+
return { error: true };
917+
}
918+
919+
return { success: true, data: postResult?.data };
900920
} catch (err) {
901921
console.error("Error when submitting content to InfraNodus", err);
902922
return { error: true };

src/settings/settingsTab.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ class InfraNodusSettingTab extends PluginSettingTab {
234234
new Setting(containerEl)
235235
.setName("Export type")
236236
.setDesc(
237-
"Use manual export for more control. Automatic export works with big files."
237+
"Applies to AI insight clips. Full graph / analyzed text exports always save directly via the API (works with big files)."
238238
)
239239
.addDropdown((dropdown) => {
240240
const key = "EXPORT_TYPE";

0 commit comments

Comments
 (0)