- Updated `inspectChains` to accept optional `editorContent`, allowing for real-time inspection without relying on potentially stale vault data. - Introduced `buildNoteIndexFromContent` to facilitate graph indexing directly from provided content. - Improved handling of template matching profiles in `ChainWorkbenchModal`, ensuring accurate context during chain inspections. - Added debug logging for better traceability of the chain inspection process.
1256 lines
47 KiB
TypeScript
1256 lines
47 KiB
TypeScript
/**
|
|
* Chain Workbench Modal - UI for chain workbench.
|
|
*/
|
|
|
|
import { Modal, Setting, Notice, TFile } from "obsidian";
|
|
import type { App } from "obsidian";
|
|
import type { WorkbenchModel, WorkbenchMatch, WorkbenchTodoUnion } from "../workbench/types";
|
|
import type { MindnetSettings } from "../settings";
|
|
import type { ChainRolesConfig, ChainTemplatesConfig } from "../dictionary/types";
|
|
import type { Vocabulary } from "../vocab/Vocabulary";
|
|
import type { IndexedEdge, SectionNode } from "../analysis/graphIndex";
|
|
import type { DictionaryLoadResult } from "../dictionary/types";
|
|
|
|
export class ChainWorkbenchModal extends Modal {
|
|
private model: WorkbenchModel;
|
|
private settings: MindnetSettings;
|
|
private chainRoles: ChainRolesConfig | null;
|
|
private chainTemplates: ChainTemplatesConfig | null;
|
|
private templatesLoadResult: DictionaryLoadResult<ChainTemplatesConfig> | undefined;
|
|
private vocabulary: Vocabulary;
|
|
private pluginInstance: any;
|
|
|
|
private selectedMatch: WorkbenchMatch | null = null;
|
|
private filterStatus: string | null = null;
|
|
private searchQuery: string = "";
|
|
|
|
constructor(
|
|
app: App,
|
|
model: WorkbenchModel,
|
|
settings: MindnetSettings,
|
|
chainRoles: ChainRolesConfig | null,
|
|
chainTemplates: ChainTemplatesConfig | null,
|
|
vocabulary: Vocabulary,
|
|
pluginInstance: any,
|
|
templatesLoadResult?: DictionaryLoadResult<ChainTemplatesConfig>
|
|
) {
|
|
super(app);
|
|
this.model = model;
|
|
this.settings = settings;
|
|
this.chainRoles = chainRoles;
|
|
this.chainTemplates = chainTemplates;
|
|
this.templatesLoadResult = templatesLoadResult;
|
|
this.vocabulary = vocabulary;
|
|
this.pluginInstance = pluginInstance;
|
|
|
|
// Add CSS class for wide modal
|
|
this.modalEl.addClass("chain-workbench-modal");
|
|
}
|
|
|
|
onOpen(): void {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
|
|
// Header
|
|
const header = contentEl.createDiv({ cls: "workbench-header" });
|
|
header.createEl("h2", { text: "Chain Workbench" });
|
|
header.createEl("p", {
|
|
cls: "context-info",
|
|
text: `Context: ${this.model.context.file}${this.model.context.heading ? `#${this.model.context.heading}` : ""}`,
|
|
});
|
|
|
|
// Filters
|
|
const filterContainer = contentEl.createDiv({ cls: "workbench-filters" });
|
|
filterContainer.createEl("label", { text: "Filter by Status:" });
|
|
const statusSelect = filterContainer.createEl("select");
|
|
statusSelect.createEl("option", { text: "All", value: "" });
|
|
statusSelect.createEl("option", { text: "Complete", value: "complete" });
|
|
statusSelect.createEl("option", { text: "Near Complete", value: "near_complete" });
|
|
statusSelect.createEl("option", { text: "Partial", value: "partial" });
|
|
statusSelect.createEl("option", { text: "Weak", value: "weak" });
|
|
statusSelect.addEventListener("change", (e) => {
|
|
const target = e.target as HTMLSelectElement;
|
|
this.filterStatus = target.value || null;
|
|
this.render();
|
|
});
|
|
|
|
filterContainer.createEl("label", { text: "Search:" });
|
|
const searchInput = filterContainer.createEl("input", { type: "text", placeholder: "Template name..." });
|
|
searchInput.addEventListener("input", (e) => {
|
|
const target = e.target as HTMLInputElement;
|
|
this.searchQuery = target.value.toLowerCase();
|
|
this.render();
|
|
});
|
|
|
|
// Main container: two-column layout
|
|
const mainContainer = contentEl.createDiv({ cls: "workbench-main" });
|
|
|
|
// Left: Tree View
|
|
const treeContainer = mainContainer.createDiv({ cls: "workbench-tree" });
|
|
treeContainer.createEl("h3", { text: "Templates & Chains" });
|
|
|
|
// Right: Details View
|
|
const detailsContainer = mainContainer.createDiv({ cls: "workbench-details" });
|
|
detailsContainer.createEl("h3", { text: "Chain Details" });
|
|
|
|
this.render();
|
|
}
|
|
|
|
private render(): void {
|
|
const { contentEl } = this;
|
|
|
|
// Debug: Log render state
|
|
console.log("[Chain Workbench] Render - model.matches.length:", this.model.matches.length);
|
|
console.log("[Chain Workbench] Render - filterStatus:", this.filterStatus, "searchQuery:", this.searchQuery);
|
|
|
|
// Filter matches
|
|
let filteredMatches = this.model.matches;
|
|
if (this.filterStatus) {
|
|
filteredMatches = filteredMatches.filter((m) => m.status === this.filterStatus);
|
|
console.log("[Chain Workbench] After status filter:", filteredMatches.length);
|
|
}
|
|
if (this.searchQuery) {
|
|
filteredMatches = filteredMatches.filter((m) =>
|
|
m.templateName.toLowerCase().includes(this.searchQuery)
|
|
);
|
|
console.log("[Chain Workbench] After search filter:", filteredMatches.length);
|
|
}
|
|
|
|
console.log("[Chain Workbench] Final filtered matches:", filteredMatches.length);
|
|
|
|
// Sort: near_complete first (default), then by score descending
|
|
// Use stable sort to preserve order within same status/score groups
|
|
filteredMatches.sort((a, b) => {
|
|
if (a.status === "near_complete" && b.status !== "near_complete") return -1;
|
|
if (a.status !== "near_complete" && b.status === "near_complete") return 1;
|
|
const scoreDiff = b.score - a.score;
|
|
if (scoreDiff !== 0) return scoreDiff;
|
|
// Preserve original order for matches with same status and score
|
|
// Use templateName + slotAssignments as tiebreaker for stability
|
|
const aId = this.getMatchIdentifier(a);
|
|
const bId = this.getMatchIdentifier(b);
|
|
return aId.localeCompare(bId);
|
|
});
|
|
|
|
// Render tree view (left column)
|
|
this.renderTreeView(filteredMatches);
|
|
|
|
// Render details (right column)
|
|
this.renderDetails();
|
|
}
|
|
|
|
private renderTreeView(matches: WorkbenchMatch[]): void {
|
|
const treeContainer = this.contentEl.querySelector(".workbench-tree");
|
|
if (!treeContainer) return;
|
|
|
|
treeContainer.empty();
|
|
treeContainer.createEl("h3", { text: "Templates & Chains" });
|
|
|
|
// Show message if no matches
|
|
if (matches.length === 0) {
|
|
const emptyMessage = treeContainer.createDiv({ cls: "workbench-empty-message" });
|
|
emptyMessage.createEl("p", { text: "Keine Chains gefunden." });
|
|
if (this.filterStatus || this.searchQuery) {
|
|
emptyMessage.createEl("p", {
|
|
cls: "workbench-empty-hint",
|
|
text: "Versuchen Sie, die Filter zu löschen, um alle Chains anzuzeigen."
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Group matches by template name
|
|
const matchesByTemplate = new Map<string, WorkbenchMatch[]>();
|
|
for (const match of matches) {
|
|
if (!matchesByTemplate.has(match.templateName)) {
|
|
matchesByTemplate.set(match.templateName, []);
|
|
}
|
|
matchesByTemplate.get(match.templateName)!.push(match);
|
|
}
|
|
|
|
// Create template tree
|
|
const templateTree = treeContainer.createDiv({ cls: "template-tree" });
|
|
|
|
for (const [templateName, templateMatches] of matchesByTemplate.entries()) {
|
|
const templateItem = templateTree.createDiv({ cls: "template-tree-item" });
|
|
|
|
const templateHeader = templateItem.createDiv({ cls: "template-tree-header" });
|
|
templateHeader.createSpan({ cls: "template-tree-toggle", text: "▶" });
|
|
templateHeader.createSpan({ cls: "template-tree-name", text: templateName });
|
|
templateHeader.createSpan({ cls: "template-tree-count", text: `(${templateMatches.length})` });
|
|
|
|
const chainsContainer = templateItem.createDiv({ cls: "template-tree-chains" });
|
|
|
|
// Add chains for this template
|
|
for (const match of templateMatches) {
|
|
const chainItem = chainsContainer.createDiv({ cls: "chain-item" });
|
|
if (this.selectedMatch === match) {
|
|
chainItem.addClass("selected");
|
|
}
|
|
|
|
const chainHeader = chainItem.createDiv({ cls: "chain-item-header" });
|
|
chainHeader.createSpan({
|
|
cls: "chain-status-icon",
|
|
text: this.getStatusIcon(match.status)
|
|
});
|
|
chainHeader.createSpan({
|
|
text: `Chain #${templateMatches.indexOf(match) + 1}`
|
|
});
|
|
|
|
chainItem.createDiv({
|
|
cls: "chain-item-info",
|
|
text: `Slots: ${match.slotsFilled}/${match.slotsTotal} | Links: ${match.satisfiedLinks}/${match.requiredLinks} | Score: ${match.score}`
|
|
});
|
|
|
|
// Show affected notes
|
|
const notes = new Set<string>();
|
|
for (const assignment of Object.values(match.slotAssignments)) {
|
|
if (assignment) {
|
|
notes.add(assignment.file);
|
|
}
|
|
}
|
|
if (notes.size > 0) {
|
|
chainItem.createDiv({
|
|
cls: "chain-item-notes",
|
|
text: `Notes: ${Array.from(notes).slice(0, 3).join(", ")}${notes.size > 3 ? "..." : ""}`
|
|
});
|
|
}
|
|
|
|
chainItem.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
this.selectedMatch = match;
|
|
this.render();
|
|
});
|
|
|
|
templateHeader.addEventListener("click", () => {
|
|
templateItem.classList.toggle("expanded");
|
|
const toggle = templateHeader.querySelector(".template-tree-toggle");
|
|
if (toggle) {
|
|
toggle.textContent = templateItem.classList.contains("expanded") ? "▼" : "▶";
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
private renderGlobalTodos(): void {
|
|
const globalTodosSection = this.contentEl.querySelector(".workbench-global-todos");
|
|
if (!globalTodosSection || !this.model.globalTodos || this.model.globalTodos.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// Clear and re-render
|
|
const existingList = globalTodosSection.querySelector(".global-todos-list");
|
|
if (existingList) {
|
|
existingList.remove();
|
|
}
|
|
|
|
const todosList = globalTodosSection.createDiv({ cls: "global-todos-list" });
|
|
|
|
for (const todo of this.model.globalTodos) {
|
|
const todoEl = todosList.createDiv({ cls: "global-todo-item" });
|
|
todoEl.createEl("div", { cls: "todo-type", text: todo.type });
|
|
todoEl.createEl("div", { cls: "todo-description", text: todo.description });
|
|
todoEl.createEl("div", { cls: "todo-priority", text: `Priority: ${todo.priority}` });
|
|
|
|
// Render actions
|
|
const actionsContainer = todoEl.createDiv({ cls: "todo-actions" });
|
|
if (
|
|
todo.type === "dangling_target" ||
|
|
todo.type === "dangling_target_heading" ||
|
|
todo.type === "only_candidates" ||
|
|
todo.type === "missing_edges" ||
|
|
todo.type === "no_causal_roles"
|
|
) {
|
|
const actions = (todo as any).actions || [];
|
|
for (const action of actions) {
|
|
const actionBtn = actionsContainer.createEl("button", {
|
|
text: this.getActionLabel(action),
|
|
});
|
|
actionBtn.addEventListener("click", () => {
|
|
this.handleGlobalTodoAction(todo, action);
|
|
});
|
|
}
|
|
} else if (todo.type === "one_sided_connectivity") {
|
|
// Informational only - no actions
|
|
actionsContainer.createEl("span", { text: "Informational only", cls: "info-text" });
|
|
}
|
|
}
|
|
}
|
|
|
|
private renderDetails(): void {
|
|
const detailsContainer = this.contentEl.querySelector(".workbench-details") as HTMLElement;
|
|
if (!detailsContainer) return;
|
|
|
|
detailsContainer.empty();
|
|
detailsContainer.createEl("h3", { text: "Chain Details" });
|
|
|
|
if (!this.selectedMatch) {
|
|
detailsContainer.createDiv({
|
|
cls: "workbench-details-placeholder",
|
|
text: "Wählen Sie eine Chain aus der linken Liste aus, um Details anzuzeigen"
|
|
});
|
|
return;
|
|
}
|
|
|
|
const match = this.selectedMatch;
|
|
const template = this.chainTemplates?.templates?.find(t => t.name === match.templateName);
|
|
|
|
// Chain Header
|
|
const header = detailsContainer.createDiv({ cls: "chain-visualization-header" });
|
|
header.createEl("h4", { text: match.templateName });
|
|
header.createDiv({
|
|
cls: "chain-meta",
|
|
text: `Status: ${match.status} | Score: ${match.score} | Confidence: ${match.confidence} | Slots: ${match.slotsFilled}/${match.slotsTotal} | Links: ${match.satisfiedLinks}/${match.requiredLinks}`
|
|
});
|
|
|
|
// Chain Visualization
|
|
const visualization = detailsContainer.createDiv({ cls: "chain-visualization" });
|
|
visualization.createEl("h4", { text: "Chain Flow" });
|
|
|
|
// Render chain flow as table
|
|
this.renderChainFlowTable(visualization, match, template);
|
|
|
|
// Edge Detection Details
|
|
this.renderEdgeDetectionDetails(detailsContainer, match, template);
|
|
|
|
// Todos Section
|
|
this.renderTodosSection(detailsContainer, match);
|
|
}
|
|
|
|
private renderChainFlowTable(
|
|
container: HTMLElement,
|
|
match: WorkbenchMatch,
|
|
template: import("../dictionary/types").ChainTemplate | undefined
|
|
): void {
|
|
const table = container.createEl("table", { cls: "chain-flow-table" });
|
|
|
|
// Header
|
|
const thead = table.createEl("thead");
|
|
const headerRow = thead.createEl("tr");
|
|
headerRow.createEl("th", { text: "Von Slot" });
|
|
headerRow.createEl("th", { text: "Referenz" });
|
|
headerRow.createEl("th", { text: "→" });
|
|
headerRow.createEl("th", { text: "Edge Type" });
|
|
headerRow.createEl("th", { text: "Rolle" });
|
|
headerRow.createEl("th", { text: "→" });
|
|
headerRow.createEl("th", { text: "Zu Slot" });
|
|
headerRow.createEl("th", { text: "Referenz" });
|
|
|
|
const tbody = table.createEl("tbody");
|
|
|
|
if (!template || !template.links || template.links.length === 0) {
|
|
const row = tbody.createEl("tr");
|
|
row.createEl("td", {
|
|
attr: { colspan: "7" },
|
|
text: "Keine Links im Template definiert",
|
|
cls: "missing"
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Build chain flow: follow links in order
|
|
const links = template.links;
|
|
const visitedSlots = new Set<string>();
|
|
|
|
for (let i = 0; i < links.length; i++) {
|
|
const link = links[i];
|
|
if (!link) continue;
|
|
const fromSlot = link.from;
|
|
const toSlot = link.to;
|
|
|
|
const row = tbody.createEl("tr");
|
|
|
|
// From Slot
|
|
const fromAssignment = match.slotAssignments[fromSlot];
|
|
if (fromAssignment) {
|
|
row.createEl("td", {
|
|
cls: "slot-cell",
|
|
text: fromSlot
|
|
});
|
|
const refCell = row.createEl("td", { cls: "node-cell" });
|
|
const refText = fromAssignment.heading
|
|
? `${fromAssignment.file}#${fromAssignment.heading}`
|
|
: fromAssignment.file;
|
|
refCell.createEl("code", { text: refText });
|
|
refCell.createEl("br");
|
|
refCell.createSpan({
|
|
text: `[${fromAssignment.noteType}]`,
|
|
cls: "node-type-badge"
|
|
});
|
|
} else {
|
|
row.createEl("td", {
|
|
cls: "slot-cell missing",
|
|
text: fromSlot
|
|
});
|
|
row.createEl("td", {
|
|
cls: "missing",
|
|
text: "FEHLEND"
|
|
});
|
|
}
|
|
|
|
// Arrow
|
|
row.createEl("td", {
|
|
cls: "edge-cell",
|
|
text: "→"
|
|
});
|
|
|
|
// Edge Type
|
|
const edgeEvidence = match.roleEvidence?.find(e => e.from === fromSlot && e.to === toSlot);
|
|
if (edgeEvidence) {
|
|
row.createEl("td", {
|
|
cls: "edge-cell",
|
|
text: edgeEvidence.rawEdgeType
|
|
});
|
|
row.createEl("td", {
|
|
cls: "edge-cell",
|
|
text: `[${edgeEvidence.edgeRole}]`
|
|
});
|
|
} else {
|
|
row.createEl("td", {
|
|
cls: "edge-cell missing",
|
|
text: "FEHLEND"
|
|
});
|
|
row.createEl("td", {
|
|
cls: "edge-cell missing",
|
|
text: ""
|
|
});
|
|
}
|
|
|
|
// Arrow
|
|
row.createEl("td", {
|
|
cls: "edge-cell",
|
|
text: "→"
|
|
});
|
|
|
|
// To Slot
|
|
const toAssignment = match.slotAssignments[toSlot];
|
|
if (toAssignment) {
|
|
row.createEl("td", {
|
|
cls: "slot-cell",
|
|
text: toSlot
|
|
});
|
|
const refCell = row.createEl("td", { cls: "node-cell" });
|
|
const refText = toAssignment.heading
|
|
? `${toAssignment.file}#${toAssignment.heading}`
|
|
: toAssignment.file;
|
|
refCell.createEl("code", { text: refText });
|
|
refCell.createEl("br");
|
|
refCell.createSpan({
|
|
text: `[${toAssignment.noteType}]`,
|
|
cls: "node-type-badge"
|
|
});
|
|
} else {
|
|
row.createEl("td", {
|
|
cls: "slot-cell missing",
|
|
text: toSlot
|
|
});
|
|
row.createEl("td", {
|
|
cls: "missing",
|
|
text: "FEHLEND"
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
private renderEdgeDetectionDetails(
|
|
container: HTMLElement,
|
|
match: WorkbenchMatch,
|
|
template: import("../dictionary/types").ChainTemplate | undefined
|
|
): void {
|
|
const section = container.createDiv({ cls: "edge-detection-details" });
|
|
section.createEl("h4", { text: "Edge-Erkennung" });
|
|
|
|
if (!template || !template.links || template.links.length === 0) {
|
|
section.createEl("p", {
|
|
text: "Keine Links im Template definiert",
|
|
cls: "missing"
|
|
});
|
|
return;
|
|
}
|
|
|
|
const edgeList = section.createDiv({ cls: "edge-detection-list" });
|
|
|
|
for (const link of template.links) {
|
|
const edgeEvidence = match.roleEvidence?.find(e => e.from === link.from && e.to === link.to);
|
|
const fromAssignment = match.slotAssignments[link.from];
|
|
const toAssignment = match.slotAssignments[link.to];
|
|
|
|
const item = edgeList.createDiv({
|
|
cls: `edge-detection-item ${edgeEvidence ? "matched" : "not-matched"}`
|
|
});
|
|
|
|
const header = item.createDiv({ cls: "edge-detection-item-header" });
|
|
header.createSpan({
|
|
cls: `edge-detection-status ${edgeEvidence ? "matched" : "not-matched"}`,
|
|
text: edgeEvidence ? "✓ GEFUNDEN" : "✗ NICHT GEFUNDEN"
|
|
});
|
|
header.createSpan({
|
|
text: `${link.from} → ${link.to}`
|
|
});
|
|
|
|
const info = item.createDiv({ cls: "edge-detection-info" });
|
|
|
|
if (edgeEvidence) {
|
|
info.createDiv({ cls: "info-row" }).innerHTML = `
|
|
<span class="info-label">Edge Type:</span>
|
|
<span><code>${edgeEvidence.rawEdgeType}</code> [${edgeEvidence.edgeRole}]</span>
|
|
`;
|
|
} else {
|
|
info.createDiv({ cls: "info-row" }).innerHTML = `
|
|
<span class="info-label">Grund:</span>
|
|
<span>Kein Edge zwischen ${link.from} und ${link.to} gefunden</span>
|
|
`;
|
|
}
|
|
|
|
if (fromAssignment && toAssignment) {
|
|
info.createDiv({ cls: "info-row" }).innerHTML = `
|
|
<span class="info-label">Von:</span>
|
|
<span><code>${fromAssignment.file}${fromAssignment.heading ? `#${fromAssignment.heading}` : ""}</code></span>
|
|
`;
|
|
info.createDiv({ cls: "info-row" }).innerHTML = `
|
|
<span class="info-label">Zu:</span>
|
|
<span><code>${toAssignment.file}${toAssignment.heading ? `#${toAssignment.heading}` : ""}</code></span>
|
|
`;
|
|
} else {
|
|
if (!fromAssignment) {
|
|
info.createDiv({ cls: "info-row" }).innerHTML = `
|
|
<span class="info-label">Von Slot:</span>
|
|
<span class="missing">${link.from} fehlt</span>
|
|
`;
|
|
}
|
|
if (!toAssignment) {
|
|
info.createDiv({ cls: "info-row" }).innerHTML = `
|
|
<span class="info-label">Zu Slot:</span>
|
|
<span class="missing">${link.to} fehlt</span>
|
|
`;
|
|
}
|
|
}
|
|
|
|
if (link.allowed_edge_roles && link.allowed_edge_roles.length > 0) {
|
|
info.createDiv({ cls: "info-row" }).innerHTML = `
|
|
<span class="info-label">Erlaubte Rollen:</span>
|
|
<span>${link.allowed_edge_roles.join(", ")}</span>
|
|
`;
|
|
}
|
|
}
|
|
}
|
|
|
|
private renderTodosSection(container: HTMLElement, match: WorkbenchMatch): void {
|
|
const section = container.createDiv({ cls: "workbench-todos-section" });
|
|
section.createEl("h4", { text: `Todos (${match.todos.length})` });
|
|
|
|
if (match.todos.length === 0) {
|
|
section.createEl("p", {
|
|
text: "Keine Todos für diese Chain",
|
|
cls: "info-text"
|
|
});
|
|
return;
|
|
}
|
|
|
|
const todosList = section.createDiv({ cls: "todos-list" });
|
|
|
|
for (const todo of match.todos) {
|
|
const todoEl = todosList.createDiv({ cls: `todo-item todo-${todo.type} ${todo.priority}` });
|
|
todoEl.createEl("div", { cls: "todo-description", text: todo.description });
|
|
todoEl.createEl("div", { cls: "todo-priority", text: `Priority: ${todo.priority}` });
|
|
|
|
// Render actions
|
|
const actionsContainer = todoEl.createDiv({ cls: "todo-actions" });
|
|
if (
|
|
todo.type === "missing_slot" ||
|
|
todo.type === "missing_link" ||
|
|
todo.type === "weak_roles" ||
|
|
todo.type === "candidate_cleanup"
|
|
) {
|
|
const actions = (todo as any).actions || [];
|
|
for (const action of actions) {
|
|
const actionBtn = actionsContainer.createEl("button", {
|
|
text: this.getActionLabel(action),
|
|
});
|
|
actionBtn.addEventListener("click", () => {
|
|
this.handleTodoAction(todo as WorkbenchTodoUnion, action, match);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private getStatusIcon(status: string): string {
|
|
switch (status) {
|
|
case "complete":
|
|
return "✓";
|
|
case "near_complete":
|
|
return "~";
|
|
case "partial":
|
|
return "○";
|
|
case "weak":
|
|
return "⚠";
|
|
default:
|
|
return "?";
|
|
}
|
|
}
|
|
|
|
private getActionLabel(action: string): string {
|
|
const labels: Record<string, string> = {
|
|
link_existing: "Link Existing",
|
|
create_note_via_interview: "Create Note",
|
|
insert_edge_forward: "Insert Edge",
|
|
insert_edge_inverse: "Insert Inverse",
|
|
choose_target_anchor: "Choose Anchor",
|
|
change_edge_type: "Change Type",
|
|
promote_candidate: "Promote",
|
|
resolve_candidate: "Resolve",
|
|
create_missing_note: "Create Missing Note",
|
|
retarget_link: "Retarget Link",
|
|
create_missing_heading: "Create Missing Heading",
|
|
retarget_to_existing_heading: "Retarget to Existing Heading",
|
|
promote_all_candidates: "Promote All Candidates",
|
|
create_explicit_edges: "Create Explicit Edges",
|
|
add_edges_to_section: "Add Edges to Section",
|
|
};
|
|
return labels[action] || action;
|
|
}
|
|
|
|
private async handleTodoAction(
|
|
todo: WorkbenchTodoUnion,
|
|
action: string,
|
|
match: WorkbenchMatch
|
|
): Promise<void> {
|
|
console.log("[Chain Workbench] Action:", action, "for todo:", todo.type);
|
|
|
|
try {
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
const activeEditor = this.app.workspace.activeEditor?.editor;
|
|
|
|
if (!activeFile || !activeEditor) {
|
|
new Notice("No active file or editor");
|
|
return;
|
|
}
|
|
|
|
// Import action handlers
|
|
const { insertEdgeForward, promoteCandidate } = await import("../workbench/writerActions");
|
|
const graphSchema = await this.pluginInstance.ensureGraphSchemaLoaded();
|
|
|
|
switch (action) {
|
|
case "insert_edge_forward":
|
|
if (todo.type === "missing_link") {
|
|
console.log("[Chain Workbench] Starting insert_edge_forward for missing_link todo");
|
|
|
|
// Check if source note has defined sections (with > [!section] callouts)
|
|
// If not, offer choice between section and note_links
|
|
let zoneChoice: "section" | "note_links" | "candidates" = "section";
|
|
|
|
// Try to find source file
|
|
let sourceFile: TFile | null = null;
|
|
const fileRef = todo.fromNodeRef.file;
|
|
const possiblePaths = [
|
|
fileRef,
|
|
fileRef + ".md",
|
|
fileRef.replace(/\.md$/, ""),
|
|
fileRef.replace(/\.md$/, "") + ".md",
|
|
];
|
|
const currentDir = activeFile.path.split("/").slice(0, -1).join("/");
|
|
if (currentDir) {
|
|
possiblePaths.push(
|
|
`${currentDir}/${fileRef}`,
|
|
`${currentDir}/${fileRef}.md`,
|
|
`${currentDir}/${fileRef.replace(/\.md$/, "")}.md`
|
|
);
|
|
}
|
|
|
|
for (const path of possiblePaths) {
|
|
const found = this.app.vault.getAbstractFileByPath(path);
|
|
if (found && found instanceof TFile) {
|
|
sourceFile = found;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!sourceFile) {
|
|
const basename = fileRef.replace(/\.md$/, "").split("/").pop() || fileRef;
|
|
const resolved = this.app.metadataCache.getFirstLinkpathDest(basename, activeFile.path);
|
|
if (resolved) {
|
|
sourceFile = resolved;
|
|
}
|
|
}
|
|
|
|
// Check if source file has defined sections
|
|
if (sourceFile) {
|
|
const { splitIntoSections } = await import("../mapping/sectionParser");
|
|
const content = await this.app.vault.read(sourceFile);
|
|
const sections = splitIntoSections(content);
|
|
|
|
// Check if any section has sectionType defined (has > [!section] callout)
|
|
const hasDefinedSections = sections.some(s => s.sectionType !== null);
|
|
|
|
// If no defined sections but multiple headings exist, offer choice
|
|
if (!hasDefinedSections && sections.length > 1) {
|
|
const contentSections = sections.filter(
|
|
s => s.heading !== "Kandidaten" && s.heading !== "Note-Verbindungen"
|
|
);
|
|
if (contentSections.length > 0) {
|
|
// Offer choice between section and note_links
|
|
const choice = await this.chooseZone(sourceFile);
|
|
if (choice === null) {
|
|
return; // User cancelled
|
|
}
|
|
zoneChoice = choice;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get edge vocabulary
|
|
console.log("[Chain Workbench] Loading edge vocabulary...");
|
|
const { VocabularyLoader } = await import("../vocab/VocabularyLoader");
|
|
const { parseEdgeVocabulary } = await import("../vocab/parseEdgeVocabulary");
|
|
const vocabText = await VocabularyLoader.loadText(this.app, this.settings.edgeVocabularyPath);
|
|
const edgeVocabulary = parseEdgeVocabulary(vocabText);
|
|
console.log("[Chain Workbench] Edge vocabulary loaded, entries:", edgeVocabulary.byCanonical.size);
|
|
|
|
console.log("[Chain Workbench] Calling insertEdgeForward with zoneChoice:", zoneChoice);
|
|
try {
|
|
await insertEdgeForward(
|
|
this.app,
|
|
activeEditor,
|
|
activeFile,
|
|
todo,
|
|
this.vocabulary,
|
|
edgeVocabulary,
|
|
this.settings,
|
|
graphSchema,
|
|
zoneChoice
|
|
);
|
|
console.log("[Chain Workbench] insertEdgeForward completed successfully");
|
|
} catch (error) {
|
|
console.error("[Chain Workbench] Error in insertEdgeForward:", error);
|
|
new Notice(`Error inserting edge: ${error instanceof Error ? error.message : String(error)}`);
|
|
return;
|
|
}
|
|
|
|
// Re-run analysis
|
|
console.log("[Chain Workbench] Refreshing workbench...");
|
|
await this.refreshWorkbench();
|
|
new Notice("Edge inserted successfully");
|
|
}
|
|
break;
|
|
|
|
case "promote_candidate":
|
|
if (todo.type === "candidate_cleanup") {
|
|
await promoteCandidate(
|
|
this.app,
|
|
activeEditor,
|
|
activeFile,
|
|
todo,
|
|
this.settings
|
|
);
|
|
|
|
// Re-run analysis
|
|
await this.refreshWorkbench();
|
|
new Notice("Candidate promoted successfully");
|
|
}
|
|
break;
|
|
|
|
case "link_existing":
|
|
if (todo.type === "missing_slot") {
|
|
const { linkExisting } = await import("../workbench/linkExistingAction");
|
|
await linkExisting(
|
|
this.app,
|
|
activeEditor,
|
|
activeFile,
|
|
todo,
|
|
this.model.context
|
|
);
|
|
|
|
// Re-run analysis
|
|
await this.refreshWorkbench();
|
|
new Notice("Link inserted. Re-run workbench to update slot assignments.");
|
|
}
|
|
break;
|
|
|
|
case "create_note_via_interview":
|
|
if (todo.type === "missing_slot") {
|
|
const { createNoteViaInterview } = await import("../workbench/interviewOrchestration");
|
|
|
|
// Get interview config from plugin instance
|
|
let interviewConfig = null;
|
|
if (this.pluginInstance.ensureInterviewConfigLoaded) {
|
|
interviewConfig = await this.pluginInstance.ensureInterviewConfigLoaded();
|
|
}
|
|
|
|
if (!interviewConfig) {
|
|
new Notice("Interview config not available");
|
|
return;
|
|
}
|
|
|
|
// Find the match for this todo
|
|
const matchForTodo = this.model.matches.find((m) =>
|
|
m.todos.some((t) => t.id === todo.id)
|
|
);
|
|
|
|
if (!matchForTodo) {
|
|
new Notice("Could not find match for todo");
|
|
return;
|
|
}
|
|
|
|
// Get edge vocabulary - we need to load it from settings
|
|
const { VocabularyLoader } = await import("../vocab/VocabularyLoader");
|
|
const { parseEdgeVocabulary } = await import("../vocab/parseEdgeVocabulary");
|
|
const vocabText = await VocabularyLoader.loadText(this.app, this.settings.edgeVocabularyPath);
|
|
const edgeVocabulary = parseEdgeVocabulary(vocabText);
|
|
|
|
await createNoteViaInterview(
|
|
this.app,
|
|
todo,
|
|
matchForTodo.templateName,
|
|
matchForTodo,
|
|
interviewConfig,
|
|
this.chainTemplates,
|
|
this.chainRoles,
|
|
this.settings,
|
|
this.vocabulary,
|
|
edgeVocabulary,
|
|
this.pluginInstance
|
|
);
|
|
|
|
// Re-run analysis after note creation
|
|
await this.refreshWorkbench();
|
|
new Notice("Note created via interview");
|
|
}
|
|
break;
|
|
|
|
default:
|
|
new Notice(`Action "${action}" not yet implemented`);
|
|
}
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
console.error("[Chain Workbench] Error handling action:", e);
|
|
new Notice(`Failed to execute action: ${msg}`);
|
|
}
|
|
}
|
|
|
|
private async handleGlobalTodoAction(todo: WorkbenchTodoUnion, action: string): Promise<void> {
|
|
console.log("[Chain Workbench] Global Todo Action:", action, "for todo:", todo.type);
|
|
|
|
try {
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
const activeEditor = this.app.workspace.activeEditor?.editor;
|
|
|
|
if (!activeFile || !activeEditor) {
|
|
new Notice("No active file or editor");
|
|
return;
|
|
}
|
|
|
|
// Import action handlers
|
|
const { promoteCandidate } = await import("../workbench/writerActions");
|
|
const { linkExisting } = await import("../workbench/linkExistingAction");
|
|
|
|
switch (action) {
|
|
case "create_missing_note":
|
|
if (todo.type === "dangling_target") {
|
|
const danglingTodo = todo as any;
|
|
// Use linkExisting action to create note
|
|
await linkExisting(
|
|
this.app,
|
|
activeEditor,
|
|
activeFile,
|
|
{
|
|
type: "missing_slot",
|
|
id: `dangling_target_create_${danglingTodo.targetFile}`,
|
|
description: `Create missing note: ${danglingTodo.targetFile}`,
|
|
priority: "high",
|
|
slotId: "",
|
|
allowedNodeTypes: [],
|
|
actions: ["create_note_via_interview"],
|
|
},
|
|
this.model.context
|
|
);
|
|
await this.refreshWorkbench();
|
|
new Notice("Note creation initiated");
|
|
}
|
|
break;
|
|
|
|
case "retarget_link":
|
|
if (todo.type === "dangling_target") {
|
|
const danglingTodo = todo as any;
|
|
// Use linkExisting to retarget
|
|
await linkExisting(
|
|
this.app,
|
|
activeEditor,
|
|
activeFile,
|
|
{
|
|
type: "missing_slot",
|
|
id: `dangling_target_retarget_${danglingTodo.targetFile}`,
|
|
description: `Retarget link to existing note`,
|
|
priority: "high",
|
|
slotId: "",
|
|
allowedNodeTypes: [],
|
|
actions: ["link_existing"],
|
|
},
|
|
this.model.context
|
|
);
|
|
await this.refreshWorkbench();
|
|
new Notice("Link retargeting initiated");
|
|
}
|
|
break;
|
|
|
|
case "promote_all_candidates":
|
|
if (todo.type === "only_candidates") {
|
|
const onlyCandidatesTodo = todo as any;
|
|
// Promote each candidate edge
|
|
for (const candidateEdge of onlyCandidatesTodo.candidateEdges || []) {
|
|
// Find the actual edge in the graph
|
|
// This is a simplified version - full implementation would need to find the edge
|
|
new Notice(`Promoting candidate edge: ${candidateEdge.rawEdgeType}`);
|
|
}
|
|
await this.refreshWorkbench();
|
|
new Notice("Candidates promoted");
|
|
}
|
|
break;
|
|
|
|
case "promote_candidate":
|
|
// Similar to template-based promote_candidate
|
|
if (todo.type === "candidate_cleanup") {
|
|
await promoteCandidate(
|
|
this.app,
|
|
activeEditor,
|
|
activeFile,
|
|
todo,
|
|
this.settings
|
|
);
|
|
await this.refreshWorkbench();
|
|
new Notice("Candidate promoted");
|
|
}
|
|
break;
|
|
|
|
case "change_edge_type":
|
|
if (todo.type === "no_causal_roles" || todo.type === "weak_roles") {
|
|
// Use existing change_edge_type logic
|
|
new Notice("Change edge type - feature coming soon");
|
|
}
|
|
break;
|
|
|
|
default:
|
|
new Notice(`Action "${action}" not yet implemented for ${todo.type}`);
|
|
break;
|
|
}
|
|
} catch (e) {
|
|
console.error("[Chain Workbench] Error handling global todo action:", e);
|
|
new Notice(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
}
|
|
}
|
|
|
|
private async chooseZone(file: any): Promise<"section" | "note_links" | "candidates" | null> {
|
|
console.log("[chooseZone] Starting zone selection");
|
|
return new Promise((resolve) => {
|
|
let resolved = false;
|
|
const { Modal, Setting } = require("obsidian");
|
|
const modal = new Modal(this.app);
|
|
modal.titleEl.textContent = "Choose Zone";
|
|
modal.contentEl.createEl("p", { text: "Where should the edge be inserted?" });
|
|
|
|
const doResolve = (value: "section" | "note_links" | "candidates" | null) => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
resolve(value);
|
|
}
|
|
};
|
|
|
|
new Setting(modal.contentEl)
|
|
.setName("Section-scope")
|
|
.setDesc("Insert in source section (standard)")
|
|
.addButton((btn: any) =>
|
|
btn.setButtonText("Select").onClick(() => {
|
|
console.log("[chooseZone] User selected: section");
|
|
doResolve("section");
|
|
modal.close();
|
|
})
|
|
);
|
|
|
|
new Setting(modal.contentEl)
|
|
.setName("Note-Verbindungen")
|
|
.setDesc("Insert in Note-Verbindungen zone (note-scope)")
|
|
.addButton((btn: any) =>
|
|
btn.setButtonText("Select").onClick(() => {
|
|
console.log("[chooseZone] User selected: note_links");
|
|
doResolve("note_links");
|
|
modal.close();
|
|
})
|
|
);
|
|
|
|
new Setting(modal.contentEl)
|
|
.setName("Kandidaten")
|
|
.setDesc("Insert in Kandidaten zone (candidate)")
|
|
.addButton((btn: any) =>
|
|
btn.setButtonText("Select").onClick(() => {
|
|
console.log("[chooseZone] User selected: candidates");
|
|
doResolve("candidates");
|
|
modal.close();
|
|
})
|
|
);
|
|
|
|
modal.onClose = () => {
|
|
console.log("[chooseZone] Modal closed, resolved:", resolved);
|
|
if (!resolved) {
|
|
console.log("[chooseZone] Resolving with null (user cancelled)");
|
|
doResolve(null);
|
|
}
|
|
};
|
|
|
|
console.log("[chooseZone] Opening modal...");
|
|
modal.open();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generate a unique identifier for a match to restore selection after refresh.
|
|
*/
|
|
private getMatchIdentifier(match: WorkbenchMatch): string {
|
|
// Create a stable identifier based on template name and slot assignments
|
|
const slotKeys = Object.keys(match.slotAssignments).sort();
|
|
const slotValues = slotKeys.map(key => {
|
|
const assignment = match.slotAssignments[key];
|
|
if (!assignment) return `${key}:null`;
|
|
return `${key}:${assignment.file}#${assignment.heading || ''}`;
|
|
});
|
|
return `${match.templateName}|${slotValues.join('|')}`;
|
|
}
|
|
|
|
/**
|
|
* Find a match in the new model that corresponds to the saved identifier.
|
|
*/
|
|
private findMatchByIdentifier(identifier: string, matches: WorkbenchMatch[]): WorkbenchMatch | null {
|
|
for (const match of matches) {
|
|
if (this.getMatchIdentifier(match) === identifier) {
|
|
return match;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private async refreshWorkbench(): Promise<void> {
|
|
try {
|
|
// Save current state before refresh
|
|
const savedSelectedMatchId = this.selectedMatch ? this.getMatchIdentifier(this.selectedMatch) : null;
|
|
const savedFilterStatus = this.filterStatus;
|
|
const savedSearchQuery = this.searchQuery;
|
|
|
|
// Reload the workbench model without closing the modal
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
const activeEditor = this.app.workspace.activeEditor?.editor;
|
|
|
|
if (!activeFile || !activeEditor) {
|
|
new Notice("No active file or editor");
|
|
return;
|
|
}
|
|
|
|
// Resolve section context
|
|
const { resolveSectionContext } = await import("../analysis/sectionContext");
|
|
const context = resolveSectionContext(activeEditor, activeFile.path);
|
|
|
|
// Build inspector options
|
|
const inspectorOptions = {
|
|
includeNoteLinks: true,
|
|
includeCandidates: this.settings.chainInspectorIncludeCandidates,
|
|
maxDepth: 3,
|
|
direction: "both" as const,
|
|
maxTemplateMatches: undefined,
|
|
maxMatchesPerTemplateDefault: this.settings.maxMatchesPerTemplateDefault,
|
|
maxAssignmentsCollectedDefault: this.settings.maxAssignmentsCollectedDefault,
|
|
debugLogging: this.settings.debugLogging,
|
|
};
|
|
|
|
// Load vocabulary
|
|
const { VocabularyLoader } = await import("../vocab/VocabularyLoader");
|
|
const { parseEdgeVocabulary } = await import("../vocab/parseEdgeVocabulary");
|
|
const vocabText = await VocabularyLoader.loadText(this.app, this.settings.edgeVocabularyPath);
|
|
const edgeVocabulary = parseEdgeVocabulary(vocabText);
|
|
|
|
// Prepare templates source info for inspectChains
|
|
// This is required for template matching to work!
|
|
let templatesSourceInfo: { path: string; status: string; loadedAt: number | null; templateCount: number } | undefined;
|
|
if (this.templatesLoadResult) {
|
|
templatesSourceInfo = {
|
|
path: this.templatesLoadResult.resolvedPath,
|
|
status: this.templatesLoadResult.status,
|
|
loadedAt: this.templatesLoadResult.loadedAt,
|
|
templateCount: this.chainTemplates?.templates?.length || 0,
|
|
};
|
|
} else if (this.chainTemplates) {
|
|
// Fallback: create a minimal templatesSourceInfo if templatesLoadResult is not available
|
|
templatesSourceInfo = {
|
|
path: this.settings.chainTemplatesPath || "unknown",
|
|
status: "loaded",
|
|
loadedAt: Date.now(),
|
|
templateCount: this.chainTemplates.templates?.length || 0,
|
|
};
|
|
}
|
|
|
|
// Inspect chains - pass editor content if available to avoid stale vault cache
|
|
const { inspectChains } = await import("../analysis/chainInspector");
|
|
const editorContent = activeEditor ? activeEditor.getValue() : undefined;
|
|
console.log("[Chain Workbench] Calling inspectChains with context:", context);
|
|
console.log("[Chain Workbench] Using editor content:", !!editorContent);
|
|
console.log("[Chain Workbench] chainTemplates:", this.chainTemplates ? "loaded" : "null");
|
|
console.log("[Chain Workbench] chainRoles:", this.chainRoles ? "loaded" : "null");
|
|
console.log("[Chain Workbench] templatesSourceInfo:", templatesSourceInfo ? "provided" : "null");
|
|
const report = await inspectChains(
|
|
this.app,
|
|
context,
|
|
inspectorOptions,
|
|
this.chainRoles,
|
|
this.settings.edgeVocabularyPath,
|
|
this.chainTemplates,
|
|
templatesSourceInfo,
|
|
this.settings.templateMatchingProfile,
|
|
editorContent
|
|
);
|
|
console.log("[Chain Workbench] inspectChains returned - templateMatches:", report.templateMatches?.length || 0);
|
|
|
|
// Build all edges index for workbench model (using same editor content if available)
|
|
const { buildNoteIndex, buildNoteIndexFromContent } = await import("../analysis/graphIndex");
|
|
const fileObj = this.app.vault.getAbstractFileByPath(activeFile.path);
|
|
if (!fileObj || !(fileObj instanceof TFile)) {
|
|
throw new Error("Active file not found");
|
|
}
|
|
|
|
// Use editor content if available to ensure consistency with inspectChains
|
|
let allEdges: IndexedEdge[];
|
|
if (editorContent) {
|
|
const result = await buildNoteIndexFromContent(this.app, fileObj, editorContent);
|
|
allEdges = result.edges;
|
|
console.log("[Chain Workbench] Built index from editor content for workbench, edges:", allEdges.length);
|
|
} else {
|
|
const result = await buildNoteIndex(this.app, fileObj);
|
|
allEdges = result.edges;
|
|
console.log("[Chain Workbench] Built index from vault for workbench, edges:", allEdges.length);
|
|
}
|
|
|
|
// Build workbench model
|
|
const { buildWorkbenchModel } = await import("../workbench/workbenchBuilder");
|
|
console.log("[Chain Workbench] Building workbench model...");
|
|
console.log("[Chain Workbench] Report has templateMatches:", report.templateMatches?.length || 0);
|
|
console.log("[Chain Workbench] allEdges count:", allEdges.length);
|
|
const newModel = await buildWorkbenchModel(
|
|
this.app,
|
|
report,
|
|
this.chainTemplates,
|
|
this.chainRoles,
|
|
edgeVocabulary,
|
|
allEdges,
|
|
this.settings.debugLogging
|
|
);
|
|
|
|
// Update model
|
|
this.model = newModel;
|
|
|
|
// Debug: Log model state
|
|
console.log("[Chain Workbench] Refresh complete - matches:", newModel.matches.length);
|
|
if (newModel.matches.length === 0) {
|
|
console.warn("[Chain Workbench] WARNING: No matches found after refresh!");
|
|
console.log("[Chain Workbench] Report templateMatches:", report.templateMatches?.length || 0);
|
|
console.log("[Chain Workbench] Context:", context);
|
|
console.log("[Chain Workbench] chainTemplates:", this.chainTemplates ? `loaded (${this.chainTemplates.templates?.length || 0} templates)` : "null");
|
|
console.log("[Chain Workbench] allEdges:", allEdges.length);
|
|
if (report.templateMatches && report.templateMatches.length > 0) {
|
|
console.warn("[Chain Workbench] Report HAS templateMatches but model.matches is empty!");
|
|
console.log("[Chain Workbench] First templateMatch:", report.templateMatches[0]);
|
|
}
|
|
} else {
|
|
console.log("[Chain Workbench] Model has matches, first match:", newModel.matches[0]?.templateName);
|
|
}
|
|
|
|
// Restore selection if possible
|
|
if (savedSelectedMatchId && newModel.matches.length > 0) {
|
|
const restoredMatch = this.findMatchByIdentifier(savedSelectedMatchId, newModel.matches);
|
|
this.selectedMatch = restoredMatch;
|
|
// If exact match not found, try to find a similar match (same template)
|
|
if (!this.selectedMatch && savedSelectedMatchId) {
|
|
const templateName = savedSelectedMatchId.split('|')[0];
|
|
const templateMatches = newModel.matches.filter(m => m.templateName === templateName);
|
|
if (templateMatches.length > 0) {
|
|
// Select the first match of the same template as fallback
|
|
this.selectedMatch = templateMatches[0] || null;
|
|
}
|
|
}
|
|
} else {
|
|
this.selectedMatch = null;
|
|
}
|
|
|
|
// Restore filters
|
|
this.filterStatus = savedFilterStatus;
|
|
this.searchQuery = savedSearchQuery;
|
|
|
|
// Debug: Log filter state
|
|
console.log("[Chain Workbench] Filter status:", this.filterStatus, "Search query:", this.searchQuery);
|
|
|
|
// Clear and re-render the entire UI
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
|
|
// Rebuild the UI structure
|
|
const header = contentEl.createDiv({ cls: "workbench-header" });
|
|
header.createEl("h2", { text: "Chain Workbench" });
|
|
header.createEl("p", {
|
|
cls: "context-info",
|
|
text: `Context: ${this.model.context.file}${this.model.context.heading ? `#${this.model.context.heading}` : ""}`,
|
|
});
|
|
|
|
// Filters
|
|
const filterContainer = contentEl.createDiv({ cls: "workbench-filters" });
|
|
filterContainer.createEl("label", { text: "Filter by Status:" });
|
|
const statusSelect = filterContainer.createEl("select");
|
|
statusSelect.createEl("option", { text: "All", value: "" });
|
|
statusSelect.createEl("option", { text: "Complete", value: "complete" });
|
|
statusSelect.createEl("option", { text: "Near Complete", value: "near_complete" });
|
|
statusSelect.createEl("option", { text: "Partial", value: "partial" });
|
|
statusSelect.createEl("option", { text: "Weak", value: "weak" });
|
|
// Restore filter value
|
|
if (this.filterStatus) {
|
|
statusSelect.value = this.filterStatus;
|
|
}
|
|
statusSelect.addEventListener("change", (e) => {
|
|
const target = e.target as HTMLSelectElement;
|
|
this.filterStatus = target.value || null;
|
|
this.render();
|
|
});
|
|
|
|
filterContainer.createEl("label", { text: "Search:" });
|
|
const searchInput = filterContainer.createEl("input", { type: "text", placeholder: "Template name..." });
|
|
// Restore search query
|
|
if (this.searchQuery) {
|
|
searchInput.value = this.searchQuery;
|
|
}
|
|
searchInput.addEventListener("input", (e) => {
|
|
const target = e.target as HTMLInputElement;
|
|
this.searchQuery = target.value.toLowerCase();
|
|
this.render();
|
|
});
|
|
|
|
// Main container: two-column layout
|
|
const mainContainer = contentEl.createDiv({ cls: "workbench-main" });
|
|
|
|
// Left: Tree View
|
|
const treeContainer = mainContainer.createDiv({ cls: "workbench-tree" });
|
|
treeContainer.createEl("h3", { text: "Templates & Chains" });
|
|
|
|
// Right: Details View
|
|
const detailsContainer = mainContainer.createDiv({ cls: "workbench-details" });
|
|
detailsContainer.createEl("h3", { text: "Chain Details" });
|
|
|
|
// Now render the content
|
|
console.log("[Chain Workbench] About to call render() - model.matches.length:", this.model.matches.length);
|
|
this.render();
|
|
console.log("[Chain Workbench] render() completed");
|
|
} catch (error) {
|
|
console.error("[Chain Workbench] Error refreshing workbench:", error);
|
|
new Notice(`Error refreshing workbench: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
}
|
|
|
|
onClose(): void {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
}
|
|
}
|