AI/AI Gantt maker
Loading live demo…from dhtmlx.com
AI
AI Gantt maker
Control a DHTMLX Gantt chart with a natural-language AI assistant that can generate projects, manage tasks, update links, and customize the timeline.
- README.md
- main.ts
- chat-widget.ts
- command-runner.ts
- style.css
- server.ts
- schemaList.ts
README.md View on GitHub
# DHTMLX Gantt - AI Gantt Manager Demo
This demo shows how to connect **DHTMLX Gantt** with an **AI-powered chatbot** that can control the Gantt chart with natural language instructions.
The chatbot understands natural language commands and can perform actions such as creating, updating, or deleting tasks directly in the chart.
### **[✨ Try the Live Demo >>>](https://dhtmlx.com/docs/demo/ai-gantt-maker/)**
**Related guide**: [Integrate an AI Assistant Using Tool Calls](https://docs.dhtmlx.com/gantt/integrations/ai-tools/ai-assistant/) — walkthrough of the integration pattern this demo implements.
The setup combines **DHTMLX Gantt** for project visualization, a **frontend app (Vite + TypeScript + HTML)** for UI, and a **backend (Express + Socket.IO)** for communication with an LLM (via OpenAI API or a compatible service). Everything is containerized with Docker.
## Features
- **AI-driven Gantt control** – interact with the Gantt Chart via chat using natural language instructions.
- **Project generation** – create complete project structures with tasks and dependencies.
- **Task management** – add, update, delete, and split tasks into subtasks with automatic chaining.
- **Dependency management** – create and modify task dependencies with different link types (Finish-to-Start, Start-to-Start, etc.).
- **Visual customization** – change task colors, text styles, progress bars, and apply different skins.
- **Timeline control** – zoom to different levels, add markers, and customize timeline scales.
- **Export functionality** – export your Gantt charts to PNG and PDF formats.
## How it works
This demo shows how a Gantt chart can be managed using natural language commands processed by an LLM. When the user types something like:
> _Generate a project called Website Relaunch with Design and QA phases._
the user's request provided via the chatbot, is sent to LLM, which then calls a function. The function returns a command and some data that is processed on the client. Finally, the chart is updated with the generated project and the user sees the result.
### The main flow works like this:
1. **Function calling with LLM**
- The backend uses the function calling feature of the OpenAI API.
- Available functions are defined in `backend/schemaList.ts`.
- Each function has a schema describing the parameters the model can return.
2. **Client-side command runner**
- On the frontend, the returned tool calls are handled in `frontend/src/command-runner.ts`
3. **System prompt and history management**
- Per-client session storage: Each Socket.IO connection maintains its own conversation history using `sessionMessagesByClient` Map.
- Smart history trimming: The `trimHistory()` function intelligently groups messages into "blocks" (user/assistant pairs + complete tool call cycles) and keeps only the most recent `MAX_MESSAGES` blocks to stay within token limits.
- Dynamic system prompt: `generateSystemPrompt()` creates a fresh system message for each new session.
- Tool call lifecycle: Assistant tool_calls are grouped with ALL their corresponding tool results before trimming, preserving complete execution cycles for the LLM context.
4. **Models and limitations**
- Works well with `gpt-5-nano` and `gpt-4.1-mini`.
- `gpt-4.1-nano` has noticeable limitations in following the schema.
- If experimenting with other providers, make sure they support **function calling**.
## Quick start
### Option 1: Production mode (Docker)
```bash
git clone https://github.com/DHTMLX/gantt-maker-ai-demo.git
cd gantt-maker-ai-demo
cp .env.example .env
# Edit .env with your API keys
docker compose up --build
```
Open http://localhost in your browser. The frontend runs on port 80, backend on port 3001. Make sure you have a valid OpenAI API key or another LLM provider configured in your .env.
### Option 2: Development mode (Docker)
Run with hot-reload for development:
```bash
git clone https://github.com/DHTMLX/gantt-maker-ai-demo.git
cd gantt-maker-ai-demo
cp .env.dev.example .env
# Edit .env with your API keys
docker compose -f docker-compose.dev.yml up --build
```
Open **http://localhost:3000** in your browser. Changes to code will auto-reload.
### Option 3: Local development (without Docker)
If you prefer running locally without Docker:
```bash
npm install
cp .env.dev.example .env
# Edit .env with your API keys
npm run dev:backend # http://localhost:3001
npm run dev:frontend # http://localhost:3000
```
---
## Environment Variables
```bash
# LLM API configuration
OPENAI_API_KEY=YOUR_OPENAI_API_KEY
OPENAI_BASE_URL=YOUR_OPENAI_BASE_URL
# Production mode (docker-compose.yml)
VITE_SOCKET_URL_DOCKER=http://localhost:3001
FRONTEND_ORIGIN_DOCKER=http://localhost
# Development mode (docker-compose.dev.yml)
VITE_SOCKET_URL_DOCKER=http://localhost:3001
FRONTEND_ORIGIN_DOCKER=http://localhost:3000
```
## Repo structure:
frontend/
├─ src/
│ ├─ gantt-utils/
│ ├─ chat-widget.ts
│ ├─ command-runner.ts
│ ├─ style.css
│ └─ main.ts
├─ vite.config.ts
├─ Dockerfile
├─ index.html
├─ .gitignore
├─ package-lock.json
└─ package.json
backend/
├─ .gitignore
├─ Dockerfile
├─ constants.ts
├─ logger.ts
├─ schemaList.ts
├─ server.ts
├─ helper.ts
├─ types.ts
├─ package-lock.json
└─ package.json
docker-compose.yml
docker-compose.dev.yml
.env.example
.env.dev.example
package.json
README.md
.gitignore
## Scripts (without Docker)
If you prefer running locally:
```bash
npm install
cp .env.example .env
# Backend
npm run dev:backend # http://localhost:3001
# Frontend
npm run dev:frontend # http://localhost:3000
```
## License
Source code in this repo is released under the **MIT License**.
**DHTMLX Gantt** is a commercial library – use under a valid [DHTMLX license](https://dhtmlx.com/docs/products/licenses.shtml) or evaluation agreement.
Usage of OpenAI API (or other LLM providers) is subject to their terms of service and billing.
## Useful links
- [Integrate an AI Assistant Using Tool Calls](https://docs.dhtmlx.com/gantt/integrations/ai-tools/ai-assistant/) — guide that walks through the integration pattern this demo implements
- [DHTMLX Gantt Product Page](https://dhtmlx.com/docs/products/dhtmlxGantt/)
- [DHTMLX Gantt Documentation](https://docs.dhtmlx.com/gantt/)
- [OpenAI API — function calling](https://platform.openai.com/docs/guides/function-calling)
- [Socket.IO Docs](https://socket.io/docs/v4/)
- [DHTMLX technical support forum](https://forum.dhtmlx.com/)
frontend/src/main.ts View on GitHub
import "@dhx/trial-gantt/codebase/dhtmlxgantt.css";
import { Gantt } from "@dhx/trial-gantt";
import { io } from "socket.io-client";
import { initChat } from "./chat-widget.ts";
import initZoom from "./gantt-utils/zoom.ts";
import fitTaskText from "./gantt-utils/fit-text.ts";
import createCommandRunner from "./command-runner.ts";
const gantt = Gantt.getGanttInstance();
gantt.config.columns = [
{ name: "wbs", label: "WBS", width: 60, resize: true, template: gantt.getWBSCode },
{ name: "text", label: "Task name", tree: true, width: 250, resize: true },
{ name: "start_date", align: "center", width: 100, resize: true },
{ name: "duration", align: "center", width: 80, resize: true },
{ name: "add", width: 40 },
];
gantt.plugins({
auto_scheduling: true,
undo: true,
export_api: true,
marker: true,
tooltip: true,
critical_path: true,
});
gantt.config.auto_scheduling = true;
gantt.config.open_tree_initially = true;
gantt.config.auto_types = true;
gantt.config.scale_height = 60;
gantt.config.open_split_tasks = true;
initZoom(gantt);
fitTaskText(gantt);
function parseSmartDate(str: string) {
if (typeof str !== "string") return null;
const p = str.trim().replace(/\//g, "-").split("-");
if (p.length !== 3) return null;
const [y, m, d] = p[0].length === 4 ? p : [p[2], p[1], p[0]];
const dt = new Date(+y, +m - 1, +d);
return isNaN(dt as any) ? null : dt;
}
gantt.templates.parse_date = (date: string): Date => {
const parsed = parseSmartDate(date);
return parsed ?? new Date();
};
gantt.init("gantt_here");
const runCommand = createCommandRunner(gantt);
const SOCKET_URL = import.meta.env.VITE_SOCKET_URL || `${window.location.origin}`;
const socket = io(SOCKET_URL);
initChat({
socket,
runCommand,
getProject: () => gantt.serialize(),
});
frontend/src/chat-widget.ts View on GitHub
import { marked } from "marked";
import DOMPurify from 'dompurify';
import MicroModal from "micromodal";
import type { Socket } from "socket.io-client";
interface InitChatOptions {
socket: Socket;
runCommand: (cmd: string, params: any) => void;
getProject: () => any;
}
interface Task {
text?: string;
type?: string;
}
interface TaskNames {
regular: string[];
summaries: string[];
any: string[];
}
export const initChat = ({ socket, runCommand, getProject }: InitChatOptions) => {
(function () {
const chatWidgetContainer = document.querySelector("#chat_panel") as HTMLElement;
if (chatWidgetContainer) {
chatWidgetContainer.dataset.id = "0";
chatWidgetContainer.innerHTML = `
<div id="chat-popup" class="relative left-0 bottom-0 w-full h-full bg-white rounded-md shadow-md flex flex-col transition-all">
<div id="chat-header" class="flex justify-between items-center p-4 bg-gray-800 text-white rounded-t-md">
<h3 class="m-0 text-lg">DHX Assistant</h3>
<button data-micromodal-trigger="modal-1" class="help-btn">?</button>
</div>
<div id="chat-messages" class="flex-1 p-4 pb-1 overflow-y-auto text-base"></div>
<div id="loader" class="hidden justify-start mb-3">
<div class="spinner m-auto"></div>
</div>
<div id="chat-input-container" class="p-4 border-t border-gray-200">
<div class="flex space-x-4 items-center">
<input type="text" id="chat-input" class="flex-1 border border-gray-300 rounded-md px-4 py-2 outline-none w-3/4" placeholder="Type your message...">
<button id="chat-submit" class="bg-gray-800 text-white rounded-md px-4 py-2 cursor-pointer">Send</button>
</div>
</div>
</div>
`;
const chatInput = document.getElementById("chat-input") as HTMLInputElement;
const chatSubmit = document.getElementById("chat-submit") as HTMLButtonElement;
const chatMessages = document.getElementById("chat-messages") as HTMLElement;
const loader = document.getElementById("loader") as HTMLElement;
chatSubmit.addEventListener("click", () => {
const message = chatInput.value.trim();
if (!message) return;
chatMessages.scrollTop = chatMessages.scrollHeight;
chatInput.value = "";
sendUserMessage(message);
});
chatMessages.addEventListener("click", (event: MouseEvent) => {
const target = (event.target as Element)?.closest(".prompt-pill") as HTMLElement;
if (target) {
const pillText = target.innerText;
sendUserMessage(pillText);
}
});
chatInput.addEventListener("keyup", (event: KeyboardEvent) => {
if (event.key === "Enter") {
chatSubmit.click();
}
});
function showLoader(): void {
loader.classList.remove("hidden");
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function hideLoader(): void {
loader.classList.add("hidden");
}
function sendUserMessage(message: string): void {
if (!message) return;
displayUserMsg(message);
chatInput.value = "";
chatSubmit.disabled = true;
showLoader();
socket.emit("user_msg", JSON.stringify({ message }));
}
function displayUserMsg(msg: string): void {
const div = document.createElement("div");
div.className = "flex justify-end mb-3";
div.innerHTML = `<div class="bg-gray-800 text-white rounded-lg py-2 px-4 max-w-[70%]">${DOMPurify.sanitize(msg)}</div>`;
chatMessages.appendChild(div);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
socket.on("assistant_msg", (txt: string) => {
hideLoader();
displayReply(txt);
chatSubmit.disabled = false;
});
socket.on("tool_call", (payload: { cmd: string; params: any }, ack?: (response: any) => void) => {
let handled = false;
try {
const { cmd, params } = payload;
if (cmd && cmd !== "none") {
runCommand(cmd, params);
onCallback(cmd, params);
if (typeof ack === "function") {
ack({
ok: true,
cmd,
data: getProject(),
});
} else {
hideLoader();
chatSubmit.disabled = false;
}
}
handled = true;
} catch (e: any) {
hideLoader();
displayReply(`Something wrong had happened: ${e.message}`);
if (typeof ack === "function") {
ack({
ok: false,
cmd: payload?.cmd || "unknown",
error: e.message,
});
}
handled = true;
}
if (!handled) {
displayReply(`Couldn't handle this: ${JSON.stringify(payload)}`);
}
});
function displayReply(message: string): void {
const div = document.createElement("div");
div.className = "flex mb-3";
const html = DOMPurify.sanitize(marked.parse(message) as string);
div.innerHTML = `<div class="bg-gray-100 text-black rounded-lg py-2 px-4 max-w-[70%]">${html}</div>`;
chatMessages.appendChild(div);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
let injectedMain = false;
let injectedChart = false;
function onCallback(cmd: string, params: { tasks: Task[] }): void {
if (!injectedMain && cmd === "generate_project") {
injectedMain = true;
displayReply(buildMainSuggestionsBlock(params.tasks));
return;
}
if (!injectedChart) {
injectedChart = true;
displayReply(buildChartSuggestionsBlock());
return;
}
}
function buildMainSuggestionsBlock(tasks: Task[]): string {
const { regular, any } = pickTaskNames(tasks, 8);
const t1 = any[0] || "Design";
const t2 = any[1] || "Build";
const t3 = any[2] || "QA";
const t4 = any[2] || "Review";
const tSplit = regular[0] || t1;
const pills = [
`Zoom to fit the screen`,
`Set "${t1}" to start after "${t2}"`,
`Split "${tSplit}" into subtasks and link them FS`,
`Set progress of "${t3}" to 60%`,
`Mark the task "${t4}" red`,
];
return `<p>Your project is ready. Keep shaping it with natural language. Try:</p>
<div class="suggestion-pills">
${pills.map((p) => `<button class="prompt-pill">${p}</button>`).join("")}
</div>`;
}
function buildChartSuggestionsBlock(): string {
const pills = [
`Add date marker "Kickoff" on Monday next week`,
`Switch to dark theme`,
`Clear the project`,
`Print project as PDF`,
];
return `<p>Pro tip: you can also configure the chart itself. For example: </p>
<div class="suggestion-pills">
${pills.map((p) => `<button class="prompt-pill">${p}</button>`).join("")}
</div>`;
}
function pickTaskNames(tasks: Task[], max: number = 8): TaskNames {
const reg: string[] = [];
const sum: string[] = [];
const all: string[] = [];
const seen = new Set<string>();
for (const t of tasks) {
const name = (t?.text || "").trim();
if (!name || seen.has(name)) continue;
seen.add(name);
all.push(name);
if (t?.type === "project") sum.push(name);
else reg.push(name);
}
shuffle(reg);
shuffle(sum);
shuffle(all);
return {
regular: reg.slice(0, max),
summaries: sum.slice(0, max),
any: all.slice(0, max),
};
}
function shuffle<T>(arr: T[]): void {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
displayReply(`## Welcome to the AI Project Wizard!
I can create, edit, or style your Gantt chart with plain-language commands.
<br/>
<br/>
Try things like:
<div class="suggestion-pills">
<button class="prompt-pill">Generate a project called Website Relaunch with Design, Build and QA phases.</button>
<button class="prompt-pill">Plan a Conference 2026 with Venue & Speakers, Sponsorships, Marketing, Run-of-Show, Post-Event.</button>
<button class="prompt-pill">Generate a Grant Application project with Eligibility Check, Narrative Draft, Budget, Reviews, Submission.</button>
</div>`);
}
})();
MicroModal.init({ disableScroll: true });
document.querySelectorAll(".copy-btn").forEach((btn) => {
btn.addEventListener("click", () => {
const text = btn.getAttribute("data-text");
if (text) {
navigator.clipboard.writeText(text).then(() => {
(btn as HTMLButtonElement).textContent = "Copied!";
setTimeout(() => ((btn as HTMLButtonElement).textContent = "Copy"), 1000);
});
}
});
});
const modalBodyWrapper = document.querySelector(".modal__body-wrapper") as HTMLElement;
const modalScrollTopBtn = document.getElementById("btn-scroll-top") as HTMLButtonElement;
modalBodyWrapper.addEventListener("scroll", () => {
modalScrollTopBtn.style.display = modalBodyWrapper.scrollTop > 200 ? "block" : "none";
});
modalScrollTopBtn.addEventListener("click", () => {
modalBodyWrapper.scrollTo({ top: 0, behavior: "smooth" });
});
};
frontend/src/command-runner.ts View on GitHub
import { type GanttStatic, type Task, type Link } from "@dhx/trial-gantt";
export default function (gantt: GanttStatic) {
return function runCommand(cmd: string, args: any) {
const strToDate = gantt.date.str_to_date("%Y-%m-%d");
const dateToStr = gantt.date.date_to_str("%Y-%m-%d");
switch (cmd) {
case "add_task":
gantt.addTask(args);
break;
case "update_tasks":{
const { tasks } = args;
gantt.batchUpdate(function () {
tasks.forEach((task: Task) => {
if(!gantt.isTaskExists(task.id)){
console.warn(`there is no such task in Gantt: ${task.id}`)
return;
}
let existedTask = gantt.getTask(task.id);
if(existedTask.type === "project") return;
if(task.start_date){
task.start_date = gantt.templates.parse_date(new Date(task.start_date).toISOString());
}
if(task.end_date){
task.end_date = gantt.templates.parse_date(new Date(task.end_date).toISOString());
}
if(task.start_date && task.duration){
task.end_date = gantt.calculateEndDate({
start_date: task.start_date,
duration: task.duration,
task: existedTask
});
} else if (task.end_date && task.duration){
task.start_date = gantt.calculateEndDate({
start_date: task.end_date,
duration: -task.duration,
task: existedTask
});
} else if(task.duration){
task.end_date = gantt.calculateEndDate({
start_date: existedTask.start_date!,
duration: task.duration,
task: existedTask
});
}
Object.assign(existedTask, task);
gantt.updateTask(task.id);
});
});
break;
}
case "delete_tasks": {
const { tasks } = args;
gantt.batchUpdate(function () {
tasks.forEach((task: Task) => {
if(gantt.isTaskExists(task.id)){
gantt.deleteTask(task.id);
}
});
});
break;
}
case "split_task": {
const parent = gantt.getTask(args.id || args.task_id);
parent.$open = true;
parent.render = "split";
const newIds: any = [];
(args.subtasks || args.new_tasks).forEach((t: Task) => {
newIds.push(gantt.addTask({ ...t, id: gantt.uid(), parent: parent.id }));
});
if (args.addFSLinks) {
for (let i = 1; i < args.subtasks.length; i++) {
gantt.addLink({
source: newIds[i - 1],
target: newIds[i],
type: gantt.config.links.finish_to_start,
});
}
}
break;
}
case "add_links":{
const { links } = args;
gantt.batchUpdate(function () {
links.forEach((link: Link) => {
gantt.addLink({
id: link.id,
source: link.source,
target: link.target,
type: link.type,
});
});
});
break;
}
case "delete_links":{
const { links } = args;
gantt.batchUpdate(function () {
links.forEach((link: Link) => {
if(gantt.isLinkExists(link.id)){
gantt.deleteLink(link.id);
}
});
})
break;
}
case "style_task": {
const { id, color } = args;
const apply = (task: Task) => {
task.color = color;
gantt.refreshTask(task.id);
};
if (id === "all") {
gantt.eachTask((task) => apply(task));
} else {
gantt.batchUpdate(() => {
const task = gantt.getTask(id);
if (task) apply(task);
});
}
break;
}
case "style_link":
const { id: linkId, color } = args;
const apply = (link: Link) => {
link.color = color;
gantt.refreshLink(link.id);
};
if (linkId === "all") {
gantt.getLinks().forEach((link) => apply(link));
} else {
gantt.batchUpdate(() => {
const link = gantt.getLink(linkId);
if (link) apply(link);
});
}
break;
case "set_link_width":
gantt.config.link_line_width = args.width;
gantt.render();
break;
case "set_link_wrapper_width":
gantt.config.link_wrapper_width = args.width;
gantt.render();
break;
case "show_links":
gantt.config.show_links = args.show;
gantt.render();
break;
case "set_text_color":
const { id: taskId, color: textColor } = args;
const applyTextColor = (task: Task) => {
task.textColor = textColor;
gantt.refreshTask(task.id);
};
if (taskId === "all") {
gantt.batchUpdate(() => {
gantt.eachTask((task) => applyTextColor(task));
});
} else {
const task = gantt.getTask(taskId);
if (task) applyTextColor(task);
}
break;
case "set_progress_color":
const { id: taskId2, color: progressColor } = args;
const applyProgressColor = (task: Task) => {
task.progressColor = progressColor;
gantt.refreshTask(task.id);
};
if (taskId2 === "all") {
gantt.batchUpdate(() => {
gantt.eachTask((task) => applyProgressColor(task));
});
} else {
const task = gantt.getTask(taskId2);
if (task) applyProgressColor(task);
}
break;
case "set_task_tooltip":
if (args.enable) {
gantt.templates.tooltip_text = (start, end, task) => {
const taskText = "<b>Task:</b>" + task.text + "<br/>";
const startDate = "<b>Start date:</b> " + dateToStr(start) + "<br/>";
const endDate = "<b>End date:</b> " + dateToStr(end);
return `${taskText} ${startDate} ${endDate}`;
};
} else {
gantt.templates.tooltip_text = () => { return ""; };
}
break;
case "add_marker":
const { id, start_date, text, title } = args;
gantt.addMarker({
id,
start_date: strToDate(start_date),
text,
title,
});
break;
case "set_scales": {
const { scales } = args;
if (!Array.isArray(scales)) break;
const newScales = scales.map((s) => {
const obj: any = { unit: s.unit, step: s.step };
if (s.format) obj.format = s.format;
if (s.cssClass) {
obj.css = (date: Date) => date.getDay() === 0 || date.getDay() === 6 ? "weekend" : "";
}
return obj;
});
if (scales.length >= 2) {
gantt.config.scale_height = 70;
} else {
gantt.config.scale_height = 40;
}
gantt.config.scales = newScales as any;
gantt.render();
break;
}
case "set_skin":
gantt.setSkin(args.skin);
break;
case "zoom":
if (args.level === "fit") {
gantt.ext.zoomToFit();
} else {
gantt.ext.zoom.setLevel(args.level);
}
break;
case "autoschedule":
gantt.autoSchedule(args.anchorTaskId || undefined);
break;
case "hide_weekdays":
const days = args.days;
gantt.ignore_time = function (date) {
if (days.includes(date.getDay())) return true;
};
gantt.render();
break;
case "undo":
gantt.ext.undo.undo();
break;
case "export_png":
gantt.exportToPNG({ name: args.name || "gantt.png" });
break;
case "export_to_pdf":
gantt.exportToPDF({
name: args.name || "gantt.pdf",
raw: args.raw ?? true,
});
break;
case "generate_project":
gantt.clearAll();
gantt.parse({
data: args.tasks,
links:
(args.links || []).map((l:Link) => {
return {
source: l.source || l.sourceId,
target: l.target || l.targetId,
type: l.type,
lag: l.lag || 0,
};
}) || [],
});
break;
case "create_tasks":
gantt.parse({
data: args.tasks,
links:
(args.links || []).map((l:Link) => {
return {
source: l.source || l.sourceId,
target: l.target || l.targetId,
type: l.type,
lag: l.lag || 0,
};
}) || [],
});
break;
case 'highlight_critical_path':
gantt.config.highlight_critical_path = args.enable;
gantt.render();
break;
case "clear_all":
gantt.clearAll();
break;
case "get_gantt_state":
break;
default:
console.warn("Unknown cmd:", cmd, args);
}
};
}
frontend/src/style.css View on GitHub
@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
:root {
--bg-body: #f5f7fa;
--txt-ai: #1a1a1a;
--font-sans: Roboto, Helvetica, Arial, sans-serif;
}
html,
body {
height: 100%;
margin: 0;
font-size: 14px;
font-family: var(--font-sans);
background: var(--bg-body);
color: var(--txt-ai);
}
#wrapper {
display: grid;
grid-template-columns: 1fr 40vw;
column-gap: 16px;
height: calc(100vh - 65px);
padding: 10px;
padding-bottom: 4px;
overflow: hidden;
}
.gantt-container {
width: 100%;
height: 100%;
min-width: 0;
}
#chat_panel {
position: relative;
overflow-y: auto;
min-width: 0;
height: 100%;
}
.spinner {
width: 24px;
height: 24px;
border: 4px solid #ccc;
border-top-color: #333;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
#chat-widget-container {
position: fixed;
bottom: 65px;
right: 30px;
flex-direction: column;
}
#chat-popup {
transition: all 0.3s;
}
@media (max-width: 1024px) {
#wrapper {
grid-template-columns: 1fr;
grid-template-rows: minmax(300px, 50vh) minmax(300px, 1fr);
row-gap: 20px;
column-gap: 0;
}
.gantt_layout_cell.gantt_ver_scroll {
z-index: 0 !important;
}
}
@media (max-width: 768px) {
#wrapper {
grid-template-rows: minmax(250px, 40vh) minmax(250px, 1fr);
grid-template-columns: 1fr;
row-gap: 20px;
column-gap: 0;
height: calc(100vh - 80px);
}
.gantt_layout_cell.gantt_ver_scroll {
z-index: 0 !important;
}
}
#chat-messages h2 {
font-weight: bold;
font-size: 1.2rem;
}
#chat-messages ul li {
list-style: disc;
margin-left: 10px;
}
.weekend {
background: var(--dhx-gantt-base-colors-background-alt);
}
.help-btn {
width: 35px;
height: 35px;
border-radius: 50%;
border: 1px solid #fff;
font-size: 1.2rem;
font-weight: 500;
margin-left: auto;
margin-right: 20px;
background-color: #fff;
color: #000;
transition: all ease-in-out 0.2s;
}
.help-btn:hover {
background-color: #1f2937;
border: 1px solid #fff;
color: #fff;
}
.help-btn:focus {
outline: none;
}
/**************************\
Basic Modal Styles
\**************************/
.modal {
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, helvetica neue, helvetica, ubuntu, roboto, noto,
segoe ui, arial, sans-serif;
}
.modal__overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 12;
}
.modal__container {
position: relative;
max-height: 75vh;
width: 700px;
padding: 25px;
padding-right: 10px;
background: #fff;
border-radius: 4px;
}
.modal__body-wrapper {
position: relative;
max-height: calc(75vh - /* header height */ 60px);
overflow-y: auto;
scroll-behavior: smooth;
}
@media screen and (max-width: 768px) {
.modal__container {
width: 85vw;
}
}
.modal__header {
display: flex;
justify-content: space-between;
align-items: center;
}
.modal__title {
margin: 0 auto;
font-weight: 600;
font-size: 1.25rem;
line-height: 1.25;
color: #00449e;
box-sizing: border-box;
}
.modal__nav {
margin-bottom: 1rem;
padding-left: 15px;
}
.modal__nav ul {
list-style: none;
padding-left: 0;
display: flex;
flex-wrap: wrap;
flex-direction: column;
gap: 0.5rem;
}
.modal__nav ul li {
list-style-type: disc;
}
.modal__nav a {
text-decoration: none;
color: #007bff;
font-weight: 500;
}
.modal__nav a:hover {
text-decoration: underline;
}
.modal__close {
background: transparent;
border: 0;
padding-right: 5px;
}
.modal__header .modal__close:before {
content: '\2715';
}
.modal__content {
margin-top: 2rem;
margin-bottom: 2rem;
line-height: 1.5;
color: rgba(0, 0, 0, 0.8);
}
.modal__content h3 {
text-transform: uppercase;
border-bottom: 1px solid #ccc;
font-weight: 600;
margin-top: 40px;
}
.modal__btn {
font-size: 0.875rem;
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
background-color: #e6e6e6;
color: rgba(0, 0, 0, 0.8);
border-radius: 0.25rem;
border-style: none;
border-width: 0;
cursor: pointer;
appearance: button;
-webkit-appearance: button;
text-transform: none;
overflow: visible;
line-height: 1.15;
margin: 0;
will-change: transform;
-moz-osx-font-smoothing: grayscale;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
-webkit-transform: translateZ(0);
transform: translateZ(0);
transition: -webkit-transform 0.25s ease-out;
transition: transform 0.25s ease-out;
transition: transform 0.25s ease-out, -webkit-transform 0.25s ease-out;
}
.scroll-top-btn {
position: fixed;
bottom: 25px;
right: 30px;
display: none;
padding: 4px;
width: 36px;
height: 36px;
border-radius: 50%;
background: #007bff;
color: white;
font-size: 1.2rem;
font-weight: bold;
border: none;
cursor: pointer;
z-index: 20;
opacity: 0.7;
transition: opacity 0.2s;
}
.scroll-top-btn:hover {
opacity: 1;
}
.cmd {
display: flex;
flex-direction: column;
gap: 7px;
margin-top: 20px;
}
.cmd strong {
color: #00449e;
}
.modal__btn:focus,
.modal__btn:hover {
-webkit-transform: scale(1.05);
transform: scale(1.05);
}
.modal__btn-primary {
background-color: #00449e;
color: #fff;
}
.copy-btn {
background-color: #efefef;
border: 1px solid #efefef;
color: #000;
padding: 4px 10px;
border-radius: 8px;
width: 75px;
transition: all ease-in-out 0.2s;
}
.copy-btn:hover {
background-color: #e2e2e2;
}
.modal__footer {
width: 100%;
display: flex;
justify-content: center;
}
.modal__footer .modal__btn {
width: 150px;
text-transform: uppercase;
}
/**************************\
Demo Animation Style
\**************************/
@keyframes mmfadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes mmfadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes mmslideIn {
from {
transform: translateY(15%);
}
to {
transform: translateY(0);
}
}
@keyframes mmslideOut {
from {
transform: translateY(0);
}
to {
transform: translateY(-10%);
}
}
.micromodal-slide {
display: none;
}
.micromodal-slide.is-open {
display: block;
}
.micromodal-slide[aria-hidden='false'] .modal__overlay {
animation: mmfadeIn 0.3s cubic-bezier(0, 0, 0.2, 1);
}
.micromodal-slide[aria-hidden='false'] .modal__container {
animation: mmslideIn 0.3s cubic-bezier(0, 0, 0.2, 1);
}
.micromodal-slide[aria-hidden='true'] .modal__overlay {
animation: mmfadeOut 0.3s cubic-bezier(0, 0, 0.2, 1);
}
.micromodal-slide[aria-hidden='true'] .modal__container {
animation: mmslideOut 0.3s cubic-bezier(0, 0, 0.2, 1);
}
.micromodal-slide .modal__container,
.micromodal-slide .modal__overlay {
will-change: transform;
}
.suggestion-pills {
padding: 8px 0;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.prompt-pill {
padding: 4px 12px;
font-size: 14px;
font-weight: 500;
background-color: #e9f3f8;
color: #0277bd;
border: 1px solid #d1e6f0;
border-radius: 16px;
cursor: pointer;
transition: background-color 0.2s, transform 0.1s;
text-align: left;
}
.prompt-pill:hover {
background-color: #d1e6f0;
transform: translateY(-1px);
}
/* =================================
Footer Styles
================================= */
.dhx_demo-footer {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background: #2d333f;
padding-left: 8px;
padding-right: 8px;
display: flex;
justify-content: space-between;
align-items: center;
min-height: 60px;
max-height: 60px;
z-index: 1000;
font-family: Roboto, sans-serif;
}
.dhx_demo-footer .dhx_layout-cell-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 12px;
width: 100%;
}
.dhx_demo-footer .dhx_slider-btn {
display: flex;
align-items: center;
justify-content: center;
outline: none;
border-radius: 50%;
border: 2px solid #fff;
height: 36px;
width: 36px;
min-width: 36px;
text-decoration: none;
transition: border-color 0.3s, fill 0.3s;
}
.dhx_demo-footer .dhx_slider-btn:hover {
border-color: #03a9f4;
}
.dhx_demo-footer .dhx_slider-btn:hover path {
fill: #03a9f4;
}
.dhx_demo-footer .dhx_slider-btn path {
transition: fill 0.3s;
}
.dhx_demo-footer .dhx_slider-btn--prev {
transform: rotate(180deg);
margin-right: 12px;
}
.dhx_demo-footer-text {
flex: 1 1 auto;
padding: 0 20px;
text-align: center;
font-weight: 500;
font-size: 15px;
line-height: 1.3;
color: rgba(255, 255, 255, 0.8);
}
.dhx_demo-footer-text h1,
.dhx_demo-footer-text p {
display: inline;
margin: 0;
padding: 0;
color: inherit;
}
.dhx_demo-footer-text h1 {
font-size: inherit;
font-weight: inherit;
}
.dhx_demo-footer-text a {
color: #03a9f4;
text-decoration: none;
transition: color 0.2s ease-in-out;
padding-left: 4px;
}
.dhx_demo-footer-text a:hover {
text-decoration: underline;
}
.dhx_demo-footer .dhx_sample-btn {
flex-shrink: 0;
border: none;
outline: none;
padding: 6px 28px;
font-family: Roboto, Arial, Tahoma, Verdana, sans-serif;
font-size: 14px;
line-height: 20px;
font-weight: 500;
display: flex;
cursor: pointer;
text-decoration: none;
white-space: nowrap;
}
.dhx_demo-footer .dhx_sample-btn--cta {
background: #0288d1;
color: #fff;
border-radius: 32px;
text-decoration: none;
transition: background-color 0.2s ease-in-out;
}
.dhx_demo-footer .dhx_sample-btn--cta:hover {
background: #027abc;
}
@media (max-width: 768px) {
.dhx_demo-footer {
max-height: 80px;
}
}
backend/server.ts View on GitHub
import "dotenv/config";
import express, { type Express } from "express";
import { createServer, type Server as HttpServer } from "http";
import { Server, type Socket } from "socket.io";
import OpenAI from "openai";
import { schemaList } from "./schemaList.js";
import { log } from "./logger.js";
import {
executeToolCall,
getHistory,
getMessagesHistoryByClient,
saveMessage,
sessionMessagesByClient,
trimHistory,
} from "./helper.js";
import { type UserMsgPayload } from "./types.js";
import { MAX_TURNS, MODEL, SKIP_MESSAGE } from "./constants.js";
const app: Express = express();
const httpServer: HttpServer = createServer(app);
const io = new Server(httpServer, {
cors: { origin: process.env.FRONTEND_ORIGIN || "http://localhost:3000" },
});
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: process.env.OPENAI_BASE_URL,
});
app.use(express.static("../frontend/dist"));
io.on("connection", (socket: Socket) => {
log.info(`Client connected: ${socket.id}`);
let idleTimer: NodeJS.Timeout | null = null;
const resetIdleTimer = () => {
if (idleTimer) clearTimeout(idleTimer);
idleTimer = setTimeout(() => {
if (!socket.connected) return;
sessionMessagesByClient.delete(socket.id);
}, 30 * 60 * 1000);
};
resetIdleTimer();
socket.on("user_msg", async (payload: UserMsgPayload | string) => {
try {
resetIdleTimer();
const { message } = typeof payload === "string" ? JSON.parse(payload) : payload;
getMessagesHistoryByClient(socket.id, generateSystemPrompt());
saveMessage(socket.id, { role: "user", content: message });
for (let turn = 0; turn < MAX_TURNS; turn++) {
const history = trimHistory(getHistory(socket.id));
const response = await openai.chat.completions.create({
model: MODEL,
messages: history,
tools: schemaList,
tool_choice: "auto",
});
const msg = response.choices[0].message;
if (!msg.tool_calls?.length) {
socket.emit("assistant_msg", msg.content ?? "");
saveMessage(socket.id, { role: "assistant", content: msg.content ?? "" });
return;
}
saveMessage(socket.id, { role: "assistant", tool_calls: msg.tool_calls });
for (const call of msg.tool_calls) {
try {
const result = await executeToolCall({ socket, call });
saveMessage(socket.id, { role: "tool", tool_call_id: call.id, content: JSON.stringify(result) });
} catch (err) {
saveMessage(socket.id, {
role: "tool",
tool_call_id: call.id,
content: JSON.stringify({ ok: false, error: String(err) }),
});
}
}
}
socket.emit("assistant_msg", "Request required too many steps. Please try a simpler command.");
} catch (err) {
log.error("Error handling message", err);
socket.emit("assistant_msg", "Something went wrong.");
}
});
socket.on("disconnect", () => {
if (idleTimer) clearTimeout(idleTimer);
sessionMessagesByClient.delete(socket.id);
log.info(`Client disconnected: ${socket.id}`);
});
});
function generateSystemPrompt(): string {
return `
You are ProjectGanttAssistant.
Your job is to help users control a DHTMLX Gantt chart using natural language.
Guidelines:
- Analyze the available tools and choose the best one for the user's request.
- If a tool matches, call it instead of describing the action abstractly.
- After all required tool calls are done, respond briefly in plain language.
Tool usage rules (STRICT):
- If the request depends on existing tasks or links, you MUST call get_gantt_state first.
- After you receive the get_gantt_state result, analyze it and then call the required tool.
- If state is already available, do not call it again.
- If a tool matches the request, you MUST call it.
- If the request does NOT match any tool → you MUST return the following message: ${SKIP_MESSAGE}.
Output rules:
- Don't return tools code.
- Final answer MUST be 1-2 sentences.
- Plain text only.
Examples:
User: "create a task called Design starting tomorrow for 3 days"
→ Call: add_task
User: "update a QA task"
→ Call: get_gantt_state
→ Call: add_task
User: "delete all tasks"
→ Call: get_gantt_state
→ Call: delete_tasks
Keep responses short and clear.
Today is ${new Date().toISOString().split("T")[0]}.
`;
}
httpServer.listen(3001, () => {
console.log("API running on :3001");
});
backend/schemaList.ts View on GitHub
import type { ChatCompletionTool } from "openai/resources/index.mjs";
const taskProperties = {
id: { type: ["string", "number"] },
text: { type: "string" },
start_date: {
type: "string",
format: "date",
description: "ISO-8601 start date, for example 2025-05-01",
},
duration: {
type: "number",
description: "Task duration in whole chart units",
},
parent: {
type: ["string", "number", "null"],
description: "Parent task id, or null for root",
},
progress: {
type: "number",
description: "Task progress ratio from 0 to 1",
minimum: 0,
maximum: 1,
},
end_date: {
type: "string",
format: "date",
description: "Optional ISO-8601 end date, for example 2025-05-10",
},
} as const;
const fullTaskSchema = {
type: "object",
additionalProperties: false,
properties: taskProperties,
required: ["id", "text", "start_date", "duration", "parent", "progress"],
} as const;
const partialTaskSchema = {
type: "object",
additionalProperties: false,
properties: taskProperties,
required: ["id"],
} as const;
const deleteTaskSchema = {
type: "object",
additionalProperties: false,
properties: {
id: taskProperties.id,
},
required: ["id"],
} as const;
const linkProperties = {
id: { type: ["string", "number"], description: "Link id" },
source: { type: ["string", "number"], description: "Source task id" },
target: { type: ["string", "number"], description: "Target task id" },
type: {
type: "string",
enum: ["0", "1", "2", "3"],
description:
"0 is Finish to Start, 1 is Start to Start, 2 is Finish to Finish, 3 is Start to Finish",
},
} as const;
const fullLinkSchema = {
type: "object",
additionalProperties: false,
properties: linkProperties,
required: ["id", "source", "target", "type"],
} as const;
const addLinkSchema = {
type: "object",
additionalProperties: false,
properties: linkProperties,
required: ["source", "target", "type"],
} as const;
const deleteLinkSchema = {
type: "object",
additionalProperties: false,
properties: {
id: linkProperties.id,
},
required: ["id"],
} as const;
const scalesItemSchema = {
type: "object",
additionalProperties: false,
properties: {
unit: {
type: "string",
description: "Scale unit: minute, hour, day, week, month, or year",
},
step: { type: "number", description: "Scale step size" },
format: {
type: ["string", "null"],
description: "Optional DHTMLX date format string",
},
cssClass: {
type: ["string", "null"],
description: "Optional CSS class hint for special styling",
},
},
required: ["unit", "step"],
} as const;
export const schemaList: ChatCompletionTool[] = [
{
type: "function",
function: {
name: "generate_project",
description:
"Generate a full project tree with tasks and optional links, ready for gantt.parse().",
parameters: {
type: "object",
additionalProperties: false,
properties: {
tasks: {
type: "array",
items: fullTaskSchema,
minItems: 1,
},
links: {
type: "array",
items: fullLinkSchema,
},
},
required: ["tasks", "links"],
},
},
},
{
type: "function",
function: {
name: "create_tasks",
description:
"Create multiple tasks and optional links from the user's request.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
tasks: {
type: "array",
items: fullTaskSchema,
minItems: 1,
},
links: {
type: "array",
items: fullLinkSchema,
},
},
required: ["tasks", "links"],
},
},
},
{
type: "function",
function: {
name: "update_tasks",
description:
"Update one or more existing tasks. Include each task id and only the fields that should change.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
tasks: {
type: "array",
items: partialTaskSchema,
minItems: 1,
},
},
required: ["tasks"],
},
},
},
{
type: "function",
function: {
name: "delete_tasks",
description: "Delete one or more existing tasks by id.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
tasks: {
type: "array",
items: deleteTaskSchema,
minItems: 1,
},
},
required: ["tasks"],
},
},
},
{
type: "function",
function: {
name: "clear_all",
description: "Clear all tasks, links, markers, and layers from the Gantt.",
parameters: {
type: "object",
additionalProperties: false,
properties: {},
required: [],
},
},
},
{
type: "function",
function: {
name: "add_task",
description: "Create a single new task, optionally under a parent task.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
id: taskProperties.id,
text: taskProperties.text,
start_date: taskProperties.start_date,
duration: taskProperties.duration,
parent: taskProperties.parent,
progress: taskProperties.progress,
},
required: ["text", "start_date", "duration"],
},
},
},
{
type: "function",
function: {
name: "split_task",
description:
"Replace a task with generated subtasks and optionally connect them with Finish-to-Start links.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
id: {
type: ["string", "number"],
description: "Existing task id to split",
},
subtasks: {
type: "array",
minItems: 1,
items: {
type: "object",
additionalProperties: false,
properties: {
text: { type: "string" },
start_date: taskProperties.start_date,
duration: taskProperties.duration,
split_placement: {
type: "string",
enum: ["auto", "inline", "subrow"],
description:
"How the subtask should be rendered relative to the parent row",
},
},
required: ["text", "start_date", "duration"],
},
},
addFSLinks: {
type: "boolean",
description: "Whether to connect generated subtasks with FS links",
},
},
required: ["id", "subtasks"],
},
},
},
{
type: "function",
function: {
name: "add_links",
description: "Create one or more dependency links.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
links: {
type: "array",
items: addLinkSchema,
minItems: 1,
},
},
required: ["links"],
},
},
},
{
type: "function",
function: {
name: "delete_links",
description: "Delete one or more dependency links by id.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
links: {
type: "array",
items: deleteLinkSchema,
minItems: 1,
},
},
required: ["links"],
},
},
},
{
type: "function",
function: {
name: "zoom",
description: "Change the timeline zoom level or fit the full chart into view.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
level: {
type: "string",
enum: ["hour", "day", "week", "month", "quarter", "year", "fit"],
},
},
required: ["level"],
},
},
},
{
type: "function",
function: {
name: "style_task",
description: "Apply a color to a task or to all tasks.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
id: {
type: ["string", "number"],
description: "Task id or all",
},
color: { type: "string", description: "CSS color value" },
},
required: ["id", "color"],
},
},
},
{
type: "function",
function: {
name: "style_link",
description: "Apply a color to a dependency link or to all links.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
id: {
type: ["string", "number"],
description: "Link id or all",
},
color: { type: "string", description: "CSS color value" },
},
required: ["id", "color"],
},
},
},
{
type: "function",
function: {
name: "set_link_width",
description: "Set the width of dependency links.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
width: { type: "number" },
},
required: ["width"],
},
},
},
{
type: "function",
function: {
name: "show_links",
description: "Show or hide dependency links.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
show: { type: "boolean" },
},
required: ["show"],
},
},
},
{
type: "function",
function: {
name: "set_text_color",
description: "Change the text color inside a task bar.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
id: {
type: ["string", "number"],
description: "Task id or all",
},
color: { type: "string" },
},
required: ["id", "color"],
},
},
},
{
type: "function",
function: {
name: "set_progress_color",
description: "Change the progress bar color for a task or for all tasks.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
id: {
type: ["string", "number"],
description: "Task id or all",
},
color: { type: "string" },
},
required: ["id", "color"],
},
},
},
{
type: "function",
function: {
name: "set_task_tooltip",
description: "Enable or disable task tooltips.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
enable: { type: "boolean" },
},
required: ["enable"],
},
},
},
{
type: "function",
function: {
name: "add_marker",
description: "Add a marker to the timeline.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
id: { type: ["string", "number"] },
start_date: {
type: "string",
format: "date",
description: "ISO marker date",
},
text: { type: "string", description: "Marker label" },
title: {
type: "string",
description: "Marker tooltip text",
},
},
required: ["id", "start_date", "text", "title"],
},
},
},
{
type: "function",
function: {
name: "set_scales",
description: "Set custom chart scales.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
scales: {
type: "array",
items: scalesItemSchema,
minItems: 1,
},
},
required: ["scales"],
},
},
},
{
type: "function",
function: {
name: "set_skin",
description: "Set the chart skin or theme.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
skin: {
type: "string",
enum: [
"terrace",
"dark",
"material",
"contrast-white",
"contrast-black",
"skyblue",
"meadow",
"broadway",
],
},
},
required: ["skin"],
},
},
},
{
type: "function",
function: {
name: "autoschedule",
description:
"Run auto-scheduling for the whole chart or starting from a specific task.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
anchorTaskId: {
type: ["string", "number", "null"],
},
},
required: [],
},
},
},
{
type: "function",
function: {
name: "hide_weekdays",
description: "Hide one or more weekdays on the chart.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
days: {
type: "array",
minItems: 1,
items: {
type: "integer",
enum: [0, 1, 2, 3, 4, 5, 6],
},
},
},
required: ["days"],
},
},
},
{
type: "function",
function: {
name: "undo",
description: "Undo the last user-visible action.",
parameters: {
type: "object",
additionalProperties: false,
properties: {},
required: [],
},
},
},
{
type: "function",
function: {
name: "export_png",
description: "Export the current Gantt chart to PNG.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
name: { type: "string", description: "Optional file name" },
},
required: [],
},
},
},
{
type: "function",
function: {
name: "export_to_pdf",
description: "Export the current Gantt chart to PDF.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
name: { type: "string", description: "Optional file name" },
raw: {
type: "boolean",
description: "Include all HTML markup and custom styles",
},
},
required: [],
},
},
},
{
type: "function",
function: {
name: "highlight_critical_path",
description: "Enable or disable critical path highlighting.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
enable: { type: "boolean" },
},
required: ["enable"],
},
},
},
{
type: "function",
function: {
name: "get_gantt_state",
description:
"Read the current chart state, including tasks and links, when the request depends on existing Gantt data.",
parameters: {
type: "object",
additionalProperties: false,
properties: {},
required: [],
},
},
},
];