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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions backend/configList.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,31 @@
{
"name": "link_line_width",
"description": "Width of dependency link lines in pixels (e.g. 2, 3, 4)",
"type": "size"
"type": "size",
"defaultValue": "2"
},
{
"name": "link_radius",
"description": "Corner roundness (radius) of link line bends",
"type": "size"
"type": "size",
"defaultValue": "4"
},
{
"name": "row_height",
"description": "Height (pixels) of each task row (e.g. 50)",
"type": "size"
"type": "size",
"defaultValue": "35"
},
{
"name": "bar_height",
"description": "Height (pixels) of task bars in the timeline area (e.g. 30)",
"type": "size"
"type": "size",
"defaultValue": "30"
},
{
"name": "show_progress",
"description": "Enables displaying of the progress inside the task bars (e.g. true). The default value is true.",
"type": "boolean"
"type": "boolean",
"defaultValue": "true"
}
]
29 changes: 29 additions & 0 deletions backend/helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export const sessionMessagesByClient = new Map();
// saving user response history
export function getMessagesHistoryByClient(socketId, systemPrompt) {
const existingHistory = sessionMessagesByClient.get(socketId);
if(existingHistory) {
trimHistory(existingHistory);
return existingHistory;
}

const newHistory = [
{
role: "assistant",
content: systemPrompt,
},
];

sessionMessagesByClient.set(socketId, newHistory);
return newHistory;
}

function trimHistory(messageHistory, maxPairs = 20) {
const maxLength = 1 + maxPairs * 2;
if (messageHistory.length > maxLength) {
const systemMessage = messageHistory[0];
const lastMessages = messageHistory.slice(-maxPairs * 2);
messageHistory.length = 0;
messageHistory.push(systemMessage, ...lastMessages);
}
}
6 changes: 4 additions & 2 deletions backend/schemaList.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ export const schemaList = [
function: {
name: "set_theme",
description:
"Update the Gantt chart theme. **variables** parameter MUST contain the **entire current theme** (all CSS variables), even if only some are changed. **Do NOT** omit any variables unless the user explicitly requests a full reset. **configs** is an optional list of layout/behavior overrides.",
`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:
"Full list of CSS variables for the current theme. Change only those explicitly mentioned by the user; keep the rest untouched.",
"list of CSS variables for the current theme. Change only those explicitly mentioned by the user; keep the rest untouched.",
items: {
type: "object",
properties: {
Expand Down
83 changes: 50 additions & 33 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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);
Expand All @@ -21,31 +22,43 @@ app.use(express.static("../frontend/dist"));

io.on("connection", (socket) => {
socket.on("user_msg", async (text) => {
const { message, theme, configs } = JSON.parse(text);

const reply = await talkToLLM(message, theme, configs);
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 (reply.call) socket.emit("tool_call", reply.call);
// 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.description}`).join("\n");
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 buildThemeVariablesList(theme) {
if(!theme.length || !Array.isArray(theme)) return '';
return theme.map(variable => `${variable.key}: ${variable.value}`).join('\n');
}

function generateSystemPrompt(theme, configs) {
function generateSystemPrompt() {
const varList = buildVariablesList();
const currentTheme = buildThemeVariablesList(theme);
const currentConfigs = buildConfigsList(configs);
const availableConfigs = buildConfigsList(configListJSON);

return `You are **ProjectGanttAssistant**, your goal is to help the user operating DHTMLX Gantt chart using natural language commands.
Expand All @@ -56,7 +69,7 @@ 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 variables:
You can customize the Gantt appearance using these CSS list:
${varList}

Here are the available config options (gantt.config.*):
Expand All @@ -65,34 +78,33 @@ ${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.
2. Always return the **entire list of variables**, even if only one was changed.
3. Modify **only** those variables that are explicitly mentioned or clearly implied by the user's message.
4. If the user says something general (e.g. "make it darker"), update only the most relevant variables, but still preserve all others.
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.

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.
**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.**

Here is the current theme (DO NOT LOSE THIS — you will be modifying it):
${currentTheme}
**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.

Here are the current configs (DO NOT LOSE THIS — you will be modifying it):
${currentConfigs}
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, theme, configs) {
const messages = [
{ role: "system", content: generateSystemPrompt(theme, configs) },
{ role: "user", content: request },
];

async function talkToLLM(request) {
log.success("calling llm");
const res = await openai.chat.completions.create({
model: "gpt-5-nano",
messages: messages,
messages: request,
tools: schemaList,
});

Expand All @@ -106,15 +118,20 @@ async function talkToLLM(request, theme, configs) {
let calls = msg.tool_calls;


const toolCall = calls ? calls[0] : null;
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) })
: null,
: "",
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
: ""
};
}

Expand Down
Loading