2599 lines
104 KiB
MQL5
2599 lines
104 KiB
MQL5
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| AI Canvas Render.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_RENDER_MQH
|
||
|
|
#define AI_CANVAS_RENDER_MQH
|
||
|
|
|
||
|
|
//--- Include required modules
|
||
|
|
#include <Canvas/Canvas.mqh>
|
||
|
|
#include "AI Canvas Theme.mqh"
|
||
|
|
#include "AI Canvas State.mqh"
|
||
|
|
#include "AI Canvas Primitives.mqh"
|
||
|
|
#include "AI Canvas Scrollbar.mqh"
|
||
|
|
#include "AI Canvas Editor.mqh"
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Canvas Objects |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
CCanvas g_ai_canvMain; // Main dashboard canvas
|
||
|
|
CCanvas g_ai_canvSmallPopup; // Small history popup canvas
|
||
|
|
CCanvas g_ai_canvBigPopup; // Big popup canvas
|
||
|
|
CCanvas g_ai_canvSearchPopup; // Search popup canvas
|
||
|
|
CCanvas g_ai_canvPrompt; // Prompt-pane overlay canvas
|
||
|
|
bool g_ai_canvPromptExists = false; // Prompt overlay init flag
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Persistent Off-screen Clip Buffers |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
CAiCanvasFast g_ai_canvChatTmp; // Chat-pane scratch canvas
|
||
|
|
bool g_ai_canvChatTmpReady = false; // Chat scratch init flag
|
||
|
|
int g_ai_canvChatTmpW = 0; // Chat scratch width
|
||
|
|
int g_ai_canvChatTmpH = 0; // Chat scratch height
|
||
|
|
|
||
|
|
CAiCanvasFast g_ai_canvSearchTmp; // Search popup scratch canvas
|
||
|
|
bool g_ai_canvSearchTmpReady = false; // Search scratch init flag
|
||
|
|
int g_ai_canvSearchTmpW = 0; // Search scratch width
|
||
|
|
int g_ai_canvSearchTmpH = 0; // Search scratch height
|
||
|
|
|
||
|
|
bool g_ai_canvMainExists = false; // Main canvas init flag
|
||
|
|
bool g_ai_canvSmallExists = false; // Small popup init flag
|
||
|
|
bool g_ai_canvBigExists = false; // Big popup init flag
|
||
|
|
bool g_ai_canvSearchExists = false; // Search popup init flag
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Drawing Primitives and Editor Instances |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
CAiCanvasPrimitives g_ai_prim; // Drawing primitives helper
|
||
|
|
CAiEditor g_ai_editor; // Main prompt editor
|
||
|
|
CAiEditor g_ai_searchEditor; // Search query editor
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Test if prompt editor is empty or whitespace-only |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
bool Ai_PromptIsEmpty()
|
||
|
|
{
|
||
|
|
//--- Read editor text and check length
|
||
|
|
const string txt = g_ai_editor.GetText();
|
||
|
|
const int n = StringLen(txt);
|
||
|
|
if(n == 0) return true;
|
||
|
|
|
||
|
|
//--- Walk characters looking for non-whitespace
|
||
|
|
for(int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
const ushort c = StringGetCharacter(txt, i);
|
||
|
|
if(c != ' ' && c != '\t' && c != '\n' && c != '\r') return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Scroll States |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
AiScrollState g_ai_chatScroll; // Chat pane scroll state
|
||
|
|
AiScrollState g_ai_bigScroll; // Big popup scroll state
|
||
|
|
AiScrollState g_ai_searchScroll; // Search popup scroll state
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Image Pixel Buffers |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
uint g_ai_pixHeader[]; // Header image pixels
|
||
|
|
uint g_ai_pixSidebarBig[]; // Expanded sidebar logo
|
||
|
|
uint g_ai_pixSidebarSmall[]; // Collapsed sidebar logo
|
||
|
|
uint g_ai_pixNewChat[]; // New chat icon pixels
|
||
|
|
uint g_ai_pixClear[]; // Clear icon pixels
|
||
|
|
uint g_ai_pixHistory[]; // History icon pixels
|
||
|
|
uint g_ai_pixSearch[]; // Search icon pixels
|
||
|
|
bool g_ai_imagesLoaded = false; // Images loaded flag
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Hover Code Constants |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
#define AI_HOV_NONE 0 // No hover
|
||
|
|
#define AI_HOV_CLOSE 1 // Close button
|
||
|
|
#define AI_HOV_TOGGLE 2 // Sidebar toggle
|
||
|
|
#define AI_HOV_NEW_CHAT 3 // New chat button
|
||
|
|
#define AI_HOV_CLEAR 4 // Clear chat button
|
||
|
|
#define AI_HOV_HISTORY 5 // History button
|
||
|
|
#define AI_HOV_SEARCH 6 // Search button
|
||
|
|
#define AI_HOV_CHART 7 // Chart action
|
||
|
|
#define AI_HOV_SIGNAL 8 // Signal action half
|
||
|
|
#define AI_HOV_SEND 9 // Send button
|
||
|
|
#define AI_HOV_REGEN 10 // Regenerate button
|
||
|
|
#define AI_HOV_EXPORT 11 // Export button
|
||
|
|
#define AI_HOV_BIG_CLOSE 12 // Big popup close
|
||
|
|
#define AI_HOV_SEARCH_CLOSE 13 // Search popup close
|
||
|
|
#define AI_HOV_SEE_MORE 14 // See more link
|
||
|
|
#define AI_HOV_THEME 15 // Theme toggle
|
||
|
|
#define AI_HOV_DRAG 16 // Header drag area
|
||
|
|
#define AI_HOV_FOOTER_DD 17 // Footer dropdown anchor
|
||
|
|
#define AI_HOV_SIGNAL_DD 18 // Signal chevron half
|
||
|
|
#define AI_HOV_SCROLL_FAB 19 // Scroll-to-bottom FAB
|
||
|
|
#define AI_HOV_SIDE_CHAT_BASE 100 // Sidebar chat row base
|
||
|
|
#define AI_HOV_SIDE_DEL_BASE 200 // Sidebar delete base
|
||
|
|
#define AI_HOV_SMALL_CHAT_BASE 300 // Small popup chat row base
|
||
|
|
#define AI_HOV_SMALL_DEL_BASE 400 // Small popup delete base
|
||
|
|
#define AI_HOV_BIG_CHAT_BASE 500 // Big popup chat row base
|
||
|
|
#define AI_HOV_BIG_DEL_BASE 600 // Big popup delete base
|
||
|
|
#define AI_HOV_SEARCH_CHAT_BASE 700 // Search popup chat row base
|
||
|
|
#define AI_HOV_SEARCH_DEL_BASE 800 // Search popup delete base
|
||
|
|
#define AI_HOV_FOOTER_DD_ITEM_BASE 900 // Footer dropdown item base
|
||
|
|
#define AI_HOV_USER_EDIT_BASE 1000 // User edit pencil base
|
||
|
|
|
||
|
|
//--- Width of chevron-half zone in split signal button
|
||
|
|
#define AI_SIGNAL_DD_ZONE_W 28
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Hover and Mouse Tracking |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
int g_ai_hover = AI_HOV_NONE; // Current hover code
|
||
|
|
int g_ai_mouseLx = -1; // Last canvas-local mouse X
|
||
|
|
int g_ai_mouseLy = -1; // Last canvas-local mouse Y
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Action Button Rectangles |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
int g_ai_regenL = 0, g_ai_regenT = 0, g_ai_regenR = 0, g_ai_regenB = 0; // Regenerate button rect
|
||
|
|
int g_ai_exportL = 0, g_ai_exportT = 0, g_ai_exportR = 0, g_ai_exportB = 0; // Export button rect
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Scroll-to-bottom FAB Rectangle |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
int g_ai_scrollFabL = 0, g_ai_scrollFabT = 0, g_ai_scrollFabR = 0, g_ai_scrollFabB = 0; // FAB rect
|
||
|
|
bool g_ai_scrollFabVisible = false; // FAB visibility flag
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Per-User-Message Edit Pencil State |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
int g_ai_userEditRectL[]; // Wide hover rect left bounds
|
||
|
|
int g_ai_userEditRectT[]; // Wide hover rect top bounds
|
||
|
|
int g_ai_userEditRectR[]; // Wide hover rect right bounds
|
||
|
|
int g_ai_userEditRectB[]; // Wide hover rect bottom bounds
|
||
|
|
int g_ai_userEditClickL[]; // Narrow click rect left bounds
|
||
|
|
int g_ai_userEditClickT[]; // Narrow click rect top bounds
|
||
|
|
int g_ai_userEditClickR[]; // Narrow click rect right bounds
|
||
|
|
int g_ai_userEditClickB[]; // Narrow click rect bottom bounds
|
||
|
|
string g_ai_userEditPrompt[]; // Cached prompt text per pencil
|
||
|
|
|
||
|
|
bool g_ai_chatScrollPin = false; // Chat scroll auto-pin to bottom flag
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Compute theme-aware border color for given fill |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
color AiBorderForBg(color bg)
|
||
|
|
{
|
||
|
|
//--- Lighten on dark theme, darken on light theme
|
||
|
|
if(g_ai_darkTheme) return AiLightenColor(bg, 0.35);
|
||
|
|
return AiDarkenColor(bg, 0.78);
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Get current sidebar width |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
int Ai_SidebarW()
|
||
|
|
{
|
||
|
|
//--- Return expanded or collapsed width
|
||
|
|
return g_ai_sidebarExpanded ? AI_SIDEBAR_W_EXPANDED : AI_SIDEBAR_W_COLLAPSED;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Get current dashboard width |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
int Ai_DashboardW()
|
||
|
|
{
|
||
|
|
//--- Sum sidebar and main pane widths
|
||
|
|
return Ai_SidebarW() + AI_MAIN_W;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Get current dashboard height |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
int Ai_DashboardH()
|
||
|
|
{
|
||
|
|
//--- Sum all vertical sections
|
||
|
|
return AI_HEADER_H + AI_PAD + AI_DISPLAY_H + AI_PAD + AI_PROMPT_H + AI_PAD + AI_FOOTER_H;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Load and scale all UI bitmap resources |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
bool Ai_LoadImages()
|
||
|
|
{
|
||
|
|
//--- Skip if already loaded
|
||
|
|
if(g_ai_imagesLoaded) return true;
|
||
|
|
|
||
|
|
//--- Load and scale header image
|
||
|
|
uint w, h;
|
||
|
|
if(ResourceReadImage("::AI MQL5.bmp", g_ai_pixHeader, w, h))
|
||
|
|
{
|
||
|
|
AiScaleImage(g_ai_pixHeader, (int)w, (int)h, 104, 40);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
ArrayResize(g_ai_pixHeader, 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Load and scale logo for both sidebar sizes
|
||
|
|
uint wL, hL;
|
||
|
|
uint pixLogoOrig[];
|
||
|
|
if(ResourceReadImage("::AI LOGO.bmp", pixLogoOrig, wL, hL))
|
||
|
|
{
|
||
|
|
ArrayResize(g_ai_pixSidebarBig, ArraySize(pixLogoOrig));
|
||
|
|
ArrayCopy(g_ai_pixSidebarBig, pixLogoOrig);
|
||
|
|
AiScaleImage(g_ai_pixSidebarBig, (int)wL, (int)hL, 81, 81);
|
||
|
|
ArrayResize(g_ai_pixSidebarSmall, ArraySize(pixLogoOrig));
|
||
|
|
ArrayCopy(g_ai_pixSidebarSmall, pixLogoOrig);
|
||
|
|
AiScaleImage(g_ai_pixSidebarSmall, (int)wL, (int)hL, 30, 30);
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Load and scale sidebar action icons
|
||
|
|
if(ResourceReadImage("::AI NEW CHAT.bmp", g_ai_pixNewChat, w, h))
|
||
|
|
AiScaleImage(g_ai_pixNewChat, (int)w, (int)h, 22, 22);
|
||
|
|
if(ResourceReadImage("::AI CLEAR.bmp", g_ai_pixClear, w, h))
|
||
|
|
AiScaleImage(g_ai_pixClear, (int)w, (int)h, 22, 22);
|
||
|
|
if(ResourceReadImage("::AI HISTORY.bmp", g_ai_pixHistory, w, h))
|
||
|
|
AiScaleImage(g_ai_pixHistory, (int)w, (int)h, 22, 22);
|
||
|
|
if(ResourceReadImage("::AI SEARCH.bmp", g_ai_pixSearch, w, h))
|
||
|
|
AiScaleImage(g_ai_pixSearch, (int)w, (int)h, 22, 22);
|
||
|
|
|
||
|
|
//--- Mark loaded and return
|
||
|
|
g_ai_imagesLoaded = true;
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Get close button rectangle |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_GetCloseBtnRect(int &outL, int &outT, int &outR, int &outB)
|
||
|
|
{
|
||
|
|
//--- Compute button dimensions and anchor to top-right
|
||
|
|
const int btnH = AI_HEADER_H - 7;
|
||
|
|
const int btnW = (int)(btnH * 1.5);
|
||
|
|
outR = Ai_DashboardW();
|
||
|
|
outL = outR - btnW;
|
||
|
|
outT = 0;
|
||
|
|
outB = outT + btnH;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Get theme toggle button rectangle |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_GetThemeBtnRect(int &outL, int &outT, int &outR, int &outB)
|
||
|
|
{
|
||
|
|
//--- Anchor to left of close button
|
||
|
|
const int btnH = AI_HEADER_H - 7;
|
||
|
|
const int btnW = btnH;
|
||
|
|
int cL, cT, cR, cB;
|
||
|
|
Ai_GetCloseBtnRect(cL, cT, cR, cB);
|
||
|
|
outR = cL;
|
||
|
|
outL = outR - btnW;
|
||
|
|
outT = 0;
|
||
|
|
outB = btnH;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Compute Y position of sidebar item by index |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
int Ai_SidebarItemY(int i)
|
||
|
|
{
|
||
|
|
//--- Account for logo height and prior items
|
||
|
|
int logoH = g_ai_sidebarExpanded ? 81 : 30;
|
||
|
|
int y = AI_HEADER_H + 10 + logoH + 8;
|
||
|
|
y += i * (AI_BUTTON_H + 6);
|
||
|
|
return y;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Get sidebar button rectangle by index |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_GetSidebarBtnRect(int idx, int &outL, int &outT, int &outR, int &outB)
|
||
|
|
{
|
||
|
|
//--- Compute rect using sidebar width and item Y
|
||
|
|
const int sw = Ai_SidebarW();
|
||
|
|
outL = 5;
|
||
|
|
outR = sw - 5;
|
||
|
|
outT = Ai_SidebarItemY(idx);
|
||
|
|
outB = outT + AI_BUTTON_H;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Get sidebar toggle button rectangle |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_GetToggleBtnRect(int &outL, int &outT, int &outR, int &outB)
|
||
|
|
{
|
||
|
|
//--- Anchor to bottom of sidebar
|
||
|
|
const int sw = Ai_SidebarW();
|
||
|
|
outL = 5;
|
||
|
|
outR = sw - 5;
|
||
|
|
outB = Ai_DashboardH() - 8;
|
||
|
|
outT = outB - AI_BUTTON_H;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Get top of sidebar chat list |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
int Ai_SidebarChatListTop()
|
||
|
|
{
|
||
|
|
//--- Below history button
|
||
|
|
return Ai_SidebarItemY(3) + AI_BUTTON_H + 8;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Get sidebar chat row rectangle by row index |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_GetSidebarChatRowRect(int row, int &outL, int &outT, int &outR, int &outB)
|
||
|
|
{
|
||
|
|
//--- Compute row layout
|
||
|
|
const int sw = Ai_SidebarW();
|
||
|
|
const int rowH = 24;
|
||
|
|
outL = 8;
|
||
|
|
outR = sw - 8;
|
||
|
|
outT = Ai_SidebarChatListTop() + row * (rowH + 3);
|
||
|
|
outB = outT + rowH;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Pane Y-coordinate Helpers |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
int Ai_MainContentX() { return Ai_SidebarW(); } // Main pane left
|
||
|
|
int Ai_ChatPaneT() { return AI_HEADER_H + AI_PAD; } // Chat pane top
|
||
|
|
int Ai_ChatPaneB() { return Ai_ChatPaneT() + AI_DISPLAY_H; } // Chat pane bottom
|
||
|
|
int Ai_PromptPaneT() { return Ai_ChatPaneB() + AI_PAD; } // Prompt pane top
|
||
|
|
int Ai_PromptPaneB() { return Ai_PromptPaneT() + AI_PROMPT_H; } // Prompt pane bottom
|
||
|
|
int Ai_FooterT() { return Ai_PromptPaneB() + AI_PAD; } // Footer top
|
||
|
|
int Ai_FooterB() { return Ai_FooterT() + AI_FOOTER_H; } // Footer bottom
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Get chat pane rectangle |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_GetChatPaneRect(int &outL, int &outT, int &outR, int &outB)
|
||
|
|
{
|
||
|
|
//--- Inset main pane bounds by side padding
|
||
|
|
outL = Ai_MainContentX() + AI_SIDE_PAD;
|
||
|
|
outT = Ai_ChatPaneT();
|
||
|
|
outR = Ai_DashboardW() - AI_SIDE_PAD;
|
||
|
|
outB = Ai_ChatPaneB();
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Get prompt pane rectangle |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_GetPromptPaneRect(int &outL, int &outT, int &outR, int &outB)
|
||
|
|
{
|
||
|
|
//--- Inset main pane bounds by side padding
|
||
|
|
outL = Ai_MainContentX() + AI_SIDE_PAD;
|
||
|
|
outT = Ai_PromptPaneT();
|
||
|
|
outR = Ai_DashboardW() - AI_SIDE_PAD;
|
||
|
|
outB = Ai_PromptPaneB();
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Compute footer dropdown / signal button width |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
int Ai_GetFooterDropdownWidth()
|
||
|
|
{
|
||
|
|
//--- Find longest action label width
|
||
|
|
int longestPx = 0;
|
||
|
|
for(int i = 0; i < AI_FOOTER_DD_COUNT; i++)
|
||
|
|
{
|
||
|
|
int w = AiTextWidth(AI_FOOTER_DD_LABELS[i], "Arial Bold", 9);
|
||
|
|
if(w > longestPx) longestPx = w;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Compose action zone + chevron zone width
|
||
|
|
const int actionZone = 8 + 18 + 8 + longestPx + 4;
|
||
|
|
int total = actionZone + AI_SIGNAL_DD_ZONE_W;
|
||
|
|
|
||
|
|
//--- Floor to minimum width
|
||
|
|
if(total < 140) total = 140;
|
||
|
|
return total;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Get signal button rectangle |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_GetSignalBtnRect(int &outL, int &outT, int &outR, int &outB)
|
||
|
|
{
|
||
|
|
//--- Compute width and vertically center in footer
|
||
|
|
const int btnH = 32;
|
||
|
|
outL = Ai_MainContentX() + AI_SIDE_PAD;
|
||
|
|
outR = outL + Ai_GetFooterDropdownWidth();
|
||
|
|
outT = Ai_FooterT() + (AI_FOOTER_H - btnH) / 2;
|
||
|
|
outB = outT + btnH;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Get send button rectangle |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_GetSendBtnRect(int &outL, int &outT, int &outR, int &outB)
|
||
|
|
{
|
||
|
|
//--- Anchor to right edge of main pane
|
||
|
|
const int btnH = 32;
|
||
|
|
outR = Ai_DashboardW() - AI_SIDE_PAD;
|
||
|
|
outL = outR - 44;
|
||
|
|
outT = Ai_FooterT() + (AI_FOOTER_H - btnH) / 2;
|
||
|
|
outB = outT + btnH;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Compute X coordinate of signal button separator |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
int Ai_SignalSeparatorX()
|
||
|
|
{
|
||
|
|
//--- Subtract chevron zone width from right edge
|
||
|
|
int sL, sT, sR, sB;
|
||
|
|
Ai_GetSignalBtnRect(sL, sT, sR, sB);
|
||
|
|
return sR - AI_SIGNAL_DD_ZONE_W;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Get signal button action half rectangle |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_GetSignalActionRect(int &outL, int &outT, int &outR, int &outB)
|
||
|
|
{
|
||
|
|
//--- Use signal button bounds with separator as right edge
|
||
|
|
int sL, sT, sR, sB;
|
||
|
|
Ai_GetSignalBtnRect(sL, sT, sR, sB);
|
||
|
|
outL = sL;
|
||
|
|
outT = sT;
|
||
|
|
outR = Ai_SignalSeparatorX();
|
||
|
|
outB = sB;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Get signal button chevron half rectangle |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_GetSignalDdRect(int &outL, int &outT, int &outR, int &outB)
|
||
|
|
{
|
||
|
|
//--- Use separator as left edge of dd half
|
||
|
|
int sL, sT, sR, sB;
|
||
|
|
Ai_GetSignalBtnRect(sL, sT, sR, sB);
|
||
|
|
outL = Ai_SignalSeparatorX();
|
||
|
|
outT = sT;
|
||
|
|
outR = sR;
|
||
|
|
outB = sB;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Render dashboard header |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_RenderHeader()
|
||
|
|
{
|
||
|
|
//--- Fill header background
|
||
|
|
const int dW = Ai_DashboardW();
|
||
|
|
g_ai_prim.FillRoundRectHR(g_ai_canvMain, 0, 0, dW, AI_HEADER_H, 0,
|
||
|
|
ColorToARGB(g_ai_headerBg, 255));
|
||
|
|
|
||
|
|
//--- Stamp logo
|
||
|
|
if(ArraySize(g_ai_pixHeader) > 0)
|
||
|
|
{
|
||
|
|
AiStampPixels(g_ai_canvMain, AI_SIDE_PAD, (AI_HEADER_H - 40) / 2,
|
||
|
|
g_ai_pixHeader, 104, 40);
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Stamp title text
|
||
|
|
const string title = "ChatGPT AI EA";
|
||
|
|
int titleY = (AI_HEADER_H - AiTextHeight("Arial Bold", AI_FONT_TITLE)) / 2;
|
||
|
|
AiStampTextAA(g_ai_canvMain, AI_SIDE_PAD + 104 + 8, titleY,
|
||
|
|
title, "Arial Bold", AI_FONT_TITLE, g_ai_titleText);
|
||
|
|
|
||
|
|
//--- Stamp current server time centered in main area
|
||
|
|
string dateStr = TimeToString(TimeTradeServer(), TIME_MINUTES);
|
||
|
|
int dW2 = AiTextWidth(dateStr, "Arial", AI_FONT_HEADING);
|
||
|
|
int dateX = Ai_MainContentX() + (AI_MAIN_W - dW2) / 2;
|
||
|
|
int dateY = (AI_HEADER_H - AiTextHeight("Arial", AI_FONT_HEADING)) / 2;
|
||
|
|
AiStampTextAA(g_ai_canvMain, dateX, dateY, dateStr, "Arial", AI_FONT_HEADING, g_ai_subText);
|
||
|
|
|
||
|
|
//--- Render theme toggle button
|
||
|
|
int tL, tT, tR, tB;
|
||
|
|
Ai_GetThemeBtnRect(tL, tT, tR, tB);
|
||
|
|
const bool themeHov = (g_ai_hover == AI_HOV_THEME);
|
||
|
|
if(themeHov)
|
||
|
|
{
|
||
|
|
const color hoverBg = AiDarkenColor(g_ai_headerBg, 0.85);
|
||
|
|
g_ai_canvMain.FillRectangle(tL, tT, tR - 1, tB - 1, ColorToARGB(hoverBg, 255));
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Stamp theme glyph
|
||
|
|
const string themeGlyph = CharToString((uchar)91);
|
||
|
|
const int themeFs = 16;
|
||
|
|
int thIcW = AiTextWidth(themeGlyph, "Wingdings", themeFs);
|
||
|
|
int thIcH = AiTextHeight("Wingdings", themeFs);
|
||
|
|
AiStampTextAA(g_ai_canvMain, tL + (tR - tL - thIcW) / 2,
|
||
|
|
tT + (tB - tT - thIcH) / 2,
|
||
|
|
themeGlyph, "Wingdings", themeFs, g_ai_titleText);
|
||
|
|
|
||
|
|
//--- Render close button
|
||
|
|
int cL, cT, cR, cB;
|
||
|
|
Ai_GetCloseBtnRect(cL, cT, cR, cB);
|
||
|
|
const bool closeHov = (g_ai_hover == AI_HOV_CLOSE);
|
||
|
|
color closeFg = closeHov ? g_ai_closeColorHover : g_ai_closeColor;
|
||
|
|
if(closeHov)
|
||
|
|
{
|
||
|
|
const uint hoverArgb = ColorToARGB(g_ai_closeBgHover, 255);
|
||
|
|
g_ai_canvMain.FillRectangle(cL, cT, cR - 1, cB - 1, hoverArgb);
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Stamp close glyph
|
||
|
|
int iconW = AiTextWidth(AI_GLYPH_CLOSE, "Webdings", 14);
|
||
|
|
int iconH = AiTextHeight("Webdings", 14);
|
||
|
|
AiStampTextAA(g_ai_canvMain, cL + (cR - cL - iconW) / 2,
|
||
|
|
cT + (cB - cT - iconH) / 2,
|
||
|
|
AI_GLYPH_CLOSE, "Webdings", 14, closeFg);
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Render sidebar background and divider line |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_RenderSidebarBg()
|
||
|
|
{
|
||
|
|
//--- Fill background and draw right divider
|
||
|
|
const int sw = Ai_SidebarW();
|
||
|
|
const int dh = Ai_DashboardH();
|
||
|
|
g_ai_prim.FillRoundRectHR(g_ai_canvMain, 0, AI_HEADER_H, sw, dh - AI_HEADER_H, 0,
|
||
|
|
ColorToARGB(g_ai_sidebarBg, 255));
|
||
|
|
g_ai_canvMain.LineVertical(sw - 1, AI_HEADER_H, dh - 1,
|
||
|
|
ColorToARGB(g_ai_border, 255));
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Render sidebar logo at appropriate size |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_RenderSidebarLogo()
|
||
|
|
{
|
||
|
|
//--- Pick logo by sidebar expansion state
|
||
|
|
const int sw = Ai_SidebarW();
|
||
|
|
if(g_ai_sidebarExpanded)
|
||
|
|
{
|
||
|
|
if(ArraySize(g_ai_pixSidebarBig) > 0)
|
||
|
|
AiStampPixels(g_ai_canvMain, (sw - 81) / 2, AI_HEADER_H + 10,
|
||
|
|
g_ai_pixSidebarBig, 81, 81);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
if(ArraySize(g_ai_pixSidebarSmall) > 0)
|
||
|
|
AiStampPixels(g_ai_canvMain, (sw - 30) / 2, AI_HEADER_H + 10,
|
||
|
|
g_ai_pixSidebarSmall, 30, 30);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Render a single sidebar button |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_RenderSidebarBtn(int idx, color bgIdle, color bgHover, int hovId,
|
||
|
|
uint &iconPix[], const string label)
|
||
|
|
{
|
||
|
|
//--- Get button rect and pick fill color by hover state
|
||
|
|
int bL, bT, bR, bB;
|
||
|
|
Ai_GetSidebarBtnRect(idx, bL, bT, bR, bB);
|
||
|
|
color bg = (g_ai_hover == hovId) ? bgHover : bgIdle;
|
||
|
|
|
||
|
|
//--- Fill button and draw border
|
||
|
|
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, bL, bT, bR - bL, bB - bT, 6,
|
||
|
|
ColorToARGB(bg, 255));
|
||
|
|
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, bL, bT, bR - bL, bB - bT, 6, 1,
|
||
|
|
ColorToARGB(AiBorderForBg(bg), 255));
|
||
|
|
|
||
|
|
//--- Position icon based on expansion state
|
||
|
|
const int sw = Ai_SidebarW();
|
||
|
|
int iconX = g_ai_sidebarExpanded ? (bL + 8) : (bL + (bR - bL - 22) / 2);
|
||
|
|
int iconY = bT + (bB - bT - 22) / 2;
|
||
|
|
if(ArraySize(iconPix) > 0)
|
||
|
|
AiStampPixels(g_ai_canvMain, iconX, iconY, iconPix, 22, 22);
|
||
|
|
|
||
|
|
//--- Stamp label only when expanded
|
||
|
|
if(g_ai_sidebarExpanded)
|
||
|
|
{
|
||
|
|
int lblY = bT + (bB - bT - AiTextHeight("Arial", AI_FONT_BUTTON)) / 2;
|
||
|
|
AiStampTextAA(g_ai_canvMain, iconX + 22 + 6, lblY,
|
||
|
|
label, "Arial", AI_FONT_BUTTON, g_ai_titleText);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Render sidebar chat list |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_RenderSidebarChats()
|
||
|
|
{
|
||
|
|
//--- Bail when sidebar is collapsed
|
||
|
|
if(!g_ai_sidebarExpanded) return;
|
||
|
|
|
||
|
|
//--- Cap visible rows at 7
|
||
|
|
const int n = MathMin(ArraySize(g_ai_chats), 7);
|
||
|
|
const int sw = Ai_SidebarW();
|
||
|
|
|
||
|
|
//--- Render each chat row
|
||
|
|
for(int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
//--- Compute chat index (most recent first)
|
||
|
|
int chatIdx = ArraySize(g_ai_chats) - 1 - i;
|
||
|
|
int bL, bT, bR, bB;
|
||
|
|
Ai_GetSidebarChatRowRect(i, bL, bT, bR, bB);
|
||
|
|
|
||
|
|
//--- Determine hover state
|
||
|
|
bool hovRow = (g_ai_hover == AI_HOV_SIDE_CHAT_BASE + i)
|
||
|
|
|| (g_ai_hover == AI_HOV_SIDE_DEL_BASE + i);
|
||
|
|
bool hovDel = (g_ai_hover == AI_HOV_SIDE_DEL_BASE + i);
|
||
|
|
color bg = hovRow ? g_ai_chatItemBgHover : g_ai_chatItemBg;
|
||
|
|
|
||
|
|
//--- Fill row and draw border
|
||
|
|
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, bL, bT, bR - bL, bB - bT, 4,
|
||
|
|
ColorToARGB(bg, 255));
|
||
|
|
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, bL, bT, bR - bL, bB - bT, 4, 1,
|
||
|
|
ColorToARGB(AiBorderForBg(bg), 255));
|
||
|
|
|
||
|
|
//--- Fit title text to available width
|
||
|
|
string fullText = g_ai_chats[chatIdx].title;
|
||
|
|
const int sbDeleteIconW = AiTextWidth(AI_GLYPH_DELETE, "Wingdings 2", 14);
|
||
|
|
const int sbAvailW = (bR - bL) - 18 - sbDeleteIconW;
|
||
|
|
string labelText = Ai_FitTextToWidth(fullText, "Arial", AI_FONT_LABEL, sbAvailW);
|
||
|
|
|
||
|
|
//--- Pick text color based on active state
|
||
|
|
color textCol = (g_ai_chats[chatIdx].id == g_ai_currentChatId)
|
||
|
|
? g_ai_chatItemActiveText : g_ai_titleText;
|
||
|
|
|
||
|
|
//--- Stamp title text
|
||
|
|
int lblY = bT + (bB - bT - AiTextHeight("Arial", AI_FONT_LABEL)) / 2;
|
||
|
|
AiStampTextAA(g_ai_canvMain, bL + 6, lblY, labelText,
|
||
|
|
"Arial", AI_FONT_LABEL, textCol);
|
||
|
|
|
||
|
|
//--- Stamp delete glyph on hover
|
||
|
|
if(hovRow)
|
||
|
|
{
|
||
|
|
color xCol = hovDel ? g_ai_chatItemDelHover : g_ai_subText;
|
||
|
|
int xW = AiTextWidth(AI_GLYPH_DELETE, "Wingdings 2", 14);
|
||
|
|
int xH = AiTextHeight("Wingdings 2", 14);
|
||
|
|
AiStampTextAA(g_ai_canvMain, bR - 6 - xW, bT + (bB - bT - xH) / 2,
|
||
|
|
AI_GLYPH_DELETE, "Wingdings 2", 14, xCol);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Render sidebar toggle button |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_RenderToggleBtn()
|
||
|
|
{
|
||
|
|
//--- Get rect and pick fill by hover state
|
||
|
|
int bL, bT, bR, bB;
|
||
|
|
Ai_GetToggleBtnRect(bL, bT, bR, bB);
|
||
|
|
color bg = (g_ai_hover == AI_HOV_TOGGLE) ? g_ai_buttonToggleBgHover : g_ai_buttonToggleBg;
|
||
|
|
|
||
|
|
//--- Fill and border button
|
||
|
|
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, bL, bT, bR - bL, bB - bT, 6,
|
||
|
|
ColorToARGB(bg, 255));
|
||
|
|
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, bL, bT, bR - bL, bB - bT, 6, 1,
|
||
|
|
ColorToARGB(AiBorderForBg(bg), 255));
|
||
|
|
|
||
|
|
//--- Stamp directional glyph
|
||
|
|
const string glyph = g_ai_sidebarExpanded ? AI_GLYPH_TOGGLE_LEFT : AI_GLYPH_TOGGLE_RIGHT;
|
||
|
|
int gW = AiTextWidth(glyph, "Webdings", 16);
|
||
|
|
int gH = AiTextHeight("Webdings", 16);
|
||
|
|
AiStampTextAA(g_ai_canvMain, bL + (bR - bL - gW) / 2,
|
||
|
|
bT + (bB - bT - gH) / 2, glyph, "Webdings", 16, g_ai_titleText);
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Render entire sidebar |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_RenderSidebar()
|
||
|
|
{
|
||
|
|
//--- Render sidebar components in order
|
||
|
|
Ai_RenderSidebarBg();
|
||
|
|
Ai_RenderSidebarLogo();
|
||
|
|
Ai_RenderSidebarBtn(0, g_ai_buttonSearchBg, g_ai_buttonSearchBgHover,
|
||
|
|
AI_HOV_SEARCH, g_ai_pixSearch, "Search");
|
||
|
|
Ai_RenderSidebarBtn(1, g_ai_buttonNewChatBg, g_ai_buttonNewChatBgHover,
|
||
|
|
AI_HOV_NEW_CHAT, g_ai_pixNewChat, "New Chat");
|
||
|
|
Ai_RenderSidebarBtn(2, g_ai_buttonClearBg, g_ai_buttonClearBgHover,
|
||
|
|
AI_HOV_CLEAR, g_ai_pixClear, "Clear");
|
||
|
|
Ai_RenderSidebarBtn(3, g_ai_buttonHistoryBg, g_ai_buttonHistoryBgHover,
|
||
|
|
AI_HOV_HISTORY, g_ai_pixHistory, "History");
|
||
|
|
Ai_RenderSidebarChats();
|
||
|
|
Ai_RenderToggleBtn();
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Render chat pane with messages and scroll FAB |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_RenderChatPane()
|
||
|
|
{
|
||
|
|
//--- Get pane rect and dimensions
|
||
|
|
int rL, rT, rR, rB;
|
||
|
|
Ai_GetChatPaneRect(rL, rT, rR, rB);
|
||
|
|
const int paneW = rR - rL;
|
||
|
|
const int paneH = rB - rT;
|
||
|
|
|
||
|
|
//--- Fill pane background and draw border
|
||
|
|
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, rL, rT, paneW, paneH, 6,
|
||
|
|
ColorToARGB(g_ai_panelAlt, 255));
|
||
|
|
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, rL, rT, paneW, paneH, 6, 1,
|
||
|
|
ColorToARGB(g_ai_border, 255));
|
||
|
|
|
||
|
|
//--- Conversation parse cache - static across renders
|
||
|
|
static string s_cacheRoles[];
|
||
|
|
static string s_cacheContents[];
|
||
|
|
static string s_cacheTimestamps[];
|
||
|
|
static int s_cacheLen = -1;
|
||
|
|
static int s_cacheCharQ1 = -1;
|
||
|
|
static int s_cacheCharQ3 = -1;
|
||
|
|
|
||
|
|
//--- Compute cache fingerprint from history
|
||
|
|
const int curLen = StringLen(g_ai_conversationHistory);
|
||
|
|
const int curQ1 = (curLen >= 4)
|
||
|
|
? (int)StringGetCharacter(g_ai_conversationHistory, curLen / 4)
|
||
|
|
: -1;
|
||
|
|
const int curQ3 = (curLen >= 4)
|
||
|
|
? (int)StringGetCharacter(g_ai_conversationHistory, (curLen * 3) / 4)
|
||
|
|
: -1;
|
||
|
|
const bool cacheHit = (s_cacheLen == curLen)
|
||
|
|
&& (s_cacheCharQ1 == curQ1)
|
||
|
|
&& (s_cacheCharQ3 == curQ3);
|
||
|
|
|
||
|
|
//--- Working arrays for parsed messages
|
||
|
|
string msgRoles[];
|
||
|
|
string msgContents[];
|
||
|
|
string msgTimestamps[];
|
||
|
|
|
||
|
|
//--- Cache hit path - reuse parsed arrays
|
||
|
|
if(cacheHit)
|
||
|
|
{
|
||
|
|
const int cn = ArraySize(s_cacheRoles);
|
||
|
|
ArrayResize(msgRoles, cn);
|
||
|
|
ArrayResize(msgContents, cn);
|
||
|
|
ArrayResize(msgTimestamps, cn);
|
||
|
|
for(int ci = 0; ci < cn; ci++)
|
||
|
|
{
|
||
|
|
msgRoles[ci] = s_cacheRoles[ci];
|
||
|
|
msgContents[ci] = s_cacheContents[ci];
|
||
|
|
msgTimestamps[ci] = s_cacheTimestamps[ci];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
//--- Cache miss path - parse and snapshot
|
||
|
|
else
|
||
|
|
{
|
||
|
|
//--- Split history into lines
|
||
|
|
string parts[];
|
||
|
|
const int numParts = StringSplit(g_ai_conversationHistory, '\n', parts);
|
||
|
|
|
||
|
|
//--- Initialize parser state
|
||
|
|
string curRole = "";
|
||
|
|
string curContent = "";
|
||
|
|
string curTs = "";
|
||
|
|
|
||
|
|
//--- Walk lines and group into role-tagged messages
|
||
|
|
for(int p = 0; p < numParts; p++)
|
||
|
|
{
|
||
|
|
string line = parts[p];
|
||
|
|
string trimmed = line;
|
||
|
|
StringTrimLeft(trimmed); StringTrimRight(trimmed);
|
||
|
|
|
||
|
|
//--- Blank lines belong to current content
|
||
|
|
if(StringLen(trimmed) == 0)
|
||
|
|
{
|
||
|
|
if(curRole != "") curContent += "\n";
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- New user block
|
||
|
|
if(StringFind(trimmed, "You: ") == 0)
|
||
|
|
{
|
||
|
|
if(curRole != "")
|
||
|
|
{
|
||
|
|
int sz = ArraySize(msgRoles);
|
||
|
|
ArrayResize(msgRoles, sz + 1); ArrayResize(msgContents, sz + 1); ArrayResize(msgTimestamps, sz + 1);
|
||
|
|
msgRoles[sz] = curRole; msgContents[sz] = curContent; msgTimestamps[sz] = curTs;
|
||
|
|
}
|
||
|
|
curRole = "User";
|
||
|
|
curContent = StringSubstr(line, StringFind(line, "You: ") + 5);
|
||
|
|
curTs = "";
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- New AI block
|
||
|
|
if(StringFind(trimmed, "AI: ") == 0)
|
||
|
|
{
|
||
|
|
if(curRole != "")
|
||
|
|
{
|
||
|
|
int sz = ArraySize(msgRoles);
|
||
|
|
ArrayResize(msgRoles, sz + 1); ArrayResize(msgContents, sz + 1); ArrayResize(msgTimestamps, sz + 1);
|
||
|
|
msgRoles[sz] = curRole; msgContents[sz] = curContent; msgTimestamps[sz] = curTs;
|
||
|
|
}
|
||
|
|
curRole = "AI";
|
||
|
|
curContent = StringSubstr(line, StringFind(line, "AI: ") + 4);
|
||
|
|
curTs = "";
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Timestamp ends current block
|
||
|
|
if(AiIsTimestamp(trimmed))
|
||
|
|
{
|
||
|
|
curTs = trimmed;
|
||
|
|
int sz = ArraySize(msgRoles);
|
||
|
|
ArrayResize(msgRoles, sz + 1); ArrayResize(msgContents, sz + 1); ArrayResize(msgTimestamps, sz + 1);
|
||
|
|
msgRoles[sz] = curRole; msgContents[sz] = curContent; msgTimestamps[sz] = curTs;
|
||
|
|
curRole = ""; curContent = ""; curTs = "";
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Continuation line
|
||
|
|
if(curRole != "") curContent += "\n" + line;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Flush trailing block
|
||
|
|
if(curRole != "")
|
||
|
|
{
|
||
|
|
int sz = ArraySize(msgRoles);
|
||
|
|
ArrayResize(msgRoles, sz + 1); ArrayResize(msgContents, sz + 1); ArrayResize(msgTimestamps, sz + 1);
|
||
|
|
msgRoles[sz] = curRole; msgContents[sz] = curContent; msgTimestamps[sz] = curTs;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Snapshot to cache
|
||
|
|
const int sn = ArraySize(msgRoles);
|
||
|
|
ArrayResize(s_cacheRoles, sn);
|
||
|
|
ArrayResize(s_cacheContents, sn);
|
||
|
|
ArrayResize(s_cacheTimestamps, sn);
|
||
|
|
for(int si = 0; si < sn; si++)
|
||
|
|
{
|
||
|
|
s_cacheRoles[si] = msgRoles[si];
|
||
|
|
s_cacheContents[si] = msgContents[si];
|
||
|
|
s_cacheTimestamps[si] = msgTimestamps[si];
|
||
|
|
}
|
||
|
|
s_cacheLen = curLen;
|
||
|
|
s_cacheCharQ1 = curQ1;
|
||
|
|
s_cacheCharQ3 = curQ3;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Find index of last AI message for action row placement
|
||
|
|
const int numMessages = ArraySize(msgRoles);
|
||
|
|
int lastAiIdx = -1;
|
||
|
|
for(int li = numMessages - 1; li >= 0; li--)
|
||
|
|
{
|
||
|
|
if(StringFind(msgRoles[li], "AI") < 0) continue;
|
||
|
|
if(StringFind(msgContents[li], "Thinking") >= 0) continue;
|
||
|
|
if(StringFind(msgContents[li], "Preparing the Request") >= 0) continue;
|
||
|
|
lastAiIdx = li;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Empty conversation - render placeholder text
|
||
|
|
if(numMessages == 0)
|
||
|
|
{
|
||
|
|
const string empty = "Type your prompt and click Send to chat with the AI.";
|
||
|
|
int eW = AiTextWidth(empty, "Arial", AI_FONT_BODY);
|
||
|
|
int eH = AiTextHeight("Arial", AI_FONT_BODY);
|
||
|
|
AiStampTextAA(g_ai_canvMain, rL + (paneW - eW) / 2, rT + (paneH - eH) / 2,
|
||
|
|
empty, "Arial", AI_FONT_BODY, g_ai_subText);
|
||
|
|
g_ai_chatScroll.totalH = 0;
|
||
|
|
g_ai_chatScroll.viewportH = paneH - 2 * AI_TEXT_PAD;
|
||
|
|
AiScrollClamp(g_ai_chatScroll);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Setup content rect and bubble layout constants
|
||
|
|
const int contentL = rL + AI_TEXT_PAD;
|
||
|
|
const int contentT = rT + AI_TEXT_PAD;
|
||
|
|
const int contentR = rR - AI_TEXT_PAD;
|
||
|
|
const int contentB = rB - AI_TEXT_PAD;
|
||
|
|
const int sbW = 4;
|
||
|
|
const int sbRightPad = 2;
|
||
|
|
const int textRight = contentR - sbW - sbRightPad - 4;
|
||
|
|
const int contentW = textRight - contentL;
|
||
|
|
const int bubbleMaxW = (int)((double)contentW * 0.78);
|
||
|
|
const int bubblePadX = 10;
|
||
|
|
const int bubblePadY = 6;
|
||
|
|
const int bubbleTextW = bubbleMaxW - 2 * bubblePadX;
|
||
|
|
const int lineHBody = AiTextHeight("Arial", AI_FONT_BODY) + 2;
|
||
|
|
const int lineHTime = AiTextHeight("Arial", AI_FONT_TIMESTAMP) + 1;
|
||
|
|
const int msgGap = 8;
|
||
|
|
|
||
|
|
//--- Per-message wrapped text and bubble dimensions
|
||
|
|
string msgWrapped[];
|
||
|
|
int msgBubbleW[];
|
||
|
|
int msgBubbleH[];
|
||
|
|
string msgRespTime[];
|
||
|
|
ArrayResize(msgWrapped, numMessages);
|
||
|
|
ArrayResize(msgBubbleW, numMessages);
|
||
|
|
ArrayResize(msgBubbleH, numMessages);
|
||
|
|
ArrayResize(msgRespTime, numMessages);
|
||
|
|
|
||
|
|
//--- Wrap each message and compute bubble size
|
||
|
|
for(int m = 0; m < numMessages; m++)
|
||
|
|
{
|
||
|
|
//--- Extract response time note if present
|
||
|
|
msgRespTime[m] = "";
|
||
|
|
string raw = msgContents[m];
|
||
|
|
int rtPos = StringFind(raw, "(Response in ");
|
||
|
|
if(rtPos >= 0)
|
||
|
|
{
|
||
|
|
int rtEnd = StringFind(raw, ")", rtPos);
|
||
|
|
if(rtEnd > rtPos)
|
||
|
|
{
|
||
|
|
msgRespTime[m] = StringSubstr(raw, rtPos, rtEnd - rtPos + 1);
|
||
|
|
int strip = rtPos;
|
||
|
|
if(strip > 0 && StringGetCharacter(raw, strip - 1) == '\n') strip--;
|
||
|
|
string before = StringSubstr(raw, 0, strip);
|
||
|
|
string after = StringSubstr(raw, rtEnd + 1);
|
||
|
|
raw = before + after;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Skip dot-replacement for loading placeholders
|
||
|
|
const bool isLoadingPlaceholder =
|
||
|
|
(StringFind(raw, "Preparing the Request") >= 0
|
||
|
|
|| StringFind(raw, "Thinking") >= 0);
|
||
|
|
string processed = isLoadingPlaceholder
|
||
|
|
? raw
|
||
|
|
: AiReplaceExactDoublePeriods(raw);
|
||
|
|
|
||
|
|
//--- Markdown line-kind preprocessing for AI bubbles
|
||
|
|
const bool isUserMsg = (StringFind(msgRoles[m], "User") >= 0);
|
||
|
|
const bool isAiMsg = !isUserMsg && !isLoadingPlaceholder;
|
||
|
|
if(isAiMsg)
|
||
|
|
{
|
||
|
|
string mdLines[];
|
||
|
|
StringSplit(processed, '\n', mdLines);
|
||
|
|
string rebuilt = "";
|
||
|
|
for(int li = 0; li < ArraySize(mdLines); li++)
|
||
|
|
{
|
||
|
|
string ln = mdLines[li];
|
||
|
|
|
||
|
|
//--- Find leading whitespace count for prefix detection
|
||
|
|
int wsCount = 0;
|
||
|
|
const int lnLen = StringLen(ln);
|
||
|
|
while(wsCount < lnLen)
|
||
|
|
{
|
||
|
|
ushort c = StringGetCharacter(ln, wsCount);
|
||
|
|
if(c == ' ' || c == '\t') wsCount++;
|
||
|
|
else break;
|
||
|
|
}
|
||
|
|
const string body = StringSubstr(ln, wsCount);
|
||
|
|
string sentLine = ln;
|
||
|
|
|
||
|
|
//--- Replace heading prefixes with sentinel chars
|
||
|
|
if(StringSubstr(body, 0, 4) == "### ")
|
||
|
|
{
|
||
|
|
sentLine = ShortToString(0x03) + StringSubstr(body, 4);
|
||
|
|
}
|
||
|
|
else if(StringSubstr(body, 0, 3) == "## ")
|
||
|
|
{
|
||
|
|
sentLine = ShortToString(0x02) + StringSubstr(body, 3);
|
||
|
|
}
|
||
|
|
else if(StringSubstr(body, 0, 2) == "# ")
|
||
|
|
{
|
||
|
|
sentLine = ShortToString(0x01) + StringSubstr(body, 2);
|
||
|
|
}
|
||
|
|
if(li > 0) rebuilt += "\n";
|
||
|
|
rebuilt += sentLine;
|
||
|
|
}
|
||
|
|
processed = rebuilt;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Split into paragraphs and wrap each
|
||
|
|
string para[];
|
||
|
|
StringSplit(processed, '\n', para);
|
||
|
|
string wrapped = "";
|
||
|
|
int maxLineW = 0;
|
||
|
|
int lineCount = 0;
|
||
|
|
int totalLineH = 0;
|
||
|
|
|
||
|
|
//--- Process each paragraph
|
||
|
|
for(int ls = 0; ls < ArraySize(para); ls++)
|
||
|
|
{
|
||
|
|
string p = para[ls];
|
||
|
|
|
||
|
|
//--- Empty paragraph emits blank line
|
||
|
|
if(StringLen(p) == 0)
|
||
|
|
{
|
||
|
|
if(wrapped != "") wrapped += "\n";
|
||
|
|
lineCount++;
|
||
|
|
totalLineH += lineHBody;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Setup default kind and font
|
||
|
|
int kind = AI_MD_KIND_BODY;
|
||
|
|
string lineFont = "Arial";
|
||
|
|
int lineSize = AI_FONT_BODY;
|
||
|
|
int lineH = lineHBody;
|
||
|
|
int lineIndent = 0;
|
||
|
|
string numPrefix = "";
|
||
|
|
|
||
|
|
//--- Detect heading sentinel for AI messages
|
||
|
|
if(isAiMsg && StringLen(p) >= 1)
|
||
|
|
{
|
||
|
|
const ushort sent = StringGetCharacter(p, 0);
|
||
|
|
if(sent == 0x01) { kind = AI_MD_KIND_H1; lineFont = "Arial Bold"; lineSize = 13; }
|
||
|
|
else if(sent == 0x02) { kind = AI_MD_KIND_H2; lineFont = "Arial Bold"; lineSize = 12; }
|
||
|
|
else if(sent == 0x03) { kind = AI_MD_KIND_H3; lineFont = "Arial Bold"; lineSize = 11; }
|
||
|
|
if(kind != AI_MD_KIND_BODY)
|
||
|
|
{
|
||
|
|
p = StringSubstr(p, 1);
|
||
|
|
lineH = AiTextHeightCached(lineFont, lineSize) + 2;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Detect numbered list prefix
|
||
|
|
if(isAiMsg && kind == AI_MD_KIND_BODY)
|
||
|
|
{
|
||
|
|
const int pl = StringLen(p);
|
||
|
|
int dEnd = 0;
|
||
|
|
while(dEnd < pl)
|
||
|
|
{
|
||
|
|
const ushort dc = StringGetCharacter(p, dEnd);
|
||
|
|
if(dc >= '0' && dc <= '9') dEnd++;
|
||
|
|
else break;
|
||
|
|
}
|
||
|
|
if(dEnd > 0
|
||
|
|
&& dEnd + 1 < pl
|
||
|
|
&& StringGetCharacter(p, dEnd) == '.'
|
||
|
|
&& StringGetCharacter(p, dEnd + 1) == ' ')
|
||
|
|
{
|
||
|
|
numPrefix = StringSubstr(p, 0, dEnd + 2);
|
||
|
|
p = StringSubstr(p, dEnd + 2);
|
||
|
|
lineIndent = AiTextWidth(numPrefix, "Arial Bold", AI_FONT_BODY);
|
||
|
|
kind = AI_MD_KIND_NUMBERED;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Detect dash or star bullet prefix
|
||
|
|
if(isAiMsg && kind == AI_MD_KIND_BODY)
|
||
|
|
{
|
||
|
|
const int pl4 = StringLen(p);
|
||
|
|
if(pl4 >= 2)
|
||
|
|
{
|
||
|
|
const ushort d0 = StringGetCharacter(p, 0);
|
||
|
|
const ushort d1 = StringGetCharacter(p, 1);
|
||
|
|
const bool isDashStart = (d0 == '-' && d1 == ' ');
|
||
|
|
const bool isStarStart = (d0 == '*' && d1 == ' ');
|
||
|
|
if(isDashStart || isStarStart)
|
||
|
|
{
|
||
|
|
numPrefix = StringSubstr(p, 0, 2);
|
||
|
|
p = StringSubstr(p, 2);
|
||
|
|
lineIndent = AiTextWidth(numPrefix, "Arial", AI_FONT_BODY);
|
||
|
|
kind = AI_MD_KIND_NUMBERED;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Detect leading whitespace for hanging indent
|
||
|
|
string leadingWhitespace = "";
|
||
|
|
if(isAiMsg && kind == AI_MD_KIND_BODY)
|
||
|
|
{
|
||
|
|
int wsCount = 0;
|
||
|
|
const int pl3 = StringLen(p);
|
||
|
|
while(wsCount < pl3)
|
||
|
|
{
|
||
|
|
const ushort wc = StringGetCharacter(p, wsCount);
|
||
|
|
if(wc == ' ' || wc == '\t') wsCount++;
|
||
|
|
else break;
|
||
|
|
}
|
||
|
|
if(wsCount > 0 && wsCount < pl3)
|
||
|
|
{
|
||
|
|
leadingWhitespace = StringSubstr(p, 0, wsCount);
|
||
|
|
p = StringSubstr(p, wsCount);
|
||
|
|
|
||
|
|
//--- Snap indent up to numbered-prefix reference width
|
||
|
|
const int wsW = AiTextWidth(leadingWhitespace, "Arial", AI_FONT_BODY);
|
||
|
|
const int refW = AiTextWidth("3. ", "Arial Bold", AI_FONT_BODY);
|
||
|
|
lineIndent = (wsW > refW) ? wsW : refW;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Compute available width accounting for indent
|
||
|
|
const int availW = bubbleTextW - lineIndent;
|
||
|
|
int paraLen = StringLen(p);
|
||
|
|
int start = 0;
|
||
|
|
bool isFirstWrap = true;
|
||
|
|
|
||
|
|
//--- Inline-style state for cross-wrap continuity
|
||
|
|
bool openBold = false, openItalic = false;
|
||
|
|
|
||
|
|
//--- Wrap paragraph into visual lines
|
||
|
|
while(start < paraLen)
|
||
|
|
{
|
||
|
|
//--- Find next visual line break point
|
||
|
|
string sub = StringSubstr(p, start, paraLen - start);
|
||
|
|
int w = AiTextWidth(sub, lineFont, lineSize);
|
||
|
|
string visualLine; int consume;
|
||
|
|
if(w <= availW) { visualLine = sub; consume = paraLen - start; }
|
||
|
|
else
|
||
|
|
{
|
||
|
|
//--- Binary search for max prefix that fits
|
||
|
|
int lo = 1, hi = paraLen - start, fit = 1;
|
||
|
|
while(lo <= hi)
|
||
|
|
{
|
||
|
|
int mid = (lo + hi) / 2;
|
||
|
|
string pre = StringSubstr(p, start, mid);
|
||
|
|
int pw = AiTextWidth(pre, lineFont, lineSize);
|
||
|
|
if(pw <= availW) { fit = mid; lo = mid + 1; }
|
||
|
|
else { hi = mid - 1; }
|
||
|
|
}
|
||
|
|
//--- Prefer breaking at last space
|
||
|
|
int breakAt = fit;
|
||
|
|
bool foundSp = false;
|
||
|
|
for(int k = fit; k >= 1; k--)
|
||
|
|
{
|
||
|
|
ushort cc = StringGetCharacter(p, start + k - 1);
|
||
|
|
if(cc == ' ') { breakAt = k - 1; foundSp = true; break; }
|
||
|
|
}
|
||
|
|
if(foundSp) { visualLine = StringSubstr(p, start, breakAt); consume = breakAt + 1; }
|
||
|
|
else { visualLine = StringSubstr(p, start, fit); consume = fit; }
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Track maximum line width including indent
|
||
|
|
int vlW = AiTextWidth(visualLine, lineFont, lineSize);
|
||
|
|
int effectiveW = vlW + lineIndent;
|
||
|
|
if(effectiveW > maxLineW) maxLineW = effectiveW;
|
||
|
|
|
||
|
|
//--- Compute markers to reopen styles on continuation
|
||
|
|
const string reopenMarkers = isFirstWrap
|
||
|
|
? ""
|
||
|
|
: AiMdReopenMarkers(openBold, openItalic);
|
||
|
|
|
||
|
|
//--- Compose output line with appropriate sentinels
|
||
|
|
string outLine;
|
||
|
|
if(kind == AI_MD_KIND_H1
|
||
|
|
|| kind == AI_MD_KIND_H2
|
||
|
|
|| kind == AI_MD_KIND_H3)
|
||
|
|
{
|
||
|
|
outLine = ShortToString((ushort)kind) + reopenMarkers + visualLine;
|
||
|
|
}
|
||
|
|
else if(kind == AI_MD_KIND_NUMBERED)
|
||
|
|
{
|
||
|
|
if(isFirstWrap)
|
||
|
|
{
|
||
|
|
outLine = numPrefix + reopenMarkers + visualLine;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
outLine = ShortToString(0x06) + IntegerToString(lineIndent)
|
||
|
|
+ ":" + reopenMarkers + visualLine;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
//--- Body paragraphs with leading whitespace get indent sentinel
|
||
|
|
if(lineIndent > 0)
|
||
|
|
{
|
||
|
|
outLine = ShortToString(0x06) + IntegerToString(lineIndent)
|
||
|
|
+ ":" + reopenMarkers + visualLine;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
outLine = reopenMarkers + visualLine;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Append to wrapped text
|
||
|
|
if(wrapped != "") wrapped += "\n";
|
||
|
|
wrapped += outLine;
|
||
|
|
lineCount++;
|
||
|
|
totalLineH += lineH;
|
||
|
|
|
||
|
|
//--- Update inline-style end state for next iteration
|
||
|
|
AiMdComputeEndState(visualLine, openBold, openItalic);
|
||
|
|
start += consume;
|
||
|
|
isFirstWrap = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Floor at one line for empty messages
|
||
|
|
if(lineCount == 0) { lineCount = 1; totalLineH = lineHBody; }
|
||
|
|
|
||
|
|
//--- Store wrapped text and bubble dimensions
|
||
|
|
msgWrapped[m] = wrapped;
|
||
|
|
msgBubbleW[m] = MathMin(bubbleMaxW, maxLineW + 2 * bubblePadX);
|
||
|
|
msgBubbleH[m] = totalLineH + 2 * bubblePadY;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Compute total content height
|
||
|
|
const int actionRowH = 22;
|
||
|
|
int totalH = 0;
|
||
|
|
for(int m = 0; m < numMessages; m++)
|
||
|
|
{
|
||
|
|
totalH += msgBubbleH[m];
|
||
|
|
if(StringLen(msgTimestamps[m]) > 0) totalH += lineHTime + 2;
|
||
|
|
if(m == lastAiIdx) totalH += actionRowH + 2;
|
||
|
|
totalH += msgGap;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Update scroll state
|
||
|
|
g_ai_chatScroll.totalH = totalH;
|
||
|
|
g_ai_chatScroll.viewportH = contentB - contentT;
|
||
|
|
if(g_ai_chatScrollPin)
|
||
|
|
{
|
||
|
|
g_ai_chatScroll.scrollPx = AiScrollMax(g_ai_chatScroll);
|
||
|
|
g_ai_chatScrollPin = false;
|
||
|
|
}
|
||
|
|
AiScrollClamp(g_ai_chatScroll);
|
||
|
|
|
||
|
|
//--- Ensure persistent chat scratch canvas matches main size
|
||
|
|
const int cw = g_ai_canvMain.Width();
|
||
|
|
const int chC = g_ai_canvMain.Height();
|
||
|
|
if(!g_ai_canvChatTmpReady || g_ai_canvChatTmpW < cw || g_ai_canvChatTmpH < chC)
|
||
|
|
{
|
||
|
|
if(g_ai_canvChatTmpReady) g_ai_canvChatTmp.Destroy();
|
||
|
|
const int newW = MathMax(cw, g_ai_canvChatTmpW);
|
||
|
|
const int newH = MathMax(chC, g_ai_canvChatTmpH);
|
||
|
|
if(!g_ai_canvChatTmp.CreateBitmap("AiChatPaneTmpPersistent", 0, 0, newW, newH, COLOR_FORMAT_ARGB_NORMALIZE)) return;
|
||
|
|
g_ai_canvChatTmpW = newW;
|
||
|
|
g_ai_canvChatTmpH = newH;
|
||
|
|
g_ai_canvChatTmpReady = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Compute clip region and seed scratch canvas
|
||
|
|
const int clipL = rL + 1;
|
||
|
|
const int clipT = rT + 1;
|
||
|
|
const int clipR = rR - 1;
|
||
|
|
const int clipB = rB - 1;
|
||
|
|
const int seedL = MathMax(clipL, 0);
|
||
|
|
const int seedT = MathMax(clipT, 0);
|
||
|
|
const int seedR = MathMin(clipR, cw);
|
||
|
|
const int seedB = MathMin(clipB, chC);
|
||
|
|
|
||
|
|
//--- Copy current pane pixels to scratch for clip blending
|
||
|
|
g_ai_canvChatTmp.CopyRectFromCanvas(g_ai_canvMain, seedL, seedT, seedR, seedB);
|
||
|
|
|
||
|
|
//--- Alias scratch canvas as tmp for the render block
|
||
|
|
#define tmp g_ai_canvChatTmp
|
||
|
|
|
||
|
|
//--- Initialize render Y cursor and reset edit-pencil arrays
|
||
|
|
int curY = contentT - g_ai_chatScroll.scrollPx;
|
||
|
|
ArrayResize(g_ai_userEditRectL, 0);
|
||
|
|
ArrayResize(g_ai_userEditRectT, 0);
|
||
|
|
ArrayResize(g_ai_userEditRectR, 0);
|
||
|
|
ArrayResize(g_ai_userEditRectB, 0);
|
||
|
|
ArrayResize(g_ai_userEditClickL, 0);
|
||
|
|
ArrayResize(g_ai_userEditClickT, 0);
|
||
|
|
ArrayResize(g_ai_userEditClickR, 0);
|
||
|
|
ArrayResize(g_ai_userEditClickB, 0);
|
||
|
|
ArrayResize(g_ai_userEditPrompt, 0);
|
||
|
|
|
||
|
|
//--- Render each message bubble
|
||
|
|
for(int m = 0; m < numMessages; m++)
|
||
|
|
{
|
||
|
|
//--- Determine role and pick colors
|
||
|
|
bool isUser = (StringFind(msgRoles[m], "User") >= 0);
|
||
|
|
string processed = msgWrapped[m];
|
||
|
|
color bubBg = isUser ? g_ai_userBubbleBg : g_ai_aiBubbleBg;
|
||
|
|
color textCol = isUser ? g_ai_userBubbleText : g_ai_aiBubbleText;
|
||
|
|
|
||
|
|
//--- Re-detect placeholder flag and adjust colors
|
||
|
|
const bool isLoadingPlaceholder =
|
||
|
|
(StringFind(processed, "Preparing the Request") >= 0
|
||
|
|
|| StringFind(processed, "Thinking") >= 0);
|
||
|
|
if(StringFind(processed, "Preparing the Request") >= 0) textCol = g_ai_loadingPrep;
|
||
|
|
if(StringFind(processed, "Thinking...") >= 0) textCol = g_ai_loadingThink;
|
||
|
|
|
||
|
|
//--- Compute bubble position
|
||
|
|
const int bubW = msgBubbleW[m];
|
||
|
|
const int bubH = msgBubbleH[m];
|
||
|
|
const int bubX = isUser ? (textRight - bubW) : contentL;
|
||
|
|
const int bubY = curY;
|
||
|
|
|
||
|
|
//--- Skip rendering for offscreen bubbles
|
||
|
|
if(bubY + bubH < contentT || bubY > contentB)
|
||
|
|
{
|
||
|
|
curY += bubH;
|
||
|
|
if(StringLen(msgTimestamps[m]) > 0) curY += lineHTime + 2;
|
||
|
|
curY += msgGap;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Fill bubble background
|
||
|
|
g_ai_prim.FillRoundRectSharp(tmp, bubX, bubY, bubW, bubH, 8,
|
||
|
|
ColorToARGB(bubBg, 255));
|
||
|
|
|
||
|
|
//--- Render each visible line within bubble
|
||
|
|
string visLines[];
|
||
|
|
int nl = StringSplit(processed, '\n', visLines);
|
||
|
|
int textY = bubY + bubblePadY;
|
||
|
|
for(int i = 0; i < nl; i++)
|
||
|
|
{
|
||
|
|
string ln = visLines[i];
|
||
|
|
|
||
|
|
//--- User and placeholder bubbles use plain text path
|
||
|
|
if(isUser || isLoadingPlaceholder)
|
||
|
|
{
|
||
|
|
int lW = AiTextWidth(ln, "Arial", AI_FONT_BODY);
|
||
|
|
int lX = isUser ? (bubX + bubW - bubblePadX - lW) : (bubX + bubblePadX);
|
||
|
|
AiStampTextAA(tmp, lX, textY, ln, "Arial", AI_FONT_BODY, textCol);
|
||
|
|
textY += lineHBody;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Detect and strip line-kind sentinel
|
||
|
|
int kind = AI_MD_KIND_BODY;
|
||
|
|
int lineSize = AI_FONT_BODY;
|
||
|
|
int lineH = lineHBody;
|
||
|
|
int contIndent = 0;
|
||
|
|
string body = ln;
|
||
|
|
if(StringLen(body) >= 1)
|
||
|
|
{
|
||
|
|
const ushort sent = StringGetCharacter(body, 0);
|
||
|
|
if(sent == 0x01)
|
||
|
|
{
|
||
|
|
kind = AI_MD_KIND_H1;
|
||
|
|
lineSize = 13;
|
||
|
|
body = StringSubstr(body, 1);
|
||
|
|
lineH = AiTextHeightCached("Arial Bold", lineSize) + 2;
|
||
|
|
}
|
||
|
|
else if(sent == 0x02)
|
||
|
|
{
|
||
|
|
kind = AI_MD_KIND_H2;
|
||
|
|
lineSize = 12;
|
||
|
|
body = StringSubstr(body, 1);
|
||
|
|
lineH = AiTextHeightCached("Arial Bold", lineSize) + 2;
|
||
|
|
}
|
||
|
|
else if(sent == 0x03)
|
||
|
|
{
|
||
|
|
kind = AI_MD_KIND_H3;
|
||
|
|
lineSize = 11;
|
||
|
|
body = StringSubstr(body, 1);
|
||
|
|
lineH = AiTextHeightCached("Arial Bold", lineSize) + 2;
|
||
|
|
}
|
||
|
|
else if(sent == 0x06)
|
||
|
|
{
|
||
|
|
//--- Numbered continuation - parse indent width
|
||
|
|
int colonAt = StringFind(body, ":", 1);
|
||
|
|
if(colonAt > 1)
|
||
|
|
{
|
||
|
|
contIndent = (int)StringToInteger(StringSubstr(body, 1, colonAt - 1));
|
||
|
|
body = StringSubstr(body, colonAt + 1);
|
||
|
|
kind = AI_MD_KIND_NUMBERED;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
body = StringSubstr(body, 1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Detect first-line list prefix (numbered or dash/star)
|
||
|
|
string numPrefix = "";
|
||
|
|
if(kind == AI_MD_KIND_BODY)
|
||
|
|
{
|
||
|
|
const int bl = StringLen(body);
|
||
|
|
|
||
|
|
//--- Try numbered first
|
||
|
|
int dEnd = 0;
|
||
|
|
while(dEnd < bl)
|
||
|
|
{
|
||
|
|
const ushort dc = StringGetCharacter(body, dEnd);
|
||
|
|
if(dc >= '0' && dc <= '9') dEnd++;
|
||
|
|
else break;
|
||
|
|
}
|
||
|
|
if(dEnd > 0
|
||
|
|
&& dEnd + 1 < bl
|
||
|
|
&& StringGetCharacter(body, dEnd) == '.'
|
||
|
|
&& StringGetCharacter(body, dEnd + 1) == ' ')
|
||
|
|
{
|
||
|
|
numPrefix = StringSubstr(body, 0, dEnd + 2);
|
||
|
|
body = StringSubstr(body, dEnd + 2);
|
||
|
|
kind = AI_MD_KIND_NUMBERED;
|
||
|
|
}
|
||
|
|
//--- Otherwise try dash or star
|
||
|
|
else if(bl >= 2)
|
||
|
|
{
|
||
|
|
const ushort r0 = StringGetCharacter(body, 0);
|
||
|
|
const ushort r1 = StringGetCharacter(body, 1);
|
||
|
|
const bool isDash = (r0 == '-' && r1 == ' ');
|
||
|
|
const bool isStar = (r0 == '*' && r1 == ' ');
|
||
|
|
if(isDash || isStar)
|
||
|
|
{
|
||
|
|
numPrefix = StringSubstr(body, 0, 2);
|
||
|
|
body = StringSubstr(body, 2);
|
||
|
|
kind = AI_MD_KIND_NUMBERED;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Compute starting X with prefix or continuation indent
|
||
|
|
int lX = bubX + bubblePadX;
|
||
|
|
if(kind == AI_MD_KIND_NUMBERED)
|
||
|
|
{
|
||
|
|
if(StringLen(numPrefix) > 0)
|
||
|
|
{
|
||
|
|
//--- Pick prefix font and color based on type
|
||
|
|
const ushort firstCh = StringGetCharacter(numPrefix, 0);
|
||
|
|
const bool isNumbered = (firstCh >= '0' && firstCh <= '9');
|
||
|
|
const string prefixFont = isNumbered ? "Arial Bold" : "Arial";
|
||
|
|
const color prefixCol = isNumbered ? g_ai_accent : textCol;
|
||
|
|
AiStampTextAA(tmp, lX, textY, numPrefix, prefixFont,
|
||
|
|
AI_FONT_BODY, prefixCol);
|
||
|
|
lX += AiTextWidth(numPrefix, prefixFont, AI_FONT_BODY);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
lX += contIndent;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Parse inline runs and stamp them
|
||
|
|
AiMdRun runs[];
|
||
|
|
AiMdParseInline(body, runs);
|
||
|
|
if(kind == AI_MD_KIND_H1 || kind == AI_MD_KIND_H2 || kind == AI_MD_KIND_H3)
|
||
|
|
{
|
||
|
|
for(int r = 0; r < ArraySize(runs); r++) runs[r].bold = true;
|
||
|
|
}
|
||
|
|
AiMdStampRuns(tmp, lX, textY, runs, lineSize, textCol);
|
||
|
|
textY += lineH;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Advance past bubble
|
||
|
|
curY += bubH;
|
||
|
|
|
||
|
|
//--- Render timestamp if present
|
||
|
|
if(StringLen(msgTimestamps[m]) > 0)
|
||
|
|
{
|
||
|
|
curY += 2;
|
||
|
|
int tsW = AiTextWidth(msgTimestamps[m], "Arial", AI_FONT_TIMESTAMP);
|
||
|
|
int tX = isUser ? (textRight - tsW) : contentL;
|
||
|
|
AiStampTextAA(tmp, tX, curY, msgTimestamps[m],
|
||
|
|
"Arial", AI_FONT_TIMESTAMP, g_ai_timestampText);
|
||
|
|
curY += lineHTime;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Edit pencil affordance for user messages
|
||
|
|
if(isUser && StringLen(msgContents[m]) > 0)
|
||
|
|
{
|
||
|
|
//--- Compute pencil rect to left of bubble
|
||
|
|
const int peSz = actionRowH;
|
||
|
|
const int peGlyph = 14;
|
||
|
|
const int peGap = 4;
|
||
|
|
const int peR = bubX - peGap;
|
||
|
|
const int peL = peR - peSz;
|
||
|
|
const int peT = bubY + (bubH - peSz) / 2;
|
||
|
|
const int peB = peT + peSz;
|
||
|
|
|
||
|
|
//--- Compute wider hover region covering bubble + pencil
|
||
|
|
const int hovL = peL - 2;
|
||
|
|
const int hovT = bubY;
|
||
|
|
const int hovR = bubX + bubW;
|
||
|
|
const int hovB = bubY + bubH;
|
||
|
|
|
||
|
|
//--- Clip hover region to visible chat content area
|
||
|
|
const int hovTClip = MathMax(hovT, contentT);
|
||
|
|
const int hovBClip = MathMin(hovB, contentB);
|
||
|
|
const bool inHoverRegion =
|
||
|
|
(g_ai_mouseLx >= hovL && g_ai_mouseLx < hovR
|
||
|
|
&& g_ai_mouseLy >= hovTClip && g_ai_mouseLy < hovBClip);
|
||
|
|
|
||
|
|
//--- Register hit rects when pencil has horizontal room
|
||
|
|
if(peL >= contentL && hovBClip > hovTClip)
|
||
|
|
{
|
||
|
|
//--- Append entries to all parallel arrays
|
||
|
|
const int peIdx = ArraySize(g_ai_userEditRectL);
|
||
|
|
ArrayResize(g_ai_userEditRectL, peIdx + 1);
|
||
|
|
ArrayResize(g_ai_userEditRectT, peIdx + 1);
|
||
|
|
ArrayResize(g_ai_userEditRectR, peIdx + 1);
|
||
|
|
ArrayResize(g_ai_userEditRectB, peIdx + 1);
|
||
|
|
ArrayResize(g_ai_userEditClickL, peIdx + 1);
|
||
|
|
ArrayResize(g_ai_userEditClickT, peIdx + 1);
|
||
|
|
ArrayResize(g_ai_userEditClickR, peIdx + 1);
|
||
|
|
ArrayResize(g_ai_userEditClickB, peIdx + 1);
|
||
|
|
ArrayResize(g_ai_userEditPrompt, peIdx + 1);
|
||
|
|
|
||
|
|
//--- Wide hover rect
|
||
|
|
g_ai_userEditRectL[peIdx] = hovL;
|
||
|
|
g_ai_userEditRectT[peIdx] = hovTClip;
|
||
|
|
g_ai_userEditRectR[peIdx] = hovR;
|
||
|
|
g_ai_userEditRectB[peIdx] = hovBClip;
|
||
|
|
|
||
|
|
//--- Narrow click rect (pencil icon only)
|
||
|
|
g_ai_userEditClickL[peIdx] = peL;
|
||
|
|
g_ai_userEditClickT[peIdx] = peT;
|
||
|
|
g_ai_userEditClickR[peIdx] = peR;
|
||
|
|
g_ai_userEditClickB[peIdx] = peB;
|
||
|
|
g_ai_userEditPrompt[peIdx] = msgContents[m];
|
||
|
|
|
||
|
|
//--- Paint pencil only when cursor is in hover region
|
||
|
|
if(inHoverRegion)
|
||
|
|
{
|
||
|
|
//--- Detect hover on icon itself for darkened tint
|
||
|
|
const bool peHov =
|
||
|
|
(g_ai_mouseLx >= peL && g_ai_mouseLx < peR
|
||
|
|
&& g_ai_mouseLy >= peT && g_ai_mouseLy < peB);
|
||
|
|
color peBg = peHov ? AiDarkenColor(g_ai_userBubbleBg, 0.92)
|
||
|
|
: g_ai_userBubbleBg;
|
||
|
|
g_ai_prim.FillRoundRectSharp(tmp, peL, peT, peSz, peSz, 4,
|
||
|
|
ColorToARGB(peBg, 255));
|
||
|
|
g_ai_prim.DrawRoundRectBorderSharp(tmp, peL, peT, peSz, peSz, 4, 1,
|
||
|
|
ColorToARGB(AiBorderForBg(peBg), 255));
|
||
|
|
const int glyphInset = (peSz - peGlyph) / 2;
|
||
|
|
AiDrawIconEdit(tmp, peL + glyphInset, peT + glyphInset,
|
||
|
|
peGlyph, ColorToARGB(g_ai_titleText, 230));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Render action row (regen + export) after last AI message
|
||
|
|
if(m == lastAiIdx)
|
||
|
|
{
|
||
|
|
curY += 2;
|
||
|
|
const int icSz = actionRowH;
|
||
|
|
const int glyphSz = 14;
|
||
|
|
const int icGap = 6;
|
||
|
|
|
||
|
|
//--- Stamp response time text
|
||
|
|
int rtX = contentL;
|
||
|
|
int rtY = curY + (icSz - AiTextHeightCached("Arial", AI_FONT_TIMESTAMP)) / 2;
|
||
|
|
int rtW = 0;
|
||
|
|
if(StringLen(msgRespTime[m]) > 0)
|
||
|
|
{
|
||
|
|
rtW = AiTextWidth(msgRespTime[m], "Arial", AI_FONT_TIMESTAMP);
|
||
|
|
AiStampTextAA(tmp, rtX, rtY, msgRespTime[m],
|
||
|
|
"Arial", AI_FONT_TIMESTAMP, g_ai_responseTimeNote);
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Compute regen and export button rects
|
||
|
|
int afterText = (rtW > 0) ? (rtX + rtW + 8) : contentL;
|
||
|
|
int regL = afterText;
|
||
|
|
int regT = curY;
|
||
|
|
int regR = regL + icSz;
|
||
|
|
int regB = regT + icSz;
|
||
|
|
int expL = regR + icGap;
|
||
|
|
int expT = curY;
|
||
|
|
int expR = expL + icSz;
|
||
|
|
int expB = expT + icSz;
|
||
|
|
|
||
|
|
//--- Cache rects for hit-testing
|
||
|
|
g_ai_regenL = regL; g_ai_regenT = regT; g_ai_regenR = regR; g_ai_regenB = regB;
|
||
|
|
g_ai_exportL = expL; g_ai_exportT = expT; g_ai_exportR = expR; g_ai_exportB = expB;
|
||
|
|
|
||
|
|
//--- Pick fill colors by hover state
|
||
|
|
color regBg = (g_ai_hover == AI_HOV_REGEN)
|
||
|
|
? AiDarkenColor(g_ai_aiBubbleBg, 0.92) : g_ai_aiBubbleBg;
|
||
|
|
color expBg = (g_ai_hover == AI_HOV_EXPORT)
|
||
|
|
? AiDarkenColor(g_ai_aiBubbleBg, 0.92) : g_ai_aiBubbleBg;
|
||
|
|
|
||
|
|
//--- Fill and border each button
|
||
|
|
g_ai_prim.FillRoundRectSharp(tmp, regL, regT, icSz, icSz, 4,
|
||
|
|
ColorToARGB(regBg, 255));
|
||
|
|
g_ai_prim.DrawRoundRectBorderSharp(tmp, regL, regT, icSz, icSz, 4, 1,
|
||
|
|
ColorToARGB(AiBorderForBg(regBg), 255));
|
||
|
|
g_ai_prim.FillRoundRectSharp(tmp, expL, expT, icSz, icSz, 4,
|
||
|
|
ColorToARGB(expBg, 255));
|
||
|
|
g_ai_prim.DrawRoundRectBorderSharp(tmp, expL, expT, icSz, icSz, 4, 1,
|
||
|
|
ColorToARGB(AiBorderForBg(expBg), 255));
|
||
|
|
|
||
|
|
//--- Draw button icons
|
||
|
|
const int glyphInsetReg = (icSz - glyphSz) / 2;
|
||
|
|
AiDrawIconRegen (tmp, regL + glyphInsetReg, regT + glyphInsetReg,
|
||
|
|
glyphSz, ColorToARGB(g_ai_titleText, 230));
|
||
|
|
const int glyphInsetExp = (icSz - glyphSz) / 2;
|
||
|
|
AiDrawIconExport(tmp, expL + glyphInsetExp, expT + glyphInsetExp,
|
||
|
|
glyphSz, ColorToARGB(g_ai_titleText, 230));
|
||
|
|
curY += icSz;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Add gap before next message
|
||
|
|
curY += msgGap;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Copy clipped chat content back to main canvas
|
||
|
|
tmp.CopyRectToCanvas(g_ai_canvMain, seedL, seedT, seedR, seedB);
|
||
|
|
|
||
|
|
//--- Undefine scratch alias
|
||
|
|
#undef tmp
|
||
|
|
|
||
|
|
//--- Draw chat scrollbar when content overflows
|
||
|
|
if(AiScrollVisible(g_ai_chatScroll))
|
||
|
|
{
|
||
|
|
g_ai_chatScroll.trackL = contentR - sbW - sbRightPad;
|
||
|
|
g_ai_chatScroll.trackT = contentT;
|
||
|
|
g_ai_chatScroll.trackR = g_ai_chatScroll.trackL + sbW;
|
||
|
|
g_ai_chatScroll.trackB = contentB;
|
||
|
|
AiScrollDraw(g_ai_canvMain, g_ai_chatScroll, g_ai_prim);
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Render scroll-to-bottom FAB
|
||
|
|
{
|
||
|
|
//--- Compute FAB geometry
|
||
|
|
const int fabSize = 30;
|
||
|
|
const int fabRadius = fabSize / 2;
|
||
|
|
const int fabIconSize = 16;
|
||
|
|
const int fabMargin = 8;
|
||
|
|
|
||
|
|
//--- Position centered horizontally near bottom
|
||
|
|
const int fabCenterX = (contentL + contentR) / 2;
|
||
|
|
const int fabL = fabCenterX - fabSize / 2;
|
||
|
|
const int fabB = contentB - fabMargin;
|
||
|
|
const int fabT = fabB - fabSize;
|
||
|
|
const int fabR = fabL + fabSize;
|
||
|
|
|
||
|
|
//--- Determine visibility from scroll distance
|
||
|
|
const int distFromBottom = AiScrollMax(g_ai_chatScroll) - g_ai_chatScroll.scrollPx;
|
||
|
|
const int viewportH = contentB - contentT;
|
||
|
|
const bool farFromBottom = (distFromBottom > viewportH / 2);
|
||
|
|
const bool shouldShow = AiScrollVisible(g_ai_chatScroll) && farFromBottom;
|
||
|
|
g_ai_scrollFabVisible = shouldShow;
|
||
|
|
g_ai_scrollFabL = fabL;
|
||
|
|
g_ai_scrollFabT = fabT;
|
||
|
|
g_ai_scrollFabR = fabR;
|
||
|
|
g_ai_scrollFabB = fabB;
|
||
|
|
|
||
|
|
//--- Render FAB when visible
|
||
|
|
if(shouldShow)
|
||
|
|
{
|
||
|
|
//--- Pick alpha and bg based on hover
|
||
|
|
const bool fabHov = (g_ai_hover == AI_HOV_SCROLL_FAB);
|
||
|
|
const uchar fabAlpha = fabHov ? (uchar)255 : (uchar)191;
|
||
|
|
color fabBg = fabHov ? AiDarkenColor(g_ai_panelAlt, 0.85)
|
||
|
|
: g_ai_panelAlt;
|
||
|
|
|
||
|
|
//--- Fill FAB background and border
|
||
|
|
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, fabL, fabT,
|
||
|
|
fabSize, fabSize, fabRadius,
|
||
|
|
ColorToARGB(fabBg, fabAlpha));
|
||
|
|
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, fabL, fabT,
|
||
|
|
fabSize, fabSize, fabRadius, 1,
|
||
|
|
ColorToARGB(g_ai_borderAccent, fabAlpha));
|
||
|
|
|
||
|
|
//--- Draw centered arrow icon
|
||
|
|
const int glyphInset = (fabSize - fabIconSize) / 2;
|
||
|
|
const uchar arrowAlpha = (uchar)((230 * fabAlpha) / 255);
|
||
|
|
AiDrawIconArrowDown(g_ai_canvMain, fabL + glyphInset, fabT + glyphInset,
|
||
|
|
fabIconSize, ColorToARGB(g_ai_titleText, arrowAlpha));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Render prompt pane and embedded editor |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_RenderPromptPane()
|
||
|
|
{
|
||
|
|
//--- Get pane rect
|
||
|
|
int rL, rT, rR, rB;
|
||
|
|
Ai_GetPromptPaneRect(rL, rT, rR, rB);
|
||
|
|
|
||
|
|
//--- Fill background and draw border
|
||
|
|
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, rL, rT, rR - rL, rB - rT, 6,
|
||
|
|
ColorToARGB(g_ai_promptBg, 255));
|
||
|
|
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, rL, rT, rR - rL, rB - rT, 6, 1,
|
||
|
|
ColorToARGB(g_ai_border, 255));
|
||
|
|
|
||
|
|
//--- Render editor inside padded inner rect
|
||
|
|
g_ai_editor.Render(g_ai_canvMain, rL + 4, rT + 4, rR - 4, rB - 4, g_ai_prim);
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Render prompt pane to overlay canvas (fast-path) |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_RenderPromptPaneOverlay(bool blank = false)
|
||
|
|
{
|
||
|
|
//--- Bail when overlay does not exist
|
||
|
|
if(!g_ai_canvPromptExists) return;
|
||
|
|
|
||
|
|
//--- Blank mode - clear overlay so popups underneath show through
|
||
|
|
if(blank)
|
||
|
|
{
|
||
|
|
g_ai_canvPrompt.Erase(0x00000000);
|
||
|
|
g_ai_canvPrompt.Update();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Get pane rect and dimensions
|
||
|
|
int rL, rT, rR, rB;
|
||
|
|
Ai_GetPromptPaneRect(rL, rT, rR, rB);
|
||
|
|
const int paneW = rR - rL;
|
||
|
|
const int paneH = rB - rT;
|
||
|
|
|
||
|
|
//--- Erase overlay to transparent
|
||
|
|
g_ai_canvPrompt.Erase(0x00000000);
|
||
|
|
|
||
|
|
//--- Render prompt pane in local coords
|
||
|
|
g_ai_prim.FillRoundRectSharp(g_ai_canvPrompt, 0, 0, paneW, paneH, 6,
|
||
|
|
ColorToARGB(g_ai_promptBg, 255));
|
||
|
|
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvPrompt, 0, 0, paneW, paneH, 6, 1,
|
||
|
|
ColorToARGB(g_ai_border, 255));
|
||
|
|
|
||
|
|
//--- Render editor onto overlay
|
||
|
|
g_ai_editor.Render(g_ai_canvPrompt, 4, 4, paneW - 4, paneH - 4, g_ai_prim);
|
||
|
|
|
||
|
|
//--- Push overlay bitmap to chart
|
||
|
|
g_ai_canvPrompt.Update();
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Render footer with split signal button and send button |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_RenderFooter()
|
||
|
|
{
|
||
|
|
//--- Fill footer background
|
||
|
|
g_ai_canvMain.FillRectangle(Ai_MainContentX(), Ai_FooterT(),
|
||
|
|
Ai_DashboardW() - 1, Ai_FooterB() - 1,
|
||
|
|
ColorToARGB(g_ai_bg, 255));
|
||
|
|
|
||
|
|
//--- Get signal button rect and split position
|
||
|
|
int sL, sT, sR, sB;
|
||
|
|
Ai_GetSignalBtnRect(sL, sT, sR, sB);
|
||
|
|
const int sepX = Ai_SignalSeparatorX();
|
||
|
|
const bool hovAct = (g_ai_hover == AI_HOV_SIGNAL);
|
||
|
|
const bool hovDd = (g_ai_hover == AI_HOV_SIGNAL_DD);
|
||
|
|
|
||
|
|
//--- Setup styling constants
|
||
|
|
const color sigBlue = (color)C'30,100,220';
|
||
|
|
const uchar fillIdle = 76;
|
||
|
|
const uchar fillHov = 128;
|
||
|
|
|
||
|
|
//--- Fill whole pill at base or action-hover alpha
|
||
|
|
{
|
||
|
|
const uchar a = hovAct ? fillHov : fillIdle;
|
||
|
|
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, sL, sT, sR - sL, sB - sT, 4,
|
||
|
|
ColorToARGB(sigBlue, a));
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Add hover tint to chevron half when hovered
|
||
|
|
if(hovDd && !hovAct)
|
||
|
|
{
|
||
|
|
const uchar extra = (uchar)(fillHov - fillIdle);
|
||
|
|
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, sepX, sT, sR - sepX, sB - sT, 4,
|
||
|
|
ColorToARGB(sigBlue, extra));
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Lift chevron half back to idle when action half is hovered
|
||
|
|
if(hovAct)
|
||
|
|
{
|
||
|
|
const uchar lift = (uchar)(fillHov - fillIdle);
|
||
|
|
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, sepX, sT, sR - sepX, sB - sT, 4,
|
||
|
|
ColorToARGB(g_ai_bg, lift));
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Draw outer pill border
|
||
|
|
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, sL, sT, sR - sL, sB - sT, 4, 1,
|
||
|
|
ColorToARGB(sigBlue, 255));
|
||
|
|
|
||
|
|
//--- Draw vertical separator between halves
|
||
|
|
const uint sepArgb = ColorToARGB(sigBlue, 255);
|
||
|
|
for(int yy = sT + 1; yy <= sB - 2; yy++)
|
||
|
|
AiBlendPixel(g_ai_canvMain, sepX, yy, sepArgb);
|
||
|
|
|
||
|
|
//--- Render action half content (icon + label)
|
||
|
|
const int ddi = MathMax(0, MathMin(g_ai_footerDropdownSelectedIdx, AI_FOOTER_DD_COUNT - 1));
|
||
|
|
const string ddLbl = AI_FOOTER_DD_LABELS[ddi];
|
||
|
|
const int contentH = sB - sT;
|
||
|
|
const int aIcSz = 18;
|
||
|
|
const int aIcX = sL + 8;
|
||
|
|
const int aIcY = sT + (contentH - aIcSz) / 2;
|
||
|
|
const uint aIcArgb = ColorToARGB(sigBlue, 255);
|
||
|
|
|
||
|
|
//--- Draw selected action's icon
|
||
|
|
const int aIconId = AI_FOOTER_DD_ICONS[ddi];
|
||
|
|
if(aIconId == 0) AiDrawIconChart (g_ai_canvMain, aIcX, aIcY, aIcSz, aIcArgb);
|
||
|
|
else if(aIconId == 1) AiDrawIconTwinBars (g_ai_canvMain, aIcX, aIcY, aIcSz, aIcArgb);
|
||
|
|
else if(aIconId == 2) AiDrawIconLightning(g_ai_canvMain, aIcX, aIcY, aIcSz, aIcArgb);
|
||
|
|
else if(aIconId == 3) AiDrawIconDay (g_ai_canvMain, aIcX, aIcY, aIcSz, aIcArgb);
|
||
|
|
else if(aIconId == 4) AiDrawIconTrend (g_ai_canvMain, aIcX, aIcY, aIcSz, aIcArgb);
|
||
|
|
else if(aIconId == 5) AiDrawIconLevel (g_ai_canvMain, aIcX, aIcY, aIcSz, aIcArgb);
|
||
|
|
else if(aIconId == 6) AiDrawIconClose (g_ai_canvMain, aIcX, aIcY, aIcSz, aIcArgb);
|
||
|
|
|
||
|
|
//--- Stamp action label text
|
||
|
|
const int sLH = AiTextHeight("Arial Bold", 9);
|
||
|
|
AiStampTextAA(g_ai_canvMain, aIcX + aIcSz + 8, sT + (contentH - sLH) / 2,
|
||
|
|
ddLbl, "Arial Bold", 9, sigBlue);
|
||
|
|
|
||
|
|
//--- Draw chevron in chevron half
|
||
|
|
const int chCx = sepX + (sR - sepX) / 2;
|
||
|
|
const int chCy = sT + contentH / 2;
|
||
|
|
const uint chevArgb = ColorToARGB(sigBlue, 255);
|
||
|
|
AiDrawChevron(g_ai_canvMain, chCx, chCy, g_ai_showFooterDropdown, chevArgb);
|
||
|
|
|
||
|
|
//--- Get send button rect and disabled state
|
||
|
|
int xL, xT, xR, xB;
|
||
|
|
Ai_GetSendBtnRect(xL, xT, xR, xB);
|
||
|
|
const bool sendDisabled = Ai_PromptIsEmpty();
|
||
|
|
|
||
|
|
//--- Track last-rendered send disabled state for fast-path tracking
|
||
|
|
g_ai_lastRenderedSendDisabled = sendDisabled;
|
||
|
|
|
||
|
|
//--- Pick send button colors based on disabled and hover state
|
||
|
|
color xBg;
|
||
|
|
uint xIconArgb;
|
||
|
|
if(sendDisabled)
|
||
|
|
{
|
||
|
|
xBg = g_ai_buttonDisabledBg;
|
||
|
|
xIconArgb = ColorToARGB(g_ai_buttonDisabledIcon, 255);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
xBg = (g_ai_hover == AI_HOV_SEND) ? g_ai_buttonSendBgHover : g_ai_buttonSendBg;
|
||
|
|
xIconArgb = ColorToARGB(clrWhite, 255);
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Fill and border send button
|
||
|
|
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, xL, xT, xR - xL, xB - xT, 6,
|
||
|
|
ColorToARGB(xBg, 255));
|
||
|
|
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, xL, xT, xR - xL, xB - xT, 6, 1,
|
||
|
|
ColorToARGB(AiBorderForBg(xBg), 255));
|
||
|
|
|
||
|
|
//--- Draw send icon
|
||
|
|
const int sendSz = 18;
|
||
|
|
const int sendIcX = xL + (xR - xL - sendSz) / 2;
|
||
|
|
const int sendIcY = xT + (xB - xT - sendSz) / 2;
|
||
|
|
AiDrawIconSend(g_ai_canvMain, sendIcX, sendIcY, sendSz, xIconArgb);
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Popup State |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
int g_ai_popupL = 0, g_ai_popupT = 0, g_ai_popupR = 0, g_ai_popupB = 0; // Popup outer rect
|
||
|
|
int g_ai_popupHovRow = -1; // Hovered row index
|
||
|
|
bool g_ai_popupHovDel = false; // Hovered delete zone flag
|
||
|
|
int g_ai_popupRowL[]; // Row left edges
|
||
|
|
int g_ai_popupRowT[]; // Row top edges
|
||
|
|
int g_ai_popupRowR[]; // Row right edges
|
||
|
|
int g_ai_popupRowB[]; // Row bottom edges
|
||
|
|
int g_ai_popupRowChatIdx[]; // Mapping from row to chat index
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Get popup rectangle for given anchor button |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_GetPopupRect(int anchorIdx, int &outL, int &outT, int &outR, int &outB)
|
||
|
|
{
|
||
|
|
//--- Search popup spans full main area
|
||
|
|
if(anchorIdx == 0)
|
||
|
|
{
|
||
|
|
g_ai_searchQuery = g_ai_searchEditor.GetText();
|
||
|
|
outL = Ai_MainContentX();
|
||
|
|
outT = AI_HEADER_H + 1;
|
||
|
|
outR = Ai_DashboardW() - 1;
|
||
|
|
outB = Ai_DashboardH() - 1;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- History popup anchored to sidebar button
|
||
|
|
int bL, bT, bR, bB;
|
||
|
|
Ai_GetSidebarBtnRect(anchorIdx, bL, bT, bR, bB);
|
||
|
|
const int popupW = 280;
|
||
|
|
const int rowH = 44;
|
||
|
|
const int titleArea = 28;
|
||
|
|
const int bottomPad = 8;
|
||
|
|
|
||
|
|
//--- Cap visible rows at 8
|
||
|
|
int nRows = ArraySize(g_ai_chats);
|
||
|
|
if(nRows > 8) nRows = 8;
|
||
|
|
if(nRows < 1) nRows = 1;
|
||
|
|
const int popupH = titleArea + nRows * rowH + bottomPad;
|
||
|
|
|
||
|
|
//--- Position popup with bounds clamping
|
||
|
|
outL = bR - 3;
|
||
|
|
outT = bT;
|
||
|
|
const int dH = Ai_DashboardH();
|
||
|
|
if(outT + popupH > dH - 8) outT = dH - 8 - popupH;
|
||
|
|
if(outT < AI_HEADER_H + 4) outT = AI_HEADER_H + 4;
|
||
|
|
outR = outL + popupW;
|
||
|
|
outB = outT + popupH;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Extract first user prompt snippet from chat history |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
string Ai_FirstPromptSnippet(const string history)
|
||
|
|
{
|
||
|
|
//--- Bail on empty input
|
||
|
|
const int lenH = StringLen(history);
|
||
|
|
if(lenH == 0) return "";
|
||
|
|
|
||
|
|
//--- Locate first content marker
|
||
|
|
int p = StringFind(history, "You: ");
|
||
|
|
int prefixLen = 5;
|
||
|
|
if(p < 0)
|
||
|
|
{
|
||
|
|
p = StringFind(history, "AI: ");
|
||
|
|
prefixLen = 4;
|
||
|
|
if(p < 0) return "";
|
||
|
|
}
|
||
|
|
const int contentStart = p + prefixLen;
|
||
|
|
|
||
|
|
//--- Walk lines collecting prompt content until terminator
|
||
|
|
string tail = StringSubstr(history, contentStart);
|
||
|
|
string lines[];
|
||
|
|
int n = StringSplit(tail, '\n', lines);
|
||
|
|
string out = "";
|
||
|
|
for(int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
string line = lines[i];
|
||
|
|
StringTrimLeft(line);
|
||
|
|
StringTrimRight(line);
|
||
|
|
|
||
|
|
//--- Stop on blank, timestamp, or block boundary
|
||
|
|
if(StringLen(line) == 0) break;
|
||
|
|
if(AiIsTimestamp(line)) break;
|
||
|
|
if(StringFind(line, "You: ") == 0) break;
|
||
|
|
if(StringFind(line, "AI: ") == 0) break;
|
||
|
|
|
||
|
|
//--- Append line with separator
|
||
|
|
if(StringLen(out) > 0) out += " | ";
|
||
|
|
out += line;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Trim and return
|
||
|
|
StringTrimLeft(out);
|
||
|
|
StringTrimRight(out);
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Search Box Rect State |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
int g_ai_popupSearchL = 0, g_ai_popupSearchT = 0; // Search box top-left
|
||
|
|
int g_ai_popupSearchR = 0, g_ai_popupSearchB = 0; // Search box bottom-right
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Render search or history popup |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_RenderPopup(int anchorIdx)
|
||
|
|
{
|
||
|
|
//--- Get popup rect and store globally
|
||
|
|
int pL, pT, pR, pB;
|
||
|
|
Ai_GetPopupRect(anchorIdx, pL, pT, pR, pB);
|
||
|
|
g_ai_popupL = pL; g_ai_popupT = pT;
|
||
|
|
g_ai_popupR = pR; g_ai_popupB = pB;
|
||
|
|
const int popupW = pR - pL;
|
||
|
|
const int popupH = pB - pT;
|
||
|
|
|
||
|
|
//--- Fill popup background (square for search, rounded for history)
|
||
|
|
if(anchorIdx == 0)
|
||
|
|
{
|
||
|
|
g_ai_canvMain.FillRectangle(pL, pT, pR - 1, pB - 1,
|
||
|
|
ColorToARGB(g_ai_panelAlt, 255));
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, pL, pT, popupW, popupH, 8,
|
||
|
|
ColorToARGB(g_ai_panelAlt, 255));
|
||
|
|
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, pL, pT, popupW, popupH, 8, 1,
|
||
|
|
ColorToARGB(g_ai_borderAccent, 255));
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Stamp popup title
|
||
|
|
const string title = (anchorIdx == 0) ? "Search Chats" : "Recent Chats";
|
||
|
|
AiStampTextAA(g_ai_canvMain, pL + 16, pT + 12, title,
|
||
|
|
"Arial Bold", AI_FONT_LABEL, g_ai_titleText);
|
||
|
|
|
||
|
|
//--- Compute layout heights
|
||
|
|
const int rowH = 44;
|
||
|
|
const int titleArea = (anchorIdx == 0) ? 36 : 28;
|
||
|
|
const int searchArea = (anchorIdx == 0) ? 38 : 0;
|
||
|
|
|
||
|
|
//--- Render search input box for search popup
|
||
|
|
if(anchorIdx == 0)
|
||
|
|
{
|
||
|
|
//--- Compute search box rect
|
||
|
|
int siL = pL + 16;
|
||
|
|
int siT = pT + titleArea;
|
||
|
|
int siR = pR - 16;
|
||
|
|
int siB = siT + searchArea - 6;
|
||
|
|
g_ai_popupSearchL = siL; g_ai_popupSearchT = siT;
|
||
|
|
g_ai_popupSearchR = siR; g_ai_popupSearchB = siB;
|
||
|
|
|
||
|
|
//--- Fill search box and draw border
|
||
|
|
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, siL, siT, siR - siL, siB - siT, 4,
|
||
|
|
ColorToARGB(g_ai_bg, 255));
|
||
|
|
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, siL, siT, siR - siL, siB - siT, 4, 1,
|
||
|
|
ColorToARGB(g_ai_borderAccent, 255));
|
||
|
|
|
||
|
|
//--- Vertically center the search editor
|
||
|
|
const int boxH = siB - siT;
|
||
|
|
const int lineH = AiTextHeightCached("Arial", AI_FONT_BODY) + 2;
|
||
|
|
const int edL = siL + 8;
|
||
|
|
const int edT = siT + (boxH - lineH) / 2 - g_ai_searchEditor.padY;
|
||
|
|
const int edR = siR - 4;
|
||
|
|
const int edB = edT + lineH + 2 * g_ai_searchEditor.padY;
|
||
|
|
g_ai_searchEditor.Render(g_ai_canvMain, edL, edT, edR, edB, g_ai_prim);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
//--- Reset search box rect when not search popup
|
||
|
|
g_ai_popupSearchL = g_ai_popupSearchT = g_ai_popupSearchR = g_ai_popupSearchB = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Build filtered chat list
|
||
|
|
int filteredIdx[];
|
||
|
|
const int total = ArraySize(g_ai_chats);
|
||
|
|
string queryLower = "";
|
||
|
|
if(anchorIdx == 0 && StringLen(g_ai_searchQuery) > 0)
|
||
|
|
{
|
||
|
|
queryLower = g_ai_searchQuery;
|
||
|
|
StringToLower(queryLower);
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Filter and lowercase cache (static across renders)
|
||
|
|
#define AI_SEARCH_HISTORY_CAP 4096
|
||
|
|
static string s_cacheLowerTitles[];
|
||
|
|
static string s_cacheLowerHistories[];
|
||
|
|
static int s_cacheChatCount = -1;
|
||
|
|
static int s_cacheTitleLenSum = -1;
|
||
|
|
static int s_cacheHistoryLenSum = -1;
|
||
|
|
static string s_cacheFilterQuery = "\x01";
|
||
|
|
static int s_cacheFilterIdx[];
|
||
|
|
|
||
|
|
//--- Compute current chats fingerprint
|
||
|
|
int curTitleLenSum = 0;
|
||
|
|
int curHistoryLenSum = 0;
|
||
|
|
for(int fi = 0; fi < total; fi++)
|
||
|
|
{
|
||
|
|
curTitleLenSum += StringLen(g_ai_chats[fi].title);
|
||
|
|
const int hLen = StringLen(g_ai_chats[fi].history);
|
||
|
|
curHistoryLenSum += (hLen < AI_SEARCH_HISTORY_CAP) ? hLen : AI_SEARCH_HISTORY_CAP;
|
||
|
|
}
|
||
|
|
const bool chatsCacheValid =
|
||
|
|
(s_cacheChatCount == total)
|
||
|
|
&& (s_cacheTitleLenSum == curTitleLenSum)
|
||
|
|
&& (s_cacheHistoryLenSum == curHistoryLenSum);
|
||
|
|
|
||
|
|
//--- Rebuild lowercase cache when chats change
|
||
|
|
if(!chatsCacheValid)
|
||
|
|
{
|
||
|
|
ArrayResize(s_cacheLowerTitles, total);
|
||
|
|
ArrayResize(s_cacheLowerHistories, total);
|
||
|
|
for(int li = 0; li < total; li++)
|
||
|
|
{
|
||
|
|
//--- Lowercase title
|
||
|
|
string tL = g_ai_chats[li].title; StringToLower(tL);
|
||
|
|
|
||
|
|
//--- Lowercase capped history head
|
||
|
|
string hRaw = g_ai_chats[li].history;
|
||
|
|
if(StringLen(hRaw) > AI_SEARCH_HISTORY_CAP)
|
||
|
|
hRaw = StringSubstr(hRaw, 0, AI_SEARCH_HISTORY_CAP);
|
||
|
|
StringToLower(hRaw);
|
||
|
|
|
||
|
|
//--- Store in cache
|
||
|
|
s_cacheLowerTitles[li] = tL;
|
||
|
|
s_cacheLowerHistories[li] = hRaw;
|
||
|
|
}
|
||
|
|
s_cacheChatCount = total;
|
||
|
|
s_cacheTitleLenSum = curTitleLenSum;
|
||
|
|
s_cacheHistoryLenSum = curHistoryLenSum;
|
||
|
|
|
||
|
|
//--- Invalidate filter cache too
|
||
|
|
s_cacheFilterQuery = "\x01";
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Use filter cache when query and chats are unchanged
|
||
|
|
if(anchorIdx == 0
|
||
|
|
&& chatsCacheValid
|
||
|
|
&& s_cacheFilterQuery == g_ai_searchQuery)
|
||
|
|
{
|
||
|
|
//--- Reuse cached filter result
|
||
|
|
const int cn = ArraySize(s_cacheFilterIdx);
|
||
|
|
ArrayResize(filteredIdx, cn);
|
||
|
|
for(int ci = 0; ci < cn; ci++) filteredIdx[ci] = s_cacheFilterIdx[ci];
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
//--- Filter chats with title-first short-circuit
|
||
|
|
for(int i = total - 1; i >= 0; i--)
|
||
|
|
{
|
||
|
|
if(StringLen(queryLower) > 0)
|
||
|
|
{
|
||
|
|
if(StringFind(s_cacheLowerTitles[i], queryLower) >= 0)
|
||
|
|
{
|
||
|
|
//--- Title hit - include without scanning history
|
||
|
|
}
|
||
|
|
else if(StringFind(s_cacheLowerHistories[i], queryLower) < 0)
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
//--- Append index to filtered list
|
||
|
|
int sz = ArraySize(filteredIdx);
|
||
|
|
ArrayResize(filteredIdx, sz + 1);
|
||
|
|
filteredIdx[sz] = i;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Snapshot to filter cache
|
||
|
|
if(anchorIdx == 0)
|
||
|
|
{
|
||
|
|
const int sn = ArraySize(filteredIdx);
|
||
|
|
ArrayResize(s_cacheFilterIdx, sn);
|
||
|
|
for(int si = 0; si < sn; si++) s_cacheFilterIdx[si] = filteredIdx[si];
|
||
|
|
s_cacheFilterQuery = g_ai_searchQuery;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Compute row content area bounds
|
||
|
|
const int rowsTop = pT + titleArea + searchArea;
|
||
|
|
const int rowsBot = pB - 8;
|
||
|
|
const int viewportH = MathMax(0, rowsBot - rowsTop);
|
||
|
|
const int totalFiltered = ArraySize(filteredIdx);
|
||
|
|
|
||
|
|
//--- Determine layout based on popup type
|
||
|
|
bool useScrollbar = false;
|
||
|
|
int visStart = 0;
|
||
|
|
int visCount = 0;
|
||
|
|
const int sbW = 4;
|
||
|
|
|
||
|
|
//--- Search popup uses virtualized list with scrollbar
|
||
|
|
if(anchorIdx == 0)
|
||
|
|
{
|
||
|
|
const int contentH = totalFiltered * rowH;
|
||
|
|
g_ai_searchScroll.totalH = contentH;
|
||
|
|
g_ai_searchScroll.viewportH = viewportH;
|
||
|
|
if(AiScrollVisible(g_ai_searchScroll)) useScrollbar = true;
|
||
|
|
AiScrollClamp(g_ai_searchScroll);
|
||
|
|
|
||
|
|
//--- Compute first partially visible row and count
|
||
|
|
visStart = MathMax(0, g_ai_searchScroll.scrollPx / rowH);
|
||
|
|
const int startScreenY = rowsTop + visStart * rowH - g_ai_searchScroll.scrollPx;
|
||
|
|
const int yRoom = rowsBot - startScreenY;
|
||
|
|
visCount = MathMin(totalFiltered - visStart, (yRoom + rowH - 1) / rowH + 1);
|
||
|
|
if(visCount < 0) visCount = 0;
|
||
|
|
if(visStart + visCount > totalFiltered) visCount = totalFiltered - visStart;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
//--- History popup caps at 8 rows
|
||
|
|
visStart = 0;
|
||
|
|
visCount = MathMin(totalFiltered, 8);
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Allocate row hit-test arrays
|
||
|
|
ArrayResize(g_ai_popupRowL, visCount);
|
||
|
|
ArrayResize(g_ai_popupRowT, visCount);
|
||
|
|
ArrayResize(g_ai_popupRowR, visCount);
|
||
|
|
ArrayResize(g_ai_popupRowB, visCount);
|
||
|
|
ArrayResize(g_ai_popupRowChatIdx, visCount);
|
||
|
|
|
||
|
|
//--- Prepare clip canvas for search popup virtualization
|
||
|
|
const bool useClipCanvas = (anchorIdx == 0 && visCount > 0);
|
||
|
|
if(useClipCanvas)
|
||
|
|
{
|
||
|
|
//--- Ensure search scratch canvas matches main size
|
||
|
|
const int needW = g_ai_canvMain.Width();
|
||
|
|
const int needH = g_ai_canvMain.Height();
|
||
|
|
if(!g_ai_canvSearchTmpReady
|
||
|
|
|| g_ai_canvSearchTmpW < needW
|
||
|
|
|| g_ai_canvSearchTmpH < needH)
|
||
|
|
{
|
||
|
|
if(g_ai_canvSearchTmpReady) g_ai_canvSearchTmp.Destroy();
|
||
|
|
const int newW = MathMax(needW, g_ai_canvSearchTmpW);
|
||
|
|
const int newH = MathMax(needH, g_ai_canvSearchTmpH);
|
||
|
|
if(g_ai_canvSearchTmp.CreateBitmap("AiSearchPopupTmpPersistent",
|
||
|
|
0, 0, newW, newH,
|
||
|
|
COLOR_FORMAT_ARGB_NORMALIZE))
|
||
|
|
{
|
||
|
|
g_ai_canvSearchTmpW = newW;
|
||
|
|
g_ai_canvSearchTmpH = newH;
|
||
|
|
g_ai_canvSearchTmpReady = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Seed scratch canvas with main pixels for clip blending
|
||
|
|
if(useClipCanvas && g_ai_canvSearchTmpReady)
|
||
|
|
{
|
||
|
|
g_ai_canvSearchTmp.CopyRectFromCanvas(g_ai_canvMain, pL, rowsTop, pR, rowsBot);
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Render each visible row
|
||
|
|
for(int v = 0; v < visCount; v++)
|
||
|
|
{
|
||
|
|
//--- Get chat index for this row
|
||
|
|
const int filterPos = visStart + v;
|
||
|
|
const int chatIdx = filteredIdx[filterPos];
|
||
|
|
|
||
|
|
//--- Compute row coords with scroll offset
|
||
|
|
const int naturalY = rowsTop + filterPos * rowH;
|
||
|
|
const int rT = naturalY - ((anchorIdx == 0) ? g_ai_searchScroll.scrollPx : 0);
|
||
|
|
const int rB = rT + rowH - 2;
|
||
|
|
const int rL = pL + 10;
|
||
|
|
const int rR = pR - 10;
|
||
|
|
|
||
|
|
//--- Clip hit rect to visible row band
|
||
|
|
const int hitT = MathMax(rT, rowsTop);
|
||
|
|
const int hitB = MathMin(rB, rowsBot);
|
||
|
|
g_ai_popupRowL[v] = rL; g_ai_popupRowT[v] = hitT;
|
||
|
|
g_ai_popupRowR[v] = rR; g_ai_popupRowB[v] = hitB;
|
||
|
|
g_ai_popupRowChatIdx[v] = chatIdx;
|
||
|
|
|
||
|
|
//--- Skip painting offscreen rows
|
||
|
|
if(hitB <= hitT) continue;
|
||
|
|
|
||
|
|
//--- Determine hover state and pick fill
|
||
|
|
bool hov = (g_ai_popupHovRow == v);
|
||
|
|
bool hovDel = (hov && g_ai_popupHovDel);
|
||
|
|
color bg = hov ? g_ai_chatItemBgHover : g_ai_chatItemBg;
|
||
|
|
|
||
|
|
//--- Render to clip canvas or main directly
|
||
|
|
if(useClipCanvas && g_ai_canvSearchTmpReady)
|
||
|
|
{
|
||
|
|
//--- Fill row and draw border on scratch
|
||
|
|
g_ai_prim.FillRoundRectSharp(g_ai_canvSearchTmp, rL, rT, rR - rL, rB - rT, 4,
|
||
|
|
ColorToARGB(bg, 255));
|
||
|
|
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvSearchTmp, rL, rT, rR - rL, rB - rT, 4, 1,
|
||
|
|
ColorToARGB(AiBorderForBg(bg), 255));
|
||
|
|
|
||
|
|
//--- Compute available width for text
|
||
|
|
const int hpDeleteIconW = AiTextWidth(AI_GLYPH_DELETE, "Wingdings 2", 14);
|
||
|
|
const int hpAvailW = (rR - rL) - 24 - hpDeleteIconW;
|
||
|
|
|
||
|
|
//--- Stamp title on top line
|
||
|
|
string titleText = Ai_FitTextToWidth(g_ai_chats[chatIdx].title,
|
||
|
|
"Arial", AI_FONT_LABEL, hpAvailW);
|
||
|
|
color titleCol = (g_ai_chats[chatIdx].id == g_ai_currentChatId)
|
||
|
|
? g_ai_chatItemActiveText : g_ai_titleText;
|
||
|
|
AiStampTextAA(g_ai_canvSearchTmp, rL + 8, rT + 6, titleText,
|
||
|
|
"Arial", AI_FONT_LABEL, titleCol);
|
||
|
|
|
||
|
|
//--- Stamp snippet on bottom line
|
||
|
|
string snippetRaw = Ai_FirstPromptSnippet(g_ai_chats[chatIdx].history);
|
||
|
|
string snippetText = Ai_FitTextToWidth(snippetRaw, "Arial", AI_FONT_SNIPPET, hpAvailW);
|
||
|
|
const int snippetY = rT + 6 + AiTextHeightCached("Arial", AI_FONT_LABEL) + 4;
|
||
|
|
AiStampTextAA(g_ai_canvSearchTmp, rL + 8, snippetY, snippetText,
|
||
|
|
"Arial", AI_FONT_SNIPPET, g_ai_subText);
|
||
|
|
|
||
|
|
//--- Stamp delete glyph on hover
|
||
|
|
if(hov)
|
||
|
|
{
|
||
|
|
color xCol = hovDel ? g_ai_chatItemDelHover : g_ai_subText;
|
||
|
|
int xW = AiTextWidth(AI_GLYPH_DELETE, "Wingdings 2", 14);
|
||
|
|
int xH = AiTextHeight("Wingdings 2", 14);
|
||
|
|
AiStampTextAA(g_ai_canvSearchTmp, rR - 8 - xW, rT + ((rB - rT) - xH) / 2,
|
||
|
|
AI_GLYPH_DELETE, "Wingdings 2", 14, xCol);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
//--- Render directly to main canvas (history popup path)
|
||
|
|
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, rL, rT, rR - rL, rB - rT, 4,
|
||
|
|
ColorToARGB(bg, 255));
|
||
|
|
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, rL, rT, rR - rL, rB - rT, 4, 1,
|
||
|
|
ColorToARGB(AiBorderForBg(bg), 255));
|
||
|
|
|
||
|
|
//--- Compute available width for text
|
||
|
|
const int hp2DeleteIconW = AiTextWidth(AI_GLYPH_DELETE, "Wingdings 2", 14);
|
||
|
|
const int hp2AvailW = (rR - rL) - 24 - hp2DeleteIconW;
|
||
|
|
|
||
|
|
//--- Stamp title and snippet
|
||
|
|
string titleText = Ai_FitTextToWidth(g_ai_chats[chatIdx].title,
|
||
|
|
"Arial", AI_FONT_LABEL, hp2AvailW);
|
||
|
|
color titleCol = (g_ai_chats[chatIdx].id == g_ai_currentChatId)
|
||
|
|
? g_ai_chatItemActiveText : g_ai_titleText;
|
||
|
|
AiStampTextAA(g_ai_canvMain, rL + 8, rT + 6, titleText,
|
||
|
|
"Arial", AI_FONT_LABEL, titleCol);
|
||
|
|
string snippetRaw = Ai_FirstPromptSnippet(g_ai_chats[chatIdx].history);
|
||
|
|
string snippetText = Ai_FitTextToWidth(snippetRaw, "Arial", AI_FONT_SNIPPET, hp2AvailW);
|
||
|
|
const int snippetY = rT + 6 + AiTextHeightCached("Arial", AI_FONT_LABEL) + 4;
|
||
|
|
AiStampTextAA(g_ai_canvMain, rL + 8, snippetY, snippetText,
|
||
|
|
"Arial", AI_FONT_SNIPPET, g_ai_subText);
|
||
|
|
|
||
|
|
//--- Stamp delete glyph on hover
|
||
|
|
if(hov)
|
||
|
|
{
|
||
|
|
color xCol = hovDel ? g_ai_chatItemDelHover : g_ai_subText;
|
||
|
|
int xW = AiTextWidth(AI_GLYPH_DELETE, "Wingdings 2", 14);
|
||
|
|
int xH = AiTextHeight("Wingdings 2", 14);
|
||
|
|
AiStampTextAA(g_ai_canvMain, rR - 8 - xW, rT + ((rB - rT) - xH) / 2,
|
||
|
|
AI_GLYPH_DELETE, "Wingdings 2", 14, xCol);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Copy clipped rows back to main canvas
|
||
|
|
if(useClipCanvas && g_ai_canvSearchTmpReady)
|
||
|
|
{
|
||
|
|
g_ai_canvSearchTmp.CopyRectToCanvas(g_ai_canvMain, pL, rowsTop, pR, rowsBot);
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Draw search popup scrollbar when needed
|
||
|
|
if(anchorIdx == 0 && useScrollbar)
|
||
|
|
{
|
||
|
|
g_ai_searchScroll.trackL = pR - 7;
|
||
|
|
g_ai_searchScroll.trackT = rowsTop;
|
||
|
|
g_ai_searchScroll.trackR = g_ai_searchScroll.trackL + sbW;
|
||
|
|
g_ai_searchScroll.trackB = rowsBot;
|
||
|
|
AiScrollDraw(g_ai_canvMain, g_ai_searchScroll, g_ai_prim);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Footer Dropdown State |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
int g_ai_footerDdL = 0, g_ai_footerDdT = 0, g_ai_footerDdR = 0, g_ai_footerDdB = 0; // Dropdown rect
|
||
|
|
int g_ai_footerDdItemL[]; // Item left edges
|
||
|
|
int g_ai_footerDdItemT[]; // Item top edges
|
||
|
|
int g_ai_footerDdItemR[]; // Item right edges
|
||
|
|
int g_ai_footerDdItemB[]; // Item bottom edges
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Render footer dropdown popup |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_RenderFooterDropdown()
|
||
|
|
{
|
||
|
|
//--- Get anchor rect and compute popup geometry
|
||
|
|
int aL, aT, aR, aB;
|
||
|
|
Ai_GetSignalBtnRect(aL, aT, aR, aB);
|
||
|
|
const int itemH = 32;
|
||
|
|
const int padTop = 4;
|
||
|
|
const int padBot = 4;
|
||
|
|
const int popupW = aR - aL;
|
||
|
|
const int popupH = padTop + AI_FOOTER_DD_COUNT * itemH + padBot;
|
||
|
|
|
||
|
|
//--- Decide whether to place above or below anchor
|
||
|
|
const int dH = Ai_DashboardH();
|
||
|
|
bool placeBelow = (aB + 4 + popupH) <= (dH - 6);
|
||
|
|
|
||
|
|
//--- Compute popup rect and store globally
|
||
|
|
int pL = aL;
|
||
|
|
int pT = placeBelow ? (aB + 4) : (aT - 4 - popupH);
|
||
|
|
int pR = pL + popupW;
|
||
|
|
int pB = pT + popupH;
|
||
|
|
g_ai_footerDdL = pL; g_ai_footerDdT = pT;
|
||
|
|
g_ai_footerDdR = pR; g_ai_footerDdB = pB;
|
||
|
|
|
||
|
|
//--- Fill popup body and border
|
||
|
|
const uint bodyArgb = ColorToARGB(g_ai_panelAlt, 200);
|
||
|
|
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, pL, pT, popupW, popupH, 6, bodyArgb);
|
||
|
|
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, pL, pT, popupW, popupH, 6, 1,
|
||
|
|
ColorToARGB(g_ai_borderAccent, 255));
|
||
|
|
|
||
|
|
//--- Allocate item rect arrays
|
||
|
|
ArrayResize(g_ai_footerDdItemL, AI_FOOTER_DD_COUNT);
|
||
|
|
ArrayResize(g_ai_footerDdItemT, AI_FOOTER_DD_COUNT);
|
||
|
|
ArrayResize(g_ai_footerDdItemR, AI_FOOTER_DD_COUNT);
|
||
|
|
ArrayResize(g_ai_footerDdItemB, AI_FOOTER_DD_COUNT);
|
||
|
|
|
||
|
|
//--- Render each dropdown item
|
||
|
|
for(int i = 0; i < AI_FOOTER_DD_COUNT; i++)
|
||
|
|
{
|
||
|
|
//--- Compute and store item rect
|
||
|
|
int iL = pL + 1;
|
||
|
|
int iT = pT + padTop + i * itemH;
|
||
|
|
int iR = pR - 1;
|
||
|
|
int iB = iT + itemH;
|
||
|
|
g_ai_footerDdItemL[i] = iL; g_ai_footerDdItemT[i] = iT;
|
||
|
|
g_ai_footerDdItemR[i] = iR; g_ai_footerDdItemB[i] = iB;
|
||
|
|
|
||
|
|
//--- Determine hover and active state
|
||
|
|
bool itemHov = (g_ai_hover == AI_HOV_FOOTER_DD_ITEM_BASE + i);
|
||
|
|
bool itemActive = (i == g_ai_footerDropdownSelectedIdx);
|
||
|
|
|
||
|
|
//--- Paint hover or active highlight
|
||
|
|
if(itemHov)
|
||
|
|
{
|
||
|
|
const uint hovArgb = ColorToARGB(g_ai_titleText, 30);
|
||
|
|
for(int yy = iT; yy < iB; yy++)
|
||
|
|
for(int xx = iL; xx < iR; xx++)
|
||
|
|
AiBlendPixel(g_ai_canvMain, xx, yy, hovArgb);
|
||
|
|
}
|
||
|
|
else if(itemActive)
|
||
|
|
{
|
||
|
|
const uint actArgb = ColorToARGB(g_ai_titleText, 18);
|
||
|
|
for(int yy = iT; yy < iB; yy++)
|
||
|
|
for(int xx = iL; xx < iR; xx++)
|
||
|
|
AiBlendPixel(g_ai_canvMain, xx, yy, actArgb);
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Draw item icon and label
|
||
|
|
const int iconSz = 18;
|
||
|
|
const int iconX = iL + 8;
|
||
|
|
const int iconY = iT + (itemH - iconSz) / 2;
|
||
|
|
const uint iconArgb = ColorToARGB(g_ai_titleText, 235);
|
||
|
|
|
||
|
|
//--- Pick icon by item id
|
||
|
|
const int iconId = AI_FOOTER_DD_ICONS[i];
|
||
|
|
if(iconId == 0) AiDrawIconChart (g_ai_canvMain, iconX, iconY, iconSz, iconArgb);
|
||
|
|
else if(iconId == 1) AiDrawIconTwinBars (g_ai_canvMain, iconX, iconY, iconSz, iconArgb);
|
||
|
|
else if(iconId == 2) AiDrawIconLightning(g_ai_canvMain, iconX, iconY, iconSz, iconArgb);
|
||
|
|
else if(iconId == 3) AiDrawIconDay (g_ai_canvMain, iconX, iconY, iconSz, iconArgb);
|
||
|
|
else if(iconId == 4) AiDrawIconTrend (g_ai_canvMain, iconX, iconY, iconSz, iconArgb);
|
||
|
|
else if(iconId == 5) AiDrawIconLevel (g_ai_canvMain, iconX, iconY, iconSz, iconArgb);
|
||
|
|
else if(iconId == 6) AiDrawIconClose (g_ai_canvMain, iconX, iconY, iconSz, iconArgb);
|
||
|
|
|
||
|
|
//--- Stamp item label
|
||
|
|
int lY = iT + (itemH - AiTextHeight("Arial Bold", 9)) / 2;
|
||
|
|
AiStampTextAA(g_ai_canvMain, iconX + iconSz + 8, lY,
|
||
|
|
AI_FOOTER_DD_LABELS[i], "Arial Bold", 9, g_ai_titleText);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Set toast notification text and start timer |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_ShowToast(string text, bool isError)
|
||
|
|
{
|
||
|
|
//--- Store toast text and 5-second expiry
|
||
|
|
g_ai_toastText = text;
|
||
|
|
g_ai_toastIsError = isError;
|
||
|
|
g_ai_toastExpiryMs = GetTickCount64() + 5000;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Render toast notification overlay |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_RenderToast()
|
||
|
|
{
|
||
|
|
//--- Bail when no active toast
|
||
|
|
if(StringLen(g_ai_toastText) == 0) return;
|
||
|
|
const ulong now = GetTickCount64();
|
||
|
|
if(now > g_ai_toastExpiryMs) return;
|
||
|
|
|
||
|
|
//--- Setup padding and font
|
||
|
|
const int padX = 16;
|
||
|
|
const int padY = 8;
|
||
|
|
const string toastFont = "Arial Bold";
|
||
|
|
const int toastSize = 10;
|
||
|
|
|
||
|
|
//--- Measure text dimensions
|
||
|
|
const int textW = AiTextWidth(g_ai_toastText, toastFont, toastSize);
|
||
|
|
const int textH = AiTextHeight(toastFont, toastSize);
|
||
|
|
|
||
|
|
//--- Setup progress bar geometry
|
||
|
|
const int barH = 2;
|
||
|
|
const int barGap = 6;
|
||
|
|
const int boxW = textW + 2 * padX;
|
||
|
|
const int boxH = textH + barGap + barH + 2 * padY;
|
||
|
|
|
||
|
|
//--- Center horizontally below header
|
||
|
|
const int mainContentL = Ai_MainContentX();
|
||
|
|
const int mainContentR = Ai_DashboardW();
|
||
|
|
const int boxL = mainContentL + ((mainContentR - mainContentL) - boxW) / 2;
|
||
|
|
const int boxT = AI_HEADER_H + 6;
|
||
|
|
|
||
|
|
//--- Fill toast box
|
||
|
|
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, boxL, boxT, boxW, boxH, 8,
|
||
|
|
ColorToARGB(g_ai_toastBg, 255));
|
||
|
|
|
||
|
|
//--- Draw box border
|
||
|
|
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, boxL, boxT, boxW, boxH, 8, 1,
|
||
|
|
ColorToARGB(g_ai_toastBorder, 255));
|
||
|
|
|
||
|
|
//--- Stamp toast text with success/error color
|
||
|
|
const color textCol = g_ai_toastIsError ? g_ai_toastError : g_ai_toastSuccess;
|
||
|
|
AiStampTextAA(g_ai_canvMain, boxL + padX, boxT + padY,
|
||
|
|
g_ai_toastText, toastFont, toastSize, textCol);
|
||
|
|
|
||
|
|
//--- Compute progress bar fill width from remaining lifetime
|
||
|
|
const int trackL = boxL + padX;
|
||
|
|
const int trackR = boxL + boxW - padX;
|
||
|
|
const int trackW = trackR - trackL;
|
||
|
|
const int trackY = boxT + padY + textH + barGap;
|
||
|
|
const ulong totalLifeMs = 5000;
|
||
|
|
const long remaining = (long)g_ai_toastExpiryMs - (long)now;
|
||
|
|
double ratio = (double)remaining / (double)totalLifeMs;
|
||
|
|
if(ratio < 0.0) ratio = 0.0;
|
||
|
|
if(ratio > 1.0) ratio = 1.0;
|
||
|
|
const int fillW = (int)(trackW * ratio);
|
||
|
|
const int fillL = trackL + (trackW - fillW) / 2;
|
||
|
|
|
||
|
|
//--- Draw track background
|
||
|
|
g_ai_canvMain.FillRectangle(trackL, trackY, trackR - 1, trackY + barH - 1,
|
||
|
|
ColorToARGB(g_ai_toastBorder, 255));
|
||
|
|
|
||
|
|
//--- Draw shrinking fill segment
|
||
|
|
if(fillW > 0)
|
||
|
|
{
|
||
|
|
g_ai_canvMain.FillRectangle(fillL, trackY, fillL + fillW - 1, trackY + barH - 1,
|
||
|
|
ColorToARGB(textCol, 255));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Render entire dashboard |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void Ai_RenderAll()
|
||
|
|
{
|
||
|
|
//--- Bail when main canvas does not exist
|
||
|
|
if(!g_ai_canvMainExists) return;
|
||
|
|
|
||
|
|
//--- Get dashboard dimensions
|
||
|
|
const int dW = Ai_DashboardW();
|
||
|
|
const int dH = Ai_DashboardH();
|
||
|
|
|
||
|
|
//--- Erase canvas and fill rounded background
|
||
|
|
g_ai_canvMain.Erase(0x00000000);
|
||
|
|
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, 0, 0, dW, dH, 10,
|
||
|
|
ColorToARGB(g_ai_bg, 255));
|
||
|
|
|
||
|
|
//--- Render sidebar and header
|
||
|
|
Ai_RenderSidebar();
|
||
|
|
Ai_RenderHeader();
|
||
|
|
|
||
|
|
//--- Draw header divider line
|
||
|
|
g_ai_canvMain.LineHorizontal(0, dW - 1, AI_HEADER_H,
|
||
|
|
ColorToARGB(g_ai_border, 255));
|
||
|
|
|
||
|
|
//--- Skip main panes when search popup covers them
|
||
|
|
if(!g_ai_showSearch)
|
||
|
|
{
|
||
|
|
Ai_RenderChatPane();
|
||
|
|
Ai_RenderPromptPane();
|
||
|
|
Ai_RenderFooter();
|
||
|
|
|
||
|
|
//--- Draw footer divider line
|
||
|
|
g_ai_canvMain.LineHorizontal(Ai_MainContentX(), dW - 1, Ai_FooterT() - 1,
|
||
|
|
ColorToARGB(g_ai_border, 255));
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Render search popup overlay
|
||
|
|
if(g_ai_showSearch) Ai_RenderPopup(0);
|
||
|
|
|
||
|
|
//--- Corner-clear pixel mask cache
|
||
|
|
const int crClear = 10;
|
||
|
|
static int s_cornerClearX[];
|
||
|
|
static int s_cornerClearY[];
|
||
|
|
static int s_cornerClearW = -1;
|
||
|
|
static int s_cornerClearH = -1;
|
||
|
|
|
||
|
|
//--- Rebuild mask when dashboard size changes
|
||
|
|
if(s_cornerClearW != dW || s_cornerClearH != dH)
|
||
|
|
{
|
||
|
|
const double rdC = (double)crClear;
|
||
|
|
ArrayResize(s_cornerClearX, 0);
|
||
|
|
ArrayResize(s_cornerClearY, 0);
|
||
|
|
int n = 0;
|
||
|
|
|
||
|
|
//--- Walk all four corners and collect outside-radius pixels
|
||
|
|
for(int corner = 0; corner < 4; corner++)
|
||
|
|
{
|
||
|
|
int cxC = (corner == 0 || corner == 2) ? crClear : (dW - 1 - crClear);
|
||
|
|
int cyC = (corner == 0 || corner == 1) ? crClear : (dH - 1 - crClear);
|
||
|
|
int xMin = (corner == 0 || corner == 2) ? 0 : (dW - crClear - 1);
|
||
|
|
int xMax = (corner == 0 || corner == 2) ? crClear : (dW - 1);
|
||
|
|
int yMin = (corner == 0 || corner == 1) ? 0 : (dH - crClear - 1);
|
||
|
|
int yMax = (corner == 0 || corner == 1) ? crClear : (dH - 1);
|
||
|
|
for(int yy = yMin; yy <= yMax; yy++)
|
||
|
|
{
|
||
|
|
for(int xx = xMin; xx <= xMax; xx++)
|
||
|
|
{
|
||
|
|
if(xx < 0 || xx >= dW || yy < 0 || yy >= dH) continue;
|
||
|
|
const double ddx = (double)(xx - cxC);
|
||
|
|
const double ddy = (double)(yy - cyC);
|
||
|
|
const double dist = MathSqrt(ddx * ddx + ddy * ddy);
|
||
|
|
if(dist > rdC + 0.5)
|
||
|
|
{
|
||
|
|
ArrayResize(s_cornerClearX, n + 1);
|
||
|
|
ArrayResize(s_cornerClearY, n + 1);
|
||
|
|
s_cornerClearX[n] = xx;
|
||
|
|
s_cornerClearY[n] = yy;
|
||
|
|
n++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
s_cornerClearW = dW;
|
||
|
|
s_cornerClearH = dH;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Apply cached corner mask to clear outside-radius pixels
|
||
|
|
const int ccN = ArraySize(s_cornerClearX);
|
||
|
|
for(int cci = 0; cci < ccN; cci++)
|
||
|
|
g_ai_canvMain.PixelSet(s_cornerClearX[cci], s_cornerClearY[cci], 0x00000000);
|
||
|
|
|
||
|
|
//--- Draw outer rounded border
|
||
|
|
const uint borderArgb = ColorToARGB(g_ai_borderAccent, 255);
|
||
|
|
g_ai_prim.DrawRoundRectBorderObStyle(g_ai_canvMain, 0, 0, dW, dH, 10, borderArgb,
|
||
|
|
true, true, true, true,
|
||
|
|
true, true, true, true);
|
||
|
|
|
||
|
|
//--- Render top-level overlay popups
|
||
|
|
if(g_ai_showFooterDropdown) Ai_RenderFooterDropdown();
|
||
|
|
if(g_ai_showSmallHistory) Ai_RenderPopup(3);
|
||
|
|
|
||
|
|
//--- Render toast last so it overlays everything
|
||
|
|
Ai_RenderToast();
|
||
|
|
|
||
|
|
//--- Push main canvas bitmap to chart
|
||
|
|
g_ai_canvMain.Update();
|
||
|
|
|
||
|
|
//--- Sync prompt overlay - blank when popup obscures, full otherwise
|
||
|
|
if(g_ai_canvPromptExists)
|
||
|
|
{
|
||
|
|
const bool anyPopupUp =
|
||
|
|
g_ai_showSearch
|
||
|
|
|| g_ai_showFooterDropdown
|
||
|
|
|| g_ai_showSmallHistory
|
||
|
|
|| (StringLen(g_ai_toastText) > 0);
|
||
|
|
Ai_RenderPromptPaneOverlay(anyPopupUp);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#endif // AI_CANVAS_RENDER_MQH
|