| //+------------------------------------------------------------------+
|
| //| AI Canvas State.mqh |
|
| //| Copyright 2026, Allan Munene Mutiiria. |
|
| //| https://t.me/Forex_Algo_Trader |
|
| //+------------------------------------------------------------------+
|
| #property copyright "Copyright 2026, Allan Munene Mutiiria."
|
| #property link "https://t.me/Forex_Algo_Trader"
|
|
|
| //--- Include guard
|
| #ifndef AI_CANVAS_STATE_MQH
|
| #define AI_CANVAS_STATE_MQH
|
|
|
| //--- Pull theme for AI_DASHBOARD_X_DEFAULT and AI_DASHBOARD_Y_DEFAULT
|
| #include "AI Canvas Theme.mqh"
|
|
|
| //--- Store all persisted data under one MQL5\\Files tree (Chats, Screenshots, Logs)
|
| #define AI_DATA_DIR "AI EA Data\\"
|
| #define AI_CHATS_DIR "AI EA Data\\Chats\\"
|
| #define AI_IMG_DIR "AI EA Data\\Screenshots\\"
|
| #define AI_LOG_DIR "AI EA Data\\Logs\\"
|
| #define AI_EXPORT_DIR "AI EA Data\\Exports\\"
|
|
|
| //+------------------------------------------------------------------+
|
| //| Canvas Object Names |
|
| //+------------------------------------------------------------------+
|
| #define AI_CANVAS_NAME_MAIN "ChatGPT_AI_MainCanvas" // Main dashboard canvas object
|
| #define AI_CANVAS_NAME_PROMPT "ChatGPT_AI_PromptCanvas" // Prompt overlay canvas object
|
|
|
| //+------------------------------------------------------------------+
|
| //| Chat Record Structure |
|
| //+------------------------------------------------------------------+
|
| struct Chat
|
| {
|
| int id; // Unique chat identifier
|
| string title; // Chat display title
|
| string history; // Full conversation transcript
|
| };
|
|
|
| //+------------------------------------------------------------------+
|
| //| Chat State |
|
| //+------------------------------------------------------------------+
|
| Chat g_ai_chats[]; // All loaded chats
|
| int g_ai_currentChatId = -1; // Active chat id (-1 = new chat)
|
| string g_ai_currentTitle = ""; // Active chat title
|
| string g_ai_conversationHistory = ""; // Active chat transcript
|
| string g_ai_currentPrompt = ""; // Persisted prompt text
|
|
|
| //+------------------------------------------------------------------+
|
| //| UI Visibility Flags |
|
| //+------------------------------------------------------------------+
|
| bool g_ai_sidebarExpanded = true; // Sidebar expanded state
|
| bool g_ai_dashboardVisible = true; // Dashboard visible state
|
|
|
| //+------------------------------------------------------------------+
|
| //| Popup State Flags |
|
| //+------------------------------------------------------------------+
|
| bool g_ai_showSmallHistory = false; // Small history popup visible
|
| bool g_ai_showBigHistory = false; // Big history popup visible
|
| bool g_ai_showSearch = false; // Search popup visible
|
| bool g_ai_justOpenedSmall = false; // Skip first-click close on small popup
|
| bool g_ai_justOpenedBig = false; // Skip first-click close on big popup
|
| bool g_ai_justOpenedSearch = false; // Skip first-click close on search popup
|
| string g_ai_searchQuery = ""; // Current search query text
|
|
|
| //+------------------------------------------------------------------+
|
| //| Drag State |
|
| //+------------------------------------------------------------------+
|
| bool g_ai_dragging = false; // Header drag in progress
|
| int g_ai_dragOffsetX = 0; // Drag X offset from anchor
|
| int g_ai_dragOffsetY = 0; // Drag Y offset from anchor
|
|
|
| //+------------------------------------------------------------------+
|
| //| Dashboard Position |
|
| //+------------------------------------------------------------------+
|
| int AI_DASHBOARD_X = AI_DASHBOARD_X_DEFAULT; // Current dashboard X
|
| int AI_DASHBOARD_Y = AI_DASHBOARD_Y_DEFAULT; // Current dashboard Y
|
|
|
| //+------------------------------------------------------------------+
|
| //| Footer Dropdown State |
|
| //+------------------------------------------------------------------+
|
| bool g_ai_showFooterDropdown = false; // Dropdown popup visible
|
| int g_ai_footerDropdownSelectedIdx = 0; // Selected action index
|
|
|
| //+------------------------------------------------------------------+
|
| //| Footer Action Dispatch Tables |
|
| //+------------------------------------------------------------------+
|
| const int AI_FOOTER_DD_COUNT = 8; // Number of dropdown actions
|
| const string AI_FOOTER_DD_LABELS[] =
|
| {
|
| "Get Chart Data", // 0 - chart data dump
|
| "Chart Screenshot", // 7 - attach a clean chart image to the next message
|
| "Twin Bars", // 1 - twin-bars signal check
|
| "Quick Scalp", // 2 - single-bar entry signal
|
| "Daily Signal", // 3 - daily H1 bias signal
|
| "Trend Read", // 4 - trendline anchors
|
| "Key Level", // 5 - significant S/R level
|
| "Clear Drawings" // 6 - wipe signal drawings
|
| };
|
| const int AI_FOOTER_DD_ACTION_IDS[]= { 0, 7, 1, 2, 3, 4, 5, 6 }; // Stable action IDs
|
| const int AI_FOOTER_DD_ICONS[] =
|
| {
|
| 0, // chart icon
|
| 7, // image/picture icon (chart screenshot)
|
| 1, // twin bars icon
|
| 2, // lightning icon
|
| 3, // day icon
|
| 4, // trend icon
|
| 5, // level icon
|
| 6 // close icon
|
| };
|
|
|
| //--- Pending manual chart-screenshot attachment, carried on the next sent message
|
| bool g_ai_attachPending = false;
|
| string g_ai_attachBase = "";
|
| int g_ai_attachW = 0;
|
| int g_ai_attachH = 0;
|
| string g_ai_attachB64 = "";
|
|
|
| //+------------------------------------------------------------------+
|
| //| API Throttle Flag |
|
| //+------------------------------------------------------------------+
|
| bool g_ai_signalRequestInFlight = false; // Block parallel API calls
|
|
|
| //+------------------------------------------------------------------+
|
| //| Tick and Animation State |
|
| //+------------------------------------------------------------------+
|
| datetime g_ai_lastBarTime = 0; // Last seen bar time (for new-bar detection)
|
| int g_ai_spinnerCycle = 0; // Loading spinner animation tick
|
|
|
| //+------------------------------------------------------------------+
|
| //| Toast Notification State |
|
| //+------------------------------------------------------------------+
|
| string g_ai_toastText = ""; // Active toast text (empty = no toast)
|
| bool g_ai_toastIsError = false;// Pick error vs success color
|
| ulong g_ai_toastExpiryMs = 0; // Tick count when toast expires
|
|
|
| //+------------------------------------------------------------------+
|
| //| Pencil Hover Tracking |
|
| //+------------------------------------------------------------------+
|
| bool g_ai_overPencilIcon = false; // Cursor over narrow pencil icon
|
|
|
| //+------------------------------------------------------------------+
|
| //| Send Button State Tracking |
|
| //+------------------------------------------------------------------+
|
| bool g_ai_lastRenderedSendDisabled = true; // Disabled state at last footer render
|
|
|
| //+------------------------------------------------------------------+
|
| //| Encode integer ID as base62 string |
|
| //+------------------------------------------------------------------+
|
| string AiEncodeID(int id)
|
| {
|
| //--- Setup base62 alphabet
|
| string chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
| string res = "";
|
| //--- Special-case zero
|
| if(id == 0)
|
| return "0";
|
| //--- Build digits from least significant
|
| while(id > 0)
|
| {
|
| res = StringSubstr(chars, id % 62, 1) + res;
|
| id /= 62;
|
| }
|
| return res;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Decode base62 string back to integer ID |
|
| //+------------------------------------------------------------------+
|
| int AiDecodeID(string enc)
|
| {
|
| //--- Setup base62 alphabet
|
| string chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
| int id = 0;
|
| //--- Accumulate digits from most significant
|
| for(int i = 0; i < StringLen(enc); i++)
|
| {
|
| id = id * 62 + StringFind(chars, StringSubstr(enc, i, 1));
|
| }
|
| return id;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Find chat array index by chat ID |
|
| //+------------------------------------------------------------------+
|
| int AiGetChatIndex(int id)
|
| {
|
| //--- Linear scan for matching id
|
| for(int i = 0; i < ArraySize(g_ai_chats); i++)
|
| {
|
| if(g_ai_chats[i].id == id)
|
| return i;
|
| }
|
| return -1;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Test if line matches HH:MM timestamp format |
|
| //+------------------------------------------------------------------+
|
| bool AiIsTimestamp(string line)
|
| {
|
| //--- Trim whitespace and check exact length
|
| StringTrimLeft(line);
|
| StringTrimRight(line);
|
| if(StringLen(line) != 5)
|
| return false;
|
| //--- Verify colon separator at position 2
|
| if(StringGetCharacter(line, 2) != ':')
|
| return false;
|
| //--- Parse and validate hour and minute fields
|
| string hh = StringSubstr(line, 0, 2);
|
| string mm = StringSubstr(line, 3, 2);
|
| int h = (int)StringToInteger(hh);
|
| int m = (int)StringToInteger(mm);
|
| if(h < 0 || h > 23 || m < 0 || m > 59)
|
| return false;
|
| return true;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Split string by delimiter into output array |
|
| //+------------------------------------------------------------------+
|
| int AiSplitOnString(string inputText, string delim, string &result[])
|
| {
|
| //--- Clear output array
|
| ArrayResize(result, 0);
|
| int pos = 0;
|
| int delim_len = StringLen(delim);
|
| //--- Walk delimiter occurrences
|
| while(true)
|
| {
|
| int found = StringFind(inputText, delim, pos);
|
| if(found == -1)
|
| {
|
| //--- Append final remainder
|
| string part = StringSubstr(inputText, pos);
|
| if(StringLen(part) > 0 || ArraySize(result) > 0)
|
| {
|
| int size = ArraySize(result);
|
| ArrayResize(result, size + 1);
|
| result[size] = part;
|
| }
|
| break;
|
| }
|
| //--- Append substring up to delimiter
|
| string part = StringSubstr(inputText, pos, found - pos);
|
| int size = ArraySize(result);
|
| ArrayResize(result, size + 1);
|
| result[size] = part;
|
| pos = found + delim_len;
|
| }
|
| return ArraySize(result);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Darken a color by scaling each channel toward black |
|
| //+------------------------------------------------------------------+
|
| color AiDarkenColor(color colorValue, double factor = 0.8)
|
| {
|
| //--- Scale RGB channels by factor
|
| int red = int(((colorValue) & 0xFF) * factor);
|
| int green = int(((colorValue >> 8) & 0xFF) * factor);
|
| int blue = int(((colorValue >> 16) & 0xFF) * factor);
|
| return (color)(red | (green << 8) | (blue << 16));
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Lighten a color by interpolating each channel toward white |
|
| //+------------------------------------------------------------------+
|
| color AiLightenColor(color colorValue, double amount = 0.25)
|
| {
|
| //--- Extract RGB channels
|
| int r = (int)((colorValue) & 0xFF);
|
| int g = (int)((colorValue >> 8) & 0xFF);
|
| int b = (int)((colorValue >> 16) & 0xFF);
|
| //--- Interpolate each channel toward 255
|
| r = r + (int)((255 - r) * amount);
|
| g = g + (int)((255 - g) * amount);
|
| b = b + (int)((255 - b) * amount);
|
| //--- Clamp at 255
|
| if(r > 255)
|
| r = 255;
|
| if(g > 255)
|
| g = 255;
|
| if(b > 255)
|
| b = 255;
|
| return (color)(r | (g << 8) | (b << 16));
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Replace standalone double-periods with newlines |
|
| //+------------------------------------------------------------------+
|
| string AiReplaceExactDoublePeriods(string text)
|
| {
|
| //--- Build result by scanning input
|
| string result = "";
|
| int len = StringLen(text);
|
| for(int i = 0; i < len; i++)
|
| {
|
| //--- Detect ".." pattern
|
| if(i + 1 < len && StringGetCharacter(text, i) == '.' && StringGetCharacter(text, i + 1) == '.')
|
| {
|
| //--- Reject when surrounded by additional dots
|
| bool preceded = (i > 0 && StringGetCharacter(text, i - 1) == '.');
|
| bool followed = (i + 2 < len && StringGetCharacter(text, i + 2) == '.');
|
| if(!preceded && !followed)
|
| {
|
| //--- Standalone ".." converts to newline
|
| result += "\n";
|
| i++;
|
| }
|
| else
|
| {
|
| //--- Part of longer dot run - keep literal
|
| result += ".";
|
| }
|
| }
|
| else
|
| {
|
| //--- Copy character through unchanged
|
| result += StringSubstr(text, i, 1);
|
| }
|
| }
|
| return result;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Pending AI Action State |
|
| //+------------------------------------------------------------------+
|
| //--- Fixed first line of an approval card message used for detection
|
| #define AI_ACTION_CARD_HEADER "Action approval required"
|
|
|
| bool g_ai_actionActive = false; // Approval card is awaiting a decision
|
| string g_ai_actionCardText = ""; // Full text of the current approval card
|
| int g_ai_actionChatId = -1; // Chat id that owns the pending approval
|
|
|
| #endif // AI_CANVAS_STATE_MQH
|
| //+------------------------------------------------------------------+
|