diff --git a/docs/assets/chartifact-examples.zip b/docs/assets/chartifact-examples.zip new file mode 100644 index 00000000..a5b5bb91 Binary files /dev/null and b/docs/assets/chartifact-examples.zip differ diff --git a/package-lock.json b/package-lock.json index 57ebead4..6151e571 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "dependencies": { "@mdit/plugin-attrs": "^0.22.0", "@mdit/plugin-container": "^0.22.0", + "fflate": "^0.8.2", "markdown-it": "^14.1.0", "tabulator-tables": "^6.3.1", "vega": "^6.1.2", @@ -8092,6 +8093,12 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -15378,7 +15385,7 @@ }, "packages/vscode": { "name": "chartifact", - "version": "0.0.3", + "version": "1.0.0", "license": "MIT", "devDependencies": { "@vscode/vsce": "^3.6.0" diff --git a/package.json b/package.json index a9612743..d0a34f94 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "dependencies": { "@mdit/plugin-attrs": "^0.22.0", "@mdit/plugin-container": "^0.22.0", + "fflate": "^0.8.2", "markdown-it": "^14.1.0", "tabulator-tables": "^6.3.1", "vega": "^6.1.2", diff --git a/packages/vscode/README.md b/packages/vscode/README.md index d1df8ba7..8fabff67 100644 --- a/packages/vscode/README.md +++ b/packages/vscode/README.md @@ -2,13 +2,15 @@ **Declarative, interactive data documents** -Chartifact is a low-code document format for creating interactive, data-driven pages such as reports, dashboards, and presentations. It travels like a document and works like an app. Easily editable and remixable, it’s a file type for an AI-native world. +Chartifact is a low-code document format for creating interactive, data-driven pages such as reports, dashboards, and presentations. It travels like a document and works like an app. Designed for use with your LLM to produce a shareable artifact of your analytic conversations. ## Features - This extension works with two file formats: `*.idoc.md` and `*.idoc.json`. +### Unzip samples +- **Explorer View**: Right-click on a folder and select "NewCreate Chartifact Examples Folder". This will create a folder of examples that you and your agent can use for refernce when creating your own document. + ### Create Documents - **Explorer View**: Right-click on a folder and select "New Chartifact Interactive Document (JSON)" or "New Chartifact Interactive Document (Markdown)". @@ -16,9 +18,6 @@ This extension works with two file formats: `*.idoc.md` and `*.idoc.json`. - **Explorer View**: Right-click on a `*.idoc.md` or `*.idoc.json` file and select "Preview Chartifact Interactive Document". - **Editor Title Bar**: Open a document and select "Preview Chartifact Interactive Document (Split View)" for side-by-side editing and previewing. -### Edit Documents -- **Explorer View**: Right-click on a `*.idoc.md` or `*.idoc.json` file and select "Edit Chartifact Interactive Document". - ### Convert Documents - **Explorer View**: Convert between formats: - "Convert to HTML" for sharing standalone files. diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 966d4ada..199c1acc 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -1,15 +1,13 @@ { "name": "chartifact", "displayName": "Chartifact", - "private": true, - "description": "", - "version": "0.0.3", + "description": "Declarative, interactive data documents", + "version": "1.0.0", "publisher": "msrvida", "repository": { "type": "git", "url": "git+https://github.com/microsoft/chartifact.git" }, - "author": "Dan Marshall", "license": "MIT", "bugs": { "url": "https://github.com/microsoft/chartifact/issues" @@ -19,7 +17,12 @@ "vscode": "^1.101.0" }, "categories": [ - "Other" + "Other", + "Visualization" + ], + "keywords": [ + "data visualization", + "dataviz" ], "activationEvents": [], "browser": "./dist/web/extension.js", @@ -51,8 +54,8 @@ "category": "Chartifact Interactive Documents" }, { - "command": "chartifact.editIdoc", - "title": "Edit Chartifact Interactive Document", + "command": "chartifact.createExamplesFolder", + "title": "Create Chartifact Examples Folder", "category": "Chartifact Interactive Documents" }, { @@ -100,8 +103,8 @@ "group": "chartifact" }, { - "command": "chartifact.editIdoc", - "when": "resourceExtname == .json && resourceFilename =~ /\\.idoc( copy( \\d+)?)?\\.json$/", + "command": "chartifact.createExamplesFolder", + "when": "explorerResourceIsFolder", "group": "chartifact" } ], @@ -142,16 +145,6 @@ "command": "chartifact.convertToMarkdown", "when": "resourceExtname == .json && resourceFilename =~ /\\.idoc( copy( \\d+)?)?\\.json$/", "group": "chartifact" - }, - { - "command": "chartifact.editIdoc", - "when": "resourceExtname == .md && resourceFilename =~ /\\.idoc( copy( \\d+)?)?\\.md$/", - "group": "chartifact" - }, - { - "command": "chartifact.editIdoc", - "when": "resourceExtname == .json && resourceFilename =~ /\\.idoc( copy( \\d+)?)?\\.json$/", - "group": "chartifact" } ] } @@ -162,6 +155,7 @@ "test": "vscode-test-web --browserType=chromium --extensionDevelopmentPath=. --extensionTestsPath=dist/web/test/suite/extensionTests.js", "pretest": "npm run compile-web", "vscode:prepublish": "npm run package-web", + "prebuild": "npm run resources", "build": "npm run resources && npm run compile-web", "build:08": "npm run build", "compile-web": "npm run check-types && npm run lint && npm run resources && node esbuild.js", @@ -179,4 +173,4 @@ "devDependencies": { "@vscode/vsce": "^3.6.0" } -} +} \ No newline at end of file diff --git a/packages/vscode/scripts/resources.mjs b/packages/vscode/scripts/resources.mjs index c8c1aae8..316fe283 100644 --- a/packages/vscode/scripts/resources.mjs +++ b/packages/vscode/scripts/resources.mjs @@ -32,6 +32,7 @@ const resources = [ //sample docs '../../docs/assets/examples/json/grocery-list.idoc.json', '../../docs/assets/examples/markdown/seattle-weather/1.idoc.md', + '../../docs/assets/chartifact-examples.zip', ]; const errors = []; diff --git a/packages/vscode/src/web/command-create-examples.ts b/packages/vscode/src/web/command-create-examples.ts new file mode 100644 index 00000000..bc093ea8 --- /dev/null +++ b/packages/vscode/src/web/command-create-examples.ts @@ -0,0 +1,108 @@ +/** +* Copyright (c) Microsoft Corporation. +* Licensed under the MIT License. +*/ +import * as vscode from 'vscode'; +import { unzipSync } from 'fflate'; +import { getBinaryResourceContent } from './resources'; + +/** + * Creates a chartifact-examples folder and extracts the examples ZIP file into it + * @param uri The URI where to create the folder (usually a workspace folder or selected folder) + */ +export async function createExamplesFolder(uri?: vscode.Uri): Promise { + // Determine the target directory + let targetDir: vscode.Uri; + + if (uri && uri.scheme === 'file') { + // Check if the URI is a directory + try { + const stat = await vscode.workspace.fs.stat(uri); + if (stat.type === vscode.FileType.Directory) { + targetDir = uri; + } else { + // It's a file, use its parent directory + const pathParts = uri.path.split('/'); + pathParts.pop(); // Remove the filename + targetDir = uri.with({ path: pathParts.join('/') }); + } + } catch { + // If stat fails, assume it's a directory + targetDir = uri; + } + } else { + // No URI provided, use the first workspace folder + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + throw new Error('No workspace folder available. Please open a folder first.'); + } + targetDir = workspaceFolders[0].uri; + } + + // Find an available folder name + const baseFolderUri = vscode.Uri.joinPath(targetDir, 'chartifact-examples'); + let availableFolderUri = baseFolderUri; + + // Check if folder exists and find an available name + let counter = 1; + while (true) { + try { + await vscode.workspace.fs.stat(availableFolderUri); + // Folder exists, try next number + availableFolderUri = vscode.Uri.joinPath(targetDir, `chartifact-examples-${counter}`); + counter++; + } catch { + // Folder doesn't exist, we can use this name + break; + } + } + + try { + // Get the examples ZIP file from the cached resources + const zipData = getBinaryResourceContent('chartifact-examples.zip'); + + // Create the examples folder + await vscode.workspace.fs.createDirectory(availableFolderUri); + + // Extract the ZIP file + const unzipped = unzipSync(zipData); + + for (const [filePath, fileData] of Object.entries(unzipped)) { + const targetPath = vscode.Uri.joinPath(availableFolderUri, filePath); + + // Create parent directories if they don't exist + const pathParts = filePath.split('/'); + if (pathParts.length > 1) { + const parentParts = pathParts.slice(0, -1); + const parentDir = vscode.Uri.joinPath(availableFolderUri, ...parentParts); + try { + await vscode.workspace.fs.createDirectory(parentDir); + } catch { + // Directory might already exist, ignore error + } + } + + // Write the file + await vscode.workspace.fs.writeFile(targetPath, fileData); + } + + // Show success message and offer to open the folder + const folderName = availableFolderUri.path.split('/').pop(); + const action = await vscode.window.showInformationMessage( + `Successfully created and extracted chartifact-examples to "${folderName}"`, + 'Open Folder', + 'Show in Explorer' + ); + + if (action === 'Open Folder') { + // Open the examples folder in the explorer + vscode.commands.executeCommand('revealInExplorer', availableFolderUri); + } else if (action === 'Show in Explorer') { + // Reveal in VS Code explorer + vscode.commands.executeCommand('revealInExplorer', availableFolderUri); + } + + } catch (error) { + throw new Error(`Failed to create examples folder: ${error instanceof Error ? error.message : error}`); + } +} diff --git a/packages/vscode/src/web/command-preview.ts b/packages/vscode/src/web/command-preview.ts index 9467ba38..d40cf5a9 100644 --- a/packages/vscode/src/web/command-preview.ts +++ b/packages/vscode/src/web/command-preview.ts @@ -216,6 +216,7 @@ function getWebviewContent(webView: vscode.Webview, context: vscode.ExtensionCon // Build the resource links block const resourceLinks = [ + script(resourceUrl('vega.min.js')), script(resourceUrl('chartifact.host.umd.js')), script(resourceUrl('preview.js')), ].join('\n '); diff --git a/packages/vscode/src/web/extension.ts b/packages/vscode/src/web/extension.ts index 08023d9d..9b1420e3 100644 --- a/packages/vscode/src/web/extension.ts +++ b/packages/vscode/src/web/extension.ts @@ -85,6 +85,18 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(convertToMarkdownDisposable); + // Register the create examples folder command + const createExamplesFolderDisposable = vscode.commands.registerCommand('chartifact.createExamplesFolder', async (uri?: vscode.Uri) => { + try { + const { createExamplesFolder } = await import('./command-create-examples.js'); + await createExamplesFolder(uri); + } catch (error) { + vscode.window.showErrorMessage(`Failed to create examples folder: ${error}`); + } + }); + + context.subscriptions.push(createExamplesFolderDisposable); + // Ensure preview manager is disposed when extension deactivates context.subscriptions.push({ dispose: () => previewManager.dispose() diff --git a/packages/vscode/src/web/resources.ts b/packages/vscode/src/web/resources.ts index 5bd20254..0720ea2e 100644 --- a/packages/vscode/src/web/resources.ts +++ b/packages/vscode/src/web/resources.ts @@ -4,8 +4,8 @@ */ import * as vscode from 'vscode'; -// Cache for resource contents - can be string content or Error object -const cachedResources: Record = {}; +// Cache for resource contents - can be string content, Uint8Array (binary), or Error object +const cachedResources: Record = {}; // Initialize resource contents (call this during extension activation) export const initializeResources = async (context: vscode.ExtensionContext): Promise => { @@ -41,6 +41,10 @@ export const initializeResources = async (context: vscode.ExtensionContext): Pro 'html-json.js', 'html-markdown.js', ]; + + const binaryResourcesToLoad: string[] = [ + 'chartifact-examples.zip', + ]; for (const filename of resourcesToLoad) { try { @@ -54,6 +58,18 @@ export const initializeResources = async (context: vscode.ExtensionContext): Pro cachedResources[filename] = error instanceof Error ? error : new Error(`Failed to load ${filename}`); } } + + for (const filename of binaryResourcesToLoad) { + try { + const resourceUri = vscode.Uri.joinPath(context.extensionUri, 'resources', filename); + const fileData = await vscode.workspace.fs.readFile(resourceUri); + + cachedResources[filename] = fileData; + } catch (error) { + console.error(`Failed to read ${filename}:`, error); + cachedResources[filename] = error instanceof Error ? error : new Error(`Failed to load ${filename}`); + } + } }; // Get a cached resource by name @@ -68,5 +84,28 @@ export const getResourceContent = (resourceName: string): string => { throw resource; } + if (typeof resource !== 'string') { + throw new Error(`Resource ${resourceName} is not a text resource`); + } + + return resource; +}; + +// Get a cached binary resource by name +export const getBinaryResourceContent = (resourceName: string): Uint8Array => { + const resource = cachedResources[resourceName]; + + if (!resource) { + throw new Error(`Resource not found: ${resourceName}`); + } + + if (resource instanceof Error) { + throw resource; + } + + if (typeof resource === 'string') { + throw new Error(`Resource ${resourceName} is not a binary resource`); + } + return resource; }; diff --git a/packages/web-deploy/agent-instructions.md b/packages/web-deploy/agent-instructions.md new file mode 100644 index 00000000..19d9c11b --- /dev/null +++ b/packages/web-deploy/agent-instructions.md @@ -0,0 +1,33 @@ +# Agent Instructions for Chartifact Examples + +## Overview + +This folder is a collection of data-driven documentation and visualization examples, organized for use with the Chartifact system. The files are structured to demonstrate features, data sources, and visualization patterns using both JSON and Markdown formats. + +## Key Directories + +- `chartifact-examples/json/`: Source data and configuration files for Chartifact, using `.idoc.json` format. +- `chartifact-examples/markdown/`: Human-readable documentation and examples, mirroring the JSON files, using `.idoc.md` format. +- `chartifact-examples/schema/`: Contains the JSON schema (`idoc_v1.json`) and TypeScript definitions (`idoc_v1.d.ts`) for the `.idoc` document format. + +## Patterns and Conventions + +- Each example or feature is represented in both JSON and Markdown, with matching filenames and directory structure. +- Features are grouped under `features/`, and scenario-based examples (e.g., `seattle-weather/`) are grouped by topic. +- The `.idoc.json` files conform to the schema in `schema/idoc_v1.json`. Validate new or edited JSON files against this schema. +- Markdown files (`.idoc.md`) are intended for human consumption and may include narrative, code snippets, and visualization descriptions. + +## Developer Workflows + +- The `chartifact-examples` directory is intended to be a read-only reference for developers working with the Chartifact system. +- Developers should not modify files directly in this directory; instead, they should use these as examples to give to the agent. + +## Key tips + +Plugins: these are markdown blocks denoted by triple backticks. In Chartifact, some of these these have both a serialization format (like json or yaml) followed by a plugin name. For example: + +```json vega +{ + "description": "Having 'vega' in the plugin block is critical, otherwise it is simply json which will not be interactive." +} +``` diff --git a/packages/web-deploy/package.json b/packages/web-deploy/package.json index d4921f8c..aad86c17 100644 --- a/packages/web-deploy/package.json +++ b/packages/web-deploy/package.json @@ -9,8 +9,8 @@ "readme": "node ./dist/readme.mjs", "clean": "rimraf dist && rimraf ../../docs/assets/examples/json && rimraf ../../docs/assets/examples/markdown", "build": "tsc -p .", - "deploy": "npm run clean && npm run build && npm run readme && npm run convert && npm run example-folders" - + "deploy": "npm run clean && npm run build && npm run readme && npm run convert && npm run example-folders && npm run zip-examples", + "zip-examples": "node ./dist/zip-examples.mjs" }, "author": "Dan Marshall", "license": "MIT", diff --git a/packages/web-deploy/src/zip-examples.mts b/packages/web-deploy/src/zip-examples.mts new file mode 100644 index 00000000..50e4d457 --- /dev/null +++ b/packages/web-deploy/src/zip-examples.mts @@ -0,0 +1,55 @@ +import { fileURLToPath } from 'url'; +import { dirname, join, resolve } from 'path'; +import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs'; +import { zipSync } from 'fflate'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const outputZip = resolve(__dirname, '../../../docs/assets/chartifact-examples.zip'); + +// Helper function to recursively read directory and create file entries +function addDirectoryToZip(dirPath: string, zipPath: string = ''): Record { + const files: Record = {}; + const items = readdirSync(dirPath); + + for (const item of items) { + const fullPath = join(dirPath, item); + const zipFilePath = zipPath ? `${zipPath}/${item}` : item; + + if (statSync(fullPath).isDirectory()) { + // Recursively add directory contents + Object.assign(files, addDirectoryToZip(fullPath, zipFilePath)); + } else { + // Add file + files[zipFilePath] = readFileSync(fullPath); + } + } + + return files; +} + +// Collect all files for the ZIP +const zipFiles: Record = {}; + +// Add folders +Object.assign(zipFiles, addDirectoryToZip(resolve(__dirname, '../../../docs/assets/examples/json'), 'json')); +Object.assign(zipFiles, addDirectoryToZip(resolve(__dirname, '../../../docs/assets/examples/markdown'), 'markdown')); + +// Add schema files +zipFiles['schema/idoc_v1.d.ts'] = readFileSync(resolve(__dirname, '../../../docs/schema/idoc_v1.d.ts')); +zipFiles['schema/idoc_v1.json'] = readFileSync(resolve(__dirname, '../../../docs/schema/idoc_v1.json')); + +// Add agent instructions file +zipFiles['agent-instructions.md'] = readFileSync(resolve(__dirname, '../agent-instructions.md')); + +// Ensure output directory exists +const outputDir = resolve(__dirname, '../../../docs/assets'); +if (!existsSync(outputDir)) { + mkdirSync(outputDir, { recursive: true }); +} + +// Create and write ZIP file +const zipped = zipSync(zipFiles); +writeFileSync(outputZip, zipped); +console.log(`Zip file created: ${outputZip}`);