Article-22495-Dispatch-Driv.../AI Canvas Interact.mqh

1328 lines
No EOL
50 KiB
MQL5

//+------------------------------------------------------------------+
//| AI Canvas Interact.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_INTERACT_MQH
#define AI_CANVAS_INTERACT_MQH
//--- Include required modules
#include "AI Canvas State.mqh"
#include "AI Canvas Render.mqh"
#include "AI Canvas Editor.mqh"
#include "AI Logic.mqh"
//+------------------------------------------------------------------+
//| Global Variables |
//+------------------------------------------------------------------+
bool g_ai_chatScrollDragging = false; // Chat-pane scrollbar drag flag
bool g_ai_editorScrollDragging = false; // Editor scrollbar drag flag
bool g_ai_searchScrollDragging = false; // Search popup scrollbar drag flag
//--- Forward declarations
void Ai_HideDashboard();
void Ai_RecomputeLayout();
//+------------------------------------------------------------------+
//| Keyboard Override State |
//+------------------------------------------------------------------+
bool g_ai_kbOverrideActive = false; // Override active flag
bool g_ai_savedKbControl = true; // Saved keyboard control state
bool g_ai_savedQuickNav = true; // Saved quick navigation state
//+------------------------------------------------------------------+
//| Begin keyboard override to capture chart input |
//+------------------------------------------------------------------+
void Ai_BeginKeyboardOverride()
{
//--- Bail if already active
if(g_ai_kbOverrideActive) return;
//--- Save current chart settings
g_ai_savedKbControl = (bool)ChartGetInteger(0, CHART_KEYBOARD_CONTROL);
g_ai_savedQuickNav = (bool)ChartGetInteger(0, CHART_QUICK_NAVIGATION);
//--- Disable chart keyboard handling
ChartSetInteger(0, CHART_KEYBOARD_CONTROL, false);
ChartSetInteger(0, CHART_QUICK_NAVIGATION, false);
g_ai_kbOverrideActive = true;
}
//+------------------------------------------------------------------+
//| End keyboard override and restore chart input |
//+------------------------------------------------------------------+
void Ai_EndKeyboardOverride()
{
//--- Bail if not active
if(!g_ai_kbOverrideActive) return;
//--- Restore saved settings
ChartSetInteger(0, CHART_KEYBOARD_CONTROL, g_ai_savedKbControl);
ChartSetInteger(0, CHART_QUICK_NAVIGATION, g_ai_savedQuickNav);
g_ai_kbOverrideActive = false;
}
//+------------------------------------------------------------------+
//| Convert chart coordinates to canvas-local coordinates |
//+------------------------------------------------------------------+
void Ai_ChartToCanvas(int chartX, int chartY, int &localX, int &localY)
{
//--- Subtract dashboard origin
localX = chartX - AI_DASHBOARD_X;
localY = chartY - AI_DASHBOARD_Y;
}
//+------------------------------------------------------------------+
//| Decide between fast prompt overlay and full render after edit |
//+------------------------------------------------------------------+
void Ai_FastRenderAfterEditorChange()
{
//--- Check if send button visual needs refresh
const bool nowDisabled = Ai_PromptIsEmpty();
const bool sendStateChanged = (nowDisabled != g_ai_lastRenderedSendDisabled);
//--- Check if any overlay is currently showing
const bool hasOverlay =
g_ai_showSearch
|| g_ai_showFooterDropdown
|| g_ai_showSmallHistory
|| (StringLen(g_ai_toastText) > 0);
//--- Choose render path based on what changed
if(sendStateChanged || hasOverlay || !g_ai_canvPromptExists)
{
//--- Slow path: full dashboard render
Ai_RenderAll();
}
else
{
//--- Fast path: prompt overlay only
Ai_RenderPromptPaneOverlay();
}
}
//+------------------------------------------------------------------+
//| Hit-test canvas coordinates and return hover code |
//+------------------------------------------------------------------+
int Ai_HitTest(int lx, int ly)
{
//--- Test footer dropdown items first
if(g_ai_showFooterDropdown)
{
for(int i = 0; i < ArraySize(g_ai_footerDdItemL); i++)
{
if(AiPointInRect(lx, ly, g_ai_footerDdItemL[i], g_ai_footerDdItemT[i],
g_ai_footerDdItemR[i] - g_ai_footerDdItemL[i],
g_ai_footerDdItemB[i] - g_ai_footerDdItemT[i]))
{
return AI_HOV_FOOTER_DD_ITEM_BASE + i;
}
}
//--- Inside dropdown body but not on item - block fall-through
if(AiPointInRect(lx, ly, g_ai_footerDdL, g_ai_footerDdT,
g_ai_footerDdR - g_ai_footerDdL,
g_ai_footerDdB - g_ai_footerDdT)) return AI_HOV_NONE;
}
//--- Test search/history popup rows
if(g_ai_showSearch || g_ai_showSmallHistory)
{
const int nRows = ArraySize(g_ai_popupRowL);
for(int r = 0; r < nRows; r++)
{
if(AiPointInRect(lx, ly, g_ai_popupRowL[r], g_ai_popupRowT[r],
g_ai_popupRowR[r] - g_ai_popupRowL[r],
g_ai_popupRowB[r] - g_ai_popupRowT[r]))
{
//--- Right edge of row is the delete zone
const int delZoneL = g_ai_popupRowR[r] - 24;
if(lx >= delZoneL)
{
g_ai_popupHovRow = r; g_ai_popupHovDel = true;
return AI_HOV_SMALL_DEL_BASE + r;
}
g_ai_popupHovRow = r; g_ai_popupHovDel = false;
return AI_HOV_SMALL_CHAT_BASE + r;
}
}
//--- Inside popup body but not on row - block fall-through
if(AiPointInRect(lx, ly, g_ai_popupL, g_ai_popupT,
g_ai_popupR - g_ai_popupL, g_ai_popupB - g_ai_popupT))
{
g_ai_popupHovRow = -1; g_ai_popupHovDel = false;
return AI_HOV_NONE;
}
}
//--- Reset popup hover state when no popup hit
g_ai_popupHovRow = -1; g_ai_popupHovDel = false;
//--- Test theme button
int tL, tT, tR, tB;
Ai_GetThemeBtnRect(tL, tT, tR, tB);
if(AiPointInRect(lx, ly, tL, tT, tR - tL, tB - tT)) return AI_HOV_THEME;
//--- Test close button
int cL, cT, cR, cB;
Ai_GetCloseBtnRect(cL, cT, cR, cB);
if(AiPointInRect(lx, ly, cL, cT, cR - cL, cB - cT)) return AI_HOV_CLOSE;
//--- Test sidebar buttons
int bL, bT, bR, bB;
for(int i = 0; i < 4; i++)
{
Ai_GetSidebarBtnRect(i, bL, bT, bR, bB);
if(AiPointInRect(lx, ly, bL, bT, bR - bL, bB - bT))
{
switch(i)
{
case 0: return AI_HOV_SEARCH;
case 1: return AI_HOV_NEW_CHAT;
case 2: return AI_HOV_CLEAR;
case 3: return AI_HOV_HISTORY;
}
}
}
//--- Test sidebar chat rows when expanded
if(g_ai_sidebarExpanded)
{
int n = MathMin(7, ArraySize(g_ai_chats));
for(int r = 0; r < n; r++)
{
int rL, rT, rR, rB;
Ai_GetSidebarChatRowRect(r, rL, rT, rR, rB);
if(AiPointInRect(lx, ly, rL, rT, rR - rL, rB - rT))
{
//--- Right edge is delete zone
if(lx >= rR - 22) return AI_HOV_SIDE_DEL_BASE + r;
return AI_HOV_SIDE_CHAT_BASE + r;
}
}
}
//--- Test sidebar toggle button
int tgL, tgT, tgR, tgB;
Ai_GetToggleBtnRect(tgL, tgT, tgR, tgB);
if(AiPointInRect(lx, ly, tgL, tgT, tgR - tgL, tgB - tgT)) return AI_HOV_TOGGLE;
//--- Test signal button (split into action half and chevron half)
int sgL, sgT, sgR, sgB;
Ai_GetSignalBtnRect(sgL, sgT, sgR, sgB);
if(AiPointInRect(lx, ly, sgL, sgT, sgR - sgL, sgB - sgT))
{
const int sepX = Ai_SignalSeparatorX();
if(lx >= sepX) return AI_HOV_SIGNAL_DD;
return AI_HOV_SIGNAL;
}
//--- Test send button
int snL, snT, snR, snB;
Ai_GetSendBtnRect(snL, snT, snR, snB);
if(AiPointInRect(lx, ly, snL, snT, snR - snL, snB - snT)) return AI_HOV_SEND;
//--- Test regenerate button
if(g_ai_regenR > g_ai_regenL
&& AiPointInRect(lx, ly, g_ai_regenL, g_ai_regenT,
g_ai_regenR - g_ai_regenL, g_ai_regenB - g_ai_regenT))
return AI_HOV_REGEN;
//--- Test export button
if(g_ai_exportR > g_ai_exportL
&& AiPointInRect(lx, ly, g_ai_exportL, g_ai_exportT,
g_ai_exportR - g_ai_exportL, g_ai_exportB - g_ai_exportT))
return AI_HOV_EXPORT;
//--- Test scroll-to-bottom FAB
if(g_ai_scrollFabVisible
&& AiPointInRect(lx, ly, g_ai_scrollFabL, g_ai_scrollFabT,
g_ai_scrollFabR - g_ai_scrollFabL,
g_ai_scrollFabB - g_ai_scrollFabT))
{
return AI_HOV_SCROLL_FAB;
}
//--- Test per-user-message edit pencils
const int userEditN = ArraySize(g_ai_userEditRectL);
for(int ueI = 0; ueI < userEditN; ueI++)
{
if(g_ai_userEditRectR[ueI] > g_ai_userEditRectL[ueI]
&& AiPointInRect(lx, ly,
g_ai_userEditRectL[ueI], g_ai_userEditRectT[ueI],
g_ai_userEditRectR[ueI] - g_ai_userEditRectL[ueI],
g_ai_userEditRectB[ueI] - g_ai_userEditRectT[ueI]))
{
return AI_HOV_USER_EDIT_BASE + ueI;
}
}
//--- Test header drag region
if(ly >= 0 && ly < AI_HEADER_H) return AI_HOV_DRAG;
//--- No hit
return AI_HOV_NONE;
}
//+------------------------------------------------------------------+
//| Test if point is inside prompt pane |
//+------------------------------------------------------------------+
bool Ai_PointInPromptPane(int lx, int ly)
{
//--- Get prompt pane rect and test
int pL, pT, pR, pB;
Ai_GetPromptPaneRect(pL, pT, pR, pB);
return AiPointInRect(lx, ly, pL, pT, pR - pL, pB - pT);
}
//+------------------------------------------------------------------+
//| Test if point is inside chat pane |
//+------------------------------------------------------------------+
bool Ai_PointInChatPane(int lx, int ly)
{
//--- Get chat pane rect and test
int cL, cT, cR, cB;
Ai_GetChatPaneRect(cL, cT, cR, cB);
return AiPointInRect(lx, ly, cL, cT, cR - cL, cB - cT);
}
//+------------------------------------------------------------------+
//| Dispatch footer dropdown action by id |
//+------------------------------------------------------------------+
void Ai_DispatchFooterAction(int actionId)
{
//--- Route to handler based on action id
switch(actionId)
{
case 0: AiGetAndAppendChartData(); break; // Get Chart Data
case 1: AiTwinBars(); break; // Twin Bars
case 2: AiGetTradeSignal(false); break; // Quick Scalp
case 3: AiDailySignal(); break; // Daily Signal
case 4: AiTrendRead(); break; // Trend Read
case 5: AiKeyLevel(); break; // Key Level
case 6: AiClearSignalDrawings(); break; // Clear Drawings
default:
//--- Unknown action - log and ignore
Print("Ai_DispatchFooterAction: unknown actionId=", actionId);
break;
}
}
//+------------------------------------------------------------------+
//| Handle action triggered by hover code |
//+------------------------------------------------------------------+
void Ai_HandleAction(int hov)
{
//--- Close dashboard
if(hov == AI_HOV_CLOSE) { Ai_HideDashboard(); return; }
//--- Toggle theme
if(hov == AI_HOV_THEME)
{
Ai_ApplyTheme(!g_ai_darkTheme);
Ai_RenderAll(); ChartRedraw(); return;
}
//--- Toggle sidebar expansion
if(hov == AI_HOV_TOGGLE)
{
g_ai_sidebarExpanded = !g_ai_sidebarExpanded;
Ai_RecomputeLayout();
Ai_RenderAll(); ChartRedraw(); return;
}
//--- Create new chat
if(hov == AI_HOV_NEW_CHAT)
{
g_ai_showSearch = false; g_ai_showSmallHistory = false;
AiCreateNewChat(); return;
}
//--- Clear current chat
if(hov == AI_HOV_CLEAR)
{
//--- Skip if already empty
if(StringLen(g_ai_conversationHistory) == 0
&& StringLen(g_ai_editor.GetText()) == 0)
{
Ai_RenderAll(); ChartRedraw(); return;
}
//--- Snapshot title for toast before clearing
string titleSnap = g_ai_currentTitle;
if(StringLen(titleSnap) > 30)
titleSnap = StringSubstr(titleSnap, 0, 27) + "...";
//--- Clear history and prompt
g_ai_conversationHistory = "";
g_ai_currentPrompt = "";
g_ai_editor.SetText("");
//--- Persist and notify
const bool savedOk = AiUpdateCurrentHistory();
if(savedOk) Ai_ShowToast("Successfully cleared chat '" + titleSnap + "'", false);
else Ai_ShowToast("Failed to clear chat", true);
Ai_RenderAll(); ChartRedraw(); return;
}
//--- Toggle history popup
if(hov == AI_HOV_HISTORY)
{
g_ai_showSmallHistory = !g_ai_showSmallHistory;
g_ai_showSearch = false;
Ai_RenderAll(); ChartRedraw(); return;
}
//--- Toggle search popup
if(hov == AI_HOV_SEARCH)
{
g_ai_showSearch = !g_ai_showSearch;
g_ai_showSmallHistory = false;
if(g_ai_showSearch)
{
//--- Focus search editor and reset state
if(g_ai_editor.focused) g_ai_editor.focused = false;
g_ai_searchEditor.focused = true;
g_ai_searchEditor.SetText("");
g_ai_searchQuery = "";
g_ai_searchScroll.scrollPx = 0;
Ai_BeginKeyboardOverride();
}
else
{
//--- Unfocus search editor and end override if no other focus
g_ai_searchEditor.focused = false;
if(!g_ai_editor.focused) Ai_EndKeyboardOverride();
}
Ai_RenderAll(); ChartRedraw(); return;
}
//--- Signal button action half - fire selected dropdown action
if(hov == AI_HOV_SIGNAL)
{
g_ai_showFooterDropdown = false;
g_ai_showSearch = false;
g_ai_showSmallHistory = false;
const int ddi = MathMax(0, MathMin(g_ai_footerDropdownSelectedIdx, AI_FOOTER_DD_COUNT - 1));
Ai_DispatchFooterAction(AI_FOOTER_DD_ACTION_IDS[ddi]);
Ai_RenderAll(); ChartRedraw(); return;
}
//--- Signal button chevron half - toggle dropdown
if(hov == AI_HOV_SIGNAL_DD)
{
g_ai_showFooterDropdown = !g_ai_showFooterDropdown;
g_ai_showSearch = false; g_ai_showSmallHistory = false;
Ai_RenderAll(); ChartRedraw(); return;
}
//--- Footer dropdown item selection
if(hov >= AI_HOV_FOOTER_DD_ITEM_BASE && hov < AI_HOV_FOOTER_DD_ITEM_BASE + 100)
{
int item = hov - AI_HOV_FOOTER_DD_ITEM_BASE;
g_ai_footerDropdownSelectedIdx = item;
g_ai_showFooterDropdown = false;
if(item >= 0 && item < AI_FOOTER_DD_COUNT)
{
Ai_DispatchFooterAction(AI_FOOTER_DD_ACTION_IDS[item]);
}
Ai_RenderAll(); ChartRedraw(); return;
}
//--- Send button - submit prompt
if(hov == AI_HOV_SEND)
{
//--- Suppress empty submissions
if(Ai_PromptIsEmpty()) return;
string txt = g_ai_editor.GetText();
g_ai_editor.SetText("");
g_ai_currentPrompt = "";
AiSubmitMessage(txt);
return;
}
//--- Regenerate button - resubmit last prompt
if(hov == AI_HOV_REGEN)
{
//--- Capture last prompt then strip the turn pair and resubmit
string lastPrompt = AiGetLastUserPrompt();
if(StringLen(lastPrompt) > 0)
{
AiRemoveLastConversationTurn();
AiSubmitMessage(lastPrompt);
}
return;
}
//--- Export button - write chat to file
if(hov == AI_HOV_EXPORT)
{
string fname = "ChatGPT_Export_Chat" + IntegerToString(g_ai_currentChatId) + ".txt";
int h = FileOpen(fname, FILE_WRITE | FILE_TXT | FILE_ANSI);
if(h != INVALID_HANDLE)
{
FileWriteString(h, "Title: " + g_ai_currentTitle + "\r\n\r\n");
FileWriteString(h, g_ai_conversationHistory);
FileClose(h);
Print("Exported chat to ", fname);
Ai_ShowToast("Chat exported to " + fname, false);
}
else
{
const int err = GetLastError();
Print("Export failed: ", err);
Ai_ShowToast("Export failed (error " + IntegerToString(err) + ")", true);
}
Ai_RenderAll(); ChartRedraw();
return;
}
//--- Scroll-to-bottom FAB - jump chat scroll to max
if(hov == AI_HOV_SCROLL_FAB)
{
g_ai_chatScroll.scrollPx = AiScrollMax(g_ai_chatScroll);
AiScrollClamp(g_ai_chatScroll);
Ai_RenderAll(); ChartRedraw();
return;
}
//--- User-message edit pencil - load prompt into editor
if(hov >= AI_HOV_USER_EDIT_BASE
&& hov < AI_HOV_USER_EDIT_BASE + 100)
{
const int peIdx = hov - AI_HOV_USER_EDIT_BASE;
if(peIdx >= 0 && peIdx < ArraySize(g_ai_userEditPrompt))
{
//--- Validate click landed in narrow pencil rect not just bubble
const bool inClickRect =
(peIdx < ArraySize(g_ai_userEditClickL))
&& (g_ai_mouseLx >= g_ai_userEditClickL[peIdx])
&& (g_ai_mouseLx < g_ai_userEditClickR[peIdx])
&& (g_ai_mouseLy >= g_ai_userEditClickT[peIdx])
&& (g_ai_mouseLy < g_ai_userEditClickB[peIdx]);
if(!inClickRect) return;
//--- Load prompt into editor
const string prompt = g_ai_userEditPrompt[peIdx];
g_ai_editor.SetText(prompt);
//--- Focus editor and start keyboard override
if(g_ai_searchEditor.focused) g_ai_searchEditor.focused = false;
if(!g_ai_editor.focused)
{
g_ai_editor.focused = true;
Ai_BeginKeyboardOverride();
}
//--- Sync currentPrompt for chat-store consistency
g_ai_currentPrompt = prompt;
Ai_RenderAll(); ChartRedraw();
}
return;
}
//--- Sidebar chat row click - switch active chat
if(hov >= AI_HOV_SIDE_CHAT_BASE && hov < AI_HOV_SIDE_DEL_BASE)
{
int row = hov - AI_HOV_SIDE_CHAT_BASE;
int total = ArraySize(g_ai_chats);
int chatIdx = total - 1 - row;
if(chatIdx >= 0 && chatIdx < total && g_ai_chats[chatIdx].id != g_ai_currentChatId)
{
AiUpdateCurrentHistory();
g_ai_currentChatId = g_ai_chats[chatIdx].id;
g_ai_currentTitle = g_ai_chats[chatIdx].title;
g_ai_conversationHistory = g_ai_chats[chatIdx].history;
Ai_RenderAll(); ChartRedraw();
}
return;
}
//--- Sidebar delete button - remove chat
if(hov >= AI_HOV_SIDE_DEL_BASE && hov < AI_HOV_SIDE_DEL_BASE + 100)
{
int row = hov - AI_HOV_SIDE_DEL_BASE;
int total = ArraySize(g_ai_chats);
int chatIdx = total - 1 - row;
if(chatIdx >= 0 && chatIdx < total)
{
//--- Snapshot title before delete for toast
string titleSnap = g_ai_chats[chatIdx].title;
if(StringLen(titleSnap) > 30)
titleSnap = StringSubstr(titleSnap, 0, 27) + "...";
const bool deletedOk = AiDeleteChat(g_ai_chats[chatIdx].id);
//--- Notify user via toast
if(deletedOk) Ai_ShowToast("Successfully deleted chat '" + titleSnap + "'", false);
else Ai_ShowToast("Failed to delete chat", true);
Ai_RenderAll(); ChartRedraw();
}
return;
}
//--- Popup chat row click - switch active chat
if(hov >= AI_HOV_SMALL_CHAT_BASE && hov < AI_HOV_SMALL_DEL_BASE)
{
int row = hov - AI_HOV_SMALL_CHAT_BASE;
if(row >= 0 && row < ArraySize(g_ai_popupRowChatIdx))
{
int chatIdx = g_ai_popupRowChatIdx[row];
if(chatIdx >= 0 && chatIdx < ArraySize(g_ai_chats)
&& g_ai_chats[chatIdx].id != g_ai_currentChatId)
{
AiUpdateCurrentHistory();
g_ai_currentChatId = g_ai_chats[chatIdx].id;
g_ai_currentTitle = g_ai_chats[chatIdx].title;
g_ai_conversationHistory = g_ai_chats[chatIdx].history;
}
}
g_ai_showSearch = false;
g_ai_showSmallHistory = false;
Ai_RenderAll(); ChartRedraw();
return;
}
//--- Popup delete button - remove chat from popup
if(hov >= AI_HOV_SMALL_DEL_BASE && hov < AI_HOV_SMALL_DEL_BASE + 100)
{
int row = hov - AI_HOV_SMALL_DEL_BASE;
if(row >= 0 && row < ArraySize(g_ai_popupRowChatIdx))
{
int chatIdx = g_ai_popupRowChatIdx[row];
if(chatIdx >= 0 && chatIdx < ArraySize(g_ai_chats))
{
//--- Snapshot title before delete for toast
string titleSnap = g_ai_chats[chatIdx].title;
if(StringLen(titleSnap) > 30)
titleSnap = StringSubstr(titleSnap, 0, 27) + "...";
const bool deletedOk = AiDeleteChat(g_ai_chats[chatIdx].id);
if(deletedOk) Ai_ShowToast("Successfully deleted chat '" + titleSnap + "'", false);
else Ai_ShowToast("Failed to delete chat", true);
}
}
Ai_RenderAll(); ChartRedraw();
return;
}
}
//+------------------------------------------------------------------+
//| Main chart event handler |
//+------------------------------------------------------------------+
void Ai_OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
//--- Bail when dashboard is hidden
if(!g_ai_dashboardVisible) return;
//--- Handle keyboard input events
if(id == CHARTEVENT_KEYDOWN)
{
//--- Read modifier states
int vk = (int)lparam;
const int shiftRaw = (int)TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT);
const bool shiftDown = (shiftRaw < 0) || ((shiftRaw & 0x8000) != 0);
const int ctrlRaw = (int)TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL);
const bool ctrlDown = (ctrlRaw < 0) || ((ctrlRaw & 0x8000) != 0);
//--- Search editor focus path
if(g_ai_searchEditor.focused)
{
//--- Escape closes search
if(vk == 27)
{
g_ai_searchEditor.focused = false;
g_ai_showSearch = false;
Ai_EndKeyboardOverride();
Ai_RenderAll(); ChartRedraw();
return;
}
//--- Enter ignored in search
if(vk == 13)
{
return;
}
//--- Shift-modified navigation extends selection
if(shiftDown)
{
if(vk == 37) { g_ai_searchEditor.ShiftExtendLeft(); Ai_RenderAll(); ChartRedraw(); return; }
if(vk == 39) { g_ai_searchEditor.ShiftExtendRight(); Ai_RenderAll(); ChartRedraw(); return; }
if(vk == 36) { g_ai_searchEditor.ShiftExtendHome(); Ai_RenderAll(); ChartRedraw(); return; }
if(vk == 35) { g_ai_searchEditor.ShiftExtendEnd(); Ai_RenderAll(); ChartRedraw(); return; }
}
//--- Plain navigation
if(vk == 37) { g_ai_searchEditor.MoveCaretLeft(); Ai_RenderAll(); ChartRedraw(); return; }
if(vk == 39) { g_ai_searchEditor.MoveCaretRight(); Ai_RenderAll(); ChartRedraw(); return; }
if(vk == 36) { g_ai_searchEditor.MoveCaretHome(); Ai_RenderAll(); ChartRedraw(); return; }
if(vk == 35) { g_ai_searchEditor.MoveCaretEnd(); Ai_RenderAll(); ChartRedraw(); return; }
//--- Backspace and delete - update query and reset scroll
if(vk == 8) { g_ai_searchEditor.Backspace(); g_ai_searchQuery = g_ai_searchEditor.GetText(); g_ai_searchScroll.scrollPx = 0; Ai_RenderAll(); ChartRedraw(); return; }
if(vk == 46) { g_ai_searchEditor.DeleteChar(); g_ai_searchQuery = g_ai_searchEditor.GetText(); g_ai_searchScroll.scrollPx = 0; Ai_RenderAll(); ChartRedraw(); return; }
//--- Filter out non-typing keys
bool isModifier2 = (vk == 16 || vk == 17 || vk == 18
|| vk == 20 || vk == 144 || vk == 145
|| vk == 91 || vk == 92 || vk == 93);
bool isNavigation2 = (vk >= 33 && vk <= 40) || (vk == 45);
bool isFunctionKey2 = (vk >= 112 && vk <= 123);
bool isTab2 = (vk == 9);
if(isModifier2 || isNavigation2 || isFunctionKey2 || isTab2) return;
//--- Filter to printable virtual keys
bool isPrintableVk2 = (vk == 32) ||
(vk >= 48 && vk <= 57) ||
(vk >= 65 && vk <= 90) ||
(vk >= 96 && vk <= 111) ||
(vk >= 186 && vk <= 223);
if(!isPrintableVk2) return;
//--- Translate and insert character
short uch2 = TranslateKey(vk);
if(uch2 > 0)
{
ushort code2 = (ushort)uch2;
string ch2 = ShortToString(code2);
g_ai_searchEditor.InsertChar(ch2);
g_ai_searchQuery = g_ai_searchEditor.GetText();
g_ai_searchScroll.scrollPx = 0;
Ai_RenderAll();
ChartRedraw();
}
return;
}
//--- Main editor focus path
if(g_ai_editor.focused)
{
//--- Enter handling - submit on Shift+Enter, newline otherwise
if(vk == 13)
{
if(shiftDown)
{
//--- Suppress empty submissions
if(Ai_PromptIsEmpty()) return;
string txt = g_ai_editor.GetText();
g_ai_editor.SetText("");
g_ai_currentPrompt = "";
AiSubmitMessage(txt);
}
else
{
g_ai_editor.InsertNewline();
g_ai_currentPrompt = g_ai_editor.GetText();
Ai_RenderAll(); ChartRedraw();
}
return;
}
//--- Escape blurs editor
if(vk == 27)
{
g_ai_editor.focused = false;
Ai_EndKeyboardOverride();
Ai_RenderAll(); ChartRedraw();
return;
}
//--- Shift-modified navigation extends selection
if(shiftDown)
{
if(vk == 37) { g_ai_editor.ShiftExtendLeft(); Ai_RenderAll(); ChartRedraw(); return; }
if(vk == 39) { g_ai_editor.ShiftExtendRight(); Ai_RenderAll(); ChartRedraw(); return; }
if(vk == 38) { g_ai_editor.ShiftExtendUp(); Ai_RenderAll(); ChartRedraw(); return; }
if(vk == 40) { g_ai_editor.ShiftExtendDown(); Ai_RenderAll(); ChartRedraw(); return; }
if(vk == 36) { g_ai_editor.ShiftExtendHome(); Ai_RenderAll(); ChartRedraw(); return; }
if(vk == 35) { g_ai_editor.ShiftExtendEnd(); Ai_RenderAll(); ChartRedraw(); return; }
}
//--- Plain navigation - use fast render path
if(vk == 37) { g_ai_editor.MoveCaretLeft(); Ai_FastRenderAfterEditorChange(); ChartRedraw(); return; }
if(vk == 39) { g_ai_editor.MoveCaretRight(); Ai_FastRenderAfterEditorChange(); ChartRedraw(); return; }
if(vk == 38) { g_ai_editor.MoveCaretUp(); Ai_FastRenderAfterEditorChange(); ChartRedraw(); return; }
if(vk == 40) { g_ai_editor.MoveCaretDown(); Ai_FastRenderAfterEditorChange(); ChartRedraw(); return; }
if(vk == 36) { g_ai_editor.MoveCaretHome(); Ai_FastRenderAfterEditorChange(); ChartRedraw(); return; }
if(vk == 35) { g_ai_editor.MoveCaretEnd(); Ai_FastRenderAfterEditorChange(); ChartRedraw(); return; }
//--- Backspace and delete with fast render
if(vk == 8) { g_ai_editor.Backspace(); g_ai_currentPrompt = g_ai_editor.GetText(); Ai_FastRenderAfterEditorChange(); ChartRedraw(); return; }
if(vk == 46) { g_ai_editor.DeleteChar(); g_ai_currentPrompt = g_ai_editor.GetText(); Ai_FastRenderAfterEditorChange(); ChartRedraw(); return; }
//--- Filter out non-typing keys
bool isModifier = (vk == 16 || vk == 17 || vk == 18
|| vk == 20 || vk == 144 || vk == 145
|| vk == 91 || vk == 92 || vk == 93);
bool isNavigation = (vk >= 33 && vk <= 40) || (vk == 45);
bool isFunctionKey = (vk >= 112 && vk <= 123);
bool isTab = (vk == 9);
if(isModifier || isNavigation || isFunctionKey || isTab) return;
//--- Filter to printable virtual keys
bool isPrintableVk = (vk == 32) ||
(vk >= 48 && vk <= 57) ||
(vk >= 65 && vk <= 90) ||
(vk >= 96 && vk <= 111) ||
(vk >= 186 && vk <= 223);
if(!isPrintableVk) return;
//--- Translate and insert character with fast render
short uch = TranslateKey(vk);
if(uch > 0)
{
ushort code = (ushort)uch;
string ch = ShortToString(code);
g_ai_editor.InsertChar(ch);
g_ai_currentPrompt = g_ai_editor.GetText();
Ai_FastRenderAfterEditorChange();
ChartRedraw();
}
return;
}
//--- Escape with no editor focused closes dashboard
if(vk == 27) { Ai_HideDashboard(); return; }
return;
}
//--- Handle mouse move events
if(id == CHARTEVENT_MOUSE_MOVE)
{
//--- Read mouse coords and button state
int mx = (int)lparam;
int my = (int)dparam;
int mstate = (int)sparam;
int lx, ly;
Ai_ChartToCanvas(mx, my, lx, ly);
//--- Cache canvas-local mouse coords for render-time hover gates
g_ai_mouseLx = lx;
g_ai_mouseLy = ly;
//--- Handle dashboard drag in progress
if(g_ai_dragging)
{
//--- End drag on button release
if((mstate & 1) == 0)
{
g_ai_dragging = false;
}
else
{
//--- Compute new dashboard position with bounds clamp
int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
int newX = MathMax(0, MathMin(chartW - Ai_DashboardW(),
mx - g_ai_dragOffsetX));
int newY = MathMax(0, MathMin(chartH - Ai_DashboardH(),
my - g_ai_dragOffsetY));
//--- Apply new position
AI_DASHBOARD_X = newX;
AI_DASHBOARD_Y = newY;
ObjectSetInteger(0, AI_CANVAS_NAME_MAIN, OBJPROP_XDISTANCE, newX);
ObjectSetInteger(0, AI_CANVAS_NAME_MAIN, OBJPROP_YDISTANCE, newY);
//--- Move prompt overlay in lockstep
if(g_ai_canvPromptExists)
{
int dpL, dpT, dpR, dpB;
Ai_GetPromptPaneRect(dpL, dpT, dpR, dpB);
ObjectSetInteger(0, AI_CANVAS_NAME_PROMPT, OBJPROP_XDISTANCE, newX + dpL);
ObjectSetInteger(0, AI_CANVAS_NAME_PROMPT, OBJPROP_YDISTANCE, newY + dpT);
}
ChartRedraw();
return;
}
}
//--- Handle chat scrollbar drag
if(g_ai_chatScrollDragging)
{
AiScrollUpdateDrag(g_ai_chatScroll, ly);
if((mstate & 1) == 0) { AiScrollEndDrag(g_ai_chatScroll); g_ai_chatScrollDragging = false; }
Ai_RenderAll(); ChartRedraw();
return;
}
//--- Handle editor scrollbar drag
if(g_ai_editorScrollDragging)
{
AiScrollUpdateDrag(g_ai_editor.scroll, ly);
if((mstate & 1) == 0)
{
AiScrollEndDrag(g_ai_editor.scroll);
g_ai_editorScrollDragging = false;
}
Ai_RenderAll(); ChartRedraw();
return;
}
//--- Handle search scrollbar drag
if(g_ai_searchScrollDragging)
{
AiScrollUpdateDrag(g_ai_searchScroll, ly);
if((mstate & 1) == 0)
{
AiScrollEndDrag(g_ai_searchScroll);
g_ai_searchScrollDragging = false;
}
Ai_RenderAll(); ChartRedraw();
return;
}
//--- Compute current hover code
int newHov = Ai_HitTest(lx, ly);
//--- Auto-show small history popup on hover when sidebar collapsed
if(!g_ai_sidebarExpanded)
{
if(newHov == AI_HOV_HISTORY && !g_ai_showSmallHistory)
{
g_ai_showSmallHistory = true;
g_ai_showSearch = false;
}
else if(g_ai_showSmallHistory)
{
//--- Hide popup when leaving anchor and popup body
int hbL, hbT, hbR, hbB;
Ai_GetSidebarBtnRect(3, hbL, hbT, hbR, hbB);
bool inAnchor = AiPointInRect(lx, ly, hbL, hbT, hbR - hbL, hbB - hbT);
bool inPopup = AiPointInRect(lx, ly, g_ai_popupL, g_ai_popupT,
g_ai_popupR - g_ai_popupL,
g_ai_popupB - g_ai_popupT);
if(!inAnchor && !inPopup) g_ai_showSmallHistory = false;
}
}
//--- Update chat scrollbar hover state
bool chatHovArea = false, chatHovThumb = false;
if(AiScrollVisible(g_ai_chatScroll))
{
chatHovArea = Ai_PointInChatPane(lx, ly);
chatHovThumb = AiScrollHitTestThumb(g_ai_chatScroll, lx, ly);
}
//--- Update editor scrollbar hover state
bool editorHovArea = false, editorHovThumb = false;
if(AiScrollVisible(g_ai_editor.scroll))
{
editorHovArea = Ai_PointInPromptPane(lx, ly);
editorHovThumb = AiScrollHitTestThumb(g_ai_editor.scroll, lx, ly);
}
//--- Update search scrollbar hover state
bool searchHovArea = false, searchHovThumb = false;
if(g_ai_showSearch && AiScrollVisible(g_ai_searchScroll))
{
searchHovArea = AiPointInRect(lx, ly, g_ai_popupL, g_ai_popupT,
g_ai_popupR - g_ai_popupL,
g_ai_popupB - g_ai_popupT);
searchHovThumb = AiScrollHitTestThumb(g_ai_searchScroll, lx, ly);
}
//--- Track if cursor is over a pencil icon's narrow click rect
bool overPencilIcon = false;
const int peClickN = ArraySize(g_ai_userEditClickL);
for(int piI = 0; piI < peClickN; piI++)
{
if(lx >= g_ai_userEditClickL[piI] && lx < g_ai_userEditClickR[piI]
&& ly >= g_ai_userEditClickT[piI] && ly < g_ai_userEditClickB[piI])
{
overPencilIcon = true;
break;
}
}
//--- Compute whether a redraw is needed for hover state changes
bool needRedraw = (newHov != g_ai_hover)
|| (chatHovArea != g_ai_chatScroll.hoveredArea)
|| (chatHovThumb != g_ai_chatScroll.hoveredThumb)
|| (editorHovArea != g_ai_editor.scroll.hoveredArea)
|| (editorHovThumb != g_ai_editor.scroll.hoveredThumb)
|| (searchHovArea != g_ai_searchScroll.hoveredArea)
|| (searchHovThumb != g_ai_searchScroll.hoveredThumb)
|| (overPencilIcon != g_ai_overPencilIcon);
//--- Update tooltip when hover code changes
if(newHov != g_ai_hover)
{
string tip = "";
if(newHov == AI_HOV_CLOSE) tip = "Close";
else if(newHov == AI_HOV_THEME) tip = "Toggle theme";
else if(newHov == AI_HOV_TOGGLE) tip = g_ai_sidebarExpanded ? "Collapse sidebar" : "Expand sidebar";
else if(newHov == AI_HOV_NEW_CHAT) tip = "New chat";
else if(newHov == AI_HOV_CLEAR) tip = "Clear current chat";
else if(newHov == AI_HOV_HISTORY) tip = "Recent chats";
else if(newHov == AI_HOV_SEARCH) tip = "Search chats";
else if(newHov == AI_HOV_SIGNAL) tip = "Run selected action";
else if(newHov == AI_HOV_SIGNAL_DD) tip = "Choose action";
else if(newHov == AI_HOV_SEND) tip = Ai_PromptIsEmpty() ? "Type a message to send" : "Send (Shift+Enter)";
else if(newHov == AI_HOV_REGEN) tip = "Regenerate response";
else if(newHov == AI_HOV_EXPORT) tip = "Export chat to file";
else if(newHov == AI_HOV_SCROLL_FAB) tip = "Scroll to latest";
else if(newHov >= AI_HOV_USER_EDIT_BASE
&& newHov < AI_HOV_USER_EDIT_BASE + 100) tip = "Edit this prompt";
ObjectSetString(0, AI_CANVAS_NAME_MAIN, OBJPROP_TOOLTIP,
(StringLen(tip) > 0) ? tip : "\n");
}
//--- Commit hover state to globals
g_ai_hover = newHov;
g_ai_chatScroll.hoveredArea = chatHovArea;
g_ai_chatScroll.hoveredThumb = chatHovThumb;
g_ai_chatScroll.hover = chatHovArea || chatHovThumb;
g_ai_editor.scroll.hoveredArea = editorHovArea;
g_ai_editor.scroll.hoveredThumb = editorHovThumb;
g_ai_editor.scroll.hover = editorHovArea || editorHovThumb;
g_ai_searchScroll.hoveredArea = searchHovArea;
g_ai_searchScroll.hoveredThumb = searchHovThumb;
g_ai_searchScroll.hover = searchHovArea || searchHovThumb;
g_ai_overPencilIcon = overPencilIcon;
//--- Toggle chart mouse scroll based on cursor inside dashboard
const bool overDashboard = (lx >= 0 && lx < Ai_DashboardW()
&& ly >= 0 && ly < Ai_DashboardH());
if(!g_ai_chatScrollDragging && !g_ai_editorScrollDragging && !g_ai_searchScrollDragging)
ChartSetInteger(0, CHART_MOUSE_SCROLL, !overDashboard);
//--- Detect fresh press/release transitions
static int prevMouseState = 0;
bool freshPress = (prevMouseState == 0 && mstate == 1);
bool freshRelease = (prevMouseState == 1 && mstate == 0);
prevMouseState = mstate;
//--- Handle fresh mouse button press
if(freshPress)
{
//--- Footer dropdown handling
if(g_ai_showFooterDropdown)
{
bool inDdBody = AiPointInRect(lx, ly, g_ai_footerDdL, g_ai_footerDdT,
g_ai_footerDdR - g_ai_footerDdL,
g_ai_footerDdB - g_ai_footerDdT);
bool onAnchor = (newHov == AI_HOV_SIGNAL || newHov == AI_HOV_SIGNAL_DD);
bool onItem = (newHov >= AI_HOV_FOOTER_DD_ITEM_BASE
&& newHov < AI_HOV_FOOTER_DD_ITEM_BASE + 100);
//--- Click on item dispatches action
if(onItem)
{
Ai_HandleAction(newHov);
return;
}
//--- Click outside body and anchor closes dropdown
if(!inDdBody && !onAnchor)
{
g_ai_showFooterDropdown = false;
Ai_RenderAll(); ChartRedraw();
return;
}
//--- Click inside body but not on item - swallow
if(inDdBody)
{
return;
}
}
//--- Search/history popup handling
if(g_ai_showSearch || g_ai_showSmallHistory)
{
bool inPopupBody = AiPointInRect(lx, ly, g_ai_popupL, g_ai_popupT,
g_ai_popupR - g_ai_popupL,
g_ai_popupB - g_ai_popupT);
bool onAnchorS = (newHov == AI_HOV_SEARCH);
bool onAnchorH = (newHov == AI_HOV_HISTORY);
bool onPopupRow = (newHov >= AI_HOV_SMALL_CHAT_BASE
&& newHov < AI_HOV_SMALL_DEL_BASE + 100);
//--- Search popup scrollbar thumb takes priority
if(g_ai_showSearch
&& AiScrollVisible(g_ai_searchScroll)
&& AiScrollHitTestThumb(g_ai_searchScroll, lx, ly))
{
AiScrollBeginDrag(g_ai_searchScroll, ly);
g_ai_searchScrollDragging = true;
Ai_RenderAll(); ChartRedraw();
return;
}
//--- Click on popup row dispatches action
if(onPopupRow)
{
Ai_HandleAction(newHov);
return;
}
//--- Click on search input box focuses search editor
if(g_ai_showSearch
&& g_ai_popupSearchR > g_ai_popupSearchL
&& AiPointInRect(lx, ly, g_ai_popupSearchL, g_ai_popupSearchT,
g_ai_popupSearchR - g_ai_popupSearchL,
g_ai_popupSearchB - g_ai_popupSearchT))
{
if(g_ai_editor.focused) g_ai_editor.focused = false;
bool wasSF = g_ai_searchEditor.focused;
g_ai_searchEditor.focused = true;
g_ai_searchEditor.SetCaretFromMouse(
lx - g_ai_popupSearchL - 4 - g_ai_searchEditor.padX,
ly - g_ai_popupSearchT - 4 - g_ai_searchEditor.padY);
if(!wasSF) Ai_BeginKeyboardOverride();
Ai_RenderAll(); ChartRedraw();
return;
}
//--- Click outside body and anchors closes popup
if(!inPopupBody && !onAnchorS && !onAnchorH)
{
g_ai_showSearch = false;
g_ai_showSmallHistory = false;
if(g_ai_searchEditor.focused)
{
g_ai_searchEditor.focused = false;
if(!g_ai_editor.focused) Ai_EndKeyboardOverride();
}
Ai_RenderAll(); ChartRedraw();
return;
}
//--- Click inside body but unhandled - swallow
if(inPopupBody)
{
return;
}
}
//--- Test scrollbar thumbs
bool editorThumbDown = AiScrollVisible(g_ai_editor.scroll)
&& AiScrollHitTestThumb(g_ai_editor.scroll, lx, ly);
bool chatThumbDown = AiScrollVisible(g_ai_chatScroll)
&& AiScrollHitTestThumb(g_ai_chatScroll, lx, ly);
//--- Begin editor scrollbar drag
if(editorThumbDown)
{
AiScrollBeginDrag(g_ai_editor.scroll, ly);
g_ai_editorScrollDragging = true;
needRedraw = true;
}
//--- Begin chat scrollbar drag
else if(chatThumbDown)
{
AiScrollBeginDrag(g_ai_chatScroll, ly);
g_ai_chatScrollDragging = true;
needRedraw = true;
}
//--- Begin dashboard drag from header
else if(newHov == AI_HOV_DRAG)
{
g_ai_dragging = true;
g_ai_dragOffsetX = lx;
g_ai_dragOffsetY = ly;
if(g_ai_editor.focused)
{
g_ai_editor.focused = false;
Ai_EndKeyboardOverride();
needRedraw = true;
}
}
//--- Generic click handling
else
{
//--- Focus editor on prompt pane click
bool wasFocused = g_ai_editor.focused;
if(Ai_PointInPromptPane(lx, ly))
{
if(g_ai_searchEditor.focused) g_ai_searchEditor.focused = false;
g_ai_editor.focused = true;
int pL, pT, pR, pB;
Ai_GetPromptPaneRect(pL, pT, pR, pB);
g_ai_editor.SetCaretFromMouse(lx - pL - 4 - g_ai_editor.padX,
ly - pT - 4 - g_ai_editor.padY);
if(!wasFocused) Ai_BeginKeyboardOverride();
needRedraw = true;
}
else
{
//--- Click outside prompt blurs editor
if(wasFocused)
{
g_ai_editor.focused = false;
if(!g_ai_searchEditor.focused) Ai_EndKeyboardOverride();
needRedraw = true;
}
}
//--- Dispatch action for non-prompt hits
if(newHov != AI_HOV_NONE && newHov != AI_HOV_DRAG
&& !Ai_PointInPromptPane(lx, ly))
{
Ai_HandleAction(newHov);
return;
}
}
}
//--- Handle fresh mouse button release
if(freshRelease)
{
g_ai_dragging = false;
if(g_ai_chatScrollDragging)
{
AiScrollEndDrag(g_ai_chatScroll);
g_ai_chatScrollDragging = false;
needRedraw = true;
}
if(g_ai_editorScrollDragging)
{
AiScrollEndDrag(g_ai_editor.scroll);
g_ai_editorScrollDragging = false;
needRedraw = true;
}
}
//--- Trigger redraw if any state changed
if(needRedraw) { Ai_RenderAll(); ChartRedraw(); }
return;
}
//--- Handle mouse wheel events
if(id == CHARTEVENT_MOUSE_WHEEL)
{
//--- Decode wheel coords and delta
const int mx = (int)(short)lparam;
const int my = (int)(short)(lparam >> 16);
const int delta = (int)dparam;
int lx, ly;
Ai_ChartToCanvas(mx, my, lx, ly);
//--- Search popup takes priority when open and hovered
if(g_ai_showSearch
&& AiPointInRect(lx, ly, g_ai_popupL, g_ai_popupT,
g_ai_popupR - g_ai_popupL,
g_ai_popupB - g_ai_popupT)
&& AiScrollVisible(g_ai_searchScroll))
{
ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
AiScrollByWheel(g_ai_searchScroll, delta, 30);
Ai_RenderAll(); ChartRedraw();
return;
}
//--- Editor scroll when over prompt pane
if(Ai_PointInPromptPane(lx, ly) && AiScrollVisible(g_ai_editor.scroll))
{
ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
AiScrollByWheel(g_ai_editor.scroll, delta, 30);
Ai_RenderAll(); ChartRedraw();
return;
}
//--- Chat scroll when over chat pane
if(Ai_PointInChatPane(lx, ly) && AiScrollVisible(g_ai_chatScroll))
{
ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
AiScrollByWheel(g_ai_chatScroll, delta, 30);
Ai_RenderAll(); ChartRedraw();
return;
}
//--- Restore chart mouse scroll outside dashboard
ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
return;
}
//--- Re-render on chart resize/scroll
if(id == CHARTEVENT_CHART_CHANGE)
{
Ai_RenderAll();
ChartRedraw();
return;
}
}
//+------------------------------------------------------------------+
//| Timer event handler for blink and toast updates |
//+------------------------------------------------------------------+
void Ai_OnTimer()
{
//--- Bail when dashboard is hidden
if(!g_ai_dashboardVisible) return;
//--- Track what changed for minimum redraw decision
bool editorBlinkChanged = false;
bool searchBlinkChanged = false;
bool toastChanged = false;
//--- Update editor blink state
if(g_ai_editor.focused)
{
bool prev = g_ai_editor.blinkOn;
g_ai_editor.UpdateBlink();
if(g_ai_editor.blinkOn != prev) editorBlinkChanged = true;
}
//--- Update search editor blink state
if(g_ai_searchEditor.focused)
{
bool prev2 = g_ai_searchEditor.blinkOn;
g_ai_searchEditor.UpdateBlink();
if(g_ai_searchEditor.blinkOn != prev2) searchBlinkChanged = true;
}
//--- Toast auto-dismiss after expiry
bool toastAlive = false;
if(StringLen(g_ai_toastText) > 0)
{
if(GetTickCount64() > g_ai_toastExpiryMs)
{
g_ai_toastText = "";
g_ai_toastExpiryMs = 0;
toastChanged = true;
}
else
{
//--- Toast still alive - flag for progress bar redraw
toastAlive = true;
}
}
//--- Choose minimum redraw path based on what changed
if(toastChanged || toastAlive || searchBlinkChanged)
{
Ai_RenderAll();
ChartRedraw();
}
else if(editorBlinkChanged)
{
//--- Fall back to full render when overlays are present
const bool hasOverlay =
g_ai_showSearch
|| g_ai_showFooterDropdown
|| g_ai_showSmallHistory
|| (StringLen(g_ai_toastText) > 0);
if(hasOverlay)
{
Ai_RenderAll();
ChartRedraw();
}
else if(g_ai_canvPromptExists)
{
//--- Fast path: render only prompt overlay
Ai_RenderPromptPaneOverlay();
ChartRedraw();
}
else
{
//--- Overlay not available - fall back to legacy path
Ai_RenderPromptPane();
g_ai_canvMain.Update();
ChartRedraw();
}
}
}
#endif // AI_CANVAS_INTERACT_MQH