Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions typescript/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions typescript/examples/drawing/README.md
Original file line number Diff line number Diff line change
@@ -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
```
24 changes: 24 additions & 0 deletions typescript/examples/drawing/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
75 changes: 75 additions & 0 deletions typescript/examples/drawing/src/drawingSchema.ts
Original file line number Diff line number Diff line change
@@ -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<Box | Ellipse | Arrow | UnknownText>;
}
5 changes: 5 additions & 0 deletions typescript/examples/drawing/src/input.txt
Original file line number Diff line number Diff line change
@@ -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
43 changes: 43 additions & 0 deletions typescript/examples/drawing/src/main.ts
Original file line number Diff line number Diff line change
@@ -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<Drawing>(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;
}
Comment thread
robgruen marked this conversation as resolved.
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}`);
});
98 changes: 98 additions & 0 deletions typescript/examples/drawing/src/render.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Arrow, Box, Drawing, Ellipse, Style } from "./drawingSchema";

function escapeXml(text: string): string {
return text
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll("\"", "&quot;")
.replaceAll("'", "&apos;");
}

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 ? `<text x="${item.x + item.width / 2}" y="${item.y + item.height / 2}" text-anchor="middle" dominant-baseline="middle">${escapeXml(item.text)}</text>` : "";
return `<rect x="${item.x}" y="${item.y}" width="${item.width}" height="${item.height}"${rounded ? " rx=\"8\" ry=\"8\"" : ""} stroke="${stroke(item.style)}" stroke-width="${strokeWidth(item.style)}" fill="${fill(item.style)}" />${text}`;
}

function renderEllipse(item: Ellipse): string {
const cx = item.x + item.width / 2;
const cy = item.y + item.height / 2;
const text = item.text ? `<text x="${cx}" y="${cy}" text-anchor="middle" dominant-baseline="middle">${escapeXml(item.text)}</text>` : "";
return `<ellipse cx="${cx}" cy="${cy}" rx="${item.width / 2}" ry="${item.height / 2}" stroke="${stroke(item.style)}" stroke-width="${strokeWidth(item.style)}" fill="${fill(item.style)}" />${text}`;
}

function renderArrow(item: Arrow): string {
const dash = lineDash(item.style);
return `<line x1="${item.startX}" y1="${item.startY}" x2="${item.endX}" y2="${item.endY}" stroke="${stroke(item.style)}" stroke-width="${strokeWidth(item.style)}"${dash ? ` stroke-dasharray="${dash}"` : ""} marker-end="url(#arrowhead)" />`;
}

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 [
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`,
"<defs>",
"<marker id=\"arrowhead\" markerWidth=\"10\" markerHeight=\"7\" refX=\"9\" refY=\"3.5\" orient=\"auto\">",
"<polygon points=\"0 0, 10 3.5, 0 7\" fill=\"black\" />",
"</marker>",
"</defs>",
"<rect width=\"100%\" height=\"100%\" fill=\"white\" />",
...shapes,
"</svg>",
].join("\n");
}
16 changes: 16 additions & 0 deletions typescript/examples/drawing/src/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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
}
}
19 changes: 19 additions & 0 deletions typescript/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.