Spaces:
Running
Running
Download lib/nodepad-format.ts from Leon4gr45/nodepad-hypotheses: direct link, hf CLI and curl.
- Browser
- Download file 6.48 kB
-
https://huggingface.co/spaces/Leon4gr45/nodepad-hypotheses/resolve/main/lib/nodepad-format.ts
- Command line
-
hf download hf://spaces/Leon4gr45/nodepad-hypotheses/lib/nodepad-format.ts
-
curl -L -o nodepad-format.ts https://huggingface.co/spaces/Leon4gr45/nodepad-hypotheses/resolve/main/lib/nodepad-format.ts
6.48 kB
| /** | |
| * .nodepad file format β full-fidelity project serialisation. | |
| * | |
| * A .nodepad file is a JSON object with a version field so future schema | |
| * changes can be handled gracefully. The file contains the complete project | |
| * state: blocks (with all AI annotations, connections, confidence scores, | |
| * sub-tasks), collapsed IDs, ghost notes history, and enough context for | |
| * the AI enrichment pipeline to continue seamlessly on re-import. | |
| * | |
| * Transient UI fields (isEnriching, isError, statusText) are stripped on | |
| * export and reset to safe defaults on import. | |
| */ | |
| import type { ContentType } from "./content-types" | |
| export const NODEPAD_FILE_VERSION = 1 as const | |
| // ββ Serialised types ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export interface NodepadBlock { | |
| id: string | |
| text: string | |
| timestamp: number | |
| contentType: ContentType | |
| category?: string | |
| annotation?: string | |
| confidence?: number | null | |
| sources?: { url: string; title: string; siteName: string }[] | |
| influencedBy?: string[] // stable block IDs (not category strings) | |
| isUnrelated?: boolean | |
| isPinned?: boolean | |
| subTasks?: { id: string; text: string; isDone: boolean; timestamp: number }[] | |
| } | |
| export interface NodepadGhostNote { | |
| id: string | |
| text: string | |
| category: string | |
| isGenerating: boolean | |
| } | |
| export interface NodepadFile { | |
| version: typeof NODEPAD_FILE_VERSION | |
| exportedAt: number | |
| project: { | |
| id: string | |
| name: string | |
| blocks: NodepadBlock[] | |
| collapsedIds: string[] | |
| ghostNotes: NodepadGhostNote[] | |
| lastGhostTexts?: string[] | |
| lastGhostBlockCount?: number | |
| lastGhostTimestamp?: number | |
| } | |
| } | |
| // ββ Export ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** Serialise a project to a NodepadFile, stripping transient UI state. */ | |
| export function serialiseProject(project: { | |
| id: string | |
| name: string | |
| blocks: any[] | |
| collapsedIds: string[] | |
| ghostNotes?: any[] | |
| lastGhostTexts?: string[] | |
| lastGhostBlockCount?: number | |
| lastGhostTimestamp?: number | |
| }): NodepadFile { | |
| return { | |
| version: NODEPAD_FILE_VERSION, | |
| exportedAt: Date.now(), | |
| project: { | |
| id: project.id, | |
| name: project.name, | |
| collapsedIds: project.collapsedIds ?? [], | |
| ghostNotes: (project.ghostNotes ?? []).map((g: any) => ({ | |
| id: g.id, | |
| text: g.text, | |
| category: g.category, | |
| isGenerating: false, // never persist a generating state | |
| })), | |
| lastGhostTexts: project.lastGhostTexts, | |
| lastGhostBlockCount: project.lastGhostBlockCount, | |
| lastGhostTimestamp: project.lastGhostTimestamp, | |
| blocks: project.blocks.map((b: any): NodepadBlock => ({ | |
| id: b.id, | |
| text: b.text, | |
| timestamp: b.timestamp, | |
| contentType: b.contentType, | |
| ...(b.category !== undefined && { category: b.category }), | |
| ...(b.annotation !== undefined && { annotation: b.annotation }), | |
| ...(b.confidence !== undefined && { confidence: b.confidence }), | |
| ...(b.sources !== undefined && { sources: b.sources }), | |
| ...(b.influencedBy !== undefined && { influencedBy: b.influencedBy }), | |
| ...(b.isUnrelated !== undefined && { isUnrelated: b.isUnrelated }), | |
| ...(b.isPinned && { isPinned: b.isPinned }), | |
| ...(b.subTasks?.length && { subTasks: b.subTasks }), | |
| })), | |
| }, | |
| } | |
| } | |
| /** Trigger a browser download of a .nodepad file for the given project. */ | |
| export function downloadNodepadFile(project: { | |
| id: string; name: string; blocks: any[]; collapsedIds: string[] | |
| ghostNotes?: any[]; lastGhostTexts?: string[] | |
| lastGhostBlockCount?: number; lastGhostTimestamp?: number | |
| }): void { | |
| const data = serialiseProject(project) | |
| const blob = new Blob([JSON.stringify(data, null, 2)], { | |
| type: "application/json;charset=utf-8", | |
| }) | |
| const url = URL.createObjectURL(blob) | |
| const a = document.createElement("a") | |
| const slug = project.name | |
| .toLowerCase() | |
| .replace(/\s+/g, "-") | |
| .replace(/[^a-z0-9-]/g, "") | |
| .slice(0, 60) | |
| a.href = url | |
| a.download = `${slug || "project"}.nodepad` | |
| a.click() | |
| URL.revokeObjectURL(url) | |
| } | |
| // ββ Import ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export class NodepadParseError extends Error {} | |
| /** | |
| * Parse a .nodepad file string and return a project object ready to push | |
| * into the projects list. Always assigns a fresh ID so there are no | |
| * collisions. Name conflicts get a copy suffix: "My Research (2)". | |
| */ | |
| export function parseNodepadFile( | |
| raw: string, | |
| existingNames: string[], | |
| ): { | |
| id: string | |
| name: string | |
| blocks: any[] | |
| collapsedIds: string[] | |
| ghostNotes: any[] | |
| lastGhostTexts?: string[] | |
| lastGhostBlockCount?: number | |
| lastGhostTimestamp?: number | |
| } { | |
| let data: any | |
| try { | |
| data = JSON.parse(raw) | |
| } catch { | |
| throw new NodepadParseError("Not a valid .nodepad file β JSON parse failed.") | |
| } | |
| if (!data || typeof data !== "object") { | |
| throw new NodepadParseError("Not a valid .nodepad file β unexpected root type.") | |
| } | |
| if (!data.project || !Array.isArray(data.project.blocks)) { | |
| throw new NodepadParseError("Not a valid .nodepad file β missing project.blocks array.") | |
| } | |
| const src = data.project | |
| // Deduplicate name β always "Name (2)", "Name (3)", β¦ | |
| let name = (src.name as string) || "Imported Project" | |
| if (existingNames.includes(name)) { | |
| let n = 2 | |
| while (existingNames.includes(`${name} (${n})`)) n++ | |
| name = `${name} (${n})` | |
| } | |
| return { | |
| id: Math.random().toString(36).substring(2, 10), | |
| name, | |
| blocks: (src.blocks as any[]).map(b => ({ | |
| ...b, | |
| isEnriching: false, | |
| isError: false, | |
| statusText: undefined, | |
| })), | |
| collapsedIds: Array.isArray(src.collapsedIds) ? src.collapsedIds : [], | |
| ghostNotes: Array.isArray(src.ghostNotes) ? src.ghostNotes : [], | |
| lastGhostTexts: src.lastGhostTexts, | |
| lastGhostBlockCount: src.lastGhostBlockCount, | |
| lastGhostTimestamp: src.lastGhostTimestamp, | |
| } | |
| } | |