AI/AI theme builder
Loading live demo…from dhtmlx.com
AI
AI theme builder
Customize DHTMLX Gantt themes and configuration through an AI chat assistant, then inspect and fine-tune the generated CSS variables and config code.
- README.md
- main.js
- chat-widget.js
- theme-manager.js
- command-runner.js
- demo-data.js
- style.css
- server.js
- schemaList.js
README.md View on GitHub
# DHTMLX Gantt - AI Theme Builder Demo
This demo shows how to connect **DHTMLX Gantt** with an **AI-powered chatbot** that can customize the theme and configuration of the Gantt chart using natural language instructions.
The chatbot generates **CSS variables** and **Gantt configs** based on user requests, which are instantly applied to the chart.
Additionally, the demo includes two tabs above the chat area:
- **Chat tab** – where you can describe the style/theme you want and send this request to an LLM via the Chatbot.
- **Code tab** – where you can review and edit the generated CSS variables and configuration manually.
The setup combines **DHTMLX Gantt** for project visualization, a **frontend app (Vite + React)** 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.
### **[✨ Try the Live Demo >>>](https://dhtmlx.com/docs/demo/ai-gantt-theme-builder/)**
---
## Features
- **AI-driven theme customization** – adjust colors, sizes, and styles by simply describing them in natural language.
- **CSS variables generation** – LLM generates reusable theme variables for Gantt.
- **Config editing** – control Gantt parameters like row/task height, link width, link radius, and more.
- **Two editing modes** – switch between AI chat and manual code editing.
---
## How it works
This demo demonstrates how AI can serve as a **theme builder** for Gantt. For example, when a user types:
> _"Create a bright, warm theme and make row and task bar height slightly bigger."_
the chatbot sends the request to the LLM, which responds with a set of CSS variables and config options. These are then applied to the chart, and the user sees the updated look.
### Main flow:
1. **Function calling with LLM**
- The backend uses the function calling feature of the OpenAI API.
- Available functions are defined in `backend/schemaList.js`.
- 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.js`, and generated variables and configs are applied to Gantt.
3. **Two modes (Chatbot and Code tab)**
- Chat - natural language to generate styles.
- In the `Code` tab, users can inspect and fine-tune the output.
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-theme-builder-ai-demo.git
cd ai-theme-builder-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-theme-builder-ai-demo.git
cd ai-theme-builder-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:
```bash
frontend/
├─ src/
│ ├─ gantt-utils/
│ ├─ chat-widget.js
│ ├─ theme-manager.js
│ ├─ demo-data.js
│ ├─ command-runner.js
│ ├─ style.css
│ └─ main.js
├─ vite.config.js
├─ Dockerfile
├─ Dockerfile.dev
├─ index.html
├─ vite.config.js
├─ .gitignore
├─ package-lock.json
└─ package.json
backend/
├─ .gitignore
├─ Dockerfile
├─ Dockerfile.dev
├─ logger.js
├─ schemaList.js
├─ server.js
├─ package-lock.json
└─ package.json
docker-compose.yml
docker-compose.dev.yml
.env.example
.env.dev.example
package.json
README.md
.gitignore
```
## 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
- [DHTMLX Gantt Product Page](https://dhtmlx.com/docs/products/dhtmlxGantt/)
- [DHTMLX Gantt Documentation](https://docs.dhtmlx.com/gantt/)
- [OpenAI API Docs](https://platform.openai.com/docs/)
- [Socket.IO Docs](https://socket.io/docs/v4/)
- [DHTMLX technical support forum](https://forum.dhtmlx.com/)
frontend/src/main.js 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.js';
import initZoom from './gantt-utils/zoom.js';
import fitTaskText from './gantt-utils/fit-text.js';
import createCommandRunner from './command-runner.js';
import { demoData } from './demo-data.js';
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,
});
gantt.config.auto_scheduling = true;
gantt.config.open_tree_initially = true;
gantt.config.auto_types = true;
gantt.config.scale_height = 60;
gantt.config.link_radius = 4;
initZoom(gantt);
fitTaskText(gantt);
// if llm can't decide in which format it returns dates
function parseSmartDate(str) {
if (typeof str !== 'string') return null;
const p = str.trim().replace(/\//g, '-').split('-');
if (p.length !== 3) return null;
// decide order by first chunk length
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) ? null : dt;
}
gantt.templates.parse_date = parseSmartDate;
gantt.init('gantt_here');
gantt.parse(demoData);
const configList = ['link_line_width', 'link_radius', 'row_height', 'bar_height', 'show_progress'];
const defaultConfigValues = configList.map((cfgName) => ({ name: cfgName, value: gantt.config[cfgName] }));
let currentThemeVariables = [];
let currentConfigs = [...defaultConfigValues];
const runCommand = createCommandRunner(
gantt,
{
onThemeSet: (variables, cmd) => {
if (cmd === 'set_theme') {
currentThemeVariables = mergeVariables(currentThemeVariables, variables);
} else if (cmd === 'reset_theme') {
currentThemeVariables = [];
return currentThemeVariables;
}
},
onConfigSet: (configs, cmd) => {
if (cmd === 'set_theme') {
currentConfigs = mergeConfigs(currentConfigs, configs);
return currentConfigs;
} else if (cmd === 'reset_theme') {
currentConfigs = defaultConfigValues;
return currentConfigs;
}
},
},
defaultConfigValues
);
function mergeVariables(oldVars, newVars) {
const map = new Map();
oldVars.forEach((v) => map.set(v.key, v.value));
(newVars || []).forEach((v) => map.set(v.key, v.value));
return Array.from(map.entries()).map(([key, value]) => ({ key, value }));
}
function mergeConfigs(oldConfigs, newConfigs) {
const map = new Map();
oldConfigs.forEach((config) => map.set(config.name, config.value));
(newConfigs || []).forEach((config) => map.set(config.name, config.value));
return Array.from(map.entries()).map(([name, value]) => ({ name, value }));
}
const SOCKET_URL = import.meta.env.VITE_SOCKET_URL || `${window.location.origin}`;
const socket = io(SOCKET_URL);
initChat({
socket,
runCommand,
getTheme: () => currentThemeVariables,
getConfigs: () => currentConfigs,
});
frontend/src/chat-widget.js View on GitHub
import { marked } from 'marked';
import DOMPurify from 'https://cdn.jsdelivr.net/npm/dompurify/dist/purify.es.js';
import MicroModal from 'micromodal';
import { ThemeManager } from './theme-manager';
export const initChat = ({ socket, runCommand, getTheme, getConfigs }) => {
function applyThemeOnFly(theme) {
let styleEl = document.getElementById('dynamic-theme');
if (!styleEl) {
styleEl = document.createElement('style');
styleEl.id = 'dynamic-theme';
document.head.appendChild(styleEl);
}
styleEl.innerHTML = theme;
}
function parseFromCSSToObj(cssThemeStr) {
const lines = cssThemeStr.split(/;|\n/);
const vars = [];
for (let line of lines) {
line = line.trim();
if (!line) continue;
const match = line.match(/^--([\w-]+)\s*:\s*(.+)$/);
if (match) {
const key = `--${match[1]}`;
const value = match[2].trim();
vars.push({ key, value });
}
}
return vars;
}
function parseConfigStringToArray(configStr) {
const lines = configStr.split('\n');
const configRegex = /^gantt\.config\.(\w+)\s*=\s*(.+);$/;
const configs = [];
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
const match = trimmed.match(configRegex);
if (match) {
const [, name, rawValue] = match;
let value;
try {
value = JSON.parse(rawValue);
} catch {
value = rawValue;
}
configs.push({ name, value });
}
}
return configs;
}
(function () {
const chatWidgetContainer = document.querySelector('#chat-tab');
chatWidgetContainer.dataset.id = '0';
chatWidgetContainer.innerHTML = `
<div id="chat-popup" class="relative left-0 bottom-0 h-full w-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 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');
const chatSubmit = document.getElementById('chat-submit');
const chatMessages = document.getElementById('chat-messages');
const themeManager = new ThemeManager();
themeManager.initEditors();
themeManager.onThemeChange((css, configs, isSave = false) => {
if (isSave) {
const variables = parseFromCSSToObj(css);
const parsedConfigs = parseConfigStringToArray(configs);
runCommand('set_theme', { variables, configs: parsedConfigs });
} else {
applyThemeOnFly(css);
const parsedConfigs = parseConfigStringToArray(configs);
runCommand('set_configs', { configs: parsedConfigs });
}
});
chatSubmit.addEventListener('click', function () {
const message = chatInput.value.trim();
if (!message) return;
chatMessages.scrollTop = chatMessages.scrollHeight;
chatInput.value = '';
sendUserMessage(message);
});
chatMessages.addEventListener('click', function (event) {
if (event.target.closest('.prompt-pill')) {
const pillText = event.target.closest('.prompt-pill').innerText;
sendUserMessage(pillText);
}
});
chatInput.addEventListener('keyup', function (event) {
if (event.key === 'Enter') {
chatSubmit.click();
}
});
const loader = document.getElementById('loader');
function showLoader() {
loader.classList.remove('hidden');
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function hideLoader() {
loader.classList.add('hidden');
}
function sendUserMessage(message) {
if (!message) return;
displayUser(message);
chatInput.value = '';
const payload = {
message,
theme: getTheme(),
configs: getConfigs(),
};
showLoader();
socket.emit('user_msg', JSON.stringify(payload));
}
function displayUser(msg) {
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%]">${msg}</div>`;
chatMessages.appendChild(div);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
socket.on('assistant_msg', (txt) => {
hideLoader();
displayReply(txt);
});
socket.on('tool_call', (txt) => {
let handled = false;
try {
const { cmd, params } = JSON.parse(txt);
if (cmd && cmd !== 'none') {
const result = runCommand(cmd, params);
hideLoader();
onCallback(cmd, params);
if (cmd === 'set_theme') {
const { styles, configStr } = result;
themeManager.setTheme(styles, configStr);
displayReply('🎨 Your theme has been generated and applied! You can now edit it in the Code tab.');
} else if (cmd === 'reset_theme') {
themeManager.resetTheme();
}
}
handled = true;
} catch (e) {
hideLoader();
displayReply(`Something wrong had happened: ${e.message}`);
handled = true;
}
if (!handled) displayReply(`Couldn't handle this: ${txt}`);
});
function displayReply(message) {
const div = document.createElement('div');
div.className = 'flex mb-3 chat-message';
const html = DOMPurify.sanitize(marked.parse(message));
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 generatedTheme = false;
function onCallback(cmd, params) {
if (!generatedTheme && cmd === 'set_theme') {
generatedTheme = true;
displayReply(buildSuggestionsBlock(cmd));
return;
}
if (generatedTheme && cmd === 'reset_theme') {
generatedTheme = false;
displayReply(buildSuggestionsBlock(cmd));
return;
}
}
function buildSuggestionsBlock(cmd) {
const afterSetThemePills = [
`Make tasks a bit lighter.`,
`Make the current theme a bit darker.`,
`Reset the current theme.`,
];
const afterResetThemePills = [
`Create me a relaxed and calm theme where blue and light-purple colors prevail.`,
`I want to customise the Gantt chart theme. Create a bright and warm theme for me. Also, increase the height of the rows and bars. Make the link width 4px.`,
];
const pills = cmd === 'set_theme' ? afterSetThemePills : afterResetThemePills;
const cmdText =
cmd === 'set_theme'
? 'Your theme is ready. Keep customizing it with natural language.'
: 'Your theme is reset. Generate a new theme with natural language.';
return `
<p>${cmdText} Try:</p>
<div class="suggestion-pills">
${pills.map((p) => `<button class="prompt-pill">${p}</button>`).join('')}
</div>`;
}
displayReply(`## Welcome to the AI Project Wizard!
I can customise the Gantt chart theme, edit, or style your Gantt chart with plain-language commands.
<br/>
<br/>
Try things like:
<div class="suggestion-pills">
<button class="prompt-pill">I want to customise the Gantt chart theme. Create a bright and warm theme for me. Also, increase the height of the rows and bars. Make the link width 4px.</button>
<button class="prompt-pill">Make tasks a bit lighter.</button>
<button class="prompt-pill">Create me a relaxed and calm theme where blue and light-purple colors prevail.</button>
</div>`);
})();
MicroModal.init({ disableScroll: true });
document.querySelectorAll('.copy-btn').forEach((btn) => {
btn.addEventListener('click', () => {
const text = btn.getAttribute('data-text');
navigator.clipboard.writeText(text).then(() => {
btn.textContent = 'Copied!';
setTimeout(() => (btn.textContent = 'Copy'), 1000);
});
});
});
const chatTabsEl = document.querySelector('.chat_tabs');
chatTabsEl.addEventListener('click', (e) => {
const tabBtn = e.target.closest('.tab-btn');
if (tabBtn && !tabBtn.classList.contains('tab-btn-active')) {
const activeBtn = chatTabsEl.querySelector('.tab-btn-active');
const tabPaneShown = chatTabsEl.querySelector('.tab-pane-show');
tabPaneShown && tabPaneShown.classList.remove('tab-pane-show');
activeBtn.classList.remove('tab-btn-active');
tabBtn.classList.add('tab-btn-active');
const targetId = tabBtn.dataset.targetId;
const tabPane = chatTabsEl.querySelector(`.tab-pane[data-id="${targetId}"]`);
tabPane.classList.add('tab-pane-show');
}
});
const modalBodyWrapper = document.querySelector('.modal__body-wrapper');
const modalScrollTopBtn = document.getElementById('btn-scroll-top');
modalBodyWrapper.addEventListener('scroll', () => {
modalScrollTopBtn.style.display = modalBodyWrapper.scrollTop > 200 ? 'block' : 'none';
});
modalScrollTopBtn.addEventListener('click', () => {
modalBodyWrapper.scrollTo({ top: 0, behavior: 'smooth' });
});
};
frontend/src/theme-manager.js View on GitHub
export class ThemeManager {
constructor() {
this.originalThemeCSS = "";
this.editedThemeCSS = "";
this.originalThemeConfigs = "";
this.editedThemeConfigs = "";
this.cssEditor = null;
this.configEditor = null;
this.onThemeChangeCallback = null;
}
initEditors() {
this.initCssEditor();
this.initConfigEditor();
}
initCssEditor() {
require.config({ paths: { vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.34.1/min/vs" } });
require(["vs/editor/editor.main"], () => {
this.cssEditor = monaco.editor.create(document.getElementById("css-editor-container"), {
value: this.originalThemeCSS,
language: "css",
theme: "vs-dark",
automaticLayout: true,
readOnly: false,
});
this.setupEditorEvents(this.cssEditor, "css");
});
}
initConfigEditor() {
require.config({ paths: { vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.34.1/min/vs" } });
require(["vs/editor/editor.main"], () => {
this.configEditor = monaco.editor.create(document.getElementById("config-editor-container"), {
value: this.originalThemeConfigs,
language: "javascript",
theme: "vs-dark",
automaticLayout: true,
readOnly: false,
});
this.setupEditorEvents(this.configEditor, "config");
});
}
setupEditorEvents(editor, type) {
editor.onDidChangeModelContent(() => {
const value = editor.getValue();
this[`editedTheme${type === "css" ? "CSS" : "Configs"}`] = value;
this.updateButtonsState(type);
this.applyThemeOnFly();
});
document.querySelector(`[data-editor="${type}"].copy-editor-btn`).addEventListener("click", () => {
this.copyContent(type);
});
document.querySelector(`[data-editor="${type}"].save-editor-btn`).addEventListener("click", () => {
this.saveChanges(type);
});
document.querySelector(`[data-editor="${type}"].cancel-editor-btn`).addEventListener("click", () => {
this.cancelChanges(type);
});
}
updateButtonsState(type) {
const original = type === "css" ? this.originalThemeCSS : this.originalThemeConfigs;
const edited = type === "css" ? this.editedThemeCSS : this.editedThemeConfigs;
const changed = edited.trim() !== original.trim();
document.querySelector(`[data-editor="${type}"].save-editor-btn`).disabled = !changed;
document.querySelector(`[data-editor="${type}"].cancel-editor-btn`).disabled = !changed;
}
setTheme(themeCSS, themeConfigs) {
this.originalThemeCSS = themeCSS;
this.editedThemeCSS = themeCSS;
this.originalThemeConfigs = themeConfigs;
this.editedThemeConfigs = themeConfigs;
if (this.cssEditor) {
this.cssEditor.setValue(themeCSS);
}
if (this.configEditor) {
this.configEditor.setValue(themeConfigs);
}
this.updateButtonsState("css");
this.updateButtonsState("config");
}
applyThemeOnFly() {
if (this.onThemeChangeCallback) {
this.onThemeChangeCallback(this.editedThemeCSS, this.editedThemeConfigs);
}
}
copyContent(type) {
const content = type === "css" ? this.editedThemeCSS : this.editedThemeConfigs;
navigator.clipboard.writeText(content);
const btn = document.querySelector(`[data-editor="${type}"].copy-editor-btn`);
btn.textContent = "Copied!";
setTimeout(() => (btn.textContent = "Copy"), 1000);
}
saveChanges(type) {
if (type === "css") {
this.originalThemeCSS = this.editedThemeCSS;
} else {
this.originalThemeConfigs = this.editedThemeConfigs;
}
this.updateButtonsState(type);
if (this.onThemeChangeCallback) {
this.onThemeChangeCallback(this.editedThemeCSS, this.editedThemeConfigs, true);
}
}
cancelChanges(type) {
if (type === "css") {
this.editedThemeCSS = this.originalThemeCSS;
this.cssEditor.setValue(this.originalThemeCSS);
} else {
this.editedThemeConfigs = this.originalThemeConfigs;
this.configEditor.setValue(this.originalThemeConfigs);
}
this.updateButtonsState(type);
this.applyThemeOnFly();
}
onThemeChange(callback) {
this.onThemeChangeCallback = callback;
}
resetTheme() {
this.setTheme("", "");
}
}
frontend/src/command-runner.js View on GitHub
export default function (gantt, { onThemeSet, onConfigSet }, defaultConfigValues = {}) {
// handlers for commands defined in /backend/schemaList.js
return function runCommand(cmd, args) {
switch (cmd) {
case "set_theme":
const variables = args.variables;
const configs = args.configs;
const styleId = "dynamic-theme";
let configStr = "";
let mergedConfigs = [];
[...defaultConfigValues, ...configs].forEach((config) => {
gantt.config[config.name] = config.value;
});
let style = document.getElementById(styleId);
if (!style) {
style = document.createElement("style");
style.id = styleId;
document.head.appendChild(style);
}
const lines = variables.map((variable) => `${variable.key}: ${variable.value};`);
const styles = `:root { \n${lines.join("\n")} \n}`;
style.innerHTML = styles;
if (onThemeSet) {
onThemeSet(variables, cmd);
}
if (onConfigSet) {
mergedConfigs = onConfigSet(configs, cmd);
}
configStr = mergedConfigs.map((config) => `gantt.config.${config.name} = ${config.value};`).join("\n");
gantt.render();
return { styles, configStr };
case "reset_theme":
const themeStyleId = "dynamic-theme";
const styleEl = document.getElementById(themeStyleId);
if (styleEl) {
styleEl.remove();
}
if (onThemeSet) {
onThemeSet([], cmd);
}
if (onConfigSet) {
onConfigSet([], cmd);
if (defaultConfigValues.length) {
defaultConfigValues.forEach((config) => {
gantt.config[config.name] = config.value;
});
}
gantt.render();
}
break;
case "set_configs":
const configsArr = args.configs;
if (configsArr.length) {
configsArr.forEach((config) => {
gantt.config[config.name] = config.value;
});
if (onConfigSet) {
onConfigSet(configsArr, cmd);
}
gantt.render();
}
break;
default:
console.warn("Unknown cmd:", cmd, args);
}
};
}
frontend/src/demo-data.js View on GitHub
export const demoData = {
tasks: [
{ id: 1, text: "Website Relaunch", type: "project", start_date: null, duration: 0, progress: 0.3, open: true },
{
id: 2,
text: "Planning Phase",
type: "project",
parent: 1,
start_date: "2025-01-15",
duration: 14,
progress: 1.0,
open: true,
},
{ id: 3, text: "Project Kickoff Meeting", parent: 2, start_date: "2025-01-15", duration: 1, progress: 1.0 },
{ id: 4, text: "Requirements Gathering", parent: 2, start_date: "2025-01-16", duration: 3, progress: 1.0 },
{ id: 5, text: "Stakeholder Interviews", parent: 4, start_date: "2025-01-16", duration: 2, progress: 1.0 },
{ id: 6, text: "Competitive Analysis", parent: 4, start_date: "2025-01-18", duration: 2, progress: 1.0 },
{ id: 7, text: "Technical Assessment", parent: 2, start_date: "2025-01-19", duration: 2, progress: 1.0 },
{ id: 8, text: "Project Scope Definition", parent: 2, start_date: "2025-01-22", duration: 2, progress: 1.0 },
{ id: 9, text: "Budget Approval", parent: 2, start_date: "2025-01-24", duration: 2, progress: 1.0 },
{
id: 10,
text: "Planning Completion Milestone",
parent: 2,
type: "milestone",
start_date: "2025-01-26",
duration: 0,
progress: 1.0,
},
{
id: 11,
text: "Design Phase",
type: "project",
parent: 1,
start_date: "2025-01-29",
duration: 16,
progress: 0.8,
open: true,
},
{ id: 12, text: "Information Architecture", parent: 11, start_date: "2025-01-29", duration: 3, progress: 1.0 },
{ id: 13, text: "Wireframing", parent: 11, start_date: "2025-02-01", duration: 4, progress: 1.0 },
{ id: 14, text: "UI Design - Homepage", parent: 11, start_date: "2025-02-06", duration: 3, progress: 1.0 },
{ id: 15, text: "UI Design - Inner Pages", parent: 11, start_date: "2025-02-09", duration: 4, progress: 0.8 },
{ id: 16, text: "Mobile Responsive Design", parent: 15, start_date: "2025-02-09", duration: 3, progress: 0.5 },
{ id: 17, text: "Design System Creation", parent: 11, start_date: "2025-02-13", duration: 3, progress: 0.3 },
{ id: 18, text: "Client Design Review", parent: 11, start_date: "2025-02-16", duration: 2, progress: 0.0 },
{ id: 19, text: "Design Revisions", parent: 11, start_date: "2025-02-20", duration: 3, progress: 0.0 },
{
id: 20,
text: "Design Approval Milestone",
parent: 11,
type: "milestone",
start_date: "2025-02-23",
duration: 0,
progress: 0.0,
},
{
id: 21,
text: "Development Phase",
type: "project",
parent: 1,
start_date: "2025-02-26",
duration: 25,
progress: 0.4,
open: true,
},
{ id: 22, text: "Frontend Setup", parent: 21, start_date: "2025-02-26", duration: 3, progress: 1.0 },
{ id: 23, text: "Backend Infrastructure", parent: 21, start_date: "2025-02-29", duration: 4, progress: 0.8 },
{ id: 24, text: "Database Design", parent: 23, start_date: "2025-02-29", duration: 2, progress: 1.0 },
{ id: 25, text: "API Development", parent: 23, start_date: "2025-03-04", duration: 5, progress: 0.6 },
{ id: 26, text: "Homepage Development", parent: 21, start_date: "2025-03-11", duration: 4, progress: 0.9 },
{ id: 27, text: "Content Pages Development", parent: 21, start_date: "2025-03-15", duration: 6, progress: 0.4 },
{ id: 28, text: "User Authentication System", parent: 21, start_date: "2025-03-21", duration: 3, progress: 0.2 },
{ id: 29, text: "E-commerce Functionality", parent: 21, start_date: "2025-03-25", duration: 5, progress: 0.0 },
{ id: 30, text: "Third-party Integrations", parent: 21, start_date: "2025-03-29", duration: 4, progress: 0.0 },
{
id: 31,
text: "Development Completion Milestone",
parent: 21,
type: "milestone",
start_date: "2025-04-03",
duration: 0,
progress: 0.0,
},
{
id: 32,
text: "Testing Phase",
type: "project",
parent: 1,
start_date: "2025-04-04",
duration: 10,
progress: 0.0,
open: true,
},
{ id: 33, text: "Unit Testing", parent: 32, start_date: "2025-04-04", duration: 3, progress: 0.0 },
{ id: 34, text: "Integration Testing", parent: 32, start_date: "2025-04-09", duration: 4, progress: 0.0 },
{ id: 35, text: "User Acceptance Testing", parent: 32, start_date: "2025-04-15", duration: 3, progress: 0.0 },
{ id: 36, text: "Bug Fixing", parent: 32, start_date: "2025-04-18", duration: 4, progress: 0.0 },
{
id: 37,
text: "Testing Sign-off Milestone",
parent: 32,
type: "milestone",
start_date: "2025-04-22",
duration: 0,
progress: 0.0,
},
{
id: 38,
text: "Deployment Phase",
type: "project",
parent: 1,
start_date: "2025-04-23",
duration: 6,
progress: 0.0,
open: true,
},
{ id: 39, text: "Production Environment Setup", parent: 38, start_date: "2025-04-23", duration: 2, progress: 0.0 },
{ id: 40, text: "Content Migration", parent: 38, start_date: "2025-04-25", duration: 2, progress: 0.0 },
{ id: 41, text: "Final Deployment", parent: 38, start_date: "2025-04-29", duration: 1, progress: 0.0 },
{ id: 42, text: "Post-launch Monitoring", parent: 38, start_date: "2025-04-30", duration: 3, progress: 0.0 },
{
id: 43,
text: "Project Launch Milestone",
parent: 38,
type: "milestone",
start_date: "2025-05-03",
duration: 0,
progress: 0.0,
},
],
links: [
{ id: 1, source: 3, target: 4, type: "0" },
{ id: 2, source: 4, target: 7, type: "0" },
{ id: 3, source: 7, target: 8, type: "0" },
{ id: 4, source: 8, target: 9, type: "0" },
{ id: 5, source: 9, target: 10, type: "0" },
{ id: 6, source: 10, target: 12, type: "0" },
{ id: 7, source: 12, target: 13, type: "0" },
{ id: 8, source: 13, target: 14, type: "0" },
{ id: 9, source: 14, target: 15, type: "0" },
{ id: 10, source: 15, target: 17, type: "0" },
{ id: 11, source: 17, target: 18, type: "0" },
{ id: 12, source: 18, target: 19, type: "0" },
{ id: 13, source: 19, target: 20, type: "0" },
{ id: 14, source: 20, target: 22, type: "0" },
{ id: 15, source: 22, target: 23, type: "0" },
{ id: 16, source: 24, target: 25, type: "0" },
{ id: 17, source: 25, target: 26, type: "0" },
{ id: 18, source: 26, target: 27, type: "0" },
{ id: 19, source: 27, target: 28, type: "0" },
{ id: 20, source: 28, target: 29, type: "0" },
{ id: 21, source: 29, target: 30, type: "0" },
{ id: 22, source: 30, target: 31, type: "0" },
{ id: 23, source: 31, target: 33, type: "0" },
{ id: 24, source: 33, target: 34, type: "0" },
{ id: 25, source: 34, target: 35, type: "0" },
{ id: 26, source: 35, target: 36, type: "0" },
{ id: 27, source: 36, target: 37, type: "0" },
{ id: 28, source: 37, target: 39, type: "0" },
{ id: 29, source: 39, target: 40, type: "0" },
{ id: 30, source: 40, target: 41, type: "0" },
{ id: 31, source: 41, target: 42, type: "0" },
{ id: 32, source: 42, target: 43, type: "0" },
],
};
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);
max-height: calc(100vh - 65px);
padding: 10px;
padding-bottom: 4px;
overflow: hidden;
}
.gantt-container {
width: 100%;
height: 100%;
min-height: 0;
min-width: 0;
}
#chat_panel {
position: relative;
min-width: 0;
height: 100%;
overflow-y: auto;
}
.chat_tabs {
display: flex;
flex-direction: column;
gap: 10px;
height: 100%;
}
.tab-nav {
display: flex;
gap: 10px;
align-items: flex-start;
height: auto;
}
.tab-content {
position: relative;
flex-grow: 1;
overflow-y: auto;
}
.tab-pane {
height: 100%;
overflow-y: auto;
}
.tab-pane:not(.tab-pane-show) {
display: none;
}
.tab-btn {
height: 40px;
width: 100px;
border-radius: 10px;
border: 1px solid #ccc;
background-color: #fff;
}
.tab-btn-active {
pointer-events: none;
background-color: #1f2937;
border: 1px solid #fff;
color: #fff;
}
.code-editors-container {
min-height: 0;
display: flex;
flex-direction: column;
gap: 20px;
}
.editor-section {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: auto;
}
.editor-section h3 {
background-color: #f9fafb;
padding: 10px 15px;
margin: 0;
border-bottom: 1px solid #e5e7eb;
font-size: 14px;
font-weight: 600;
}
.editor-container {
height: 100%;
min-height: 320px;
flex-grow: 1;
border-bottom: 1px solid #e5e7eb;
}
.editor-actions {
display: flex;
gap: 10px;
padding: 10px 15px;
background-color: #f9fafb;
}
.code-editor-btn {
padding: 2px 8px;
width: 97px;
height: 36px;
border: 1px solid #efefef;
border-radius: 8px;
font-size: 0.9rem;
background-color: #007bff;
color: #fff;
cursor: pointer;
transition: all ease-in-out 0.2s;
}
.code-editor-btn:disabled {
pointer-events: none;
background-color: #5ca9fa;
}
.code-editor-btn:hover {
background-color: #5ca9fa;
}
.cancel-editor-btn {
background-color: #efefef;
color: #000;
}
.cancel-editor-btn:disabled {
background-color: #ebebeb;
}
.cancel-editor-btn:disabled {
color: #adadad;
}
.cancel-editor-btn:hover {
background-color: #e2e2e2;
}
.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-popup {
transition: all 0.3s;
}
@media (max-width: 992px) {
#wrapper {
grid-template-rows: minmax(250px, 45vh) minmax(250px, 1fr);
grid-template-columns: 1fr;
row-gap: 20px;
column-gap: 0;
}
.gantt_layout_cell.gantt_ver_scroll {
z-index: 0 !important;
}
}
@media (max-width: 768px) {
#wrapper {
height: calc(100vh - 80px);
}
}
#chat-messages h2 {
font-weight: bold;
font-size: 1.2rem;
}
#chat-messages ul li {
list-style: disc;
margin-left: 10px;
}
.chat-message {
overflow-x: auto;
}
.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 - 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__close {
background: transparent;
border: 0;
}
.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,
.theme-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;
}
.theme-btn {
width: 97px;
padding: 6px 8px;
background-color: #007bff;
color: #fff;
}
.copy-btn:hover {
background-color: #e2e2e2;
}
.theme-btn:hover {
background-color: #5ca9fa;
}
.theme-btn:disabled {
pointer-events: none;
background-color: #5ca9fa;
}
.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.js View on GitHub
import "dotenv/config";
import express from "express";
import { createServer } from "http";
import { Server } from "socket.io";
import OpenAI from "openai";
import { schemaList } from "./schemaList.js";
import { log } from "./logger.js";
import variables from "./variablesList.json" with {type: 'json'};
import configListJSON from "./configList.json" with {type: 'json'};
import { getMessagesHistoryByClient, sessionMessagesByClient } from "./helper.js";
const app = express();
const http = createServer(app);
const io = new Server(http, { 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.on("user_msg", async (text) => {
const { message } = JSON.parse(text);
const messages = getMessagesHistoryByClient(socket.id, generateSystemPrompt());
messages.push({ role: "user", content: message });
const reply = await talkToLLM(messages);
// if assistant ask additional question
if (reply.assistant_msg) socket.emit("assistant_msg", reply.assistant_msg);
// if assistant used tool_call
if (reply.call){
messages.push({
role: "assistant",
tool_calls: reply.tool_calls,
content: reply.content ?? "",
});
messages.push({
role: "tool",
tool_call_id: reply.tool_call_id,
content: `current_theme succesfully set: ${reply.current_theme}`,
});
socket.emit("tool_call", reply.call);
}
});
socket.on("disconnect", () => {
sessionMessagesByClient.delete(socket.id);
});
});
function buildVariablesList() {
return variables.map((variable) => `${variable.name}: ${variable.defaultValue} -- ${variable.description}`).join("\n");
}
function buildConfigsList(configArr) {
return configArr.map(config => `${config.name}: ${config.description}`).join('\n')
}
function generateSystemPrompt() {
const varList = buildVariablesList();
const availableConfigs = buildConfigsList(configListJSON);
return `You are **ProjectGanttAssistant**, your goal is to help the user operating DHTMLX Gantt chart using natural language commands.
Today is ${new Date().getFullYear()}-${new Date().getMonth() + 1}-${new Date().getDate()}
Always use one tool call for one command.
Your replies will be displayed in chat side panel, so try to be short and clear. You can use markdown formatting.
You can customize the Gantt appearance using these CSS list:
${varList}
Here are the available config options (gantt.config.*):
${availableConfigs}
When changing the current theme in some way (for example, making the task bars lighter) or adding new styles to the current theme, use the active theme CSS variables created earlier and update its variables according to the user's requirements, or add new variables.
Rules for changing the current theme:
1. **Never** delete, omit, or reorder existing variables from the theme (key and value must mot change inside variables).
2. Modify **only** those variables that are explicitly mentioned or clearly implied by the user's message.
3. If the user says something general (e.g. "make it darker"), update only the most relevant variables, but still preserve all others.
4. **ALWAYS** Before call "set_theme" check if in your history there is a previous 'current_theme'.
5. **ALWAYS** After 'reset_theme' clean theme history, current_theme variables and config should be empty.
**CRITICAL: Current theme state is stored in your conversation history as 'tool' role messages from previous set_theme calls.
ALWAYS check recent history for the latest 'current_theme' before calling set_theme again. Reference exact variable values from those tool responses when modifying the theme.**
**MANDATORY**: When calling "set_theme", ALWAYS include the COMPLETE current_theme object:
1. Parse latest 'tool' message content as JSON to get existing current_theme
2. Copy ALL variables unchanged, modify only requested ones
3. Output FULL object in arguments: {"--var1": "val1", "--var2": "val2", ...}
Example: If current_theme has 10 vars and user changes 1, return all 10.
For example:
If the user says “Make the task background lighter,” you should only change the value of --dhx-gantt-task-background (if that's the relevant variable), and return all others unchanged.
Remember to use tools in your replies.
`;
}
async function talkToLLM(request) {
log.success("calling llm");
const res = await openai.chat.completions.create({
model: "gpt-5-nano",
messages: request,
tools: schemaList,
});
log.success("Got LLM reply");
log.info(
`Processing took ${res.usage.approximate_total}. Prompt tokens: ${res.usage.prompt_tokens}, response tokens: ${res.usage.completion_tokens}, perf ${res.usage["response_token/s"]}T/s`
);
const msg = res.choices[0].message;
let content = msg.content;
let calls = msg.tool_calls;
const toolCall = calls ? calls[0] : "";
log.info(`output: ${content}`);
log.info(`tool call: ${JSON.stringify(toolCall)}`);
return {
assistant_msg: content,
call: toolCall
? JSON.stringify({ cmd: toolCall.function.name, params: JSON.parse(toolCall.function.arguments) })
: "",
tool_call_id: msg.tool_calls ? msg.tool_calls[0].id : "",
tool_calls: msg.tool_calls ? msg.tool_calls : "",
current_theme: toolCall
? toolCall.function.arguments
: ""
};
}
http.listen(3001, () => console.log("API on :3001"));
backend/schemaList.js View on GitHub
// ---------------------------------------------------------------------------
// JSON schemas for OpenAI "function-calling" mode
// ---------------------------------------------------------------------------
export const schemaList = [
{
type: "function",
function: {
name: "set_theme",
description:
`Update the Gantt chart theme. **variables** parameter MUST contain the **current theme**, even if only some are changed.
If current theme doesn't have variable according to users question add it. If it exists update.
**Do NOT** omit any variables unless the user explicitly requests a full reset. **configs** is a required list of layout/behavior overrides.`,
parameters: {
type: "object",
properties: {
variables: {
type: "array",
description:
"list of CSS variables for the current theme. Change only those explicitly mentioned by the user; keep the rest untouched.",
items: {
type: "object",
properties: {
key: { type: "string", description: "Name of the css variable (e.g. --dhx-gantt-task-blue)" },
value: { type: "string", description: "Value of the css variable (e.g. #e0e0e0)" },
},
required: ["key", "value"],
},
minItems: 0,
},
configs: {
type: "array",
description:
"Complete list of config settings. Modify only those the user changed; preserve the rest. Omit entirely if no configs added or changed.",
items: {
type: "object",
properties: {
name: {
type: "string",
description: "Name of the config, e.g. 'link_line_width', 'row_height'",
},
value: {
type: ["number", "boolean"],
description: "New value for the config (pixels or boolean)",
},
},
required: ["name", "value"],
},
minItems: 0,
},
},
required: ["variables", "configs"],
},
},
},
{
type: "function",
function: {
name: "reset_theme",
description: "Reset the current Gantt theme by clearing all custom CSS variables and configs (back to defaults).",
parameters: {
type: "object",
properties: {},
required: [],
},
},
},
];