diff --git a/typescript/examples/README.md b/typescript/examples/README.md index dab7569a..01751304 100644 --- a/typescript/examples/README.md +++ b/typescript/examples/README.md @@ -15,6 +15,7 @@ We recommend reading each example in the following order. | [Calendar](https://github.com/microsoft/TypeChat/tree/main/typescript/examples/calendar) | An intelligent scheduler. This sample translates user intent into a sequence of actions to modify a calendar. | | [Restaurant](https://github.com/microsoft/TypeChat/tree/main/typescript/examples/restaurant) | An intelligent agent for taking orders at a restaurant. Similar to the coffee shop example, but uses a more complex schema to model more complex linguistic input. The prose files illustrate the line between simpler and more advanced language models in handling compound sentences, distractions, and corrections. This example also shows how we can use TypeScript to provide a user intent summary. | | [Math](https://github.com/microsoft/TypeChat/tree/main/typescript/examples/math) | Translate calculations into simple programs given an API that can perform the 4 basic mathematical operators. This example highlights TypeChat's program generation capabilities. | +| [Drawing](https://github.com/microsoft/TypeChat/tree/main/typescript/examples/drawing) | Translate drawing requests into structured shapes (boxes, ellipses, arrows), then render the result as SVG. | | [Music](https://github.com/microsoft/TypeChat/tree/main/typescript/examples/music) | An app for playing music, creating playlists, etc. on Spotify through natural language. Each user intent is translated into a series of actions in JSON which correspond to a simple dataflow program, where each step can consume data produced from previous step. | ## Step 1: Configure your development environment diff --git a/typescript/examples/drawing/README.md b/typescript/examples/drawing/README.md new file mode 100644 index 00000000..4f52235a --- /dev/null +++ b/typescript/examples/drawing/README.md @@ -0,0 +1,17 @@ +# Drawing + +The Drawing example mirrors the Python drawing sample from PR #238. It translates natural language requests into a [`Drawing`](./src/drawingSchema.ts) object containing boxes, ellipses, arrows, and unknown text. + +For each successful translation, it writes an SVG rendering to `dist/drawing.svg`. + +# Try Drawing +To run the Drawing example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment). + +# Usage +Example prompts can be found in [`src/input.txt`](./src/input.txt). + +From the `drawing` directory: + +```sh +node ./dist/main.js ./dist/input.txt +``` diff --git a/typescript/examples/drawing/package.json b/typescript/examples/drawing/package.json new file mode 100644 index 00000000..1cc8d5e9 --- /dev/null +++ b/typescript/examples/drawing/package.json @@ -0,0 +1,24 @@ +{ + "name": "drawing", + "version": "0.0.1", + "private": true, + "description": "", + "main": "dist/main.js", + "scripts": { + "build": "tsc -p src", + "postbuild": "copyfiles -u 1 src/**/*Schema.ts src/**/*.txt dist" + }, + "author": "", + "license": "MIT", + "dependencies": { + "dotenv": "^16.3.1", + "find-config": "^1.0.0", + "typechat": "^0.1.0", + "typescript": "^5.3.3" + }, + "devDependencies": { + "@types/find-config": "1.0.4", + "@types/node": "^20.10.4", + "copyfiles": "^2.4.1" + } +} diff --git a/typescript/examples/drawing/src/drawingSchema.ts b/typescript/examples/drawing/src/drawingSchema.ts new file mode 100644 index 00000000..f00c05e2 --- /dev/null +++ b/typescript/examples/drawing/src/drawingSchema.ts @@ -0,0 +1,75 @@ +// Schema for a drawing with boxes, ellipses, arrows, and unknown text. + +export interface Style { + type: "Style"; + // Corner style for boxes. + corners?: "rounded" | "sharp"; + // Thickness of outlines and arrows. + lineThickness?: number; + // CSS color for outlines and arrows. + lineColor?: string; + // CSS color used to fill boxes and ellipses. + fillColor?: string; + // Style of arrow lines. + lineStyle?: "solid" | "dashed" | "dotted"; +} + +export interface Box { + type: "Box"; + // X-coordinate of top left corner. + x: number; + // Y-coordinate of top left corner. + y: number; + // Width in pixels. + width: number; + // Height in pixels. + height: number; + // Optional label centered in the box. + text?: string; + // Optional style settings. + style?: Style; +} + +export interface Ellipse { + type: "Ellipse"; + // X-coordinate of top left corner of bounding box. + x: number; + // Y-coordinate of top left corner of bounding box. + y: number; + // Width in pixels. + width: number; + // Height in pixels. + height: number; + // Optional label centered in the ellipse. + text?: string; + // Optional style settings. + style?: Style; +} + +export interface Arrow { + type: "Arrow"; + // Starting X-coordinate. + startX: number; + // Starting Y-coordinate. + startY: number; + // Ending X-coordinate. + endX: number; + // Ending Y-coordinate. + endY: number; + // Optional style settings. + style?: Style; + // Optional arrowhead size hint. + headSize?: number; +} + +export interface UnknownText { + type: "UnknownText"; + // Text that was not understood. + text: string; +} + +export interface Drawing { + type: "Drawing"; + // Items in the drawing. + items: Array; +} diff --git a/typescript/examples/drawing/src/input.txt b/typescript/examples/drawing/src/input.txt new file mode 100644 index 00000000..72f46146 --- /dev/null +++ b/typescript/examples/drawing/src/input.txt @@ -0,0 +1,5 @@ +draw three red squares in a diagonal +red is the fill color +make the corners touch +add labels "foo", etc. +make them pink diff --git a/typescript/examples/drawing/src/main.ts b/typescript/examples/drawing/src/main.ts new file mode 100644 index 00000000..bab1a0bd --- /dev/null +++ b/typescript/examples/drawing/src/main.ts @@ -0,0 +1,43 @@ +import assert from "assert"; +import dotenv from "dotenv"; +import findConfig from "find-config"; +import fs from "fs"; +import path from "path"; +import { createJsonTranslator, createLanguageModel } from "typechat"; +import { processRequests } from "typechat/interactive"; +import { createTypeScriptJsonValidator } from "typechat/ts"; +import { Drawing } from "./drawingSchema"; +import { renderDrawingToSvg } from "./render"; + +const dotEnvPath = findConfig(".env"); +assert(dotEnvPath, ".env file not found!"); +dotenv.config({ path: dotEnvPath }); + +const model = createLanguageModel(process.env); +const schema = fs.readFileSync(path.join(__dirname, "drawingSchema.ts"), "utf8"); +const validator = createTypeScriptJsonValidator(schema, "Drawing"); +const translator = createJsonTranslator(model, validator); +const outputPath = path.join(__dirname, "drawing.svg"); + +const history: string[] = []; + +// Process requests interactively or from the input file specified on the command line +processRequests("🎨> ", process.argv[2], async (request) => { + const response = await translator.translate([...history, request].join("\n")); + if (!response.success) { + console.log(response.message); + return; + } + history.push(request); + + const drawing = response.data; + console.log(JSON.stringify(drawing, undefined, 2)); + for (const item of drawing.items) { + if (item.type === "UnknownText") { + console.log(`Unknown text: ${item.text}`); + } + } + + fs.writeFileSync(outputPath, renderDrawingToSvg(drawing), "utf8"); + console.log(`Wrote ${outputPath}`); +}); diff --git a/typescript/examples/drawing/src/render.ts b/typescript/examples/drawing/src/render.ts new file mode 100644 index 00000000..52d785f1 --- /dev/null +++ b/typescript/examples/drawing/src/render.ts @@ -0,0 +1,98 @@ +import { Arrow, Box, Drawing, Ellipse, Style } from "./drawingSchema"; + +function escapeXml(text: string): string { + return text + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll("\"", """) + .replaceAll("'", "'"); +} + +function lineDash(style?: Style): string | undefined { + switch (style?.lineStyle) { + case "dashed": + return "6 4"; + case "dotted": + return "2 3"; + default: + return undefined; + } +} + +function stroke(style?: Style): string { + return style?.lineColor ?? "black"; +} + +function strokeWidth(style?: Style): number { + return style?.lineThickness ?? 1; +} + +function fill(style?: Style): string { + return style?.fillColor ?? "none"; +} + +function renderBox(item: Box): string { + const rounded = item.style?.corners === "rounded"; + const text = item.text ? `${escapeXml(item.text)}` : ""; + return `${text}`; +} + +function renderEllipse(item: Ellipse): string { + const cx = item.x + item.width / 2; + const cy = item.y + item.height / 2; + const text = item.text ? `${escapeXml(item.text)}` : ""; + return `${text}`; +} + +function renderArrow(item: Arrow): string { + const dash = lineDash(item.style); + return ``; +} + +function getCanvasSize(drawing: Drawing): { width: number; height: number } { + let maxX = 800; + let maxY = 600; + for (const item of drawing.items) { + switch (item.type) { + case "Box": + case "Ellipse": + maxX = Math.max(maxX, item.x + item.width + 40); + maxY = Math.max(maxY, item.y + item.height + 40); + break; + case "Arrow": + maxX = Math.max(maxX, item.startX + 40, item.endX + 40); + maxY = Math.max(maxY, item.startY + 40, item.endY + 40); + break; + } + } + return { width: maxX, height: maxY }; +} + +export function renderDrawingToSvg(drawing: Drawing): string { + const { width, height } = getCanvasSize(drawing); + const shapes = drawing.items.flatMap((item) => { + switch (item.type) { + case "Box": + return [renderBox(item)]; + case "Ellipse": + return [renderEllipse(item)]; + case "Arrow": + return [renderArrow(item)]; + default: + return []; + } + }); + + return [ + ``, + "", + "", + "", + "", + "", + "", + ...shapes, + "", + ].join("\n"); +} diff --git a/typescript/examples/drawing/src/tsconfig.json b/typescript/examples/drawing/src/tsconfig.json new file mode 100644 index 00000000..0f8bbec6 --- /dev/null +++ b/typescript/examples/drawing/src/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es2021", + "lib": ["es2021"], + "module": "node16", + "types": ["node"], + "outDir": "../dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "exactOptionalPropertyTypes": true, + "inlineSourceMap": true + } +} diff --git a/typescript/package-lock.json b/typescript/package-lock.json index 7b582146..4ee059c2 100644 --- a/typescript/package-lock.json +++ b/typescript/package-lock.json @@ -94,6 +94,21 @@ "copyfiles": "^2.4.1" } }, + "examples/drawing": { + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "dotenv": "^16.3.1", + "find-config": "^1.0.0", + "typechat": "^0.1.0", + "typescript": "^5.3.3" + }, + "devDependencies": { + "@types/find-config": "1.0.4", + "@types/node": "^20.10.4", + "copyfiles": "^2.4.1" + } + }, "examples/healthData": { "name": "health-data", "version": "0.0.1", @@ -1022,6 +1037,10 @@ "url": "https://dotenvx.com" } }, + "node_modules/drawing": { + "resolved": "examples/drawing", + "link": true + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",