1614 lines
60 KiB
MQL5
1614 lines
60 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)
| |||
{
| |||
//--- Snapshot popup open-state so an opening or closing popup forces a full render
| |||
const bool _wasSearch0 = g_ai_showSearch;
| |||
const bool _wasFootDd0 = g_ai_showFooterDropdown;
| |||
const bool _wasHist0 = g_ai_showSmallHistory;
| |||
//--- 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)
| |||
{
| |||
//--- Position is written every event (always exact); the repaint is capped at ~60fps
| |||
AiScrollUpdateDrag(g_ai_chatScroll, ly);
| |||
if((mstate & 1) == 0)
| |||
{
| |||
//--- Drop: paint immediately so the final frame lands on the true position
| |||
AiScrollEndDrag(g_ai_chatScroll);
| |||
g_ai_chatScrollDragging = false;
| |||
g_ai_frameDirty = false;
| |||
g_ai_partialTarget = AI_RT_NONE;
| |||
Ai_RenderAll();
| |||
ChartRedraw();
| |||
}
| |||
else
| |||
Ai_RequestPartial(AI_RT_CHAT);
| |||
return;
| |||
}
| |||
//--- Handle editor scrollbar drag
| |||
if(g_ai_editorScrollDragging)
| |||
{
| |||
//--- Position is written every event (always exact); the repaint is capped at ~60fps
| |||
AiScrollUpdateDrag(g_ai_editor.scroll, ly);
| |||
if((mstate & 1) == 0)
| |||
{
| |||
//--- Drop: paint immediately so the final frame lands on the true position
| |||
AiScrollEndDrag(g_ai_editor.scroll);
| |||
g_ai_editorScrollDragging = false;
| |||
g_ai_frameDirty = false;
| |||
g_ai_partialTarget = AI_RT_NONE;
| |||
Ai_RenderAll();
| |||
ChartRedraw();
| |||
}
| |||
else
| |||
Ai_RequestPartial(AI_RT_PROMPT);
| |||
return;
| |||
}
| |||
//--- Handle search scrollbar drag
| |||
if(g_ai_searchScrollDragging)
| |||
{
| |||
//--- Position is written every event (always exact); the repaint is capped at ~60fps
| |||
AiScrollUpdateDrag(g_ai_searchScroll, ly);
| |||
if((mstate & 1) == 0)
| |||
{
| |||
//--- Drop: paint immediately so the final frame lands on the true position
| |||
AiScrollEndDrag(g_ai_searchScroll);
| |||
g_ai_searchScrollDragging = false;
| |||
g_ai_frameDirty = false;
| |||
g_ai_partialTarget = AI_RT_NONE;
| |||
Ai_RenderAll();
| |||
ChartRedraw();
| |||
}
| |||
else
| |||
Ai_RequestPartial(AI_RT_SEARCH);
| |||
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);
| |||
//--- Compute which pane changes appearance this event to skip re-rendering the rest
| |||
const bool oldChatCode = (g_ai_hover == AI_HOV_SCROLL_FAB)
| |||
|| (g_ai_hover >= AI_HOV_USER_EDIT_BASE
| |||
&& g_ai_hover < AI_HOV_USER_EDIT_BASE + 100);
| |||
const bool newChatCode = (newHov == AI_HOV_SCROLL_FAB)
| |||
|| (newHov >= AI_HOV_USER_EDIT_BASE
| |||
&& newHov < AI_HOV_USER_EDIT_BASE + 100);
| |||
const bool hoverChatDirty = oldChatCode || newChatCode
| |||
|| (chatHovArea != g_ai_chatScroll.hoveredArea)
| |||
|| (chatHovThumb != g_ai_chatScroll.hoveredThumb)
| |||
|| (overPencilIcon != g_ai_overPencilIcon);
| |||
const bool hoverPromptDirty = (editorHovArea != g_ai_editor.scroll.hoveredArea)
| |||
|| (editorHovThumb != g_ai_editor.scroll.hoveredThumb);
| |||
//--- Repaint the chrome regions the hover left and entered; unknown codes force all
| |||
const int hovRgMask = Ai_HovRegionMask(g_ai_hover) | Ai_HovRegionMask(newHov);
| |||
const bool hovRgUnknown = (hovRgMask & AI_RG_UNKNOWN) != 0;
| |||
const bool hoverSideDirty = hovRgUnknown || ((hovRgMask & AI_RG_SIDEBAR) != 0);
| |||
const bool hoverHeadDirty = hovRgUnknown || ((hovRgMask & AI_RG_HEADER) != 0);
| |||
const bool hoverFootDirty = hovRgUnknown || ((hovRgMask & AI_RG_FOOTER) != 0);
| |||
//--- 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 = g_ai_darkTheme ? "Switch to light theme" : "Switch to dark 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;
| |||
}
| |||
}
| |||
//--- Repaint on state change: instant on press or release, hover motion capped near 60fps
| |||
if(needRedraw)
| |||
{
| |||
if(freshPress || freshRelease)
| |||
{
| |||
g_ai_frameDirty = false;
| |||
g_ai_partialTarget = AI_RT_NONE;
| |||
Ai_RenderAll();
| |||
ChartRedraw();
| |||
}
| |||
else
| |||
{
| |||
//--- Pure hover motion: full render if a popup just toggled, else repaint that overlay
| |||
const bool popupChanged = (g_ai_showSearch != _wasSearch0)
| |||
|| (g_ai_showFooterDropdown != _wasFootDd0)
| |||
|| (g_ai_showSmallHistory != _wasHist0);
| |||
const bool anyPopupUp = g_ai_showSearch || g_ai_showFooterDropdown
| |||
|| g_ai_showSmallHistory
| |||
|| (StringLen(g_ai_toastText) > 0);
| |||
if(popupChanged
| |||
|| (anyPopupUp && (g_ai_showSearch || StringLen(g_ai_toastText) > 0)))
| |||
Ai_RequestFrame();
| |||
else
| |||
if(anyPopupUp && (g_ai_showFooterDropdown || g_ai_showSmallHistory))
| |||
Ai_RequestOverlayFrame();
| |||
else
| |||
Ai_RequestHoverFrame(hoverSideDirty, hoverHeadDirty, hoverFootDirty,
| |||
hoverChatDirty || hovRgUnknown,
| |||
hoverPromptDirty || hovRgUnknown);
| |||
}
| |||
}
| |||
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);
| |||
//--- Skip repaint when already clamped at an extreme so the ends do not hang
| |||
const int beforeS = g_ai_searchScroll.scrollPx;
| |||
AiScrollByWheel(g_ai_searchScroll, delta, 30);
| |||
if(g_ai_searchScroll.scrollPx == beforeS)
| |||
return;
| |||
//--- Only the search popup moved - repaint just that pane, capped ~60fps
| |||
Ai_RequestPartial(AI_RT_SEARCH);
| |||
return;
| |||
}
| |||
//--- Editor scroll when over prompt pane
| |||
if(Ai_PointInPromptPane(lx, ly) && AiScrollVisible(g_ai_editor.scroll))
| |||
{
| |||
ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
| |||
//--- Clamp guard - no repaint when already at an extreme
| |||
const int beforeP = g_ai_editor.scroll.scrollPx;
| |||
AiScrollByWheel(g_ai_editor.scroll, delta, 30);
| |||
if(g_ai_editor.scroll.scrollPx == beforeP)
| |||
return;
| |||
//--- Only the prompt pane moved
| |||
Ai_RequestPartial(AI_RT_PROMPT);
| |||
return;
| |||
}
| |||
//--- Chat scroll when over chat pane
| |||
if(Ai_PointInChatPane(lx, ly) && AiScrollVisible(g_ai_chatScroll))
| |||
{
| |||
ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
| |||
//--- Clamp guard - no repaint when already at an extreme
| |||
const int beforeC = g_ai_chatScroll.scrollPx;
| |||
AiScrollByWheel(g_ai_chatScroll, delta, 30);
| |||
if(g_ai_chatScroll.scrollPx == beforeC)
| |||
return;
| |||
//--- Only the chat pane moved - repaint just that pane, capped ~60fps
| |||
Ai_RequestPartial(AI_RT_CHAT);
| |||
return;
| |||
}
| |||
//--- Restore chart mouse scroll outside dashboard
| |||
ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
| |||
return;
| |||
}
| |||
//--- Re-render on chart resize or scroll with repaint capped and final state guaranteed
| |||
if(id == CHARTEVENT_CHART_CHANGE)
| |||
{
| |||
Ai_RequestFrame();
| |||
return;
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Timer event handler for blink and toast updates |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_OnTimer()
| |||
{
| |||
//--- Bail when dashboard is hidden
| |||
if(!g_ai_dashboardVisible)
| |||
{
| |||
//--- Keep the timer from idling in fast mode while hidden
| |||
if(g_ai_timerFast)
| |||
{
| |||
EventSetMillisecondTimer(500);
| |||
g_ai_timerFast = false;
| |||
g_ai_frameDirty = false;
| |||
}
| |||
return;
| |||
}
| |||
//--- Flush any skipped frame and restore the idle timer cadence after the burst
| |||
Ai_FlushFrame();
| |||
//--- Keep blink and toast logic on the original 500ms cadence during fast flush
| |||
static uint s_lastSlowTickMs = 0;
| |||
const uint slowNowMs = GetTickCount();
| |||
if(g_ai_timerFast && slowNowMs - s_lastSlowTickMs < 500)
| |||
return;
| |||
s_lastSlowTickMs = slowNowMs;
| |||
//--- 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
| |||
//+------------------------------------------------------------------+
|