#
tokens: 46040/50000 9/101 files (page 4/8)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 4 of 8. Use http://codebase.md/sammcj/bybit-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .env.example
├── .gitignore
├── client
│   ├── .env.example
│   ├── .gitignore
│   ├── package.json
│   ├── pnpm-lock.yaml
│   ├── README.md
│   ├── src
│   │   ├── cli.ts
│   │   ├── client.ts
│   │   ├── config.ts
│   │   ├── env.ts
│   │   ├── index.ts
│   │   └── launch.ts
│   └── tsconfig.json
├── DEV_PLAN.md
├── docs
│   └── HTTP_SERVER.md
├── eslint.config.js
├── jest.config.js
├── LICENSE
├── package-lock.json
├── package.json
├── pnpm-lock.yaml
├── README.md
├── specs
│   ├── bybit
│   │   ├── bybit-api-v5-openapi.yaml
│   │   └── bybit-api-v5-postman-collection.json
│   ├── mcp
│   │   ├── mcp-schema.json
│   │   └── mcp-schema.ts
│   └── README.md
├── src
│   ├── __tests__
│   │   ├── GetMLRSI.test.ts
│   │   ├── integration.test.ts
│   │   ├── test-setup.ts
│   │   └── tools.test.ts
│   ├── constants.ts
│   ├── env.ts
│   ├── httpServer.ts
│   ├── index.ts
│   ├── tools
│   │   ├── BaseTool.ts
│   │   ├── GetInstrumentInfo.ts
│   │   ├── GetKline.ts
│   │   ├── GetMarketInfo.ts
│   │   ├── GetMarketStructure.ts
│   │   ├── GetMLRSI.ts
│   │   ├── GetOrderBlocks.ts
│   │   ├── GetOrderbook.ts
│   │   ├── GetOrderHistory.ts
│   │   ├── GetPositions.ts
│   │   ├── GetTicker.ts
│   │   ├── GetTrades.ts
│   │   └── GetWalletBalance.ts
│   └── utils
│       ├── knnAlgorithm.ts
│       ├── mathUtils.ts
│       ├── toolLoader.ts
│       └── volumeAnalysis.ts
├── tsconfig.json
└── webui
    ├── .dockerignore
    ├── .env.example
    ├── build-docker.sh
    ├── docker-compose.yml
    ├── docker-entrypoint.sh
    ├── docker-healthcheck.sh
    ├── DOCKER.md
    ├── Dockerfile
    ├── index.html
    ├── package.json
    ├── pnpm-lock.yaml
    ├── pnpm-workspace.yaml
    ├── public
    │   ├── favicon.svg
    │   └── inter.woff2
    ├── README.md
    ├── screenshot.png
    ├── src
    │   ├── assets
    │   │   └── fonts
    │   │       └── fonts.css
    │   ├── components
    │   │   ├── AgentDashboard.ts
    │   │   ├── chat
    │   │   │   ├── DataCard.ts
    │   │   │   └── MessageRenderer.ts
    │   │   ├── ChatApp.ts
    │   │   ├── DataVerificationPanel.ts
    │   │   ├── DebugConsole.ts
    │   │   └── ToolsManager.ts
    │   ├── main.ts
    │   ├── services
    │   │   ├── agentConfig.ts
    │   │   ├── agentMemory.ts
    │   │   ├── aiClient.ts
    │   │   ├── citationProcessor.ts
    │   │   ├── citationStore.ts
    │   │   ├── configService.ts
    │   │   ├── logService.ts
    │   │   ├── mcpClient.ts
    │   │   ├── multiStepAgent.ts
    │   │   ├── performanceOptimiser.ts
    │   │   └── systemPrompt.ts
    │   ├── styles
    │   │   ├── agent-dashboard.css
    │   │   ├── base.css
    │   │   ├── citations.css
    │   │   ├── components.css
    │   │   ├── data-cards.css
    │   │   ├── main.css
    │   │   ├── processing.css
    │   │   ├── variables.css
    │   │   └── verification-panel.css
    │   ├── types
    │   │   ├── agent.ts
    │   │   ├── ai.ts
    │   │   ├── citation.ts
    │   │   ├── mcp.ts
    │   │   └── workflow.ts
    │   └── utils
    │       ├── dataDetection.ts
    │       └── formatters.ts
    ├── tsconfig.json
    └── vite.config.ts
```

# Files

--------------------------------------------------------------------------------
/src/tools/BaseTool.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Tool, TextContent, CallToolResult } from "@modelcontextprotocol/sdk/types.js"
  2 | import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js"
  3 | import { z } from "zod"
  4 | import { RestClientV5, APIResponseV3WithTime } from "bybit-api"
  5 | import { getEnvConfig } from "../env.js"
  6 | 
  7 | // Error categories for better error handling
  8 | export enum ErrorCategory {
  9 |   VALIDATION = "VALIDATION",
 10 |   API_ERROR = "API_ERROR",
 11 |   RATE_LIMIT = "RATE_LIMIT",
 12 |   NETWORK = "NETWORK",
 13 |   AUTHENTICATION = "AUTHENTICATION",
 14 |   PERMISSION = "PERMISSION",
 15 |   INTERNAL = "INTERNAL"
 16 | }
 17 | 
 18 | // Structured error interface
 19 | export interface ToolError {
 20 |   category: ErrorCategory
 21 |   code?: string | number
 22 |   message: string
 23 |   details?: any
 24 |   timestamp: string
 25 |   tool: string
 26 | }
 27 | 
 28 | // Standard error codes
 29 | export const ERROR_CODES = {
 30 |   INVALID_INPUT: "INVALID_INPUT",
 31 |   MISSING_REQUIRED_FIELD: "MISSING_REQUIRED_FIELD",
 32 |   INVALID_SYMBOL: "INVALID_SYMBOL",
 33 |   INVALID_CATEGORY: "INVALID_CATEGORY",
 34 |   API_KEY_REQUIRED: "API_KEY_REQUIRED",
 35 |   RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED",
 36 |   BYBIT_API_ERROR: "BYBIT_API_ERROR",
 37 |   NETWORK_ERROR: "NETWORK_ERROR",
 38 |   TIMEOUT: "TIMEOUT",
 39 |   UNKNOWN_ERROR: "UNKNOWN_ERROR"
 40 | } as const
 41 | 
 42 | // Rate limit configuration (as per Bybit docs)
 43 | const RATE_LIMIT = {
 44 |   maxRequestsPerSecond: 10,
 45 |   maxRequestsPerMinute: 120,
 46 |   retryAfter: 2000, // ms
 47 |   maxRetries: 3
 48 | }
 49 | 
 50 | interface QueuedRequest {
 51 |   execute: () => Promise<any>
 52 |   resolve: (value: any) => void
 53 |   reject: (error: any) => void
 54 | }
 55 | 
 56 | export abstract class BaseToolImplementation {
 57 |   abstract name: string
 58 |   abstract toolDefinition: Tool
 59 |   abstract toolCall(request: z.infer<typeof CallToolRequestSchema>): Promise<CallToolResult>
 60 | 
 61 |   protected client: RestClientV5
 62 |   protected isDevMode: boolean
 63 |   protected isTestMode: boolean = false
 64 |   private requestQueue: QueuedRequest[] = []
 65 |   private processingQueue = false
 66 |   private requestCount = 0
 67 |   private lastRequestTime = 0
 68 |   private requestHistory: number[] = [] // Timestamps of requests within the last minute
 69 |   private initialized = false
 70 |   private activeTimeouts: NodeJS.Timeout[] = []
 71 | 
 72 |   constructor(mockClient?: RestClientV5) {
 73 |     if (mockClient) {
 74 |       // Use provided mock client for testing
 75 |       this.client = mockClient
 76 |       this.isDevMode = true
 77 |       this.isTestMode = true
 78 |     } else {
 79 |       // Normal production/development initialization
 80 |       const config = getEnvConfig()
 81 |       this.isDevMode = !config.apiKey || !config.apiSecret
 82 | 
 83 |       if (this.isDevMode) {
 84 |         this.client = new RestClientV5({
 85 |           testnet: true,
 86 |         })
 87 |       } else {
 88 |         this.client = new RestClientV5({
 89 |           key: config.apiKey,
 90 |           secret: config.apiSecret,
 91 |           testnet: config.useTestnet,
 92 |           recv_window: 5000, // 5 second receive window
 93 |         })
 94 |       }
 95 |     }
 96 |   }
 97 | 
 98 |   protected ensureInitialized() {
 99 |     if (!this.initialized) {
100 |       if (this.isDevMode) {
101 |         this.logWarning("Running in development mode with limited functionality")
102 |       }
103 |       this.initialized = true
104 |     }
105 |   }
106 | 
107 |   /**
108 |    * Enqueues a request with rate limiting and retry logic
109 |    */
110 |   protected async executeRequest<T>(
111 |     operation: () => Promise<APIResponseV3WithTime<T>>,
112 |     retryCount = 0
113 |   ): Promise<T> {
114 |     this.ensureInitialized()
115 |     return new Promise((resolve, reject) => {
116 |       this.requestQueue.push({
117 |         execute: async () => {
118 |           try {
119 |             // Check rate limits
120 |             if (!this.canMakeRequest() && !this.isTestMode) {
121 |               const waitTime = this.getWaitTime()
122 |               this.logInfo(`Rate limit reached. Waiting ${waitTime}ms`)
123 |               await new Promise(resolve => setTimeout(resolve, waitTime))
124 |             }
125 | 
126 |             // Execute request with timeout
127 |             let response: APIResponseV3WithTime<T>
128 | 
129 |             if (this.isTestMode) {
130 |               // In test mode, don't create timeout promises to avoid open handles
131 |               response = await operation() as APIResponseV3WithTime<T>
132 |             } else {
133 |               // In production mode, use timeout for real API calls
134 |               response = await Promise.race([
135 |                 operation(),
136 |                 new Promise<never>((_, reject) =>
137 |                   setTimeout(() => reject(new Error("Request timeout")), 10000)
138 |                 )
139 |               ]) as APIResponseV3WithTime<T>
140 |             }
141 | 
142 |             // Update rate limit tracking
143 |             this.updateRequestHistory()
144 | 
145 |             // Handle Bybit API errors
146 |             if (response.retCode !== 0) {
147 |               throw this.createBybitError(response.retCode, response.retMsg)
148 |             }
149 | 
150 |             return response.result
151 |           } catch (error) {
152 |             // Retry logic for specific errors
153 |             if (
154 |               retryCount < RATE_LIMIT.maxRetries &&
155 |               this.shouldRetry(error)
156 |             ) {
157 |               this.logWarning(`Retrying request (attempt ${retryCount + 1})`)
158 |               if (!this.isTestMode) {
159 |                 await new Promise(resolve =>
160 |                   setTimeout(resolve, RATE_LIMIT.retryAfter)
161 |                 )
162 |               }
163 |               return this.executeRequest(operation, retryCount + 1)
164 |             }
165 |             throw error
166 |           }
167 |         },
168 |         resolve,
169 |         reject
170 |       })
171 | 
172 |       if (!this.processingQueue) {
173 |         this.processQueue()
174 |       }
175 |     })
176 |   }
177 | 
178 |   private async processQueue() {
179 |     if (this.requestQueue.length === 0) {
180 |       this.processingQueue = false
181 |       return
182 |     }
183 | 
184 |     this.processingQueue = true
185 |     const request = this.requestQueue.shift()
186 | 
187 |     if (request) {
188 |       try {
189 |         const result = await request.execute()
190 |         request.resolve(result)
191 |       } catch (error) {
192 |         request.reject(error)
193 |       }
194 |     }
195 | 
196 |     // Process next request
197 |     setImmediate(() => this.processQueue())
198 |   }
199 | 
200 |   private canMakeRequest(): boolean {
201 |     const now = Date.now()
202 |     // Clean up old requests
203 |     this.requestHistory = this.requestHistory.filter(
204 |       time => now - time < 60000
205 |     )
206 | 
207 |     return (
208 |       this.requestHistory.length < RATE_LIMIT.maxRequestsPerMinute &&
209 |       now - this.lastRequestTime >= (1000 / RATE_LIMIT.maxRequestsPerSecond)
210 |     )
211 |   }
212 | 
213 |   private getWaitTime(): number {
214 |     const now = Date.now()
215 |     const timeToWaitForSecondLimit = Math.max(
216 |       0,
217 |       this.lastRequestTime + (1000 / RATE_LIMIT.maxRequestsPerSecond) - now
218 |     )
219 | 
220 |     if (this.requestHistory.length >= RATE_LIMIT.maxRequestsPerMinute) {
221 |       const timeToWaitForMinuteLimit = Math.max(
222 |         0,
223 |         this.requestHistory[0] + 60000 - now
224 |       )
225 |       return Math.max(timeToWaitForSecondLimit, timeToWaitForMinuteLimit)
226 |     }
227 | 
228 |     return timeToWaitForSecondLimit
229 |   }
230 | 
231 |   private updateRequestHistory() {
232 |     const now = Date.now()
233 |     this.requestHistory.push(now)
234 |     this.lastRequestTime = now
235 |   }
236 | 
237 |   private shouldRetry(error: any): boolean {
238 |     // Retry on network errors or specific Bybit error codes
239 |     return (
240 |       error.name === "NetworkError" ||
241 |       error.code === 10002 || // Rate limit
242 |       error.code === 10006 || // System busy
243 |       error.code === -1      // Unknown error
244 |     )
245 |   }
246 | 
247 |   private createBybitError(code: number, message: string): Error {
248 |     const errorMap: Record<number, string> = {
249 |       10001: "Parameter error",
250 |       10002: "Rate limit exceeded",
251 |       10003: "Invalid API key",
252 |       10004: "Invalid sign",
253 |       10005: "Permission denied",
254 |       10006: "System busy",
255 |       10009: "Order not found",
256 |       10010: "Insufficient balance",
257 |     }
258 | 
259 |     const errorMessage = errorMap[code] || message
260 |     const error = new Error(`Bybit API Error ${code}: ${errorMessage}`)
261 |     ; (error as any).code = code
262 |     ; (error as any).bybitCode = code
263 |     ; (error as any).category = this.categoriseBybitError(code)
264 |     return error
265 |   }
266 | 
267 |   /**
268 |    * Creates a standardised ToolError object
269 |    */
270 |   protected createToolError(
271 |     category: ErrorCategory,
272 |     message: string,
273 |     code?: string | number,
274 |     details?: any
275 |   ): ToolError {
276 |     return {
277 |       category,
278 |       code,
279 |       message,
280 |       details,
281 |       timestamp: new Date().toISOString(),
282 |       tool: this.name
283 |     }
284 |   }
285 | 
286 |   /**
287 |    * Creates a validation error for invalid input
288 |    */
289 |   protected createValidationError(message: string, details?: any): ToolError {
290 |     return this.createToolError(
291 |       ErrorCategory.VALIDATION,
292 |       message,
293 |       ERROR_CODES.INVALID_INPUT,
294 |       details
295 |     )
296 |   }
297 | 
298 |   /**
299 |    * Creates an API error from Bybit response
300 |    */
301 |   protected createApiError(code: number, message: string): ToolError {
302 |     const category = this.categoriseBybitError(code)
303 |     return this.createToolError(
304 |       category,
305 |       `Bybit API Error ${code}: ${message}`,
306 |       code
307 |     )
308 |   }
309 | 
310 |   /**
311 |    * Categorises Bybit API errors
312 |    */
313 |   private categoriseBybitError(code: number): ErrorCategory {
314 |     switch (code) {
315 |       case 10002:
316 |         return ErrorCategory.RATE_LIMIT
317 |       case 10003:
318 |       case 10004:
319 |         return ErrorCategory.AUTHENTICATION
320 |       case 10005:
321 |         return ErrorCategory.PERMISSION
322 |       case 10001:
323 |         return ErrorCategory.VALIDATION
324 |       default:
325 |         return ErrorCategory.API_ERROR
326 |     }
327 |   }
328 | 
329 |   /**
330 |    * Handles errors and returns MCP-compliant CallToolResult
331 |    */
332 |   protected handleError(error: any): CallToolResult {
333 |     let toolError: ToolError
334 | 
335 |     if (error instanceof Error) {
336 |       // Check if it's a Bybit API error (has bybitCode property)
337 |       if ((error as any).bybitCode) {
338 |         toolError = this.createApiError((error as any).bybitCode, error.message)
339 |       }
340 |       // Check if it's a validation error (from Zod)
341 |       else if (error.message.includes("Invalid input")) {
342 |         toolError = this.createValidationError(error.message)
343 |       }
344 |       // Check for specific error patterns
345 |       else if (error.message.includes("API credentials required") || error.message.includes("development mode")) {
346 |         toolError = this.createToolError(
347 |           ErrorCategory.AUTHENTICATION,
348 |           error.message,
349 |           ERROR_CODES.API_KEY_REQUIRED
350 |         )
351 |       } else if (error.message.includes("Rate limit")) {
352 |         toolError = this.createToolError(
353 |           ErrorCategory.RATE_LIMIT,
354 |           error.message,
355 |           ERROR_CODES.RATE_LIMIT_EXCEEDED
356 |         )
357 |       } else if (error.message.includes("timeout") || error.message.includes("Request timeout")) {
358 |         toolError = this.createToolError(
359 |           ErrorCategory.NETWORK,
360 |           error.message,
361 |           ERROR_CODES.TIMEOUT
362 |         )
363 |       } else if (error.name === "NetworkError") {
364 |         toolError = this.createToolError(
365 |           ErrorCategory.NETWORK,
366 |           error.message,
367 |           ERROR_CODES.NETWORK_ERROR
368 |         )
369 |       } else {
370 |         toolError = this.createToolError(
371 |           ErrorCategory.INTERNAL,
372 |           error.message,
373 |           ERROR_CODES.UNKNOWN_ERROR
374 |         )
375 |       }
376 |     } else {
377 |       toolError = this.createToolError(
378 |         ErrorCategory.INTERNAL,
379 |         String(error),
380 |         ERROR_CODES.UNKNOWN_ERROR
381 |       )
382 |     }
383 | 
384 |     // Log the error
385 |     console.error(JSON.stringify({
386 |       jsonrpc: "2.0",
387 |       method: "notify",
388 |       params: {
389 |         level: "error",
390 |         message: `${this.name} tool error: ${toolError.message}`
391 |       }
392 |     }))
393 | 
394 |     // Create MCP-compliant error response
395 |     const content: TextContent = {
396 |       type: "text",
397 |       text: JSON.stringify(toolError, null, 2),
398 |       annotations: {
399 |         audience: ["assistant", "user"],
400 |         priority: 1
401 |       }
402 |     }
403 | 
404 |     return {
405 |       content: [content],
406 |       isError: true
407 |     }
408 |   }
409 | 
410 |   // Reference ID counter for generating unique IDs
411 |   private static referenceIdCounter = 0
412 | 
413 |   /**
414 |    * Generate a unique reference ID
415 |    */
416 |   protected generateReferenceId(): string {
417 |     BaseToolImplementation.referenceIdCounter += 1
418 |     return `REF${String(BaseToolImplementation.referenceIdCounter).padStart(3, '0')}`
419 |   }
420 | 
421 |   /**
422 |    * Add reference ID metadata to response if requested
423 |    */
424 |   protected addReferenceMetadata(data: any, includeReferenceId: boolean, toolName: string, endpoint?: string): any {
425 |     if (!includeReferenceId) {
426 |       return data
427 |     }
428 | 
429 |     return {
430 |       ...data,
431 |       _referenceId: this.generateReferenceId(),
432 |       _timestamp: new Date().toISOString(),
433 |       _toolName: toolName,
434 |       _endpoint: endpoint
435 |     }
436 |   }
437 | 
438 |   protected formatResponse(data: any): CallToolResult {
439 |     this.ensureInitialized()
440 |     const content: TextContent = {
441 |       type: "text",
442 |       text: JSON.stringify(data, null, 2),
443 |       annotations: {
444 |         audience: ["assistant", "user"],
445 |         priority: 1
446 |       }
447 |     }
448 | 
449 |     return {
450 |       content: [content]
451 |     }
452 |   }
453 | 
454 |   protected logInfo(message: string) {
455 |     console.info(JSON.stringify({
456 |       jsonrpc: "2.0",
457 |       method: "notify",
458 |       params: {
459 |         level: "info",
460 |         message: `${this.name}: ${message}`
461 |       }
462 |     }))
463 |   }
464 | 
465 |   protected logWarning(message: string) {
466 |     console.warn(JSON.stringify({
467 |       jsonrpc: "2.0",
468 |       method: "notify",
469 |       params: {
470 |         level: "warning",
471 |         message: `${this.name}: ${message}`
472 |       }
473 |     }))
474 |   }
475 | 
476 |   /**
477 |    * Cleanup method for tests to clear any remaining timeouts
478 |    */
479 |   public cleanup() {
480 |     this.activeTimeouts.forEach(timeout => clearTimeout(timeout))
481 |     this.activeTimeouts = []
482 |     this.requestQueue = []
483 |     this.processingQueue = false
484 |   }
485 | }
486 | 
```

--------------------------------------------------------------------------------
/webui/src/services/mcpClient.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * MCP (Model Context Protocol) client for communicating with the Bybit MCP server
  3 |  * Uses the official MCP SDK with StreamableHTTPClientTransport for browser compatibility
  4 |  */
  5 | 
  6 | import { Client } from "@modelcontextprotocol/sdk/client/index.js";
  7 | import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
  8 | import { citationStore } from './citationStore';
  9 | import type {
 10 |   MCPTool,
 11 |   MCPToolCall,
 12 |   MCPToolResult,
 13 |   MCPToolName,
 14 |   MCPToolParams,
 15 |   MCPToolResponse,
 16 | } from '@/types/mcp';
 17 | 
 18 | export class MCPClient {
 19 |   private baseUrl: string;
 20 |   private timeout: number;
 21 |   private client: Client | null = null;
 22 |   private transport: StreamableHTTPClientTransport | null = null;
 23 |   private tools: MCPTool[] = [];
 24 |   private connected: boolean = false;
 25 | 
 26 |   constructor(baseUrl: string = '', timeout: number = 30000) {
 27 |     // Determine the correct base URL based on environment
 28 |     if (typeof window !== 'undefined') {
 29 |       if (window.location.hostname === 'localhost' && window.location.port === '3000') {
 30 |         // Development mode with Vite dev server
 31 |         this.baseUrl = '/api/mcp'; // Use Vite proxy in development
 32 |       } else if (baseUrl && baseUrl !== '' && baseUrl !== 'auto') {
 33 |         // Explicit base URL provided (not empty or 'auto')
 34 |         this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
 35 |       } else {
 36 |         // Production mode or Docker - use current origin
 37 |         this.baseUrl = window.location.origin;
 38 |       }
 39 |     } else {
 40 |       // Server-side or fallback
 41 |       this.baseUrl = baseUrl || 'http://localhost:8080';
 42 |       this.baseUrl = this.baseUrl.replace(/\/$/, ''); // Remove trailing slash
 43 |     }
 44 |     this.timeout = timeout;
 45 | 
 46 |     console.log('🔧 MCP Client initialised with baseUrl:', this.baseUrl);
 47 |     console.log('🔧 Environment check:', {
 48 |       hostname: typeof window !== 'undefined' ? window.location.hostname : 'server-side',
 49 |       port: typeof window !== 'undefined' ? window.location.port : 'server-side',
 50 |       origin: typeof window !== 'undefined' ? window.location.origin : 'server-side',
 51 |       providedBaseUrl: baseUrl,
 52 |       finalBaseUrl: this.baseUrl
 53 |     });
 54 |   }
 55 | 
 56 |   /**
 57 |    * Initialize the client and connect to the MCP server
 58 |    */
 59 |   async initialize(): Promise<void> {
 60 |     try {
 61 |       console.log('🔌 Initialising MCP client...');
 62 |       console.log('🔗 MCP endpoint:', this.baseUrl);
 63 | 
 64 |       // For now, skip the complex MCP client setup and just load tools
 65 |       // This allows the WebUI to work while we debug the MCP protocol issues
 66 |       console.log('🔄 Loading tools via HTTP...');
 67 |       await this.listTools();
 68 | 
 69 |       // Mark as connected if we successfully loaded tools
 70 |       this.connected = this.tools.length > 0;
 71 | 
 72 |       if (this.connected) {
 73 |         console.log('✅ MCP client initialised via HTTP');
 74 |       } else {
 75 |         console.warn('⚠️ No tools loaded, but continuing...');
 76 |       }
 77 |     } catch (error) {
 78 |       console.error('❌ Failed to initialise MCP client:', error);
 79 |       console.error('❌ MCP Error details:', {
 80 |         name: error instanceof Error ? error.name : 'Unknown',
 81 |         message: error instanceof Error ? error.message : String(error),
 82 |         stack: error instanceof Error ? error.stack : undefined
 83 |       });
 84 |       this.connected = false;
 85 |       // Don't throw error, allow WebUI to continue
 86 |       console.log('💡 Continuing without MCP tools...');
 87 |     }
 88 |   }
 89 | 
 90 |   /**
 91 |    * Check if the MCP server is reachable
 92 |    */
 93 |   async isConnected(): Promise<boolean> {
 94 |     try {
 95 |       // Simple health check to the HTTP server
 96 |       const response = await fetch(`${this.baseUrl}/health`);
 97 |       return response.ok;
 98 |     } catch (error) {
 99 |       console.warn('🔍 MCP health check failed:', error);
100 |       return false;
101 |     }
102 |   }
103 | 
104 |   /**
105 |    * List all available tools from the MCP server using direct HTTP
106 |    */
107 |   async listTools(): Promise<MCPTool[]> {
108 |     try {
109 |       // Use direct HTTP request to get tools
110 |       const response = await fetch(`${this.baseUrl}/tools`, {
111 |         method: 'GET',
112 |         headers: {
113 |           'Content-Type': 'application/json',
114 |         },
115 |       });
116 | 
117 |       if (!response.ok) {
118 |         throw new Error(`HTTP ${response.status}: ${response.statusText}`);
119 |       }
120 | 
121 |       const data = await response.json();
122 | 
123 |       // Handle different response formats
124 |       let tools = [];
125 |       if (Array.isArray(data)) {
126 |         tools = data;
127 |       } else if (data.tools && Array.isArray(data.tools)) {
128 |         tools = data.tools;
129 |       } else {
130 |         console.warn('Unexpected tools response format:', data);
131 |         return [];
132 |       }
133 | 
134 |       this.tools = tools.map((tool: any) => ({
135 |         name: tool.name,
136 |         description: tool.description || '',
137 |         inputSchema: tool.inputSchema || { type: 'object', properties: {} },
138 |       }));
139 | 
140 |       console.log('🔧 Loaded tools via HTTP:', this.tools.length);
141 |       return this.tools;
142 |     } catch (error) {
143 |       console.error('Failed to list tools via HTTP:', error);
144 |       // Fallback: return empty array instead of throwing
145 |       this.tools = [];
146 |       return this.tools;
147 |     }
148 |   }
149 | 
150 |   /**
151 |    * Get information about a specific tool
152 |    */
153 |   getTool(name: string): MCPTool | undefined {
154 |     return this.tools.find(tool => tool.name === name);
155 |   }
156 | 
157 |   /**
158 |    * Get all available tools
159 |    */
160 |   getTools(): MCPTool[] {
161 |     return [...this.tools];
162 |   }
163 | 
164 |   /**
165 |    * Validate and convert parameters based on tool schema
166 |    */
167 |   private validateAndConvertParams(toolName: string, params: Record<string, any>): Record<string, any> {
168 |     const tool = this.getTool(toolName);
169 |     if (!tool || !tool.inputSchema || !tool.inputSchema.properties) {
170 |       return params;
171 |     }
172 | 
173 |     const convertedParams: Record<string, any> = {};
174 |     const schema = tool.inputSchema.properties;
175 | 
176 |     for (const [key, value] of Object.entries(params)) {
177 |       if (value === undefined || value === null) {
178 |         continue;
179 |       }
180 | 
181 |       const propertySchema = schema[key] as any;
182 |       if (!propertySchema) {
183 |         convertedParams[key] = value;
184 |         continue;
185 |       }
186 | 
187 |       // Convert based on schema type
188 |       if (propertySchema.type === 'number') {
189 |         const numValue = typeof value === 'string' ? parseFloat(value) : value;
190 |         if (!isNaN(numValue)) {
191 |           convertedParams[key] = numValue;
192 |         } else {
193 |           console.warn(`⚠️ Invalid number value for ${key}: ${value}`);
194 |           convertedParams[key] = value; // Keep original value
195 |         }
196 |       } else if (propertySchema.type === 'integer') {
197 |         const intValue = typeof value === 'string' ? parseInt(value, 10) : value;
198 |         if (!isNaN(intValue)) {
199 |           convertedParams[key] = intValue;
200 |         } else {
201 |           console.warn(`⚠️ Invalid integer value for ${key}: ${value}`);
202 |           convertedParams[key] = value; // Keep original value
203 |         }
204 |       } else if (propertySchema.type === 'boolean') {
205 |         if (typeof value === 'string') {
206 |           convertedParams[key] = value.toLowerCase() === 'true';
207 |         } else {
208 |           convertedParams[key] = Boolean(value);
209 |         }
210 |       } else {
211 |         // String or other types - keep as is
212 |         convertedParams[key] = value;
213 |       }
214 |     }
215 | 
216 |     return convertedParams;
217 |   }
218 | 
219 |   /**
220 |    * Call a specific MCP tool using HTTP
221 |    */
222 |   async callTool<T extends MCPToolName>(
223 |     name: T,
224 |     params: MCPToolParams<T>
225 |   ): Promise<MCPToolResponse<T>> {
226 |     try {
227 |       console.log(`🔧 Calling tool ${name} with params:`, params);
228 | 
229 |       // Validate and convert parameters
230 |       const convertedParams = this.validateAndConvertParams(name as string, params as Record<string, any>);
231 |       console.log(`🔧 Converted params:`, convertedParams);
232 | 
233 |       const response = await fetch(`${this.baseUrl}/call-tool`, {
234 |         method: 'POST',
235 |         headers: {
236 |           'Content-Type': 'application/json',
237 |         },
238 |         body: JSON.stringify({
239 |           name: name as string,
240 |           arguments: convertedParams,
241 |         }),
242 |       });
243 | 
244 |       if (!response.ok) {
245 |         const errorText = await response.text();
246 |         throw new Error(`HTTP ${response.status}: ${errorText}`);
247 |       }
248 | 
249 |       const result = await response.json();
250 |       console.log(`✅ Tool ${name} result:`, result);
251 | 
252 |       // Process tool response for citation storage
253 |       console.log(`🔍 About to process tool response for citations...`);
254 |       citationStore.processToolResponse(result);
255 | 
256 |       return result as MCPToolResponse<T>;
257 |     } catch (error) {
258 |       console.error(`❌ Failed to call tool ${name}:`, error);
259 |       throw error;
260 |     }
261 |   }
262 | 
263 |   /**
264 |    * Call multiple tools in sequence
265 |    */
266 |   async callTools(toolCalls: MCPToolCall[]): Promise<MCPToolResult[]> {
267 |     const results: MCPToolResult[] = [];
268 | 
269 |     for (const toolCall of toolCalls) {
270 |       try {
271 |         const result = await this.callTool(
272 |           toolCall.name as MCPToolName,
273 |           toolCall.arguments as any
274 |         );
275 | 
276 |         results.push({
277 |           content: [{
278 |             type: 'text',
279 |             text: JSON.stringify(result, null, 2),
280 |           }],
281 |           isError: false,
282 |         });
283 |       } catch (error) {
284 |         results.push({
285 |           content: [{
286 |             type: 'text',
287 |             text: `Error calling ${toolCall.name}: ${error instanceof Error ? error.message : 'Unknown error'}`,
288 |           }],
289 |           isError: true,
290 |         });
291 |       }
292 |     }
293 | 
294 |     return results;
295 |   }
296 | 
297 |   /**
298 |    * Disconnect from the MCP server
299 |    */
300 |   async disconnect(): Promise<void> {
301 |     if (this.client && this.transport) {
302 |       try {
303 |         await this.client.close();
304 |       } catch (error) {
305 |         console.error('Error disconnecting from MCP server:', error);
306 |       }
307 |     }
308 | 
309 |     this.client = null;
310 |     this.transport = null;
311 |     this.connected = false;
312 |     this.tools = [];
313 |   }
314 | 
315 |   /**
316 |    * Update the base URL for the MCP server
317 |    */
318 |   setBaseUrl(url: string): void {
319 |     // Handle empty string or 'auto' to use current origin
320 |     if (!url || url === '' || url === 'auto') {
321 |       if (typeof window !== 'undefined') {
322 |         this.baseUrl = window.location.origin;
323 |         console.log('🔧 setBaseUrl: Using current origin:', this.baseUrl);
324 |       } else {
325 |         this.baseUrl = 'http://localhost:8080';
326 |         console.log('🔧 setBaseUrl: Using server-side fallback:', this.baseUrl);
327 |       }
328 |     } else {
329 |       this.baseUrl = url.replace(/\/$/, '');
330 |       console.log('🔧 setBaseUrl: Using explicit URL:', this.baseUrl);
331 |     }
332 | 
333 |     // If connected, disconnect and reconnect with new URL
334 |     if (this.connected) {
335 |       this.disconnect().then(() => {
336 |         this.initialize().catch(console.error);
337 |       });
338 |     }
339 |   }
340 | 
341 |   /**
342 |    * Update the request timeout
343 |    */
344 |   setTimeout(timeout: number): void {
345 |     this.timeout = timeout;
346 |   }
347 | 
348 |   /**
349 |    * Get current configuration
350 |    */
351 |   getConfig(): { baseUrl: string; timeout: number; isConnected: boolean } {
352 |     return {
353 |       baseUrl: this.baseUrl,
354 |       timeout: this.timeout,
355 |       isConnected: this.connected,
356 |     };
357 |   }
358 | }
359 | 
360 | // Create a singleton instance with environment-aware defaults
361 | const getDefaultMCPUrl = (): string => {
362 |   // Check for build-time injected environment variable
363 |   const envEndpoint = (typeof window !== 'undefined' && (window as any).__MCP_ENDPOINT__) || '';
364 | 
365 |   console.log('🔧 MCP URL Detection:', {
366 |     envEndpoint,
367 |     isWindow: typeof window !== 'undefined',
368 |     windowMcpEndpoint: typeof window !== 'undefined' ? (window as any).__MCP_ENDPOINT__ : 'N/A',
369 |     hostname: typeof window !== 'undefined' ? window.location.hostname : 'N/A',
370 |     origin: typeof window !== 'undefined' ? window.location.origin : 'N/A'
371 |   });
372 | 
373 |   // If we have an explicit endpoint from build-time injection, use it
374 |   if (envEndpoint && envEndpoint !== '' && envEndpoint !== 'auto') {
375 |     console.log('🔧 Using explicit MCP endpoint:', envEndpoint);
376 |     return envEndpoint;
377 |   }
378 | 
379 |   // In browser, always use empty string to trigger current origin logic
380 |   if (typeof window !== 'undefined') {
381 |     console.log('🔧 Using current origin for MCP endpoint (empty string)');
382 |     return ''; // Empty string means use current origin
383 |   }
384 | 
385 |   // Server-side fallback
386 |   console.log('🔧 Using server-side fallback for MCP endpoint');
387 |   return 'http://localhost:8080';
388 | };
389 | 
390 | export const mcpClient = new MCPClient(getDefaultMCPUrl());
391 | 
392 | // Convenience functions for common operations
393 | export async function getTicker(symbol: string, category?: 'spot' | 'linear' | 'inverse' | 'option') {
394 |   return mcpClient.callTool('get_ticker', { symbol, category });
395 | }
396 | 
397 | export async function getKlineData(symbol: string, interval?: string, limit?: number) {
398 |   return mcpClient.callTool('get_kline', { symbol, interval, limit });
399 | }
400 | 
401 | export async function getOrderbook(symbol: string, category?: 'spot' | 'linear' | 'inverse' | 'option', limit?: number) {
402 |   return mcpClient.callTool('get_orderbook', { symbol, category, limit });
403 | }
404 | 
405 | export async function getMLRSI(symbol: string, category: 'spot' | 'linear' | 'inverse' | 'option', interval: string, options?: Partial<MCPToolParams<'get_ml_rsi'>>) {
406 |   return mcpClient.callTool('get_ml_rsi', { symbol, category, interval, ...options });
407 | }
408 | 
409 | export async function getOrderBlocks(symbol: string, category: 'spot' | 'linear' | 'inverse' | 'option', interval: string, options?: Partial<MCPToolParams<'get_order_blocks'>>) {
410 |   return mcpClient.callTool('get_order_blocks', { symbol, category, interval, ...options });
411 | }
412 | 
413 | export async function getMarketStructure(symbol: string, category: 'spot' | 'linear' | 'inverse' | 'option', interval: string, options?: Partial<MCPToolParams<'get_market_structure'>>) {
414 |   return mcpClient.callTool('get_market_structure', { symbol, category, interval, ...options });
415 | }
416 | 
```

--------------------------------------------------------------------------------
/webui/src/services/multiStepAgent.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Multi-step agent service for enhanced agentic capabilities
  3 |  * Implements multi-step tool calling and workflow orchestration
  4 |  */
  5 | 
  6 | import type { AgentConfig, AgentState } from '@/types/agent';
  7 | import type { WorkflowEvent } from '@/types/workflow';
  8 | import { WorkflowEventEmitter, createWorkflowEvent } from '@/types/workflow';
  9 | import { agentConfigService } from './agentConfig';
 10 | import { aiClient } from './aiClient';
 11 | import { mcpClient } from './mcpClient';
 12 | import { agentMemory } from './agentMemory';
 13 | import { performanceOptimiser } from './performanceOptimiser';
 14 | import { systemPromptService } from './systemPrompt';
 15 | import type { ChatMessage } from '@/types/ai';
 16 | 
 17 | export class MultiStepAgentService {
 18 |   private availableTools: any[] = [];
 19 |   private isInitialized = false;
 20 |   private eventEmitter: WorkflowEventEmitter;
 21 |   private currentConfig: AgentConfig;
 22 |   private conversationHistory: ChatMessage[] = [];
 23 |   private currentConversationId?: string;
 24 | 
 25 |   constructor() {
 26 |     this.eventEmitter = new WorkflowEventEmitter();
 27 |     this.currentConfig = agentConfigService.getConfig();
 28 | 
 29 |     // Subscribe to config changes
 30 |     agentConfigService.subscribe((config) => {
 31 |       this.currentConfig = config;
 32 |       this.reinitializeAgents();
 33 |     });
 34 |   }
 35 | 
 36 |   /**
 37 |    * Initialize the agent service
 38 |    */
 39 |   async initialize(): Promise<void> {
 40 |     if (this.isInitialized) return;
 41 | 
 42 |     try {
 43 |       console.log('🤖 Initializing Multi-Step Agent Service...');
 44 | 
 45 |       // Load MCP tools
 46 |       await this.loadMCPTools();
 47 | 
 48 |       // Initialize agents based on configuration
 49 |       await this.initializeAgents();
 50 | 
 51 |       // Update state
 52 |       agentConfigService.updateState({
 53 |         isProcessing: false
 54 |       });
 55 | 
 56 |       this.isInitialized = true;
 57 |       console.log('✅ Multi-Step Agent Service initialized successfully');
 58 | 
 59 |     } catch (error) {
 60 |       console.error('❌ Failed to initialize Multi-Step Agent Service:', error);
 61 | 
 62 |       agentConfigService.updateState({
 63 |         isProcessing: false
 64 |       });
 65 | 
 66 |       throw error;
 67 |     }
 68 |   }
 69 | 
 70 |   /**
 71 |    * Load MCP tools from the server
 72 |    */
 73 |   private async loadMCPTools(): Promise<void> {
 74 |     try {
 75 |       console.log('🔧 Loading MCP tools...');
 76 | 
 77 |       // Get available tools from MCP client
 78 |       const tools = await mcpClient.listTools();
 79 |       this.availableTools = tools;
 80 | 
 81 |       console.log(`🔧 Loaded ${this.availableTools.length} MCP tools:`, this.availableTools.map(t => t.name));
 82 | 
 83 |     } catch (error) {
 84 |       console.error('❌ Failed to load MCP tools:', error);
 85 |       // Continue with empty tools array for now
 86 |       this.availableTools = [];
 87 |     }
 88 |   }
 89 | 
 90 |   /**
 91 |    * Initialize agent system
 92 |    */
 93 |   private async initializeAgents(): Promise<void> {
 94 |     console.log('🤖 Initializing multi-step agent system...');
 95 | 
 96 |     // Agent system is ready - we'll use the existing AI client with multi-step logic
 97 |     console.log('✅ Multi-step agent system initialized');
 98 |   }
 99 | 
100 |   /**
101 |    * Build system prompt based on configuration and memory context
102 |    */
103 |   private async buildSystemPrompt(symbol?: string): Promise<string> {
104 |     // Get base system prompt from centralized service
105 |     const basePrompt = await systemPromptService.generateSystemPrompt({
106 |       includeTimestamp: true,
107 |       includeTools: true,
108 |       includeMemoryContext: false
109 |     });
110 | 
111 |     // Add memory context if available
112 |     const memoryContext = agentMemory.buildContextSummary(symbol);
113 |     const finalPrompt = basePrompt + memoryContext;
114 | 
115 |     return finalPrompt;
116 |   }
117 | 
118 |   /**
119 |    * Process a chat message with the agent using multi-step reasoning
120 |    */
121 |   async chat(message: string): Promise<string> {
122 |     if (!this.isInitialized) {
123 |       await this.initialize();
124 |     }
125 | 
126 |     const startTime = Date.now();
127 |     const toolCallsCount = 0;
128 | 
129 |     try {
130 |       agentConfigService.updateState({ isProcessing: true });
131 | 
132 |       console.log('💬 Processing chat message with multi-step agent...');
133 | 
134 |       // Start new conversation if needed
135 |       if (!this.currentConversationId) {
136 |         this.currentConversationId = agentMemory.startConversation();
137 |       }
138 | 
139 |       // Add user message to conversation history
140 |       const userMessage: ChatMessage = {
141 |         role: 'user',
142 |         content: message
143 |       };
144 | 
145 |       this.conversationHistory.push(userMessage);
146 |       agentMemory.addMessage(this.currentConversationId, userMessage);
147 | 
148 |       // Extract symbol from message for context
149 |       const symbolMatch = message.match(/\b([A-Z]{2,5})(?:USD|USDT)?\b/);
150 |       const symbol = symbolMatch ? symbolMatch[1] : undefined;
151 | 
152 |       // Run multi-step agent loop
153 |       const result = await this.runAgentLoop(symbol);
154 | 
155 |       // Add assistant response to memory
156 |       if (this.currentConversationId) {
157 |         const assistantMessage: ChatMessage = {
158 |           role: 'assistant',
159 |           content: result
160 |         };
161 |         agentMemory.addMessage(this.currentConversationId, assistantMessage);
162 |       }
163 | 
164 |       // Record analysis in memory
165 |       if (symbol) {
166 |         const duration = Date.now() - startTime;
167 |         agentMemory.recordAnalysis({
168 |           symbol,
169 |           analysisType: this.determineAnalysisType(),
170 |           query: message,
171 |           response: result,
172 |           toolsUsed: [], // Will be populated by runAgentLoop
173 |           duration
174 |         });
175 |       }
176 | 
177 |       // Record successful query
178 |       const duration = Date.now() - startTime;
179 |       agentConfigService.recordQuery(duration, toolCallsCount);
180 | 
181 |       return result;
182 | 
183 |     } catch (error) {
184 |       console.error('❌ Multi-step agent chat failed:', error);
185 |       agentConfigService.recordFailure();
186 | 
187 |       agentConfigService.updateState({
188 |         isProcessing: false
189 |       });
190 | 
191 |       throw error;
192 | 
193 |     } finally {
194 |       agentConfigService.updateState({ isProcessing: false });
195 |     }
196 |   }
197 | 
198 |   /**
199 |    * Run the multi-step agent reasoning loop
200 |    */
201 |   private async runAgentLoop(symbol?: string): Promise<string> {
202 |     const maxIterations = this.currentConfig.maxIterations;
203 |     let iteration = 0;
204 | 
205 |     // Build system prompt once and cache it for this conversation
206 |     const systemPrompt = await this.buildSystemPrompt(symbol);
207 |     console.log('🎯 System prompt generated once for conversation');
208 | 
209 |     const messages: ChatMessage[] = [
210 |       { role: 'system', content: systemPrompt },
211 |       ...this.conversationHistory
212 |     ];
213 | 
214 |     while (iteration < maxIterations) {
215 |       iteration++;
216 |       console.log(`🔄 Agent iteration ${iteration}/${maxIterations}`);
217 | 
218 |       // Emit workflow step event
219 |       this.emitEvent(createWorkflowEvent('workflow_step', {
220 |         stepName: `Iteration ${iteration}`,
221 |         stepDescription: 'Agent reasoning and tool execution',
222 |         progress: iteration,
223 |         totalSteps: maxIterations
224 |       }));
225 | 
226 |       // Get AI response with tool calling
227 |       const response = await aiClient.chatWithTools(messages);
228 | 
229 |       // Find the latest assistant message
230 |       const assistantMessages = response.filter(msg => msg.role === 'assistant');
231 |       const latestAssistant = assistantMessages[assistantMessages.length - 1];
232 | 
233 |       if (!latestAssistant) {
234 |         throw new Error('No assistant response received');
235 |       }
236 | 
237 |       // Check if there are tool calls
238 |       if (latestAssistant.tool_calls && latestAssistant.tool_calls.length > 0) {
239 |         console.log(`🔧 Processing ${latestAssistant.tool_calls.length} tool calls`);
240 | 
241 |         // Update conversation history with the complete response
242 |         this.conversationHistory = response.slice(1); // Remove system message
243 | 
244 |         // Continue the loop for next iteration - rebuild messages with cached system prompt
245 |         messages.length = 1; // Keep only system message (already cached)
246 |         messages.push(...this.conversationHistory);
247 | 
248 |         continue;
249 |       }
250 | 
251 |       // No more tool calls - we have the final response
252 |       if (latestAssistant.content) {
253 |         // Check if content is meaningful (not just placeholder text)
254 |         const trimmedContent = latestAssistant.content.trim();
255 |         const isPlaceholder = trimmedContent === '...' ||
256 |                              trimmedContent === '' ||
257 |                              trimmedContent.length < 3;
258 | 
259 |         if (!isPlaceholder) {
260 |           // Add final response to conversation history
261 |           this.conversationHistory.push({
262 |             role: 'assistant',
263 |             content: latestAssistant.content
264 |           });
265 | 
266 |           console.log(`✅ Multi-step agent completed in ${iteration} iterations`);
267 |           return latestAssistant.content;
268 |         } else {
269 |           console.log(`⚠️ Received placeholder content: "${trimmedContent}", continuing iteration...`);
270 |           // Continue to next iteration - treat as if no meaningful response
271 |         }
272 |       }
273 | 
274 |       // If we get here and it's not the last iteration, continue
275 |       if (iteration < maxIterations) {
276 |         console.log(`🔄 No meaningful response in iteration ${iteration}, continuing...`);
277 |         continue;
278 |       }
279 | 
280 |       // If we get here on the last iteration, something went wrong
281 |       throw new Error('Assistant response has no meaningful content and no tool calls');
282 |     }
283 | 
284 |     // Max iterations reached
285 |     const fallbackResponse = 'I apologise, but I reached the maximum number of reasoning steps. Let me provide what I can based on the analysis so far.';
286 | 
287 |     this.conversationHistory.push({
288 |       role: 'assistant',
289 |       content: fallbackResponse
290 |     });
291 | 
292 |     return fallbackResponse;
293 |   }
294 | 
295 |   /**
296 |    * Stream chat with real-time events
297 |    */
298 |   async streamChat(
299 |     message: string,
300 |     onChunk: (chunk: string) => void,
301 |     onEvent?: (event: WorkflowEvent) => void
302 |   ): Promise<void> {
303 |     if (!this.isInitialized) {
304 |       await this.initialize();
305 |     }
306 | 
307 |     const startTime = Date.now();
308 |     const toolCallsCount = 0;
309 | 
310 |     try {
311 |       agentConfigService.updateState({ isProcessing: true });
312 | 
313 |       console.log('💬 Streaming chat with multi-step agent...');
314 | 
315 |       // Subscribe to events if callback provided
316 |       let unsubscribe: (() => void) | undefined;
317 |       if (onEvent) {
318 |         unsubscribe = this.onEvent(onEvent);
319 |       }
320 | 
321 |       // Add user message to conversation history
322 |       this.conversationHistory.push({
323 |         role: 'user',
324 |         content: message
325 |       });
326 | 
327 |       // Run multi-step agent loop and stream the final response
328 |       const result = await this.runAgentLoop();
329 | 
330 |       // Stream the final result
331 |       const words = result.split(' ');
332 |       for (let i = 0; i < words.length; i++) {
333 |         const chunk = (i === 0 ? '' : ' ') + words[i];
334 |         onChunk(chunk);
335 | 
336 |         // Small delay for streaming effect
337 |         await new Promise(resolve => setTimeout(resolve, 30));
338 |       }
339 | 
340 |       // Clean up event subscription
341 |       if (unsubscribe) {
342 |         unsubscribe();
343 |       }
344 | 
345 |       // Record successful query
346 |       const duration = Date.now() - startTime;
347 |       agentConfigService.recordQuery(duration, toolCallsCount);
348 | 
349 |     } catch (error) {
350 |       console.error('❌ Multi-step agent stream chat failed:', error);
351 |       agentConfigService.recordFailure();
352 | 
353 |       agentConfigService.updateState({
354 |         isProcessing: false
355 |       });
356 | 
357 |       throw error;
358 | 
359 |     } finally {
360 |       agentConfigService.updateState({ isProcessing: false });
361 |     }
362 |   }
363 | 
364 |   /**
365 |    * Emit a workflow event
366 |    */
367 |   private emitEvent(event: WorkflowEvent): void {
368 |     this.eventEmitter.emit(event);
369 |   }
370 | 
371 |   /**
372 |    * Check if the service is connected and ready
373 |    */
374 |   async isConnected(): Promise<boolean> {
375 |     return this.isInitialized && this.availableTools.length > 0;
376 |   }
377 | 
378 |   /**
379 |    * Get current agent state
380 |    */
381 |   getState(): AgentState {
382 |     return agentConfigService.getState();
383 |   }
384 | 
385 |   /**
386 |    * Subscribe to workflow events
387 |    */
388 |   onEvent(listener: (event: WorkflowEvent) => void): () => void {
389 |     this.eventEmitter.on('all', listener);
390 |     return () => this.eventEmitter.off('all', listener);
391 |   }
392 | 
393 |   /**
394 |    * Determine analysis type based on current configuration
395 |    */
396 |   private determineAnalysisType(): 'quick' | 'standard' | 'comprehensive' {
397 |     const maxIterations = this.currentConfig.maxIterations;
398 |     if (maxIterations <= 2) return 'quick';
399 |     if (maxIterations <= 5) return 'standard';
400 |     return 'comprehensive';
401 |   }
402 | 
403 |   // Note: Market context updating will be implemented in future iterations
404 |   // when tool response interception is added to the agent loop
405 | 
406 |   /**
407 |    * Reinitialise agents when configuration changes
408 |    */
409 |   private async reinitializeAgents(): Promise<void> {
410 |     if (!this.isInitialized) return;
411 | 
412 |     console.log('🔄 Reinitialising multi-step agents due to configuration change...');
413 | 
414 |     try {
415 |       await this.initializeAgents();
416 |       console.log('✅ Multi-step agents reinitialised successfully');
417 |     } catch (error) {
418 |       console.error('❌ Failed to reinitialise multi-step agents:', error);
419 |     }
420 |   }
421 | 
422 |   /**
423 |    * Get memory statistics
424 |    */
425 |   getMemoryStats() {
426 |     return agentMemory.getMemoryStats();
427 |   }
428 | 
429 |   /**
430 |    * Get performance statistics
431 |    */
432 |   getPerformanceStats() {
433 |     return performanceOptimiser.getPerformanceStats();
434 |   }
435 | 
436 |   /**
437 |    * Get conversation history for a symbol
438 |    */
439 |   getSymbolHistory(symbol: string, limit: number = 5) {
440 |     return agentMemory.getSymbolContext(symbol, limit);
441 |   }
442 | 
443 |   /**
444 |    * Get recent analysis history
445 |    */
446 |   getAnalysisHistory(symbol?: string, limit: number = 10) {
447 |     if (symbol) {
448 |       return agentMemory.getSymbolAnalysisHistory(symbol, limit);
449 |     }
450 |     return agentMemory.getRecentAnalysisHistory(limit);
451 |   }
452 | 
453 |   /**
454 |    * Clear all memory data
455 |    */
456 |   clearMemory(): void {
457 |     agentMemory.clearAllMemory();
458 |     this.conversationHistory = [];
459 |     this.currentConversationId = undefined;
460 |     console.log('🧹 Multi-step agent memory cleared');
461 |   }
462 | 
463 |   /**
464 |    * Start a new conversation session
465 |    */
466 |   startNewConversation(): void {
467 |     this.conversationHistory = [];
468 |     this.currentConversationId = undefined;
469 |     console.log('🆕 New conversation session started');
470 |   }
471 | }
472 | 
473 | // Singleton instance
474 | export const multiStepAgent = new MultiStepAgentService();
475 | 
```

--------------------------------------------------------------------------------
/webui/src/services/aiClient.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * AI client for OpenAI-compatible API integration (Ollama, etc.)
  3 |  */
  4 | 
  5 | import type {
  6 |   AIService,
  7 |   ChatMessage,
  8 |   ChatCompletionRequest,
  9 |   ChatCompletionResponse,
 10 |   ChatCompletionStreamResponse,
 11 |   AIConfig,
 12 |   AIError,
 13 |   ModelInfo,
 14 | } from '@/types/ai';
 15 | import { mcpClient } from './mcpClient';
 16 | import { systemPromptService } from './systemPrompt';
 17 | 
 18 | export class AIClient implements AIService {
 19 |   private config: AIConfig;
 20 |   private controller?: AbortController;
 21 | 
 22 |   constructor(config: AIConfig) {
 23 |     this.config = { ...config };
 24 |   }
 25 | 
 26 |   /**
 27 |    * Send a chat completion request with tool calling support
 28 |    */
 29 |   async chat(
 30 |     messages: ChatMessage[],
 31 |     options?: Partial<ChatCompletionRequest>
 32 |   ): Promise<ChatCompletionResponse> {
 33 |     // First, try to get available tools from MCP
 34 |     let tools: any[] = [];
 35 |     try {
 36 |       const mcpTools = await mcpClient.getTools();
 37 |       tools = mcpTools.map(tool => ({
 38 |         type: 'function',
 39 |         function: {
 40 |           name: tool.name,
 41 |           description: tool.description,
 42 |           parameters: tool.inputSchema,
 43 |         },
 44 |       }));
 45 |       console.log('🔧 Available MCP tools:', tools.length);
 46 |       console.log('🔧 Tool definitions:', tools);
 47 |     } catch (error) {
 48 |       console.warn('Failed to get MCP tools:', error);
 49 |     }
 50 | 
 51 |     const request: ChatCompletionRequest = {
 52 |       model: this.config.model,
 53 |       messages,
 54 |       temperature: this.config.temperature,
 55 |       max_tokens: this.config.maxTokens,
 56 |       stream: false,
 57 |       tools: tools.length > 0 ? tools : undefined,
 58 |       tool_choice: tools.length > 0 ? 'auto' : undefined,
 59 |       ...options,
 60 |     };
 61 | 
 62 |     console.log('🚀 Sending request to AI:', {
 63 |       model: request.model,
 64 |       toolsCount: tools.length,
 65 |       hasTools: !!request.tools,
 66 |       toolChoice: request.tool_choice
 67 |     });
 68 | 
 69 |     try {
 70 |       console.log('🌐 Making request to:', `${this.config.endpoint}/v1/chat/completions`);
 71 |       console.log('📤 Request body:', JSON.stringify(request, null, 2));
 72 | 
 73 |       const response = await fetch(`${this.config.endpoint}/v1/chat/completions`, {
 74 |         method: 'POST',
 75 |         headers: {
 76 |           'Content-Type': 'application/json',
 77 |         },
 78 |         body: JSON.stringify(request),
 79 |       });
 80 | 
 81 |       console.log('📡 Response status:', response.status, response.statusText);
 82 | 
 83 |       if (!response.ok) {
 84 |         const errorText = await response.text();
 85 |         console.error('❌ Error response body:', errorText);
 86 | 
 87 |         let errorData;
 88 |         try {
 89 |           errorData = JSON.parse(errorText);
 90 |         } catch {
 91 |           errorData = { message: errorText };
 92 |         }
 93 | 
 94 |         throw this.createError(
 95 |           'API_ERROR',
 96 |           `HTTP ${response.status}: ${response.statusText}`,
 97 |           errorData
 98 |         );
 99 |       }
100 | 
101 |       const data = await response.json();
102 |       console.log('📥 AI Response:', {
103 |         choices: data.choices?.length,
104 |         hasToolCalls: !!data.choices?.[0]?.message?.tool_calls,
105 |         toolCallsCount: data.choices?.[0]?.message?.tool_calls?.length || 0,
106 |         content: data.choices?.[0]?.message?.content?.substring(0, 100) + '...'
107 |       });
108 |       return data as ChatCompletionResponse;
109 |     } catch (error) {
110 |       if (error instanceof Error && error.name === 'AbortError') {
111 |         throw this.createError('REQUEST_CANCELLED', 'Request was cancelled');
112 |       }
113 | 
114 |       if (error instanceof Error) {
115 |         throw error;
116 |       }
117 | 
118 |       throw this.createError('UNKNOWN_ERROR', 'An unknown error occurred');
119 |     }
120 |   }
121 | 
122 |   /**
123 |    * Execute tool calls and return results
124 |    */
125 |   async executeToolCalls(toolCalls: any[]): Promise<any[]> {
126 |     console.log('🔧 Executing tool calls:', toolCalls.length);
127 |     const results = [];
128 | 
129 |     for (const toolCall of toolCalls) {
130 |       try {
131 |         console.log('🔧 Processing tool call:', toolCall);
132 |         const { function: func } = toolCall;
133 | 
134 |         // Parse arguments if they're a string (from Ollama format)
135 |         const args = typeof func.arguments === 'string'
136 |           ? JSON.parse(func.arguments)
137 |           : func.arguments;
138 | 
139 |         console.log(`🔧 Calling tool ${func.name} with args:`, args);
140 |         const result = await mcpClient.callTool(func.name, args);
141 |         console.log(`✅ Tool ${func.name} result:`, result);
142 | 
143 |         // Extract reference ID from the result to include in AI context
144 |         let referenceId: string | null = null;
145 |         let actualData: any = result;
146 | 
147 |         // Check if response has content array (MCP format)
148 |         if ((result as any).content && Array.isArray((result as any).content) && (result as any).content.length > 0) {
149 |           const contentItem = (result as any).content[0];
150 |           if (contentItem.type === 'text' && contentItem.text) {
151 |             try {
152 |               actualData = JSON.parse(contentItem.text);
153 |               referenceId = actualData._referenceId;
154 |             } catch (e) {
155 |               // If parsing fails, just use the original result
156 |             }
157 |           }
158 |         } else if ((result as any)._referenceId) {
159 |           referenceId = (result as any)._referenceId;
160 |         }
161 | 
162 |         // Prepare content for AI with reference ID hint
163 |         let toolContent = JSON.stringify(result, null, 2);
164 |         if (referenceId) {
165 |           toolContent += `\n\n📋 Reference ID: ${referenceId}\n🔗 When responding to the user, please include this reference ID in square brackets like [${referenceId}] to enable data verification and interactive features.`;
166 |         }
167 | 
168 |         results.push({
169 |           tool_call_id: toolCall.id,
170 |           role: 'tool',
171 |           content: toolContent,
172 |         });
173 |       } catch (error) {
174 |         console.error(`❌ Tool execution failed for ${toolCall.function?.name}:`, error);
175 |         results.push({
176 |           tool_call_id: toolCall.id,
177 |           role: 'tool',
178 |           content: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
179 |         });
180 |       }
181 |     }
182 | 
183 |     console.log('🔧 Tool execution results:', results);
184 |     return results;
185 |   }
186 | 
187 |   /**
188 |    * Parse tool calls from text content (fallback for models that don't support function calling)
189 |    */
190 |   private parseToolCallsFromText(content: string): { toolCalls: any[], cleanContent: string } {
191 |     // Match both single and triple backticks
192 |     const toolCallPattern = /(`{1,3})tool_code\s*\n?([^`]+)\1/g;
193 |     const toolCalls: any[] = [];
194 |     let cleanContent = content;
195 | 
196 |     let match;
197 |     while ((match = toolCallPattern.exec(content)) !== null) {
198 |       const toolCallText = match[2].trim(); // match[2] is the content, match[1] is the backticks
199 | 
200 |       // Parse function call like: get_ticker(symbol="BTCUSDT")
201 |       const functionCallPattern = /(\w+)\s*\(\s*([^)]*)\s*\)/;
202 |       const funcMatch = functionCallPattern.exec(toolCallText);
203 | 
204 |       if (funcMatch) {
205 |         const functionName = funcMatch[1];
206 |         const argsString = funcMatch[2];
207 | 
208 |         // Parse arguments (simple key=value parsing)
209 |         const args: Record<string, any> = {};
210 |         if (argsString) {
211 |           const argPattern = /(\w+)\s*=\s*"([^"]+)"/g;
212 |           let argMatch;
213 |           while ((argMatch = argPattern.exec(argsString)) !== null) {
214 |             args[argMatch[1]] = argMatch[2];
215 |           }
216 |         }
217 | 
218 |         toolCalls.push({
219 |           id: `call_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,
220 |           type: 'function',
221 |           function: {
222 |             name: functionName,
223 |             arguments: JSON.stringify(args)
224 |           }
225 |         });
226 | 
227 |         // Remove the tool call from content
228 |         cleanContent = cleanContent.replace(match[0], '').trim();
229 |       }
230 |     }
231 | 
232 |     return { toolCalls, cleanContent };
233 |   }
234 | 
235 |   /**
236 |    * Send a chat completion with automatic tool calling
237 |    */
238 |   async chatWithTools(messages: ChatMessage[]): Promise<ChatMessage[]> {
239 |     const conversationMessages = [...messages];
240 |     let response = await this.chat(conversationMessages);
241 | 
242 |     // Check if the response contains tool calls
243 |     const choice = response.choices[0];
244 |     let toolCalls = choice?.message?.tool_calls;
245 |     let content = choice?.message?.content || '';
246 | 
247 |     // If no native tool calls, try to parse from text content
248 |     if (!toolCalls && content) {
249 |       const parsed = this.parseToolCallsFromText(content);
250 |       if (parsed.toolCalls.length > 0) {
251 |         toolCalls = parsed.toolCalls;
252 |         content = parsed.cleanContent;
253 |         console.log('🔍 Parsed tool calls from text:', toolCalls);
254 |       }
255 |     }
256 | 
257 |     if (toolCalls && toolCalls.length > 0) {
258 |       // Add the assistant's message with tool calls
259 |       conversationMessages.push({
260 |         role: 'assistant',
261 |         content: content,
262 |         tool_calls: toolCalls,
263 |       });
264 | 
265 |       // Execute tool calls
266 |       const toolResults = await this.executeToolCalls(toolCalls);
267 | 
268 |       // Add tool results to conversation
269 |       conversationMessages.push(...toolResults);
270 | 
271 |       // Get final response with tool results - reuse the same conversation context
272 |       console.log('🔄 Getting final response with tool results (system prompt already included)');
273 |       response = await this.chat(conversationMessages);
274 |     }
275 | 
276 |     return conversationMessages.concat({
277 |       role: 'assistant',
278 |       content: response.choices[0]?.message?.content || '',
279 |     });
280 |   }
281 | 
282 |   /**
283 |    * Send a streaming chat completion request
284 |    */
285 |   async streamChat(
286 |     messages: ChatMessage[],
287 |     onChunk: (chunk: ChatCompletionStreamResponse) => void,
288 |     options?: Partial<ChatCompletionRequest>
289 |   ): Promise<void> {
290 |     // Cancel any existing stream
291 |     this.cancelStream();
292 | 
293 |     this.controller = new AbortController();
294 | 
295 |     const request: ChatCompletionRequest = {
296 |       model: this.config.model,
297 |       messages,
298 |       temperature: this.config.temperature,
299 |       max_tokens: this.config.maxTokens,
300 |       stream: true,
301 |       ...options,
302 |     };
303 | 
304 |     try {
305 |       const response = await fetch(`${this.config.endpoint}/v1/chat/completions`, {
306 |         method: 'POST',
307 |         headers: {
308 |           'Content-Type': 'application/json',
309 |         },
310 |         body: JSON.stringify(request),
311 |         signal: this.controller.signal,
312 |       });
313 | 
314 |       if (!response.ok) {
315 |         const errorData = await response.json().catch(() => ({}));
316 |         throw this.createError(
317 |           'API_ERROR',
318 |           `HTTP ${response.status}: ${response.statusText}`,
319 |           errorData
320 |         );
321 |       }
322 | 
323 |       if (!response.body) {
324 |         throw this.createError('STREAM_ERROR', 'No response body received');
325 |       }
326 | 
327 |       const reader = response.body.getReader();
328 |       const decoder = new TextDecoder();
329 | 
330 |       try {
331 |         while (true) {
332 |           const { done, value } = await reader.read();
333 | 
334 |           if (done) break;
335 | 
336 |           const chunk = decoder.decode(value, { stream: true });
337 |           const lines = chunk.split('\n').filter(line => line.trim());
338 | 
339 |           for (const line of lines) {
340 |             if (line.startsWith('data: ')) {
341 |               const data = line.slice(6);
342 | 
343 |               if (data === '[DONE]') {
344 |                 return;
345 |               }
346 | 
347 |               try {
348 |                 const parsed = JSON.parse(data) as ChatCompletionStreamResponse;
349 |                 onChunk(parsed);
350 |               } catch (parseError) {
351 |                 console.warn('Failed to parse streaming chunk:', parseError);
352 |               }
353 |             }
354 |           }
355 |         }
356 |       } finally {
357 |         reader.releaseLock();
358 |       }
359 |     } catch (error) {
360 |       if (error instanceof Error && error.name === 'AbortError') {
361 |         throw this.createError('REQUEST_CANCELLED', 'Stream was cancelled');
362 |       }
363 | 
364 |       if (error instanceof Error) {
365 |         throw error;
366 |       }
367 | 
368 |       throw this.createError('STREAM_ERROR', 'Streaming failed');
369 |     } finally {
370 |       this.controller = undefined;
371 |     }
372 |   }
373 | 
374 |   /**
375 |    * Cancel the current streaming request
376 |    */
377 |   cancelStream(): void {
378 |     if (this.controller) {
379 |       this.controller.abort();
380 |       this.controller = undefined;
381 |     }
382 |   }
383 | 
384 |   /**
385 |    * Check if the AI service is connected and available
386 |    */
387 |   async isConnected(): Promise<boolean> {
388 |     try {
389 |       const response = await fetch(`${this.config.endpoint}/v1/models`, {
390 |         method: 'GET',
391 |         signal: AbortSignal.timeout(5000), // 5 second timeout
392 |       });
393 | 
394 |       return response.ok;
395 |     } catch {
396 |       return false;
397 |     }
398 |   }
399 | 
400 |   /**
401 |    * Get available models from the AI service
402 |    */
403 |   async getModels(): Promise<ModelInfo[]> {
404 |     try {
405 |       const response = await fetch(`${this.config.endpoint}/v1/models`);
406 | 
407 |       if (!response.ok) {
408 |         throw this.createError('API_ERROR', 'Failed to fetch models');
409 |       }
410 | 
411 |       const data = await response.json();
412 | 
413 |       if (data.data && Array.isArray(data.data)) {
414 |         return data.data.map((model: any) => ({
415 |           id: model.id,
416 |           name: model.id,
417 |           description: model.description,
418 |           contextLength: model.context_length,
419 |           capabilities: model.capabilities,
420 |         }));
421 |       }
422 | 
423 |       return [];
424 |     } catch (error) {
425 |       console.error('Failed to fetch models:', error);
426 |       return [];
427 |     }
428 |   }
429 | 
430 |   /**
431 |    * Update the AI configuration
432 |    */
433 |   updateConfig(newConfig: Partial<AIConfig>): void {
434 |     this.config = { ...this.config, ...newConfig };
435 |   }
436 | 
437 |   /**
438 |    * Get current configuration
439 |    */
440 |   getConfig(): AIConfig {
441 |     return { ...this.config };
442 |   }
443 | 
444 | 
445 | 
446 |   /**
447 |    * Create a standardised error object
448 |    */
449 |   private createError(code: string, message: string, details?: unknown): AIError {
450 |     const error = new Error(message) as Error & AIError;
451 |     error.code = code;
452 |     error.message = message;
453 |     error.details = details;
454 |     return error;
455 |   }
456 | }
457 | 
458 | // Function to generate system prompt with current timestamp
459 | export function generateSystemPrompt(): string {
460 |   // Use the centralized system prompt service for legacy compatibility
461 |   return systemPromptService.generateLegacySystemPrompt();
462 | }
463 | 
464 | // Async function to generate system prompt with dynamic tools
465 | export async function generateDynamicSystemPrompt(): Promise<string> {
466 |   return await systemPromptService.generateSystemPrompt({
467 |     includeTimestamp: true,
468 |     includeTools: true,
469 |     includeMemoryContext: false
470 |   });
471 | }
472 | 
473 | // Default system prompt for Bybit MCP integration (for backward compatibility)
474 | export const DEFAULT_SYSTEM_PROMPT = generateSystemPrompt();
475 | 
476 | // Create default AI client instance
477 | export function createAIClient(config?: Partial<AIConfig>): AIClient {
478 |   const defaultConfig: AIConfig = {
479 |     endpoint: 'http://localhost:11434',
480 |     model: 'qwen3-30b-a3b-ud-nothink-128k:q4_k_xl',
481 |     temperature: 0.7,
482 |     maxTokens: 2048,
483 |     systemPrompt: DEFAULT_SYSTEM_PROMPT,
484 |     ...config,
485 |   };
486 | 
487 |   return new AIClient(defaultConfig);
488 | }
489 | 
490 | // Singleton instance
491 | export const aiClient = createAIClient();
492 | 
```

--------------------------------------------------------------------------------
/webui/src/components/AgentDashboard.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Agent Dashboard - Shows memory, performance, and analysis statistics
  3 |  */
  4 | 
  5 | import { multiStepAgent } from '@/services/multiStepAgent';
  6 | import type { ChatApp } from './ChatApp';
  7 | 
  8 | // Interface for memory statistics
  9 | interface MemoryStats {
 10 |   conversations: number;
 11 |   marketContexts: number;
 12 |   analysisHistory: number;
 13 |   totalSymbols: number;
 14 | }
 15 | 
 16 | export class AgentDashboard {
 17 |   private container: HTMLElement;
 18 |   private isVisible: boolean = false;
 19 |   private refreshInterval: NodeJS.Timeout | null = null;
 20 |   private chatApp?: ChatApp;
 21 | 
 22 |   constructor(containerId: string, chatApp?: ChatApp) {
 23 |     this.container = document.getElementById(containerId)!;
 24 |     if (!this.container) {
 25 |       throw new Error(`Container element with id "${containerId}" not found`);
 26 |     }
 27 | 
 28 |     this.chatApp = chatApp;
 29 |     this.initialize();
 30 |   }
 31 | 
 32 |   private initialize(): void {
 33 |     this.createDashboardStructure();
 34 |     this.setupEventListeners();
 35 |     this.startAutoRefresh();
 36 |   }
 37 | 
 38 |   private createDashboardStructure(): void {
 39 |     this.container.innerHTML = `
 40 |       <div class="agent-dashboard ${this.isVisible ? 'visible' : 'hidden'}">
 41 |         <div class="dashboard-header">
 42 |           <h3>Agent Dashboard</h3>
 43 |           <div class="dashboard-controls">
 44 |             <button class="refresh-btn" id="dashboard-refresh" aria-label="Refresh dashboard">
 45 |               🔄
 46 |             </button>
 47 |             <button class="toggle-btn" id="dashboard-toggle" aria-label="Toggle dashboard">
 48 |               📊
 49 |             </button>
 50 |           </div>
 51 |         </div>
 52 | 
 53 |         <div class="dashboard-content">
 54 |           <!-- Empty State Notice -->
 55 |           <div class="dashboard-notice" id="dashboard-notice" style="display: none;">
 56 |             <div class="notice-content">
 57 |               <h4>🤖 Agent Dashboard</h4>
 58 |               <p>This dashboard will show agent performance metrics, memory usage, and analysis history once you start using the agent mode.</p>
 59 |               <p><strong>To get started:</strong></p>
 60 |               <ol>
 61 |                 <li>Enable Agent Mode in Settings (⚙️)</li>
 62 |                 <li>Ask questions about cryptocurrency markets</li>
 63 |                 <li>Watch the dashboard populate with data!</li>
 64 |               </ol>
 65 |             </div>
 66 |           </div>
 67 | 
 68 |           <!-- Memory Statistics -->
 69 |           <div class="dashboard-section">
 70 |             <h4>Memory Statistics</h4>
 71 |             <div class="stats-grid" id="memory-stats">
 72 |               <div class="stat-item">
 73 |                 <span class="stat-label">Conversations:</span>
 74 |                 <span class="stat-value" id="memory-conversations">0</span>
 75 |               </div>
 76 |               <div class="stat-item">
 77 |                 <span class="stat-label">Market Contexts:</span>
 78 |                 <span class="stat-value" id="memory-contexts">0</span>
 79 |               </div>
 80 |               <div class="stat-item">
 81 |                 <span class="stat-label">Analysis History:</span>
 82 |                 <span class="stat-value" id="memory-analyses">0</span>
 83 |               </div>
 84 |               <div class="stat-item">
 85 |                 <span class="stat-label">Tracked Symbols:</span>
 86 |                 <span class="stat-value" id="memory-symbols">0</span>
 87 |               </div>
 88 |             </div>
 89 |           </div>
 90 | 
 91 |           <!-- Performance Statistics -->
 92 |           <div class="dashboard-section">
 93 |             <h4>Performance Statistics</h4>
 94 |             <div class="stats-grid" id="performance-stats">
 95 |               <div class="stat-item">
 96 |                 <span class="stat-label">Success Rate:</span>
 97 |                 <span class="stat-value" id="perf-success-rate">0%</span>
 98 |               </div>
 99 |               <div class="stat-item">
100 |                 <span class="stat-label">Avg Tool Time:</span>
101 |                 <span class="stat-value" id="perf-avg-time">0ms</span>
102 |               </div>
103 |               <div class="stat-item">
104 |                 <span class="stat-label">Parallel Savings:</span>
105 |                 <span class="stat-value" id="perf-savings">0ms</span>
106 |               </div>
107 |               <div class="stat-item">
108 |                 <span class="stat-label">Total Tools:</span>
109 |                 <span class="stat-value" id="perf-tool-count">0</span>
110 |               </div>
111 |             </div>
112 |           </div>
113 | 
114 |           <!-- Recent Analysis -->
115 |           <div class="dashboard-section">
116 |             <h4>Recent Analysis</h4>
117 |             <div class="analysis-list" id="recent-analysis">
118 |               <div class="empty-state">
119 |                 <p>No recent analysis available.</p>
120 |               </div>
121 |             </div>
122 |           </div>
123 | 
124 |           <!-- Actions -->
125 |           <div class="dashboard-section">
126 |             <h4>Actions</h4>
127 |             <div class="action-buttons">
128 |               <button class="action-btn" id="clear-memory">Clear Memory</button>
129 |               <button class="action-btn" id="new-conversation">New Conversation</button>
130 |               <button class="action-btn" id="export-data">Export Data</button>
131 |             </div>
132 |           </div>
133 |         </div>
134 |       </div>
135 |     `;
136 |   }
137 | 
138 |   private setupEventListeners(): void {
139 |     // Toggle dashboard visibility
140 |     const toggleBtn = this.container.querySelector('#dashboard-toggle') as HTMLButtonElement;
141 |     toggleBtn?.addEventListener('click', () => {
142 |       this.toggleVisibility();
143 |     });
144 | 
145 |     // Refresh dashboard
146 |     const refreshBtn = this.container.querySelector('#dashboard-refresh') as HTMLButtonElement;
147 |     refreshBtn?.addEventListener('click', () => {
148 |       this.refreshDashboard();
149 |     });
150 | 
151 |     // Clear memory
152 |     const clearMemoryBtn = this.container.querySelector('#clear-memory') as HTMLButtonElement;
153 |     clearMemoryBtn?.addEventListener('click', () => {
154 |       this.clearMemory();
155 |     });
156 | 
157 |     // New conversation
158 |     const newConversationBtn = this.container.querySelector('#new-conversation') as HTMLButtonElement;
159 |     newConversationBtn?.addEventListener('click', () => {
160 |       this.startNewConversation();
161 |     });
162 | 
163 |     // Export data
164 |     const exportDataBtn = this.container.querySelector('#export-data') as HTMLButtonElement;
165 |     exportDataBtn?.addEventListener('click', () => {
166 |       this.exportData();
167 |     });
168 | 
169 |     // Keyboard shortcut to toggle dashboard (Ctrl/Cmd + M)
170 |     document.addEventListener('keydown', (e) => {
171 |       if ((e.ctrlKey || e.metaKey) && e.key === 'm') {
172 |         e.preventDefault();
173 |         this.toggleVisibility();
174 |       }
175 |     });
176 |   }
177 | 
178 |   private startAutoRefresh(): void {
179 |     // Refresh every 5 seconds when dashboard is visible
180 |     this.refreshInterval = setInterval(() => {
181 |       if (this.isVisible) {
182 |         this.refreshDashboard();
183 |       }
184 |     }, 5000);
185 |   }
186 | 
187 |   public toggleVisibility(): void {
188 |     this.isVisible = !this.isVisible;
189 |     const dashboard = this.container.querySelector('.agent-dashboard');
190 | 
191 |     if (this.isVisible) {
192 |       dashboard?.classList.remove('hidden');
193 |       dashboard?.classList.add('visible');
194 |       this.refreshDashboard();
195 |     } else {
196 |       dashboard?.classList.remove('visible');
197 |       dashboard?.classList.add('hidden');
198 |     }
199 |   }
200 | 
201 |   public show(): void {
202 |     if (!this.isVisible) {
203 |       this.toggleVisibility();
204 |     }
205 |   }
206 | 
207 |   public hide(): void {
208 |     if (this.isVisible) {
209 |       this.toggleVisibility();
210 |     }
211 |   }
212 | 
213 |   public get visible(): boolean {
214 |     return this.isVisible;
215 |   }
216 | 
217 |   private refreshDashboard(): void {
218 |     this.updateMemoryStats();
219 |     this.updatePerformanceStats();
220 |     this.updateRecentAnalysis();
221 |   }
222 | 
223 |   private updateMemoryStats(): void {
224 |     try {
225 |       const memoryStats = multiStepAgent.getMemoryStats();
226 | 
227 |       const conversationsEl = this.container.querySelector('#memory-conversations');
228 |       const contextsEl = this.container.querySelector('#memory-contexts');
229 |       const analysesEl = this.container.querySelector('#memory-analyses');
230 |       const symbolsEl = this.container.querySelector('#memory-symbols');
231 | 
232 |       if (conversationsEl) conversationsEl.textContent = memoryStats.conversations.toString();
233 |       if (contextsEl) contextsEl.textContent = memoryStats.marketContexts.toString();
234 |       if (analysesEl) analysesEl.textContent = memoryStats.analysisHistory.toString();
235 |       if (symbolsEl) symbolsEl.textContent = memoryStats.totalSymbols.toString();
236 | 
237 |       // Show helpful message if no data yet
238 |       this.updateEmptyStateMessage(memoryStats);
239 | 
240 |     } catch (error) {
241 |       console.warn('Failed to update memory stats:', error);
242 |     }
243 |   }
244 | 
245 |   private updatePerformanceStats(): void {
246 |     try {
247 |       const perfStats = multiStepAgent.getPerformanceStats();
248 | 
249 |       const successRateEl = this.container.querySelector('#perf-success-rate');
250 |       const avgTimeEl = this.container.querySelector('#perf-avg-time');
251 |       const savingsEl = this.container.querySelector('#perf-savings');
252 |       const toolCountEl = this.container.querySelector('#perf-tool-count');
253 | 
254 |       if (successRateEl) {
255 |         successRateEl.textContent = `${(perfStats.successRate * 100).toFixed(1)}%`;
256 |       }
257 |       if (avgTimeEl) {
258 |         avgTimeEl.textContent = `${Math.round(perfStats.averageToolTime)}ms`;
259 |       }
260 |       if (savingsEl) {
261 |         savingsEl.textContent = `${Math.round(perfStats.parallelSavings)}ms`;
262 |       }
263 |       if (toolCountEl) {
264 |         toolCountEl.textContent = perfStats.toolCount.toString();
265 |       }
266 | 
267 |     } catch (error) {
268 |       console.warn('Failed to update performance stats:', error);
269 |     }
270 |   }
271 | 
272 |   private updateRecentAnalysis(): void {
273 |     try {
274 |       const recentAnalysis = multiStepAgent.getAnalysisHistory(undefined, 5);
275 |       const listContainer = this.container.querySelector('#recent-analysis');
276 | 
277 |       if (!listContainer) return;
278 | 
279 |       if (recentAnalysis.length === 0) {
280 |         listContainer.innerHTML = `
281 |           <div class="empty-state">
282 |             <p>No recent analysis available.</p>
283 |           </div>
284 |         `;
285 |         return;
286 |       }
287 | 
288 |       const analysisHtml = recentAnalysis.map(analysis => `
289 |         <div class="analysis-item">
290 |           <div class="analysis-header">
291 |             <span class="analysis-symbol">${analysis.symbol}</span>
292 |             <span class="analysis-type">${analysis.analysisType}</span>
293 |             <span class="analysis-time">${this.getTimeAgo(analysis.timestamp)}</span>
294 |           </div>
295 |           <div class="analysis-query">${this.truncateText(analysis.query, 60)}</div>
296 |           <div class="analysis-metrics">
297 |             <span class="metric">Duration: ${analysis.duration}ms</span>
298 |             <span class="metric">Tools: ${analysis.toolsUsed.length}</span>
299 |             ${analysis.accuracy ? `<span class="metric">Accuracy: ${(analysis.accuracy * 100).toFixed(0)}%</span>` : ''}
300 |           </div>
301 |         </div>
302 |       `).join('');
303 | 
304 |       listContainer.innerHTML = analysisHtml;
305 | 
306 |     } catch (error) {
307 |       console.warn('Failed to update recent analysis:', error);
308 |     }
309 |   }
310 | 
311 |   private clearMemory(): void {
312 |     if (confirm('Are you sure you want to clear all agent memory? This action cannot be undone.')) {
313 |       multiStepAgent.clearMemory();
314 |       this.refreshDashboard();
315 |       this.showToast('Memory cleared successfully!');
316 |     }
317 |   }
318 | 
319 |   private startNewConversation(): void {
320 |     // Clear agent memory
321 |     multiStepAgent.startNewConversation();
322 | 
323 |     // Clear chat UI if available
324 |     if (this.chatApp) {
325 |       this.chatApp.clearMessages();
326 |     }
327 | 
328 |     this.showToast('New conversation started!');
329 |   }
330 | 
331 |   private exportData(): void {
332 |     try {
333 |       const data = {
334 |         memoryStats: multiStepAgent.getMemoryStats(),
335 |         performanceStats: multiStepAgent.getPerformanceStats(),
336 |         recentAnalysis: multiStepAgent.getAnalysisHistory(undefined, 20),
337 |         exportedAt: new Date().toISOString()
338 |       };
339 | 
340 |       const dataStr = JSON.stringify(data, null, 2);
341 |       const blob = new Blob([dataStr], { type: 'application/json' });
342 |       const url = URL.createObjectURL(blob);
343 | 
344 |       const a = document.createElement('a');
345 |       a.href = url;
346 |       a.download = `agent-data-${new Date().toISOString().split('T')[0]}.json`;
347 |       document.body.appendChild(a);
348 |       a.click();
349 |       document.body.removeChild(a);
350 |       URL.revokeObjectURL(url);
351 | 
352 |       this.showToast('Data exported successfully!');
353 | 
354 |     } catch (error) {
355 |       console.error('Failed to export data:', error);
356 |       this.showToast('Failed to export data', 'error');
357 |     }
358 |   }
359 | 
360 |   private showToast(message: string, type: 'success' | 'error' = 'success'): void {
361 |     const toast = document.createElement('div');
362 |     toast.className = `dashboard-toast toast-${type}`;
363 |     toast.textContent = message;
364 | 
365 |     document.body.appendChild(toast);
366 | 
367 |     // Animate in
368 |     setTimeout(() => toast.classList.add('show'), 10);
369 | 
370 |     // Remove after 3 seconds
371 |     setTimeout(() => {
372 |       toast.classList.remove('show');
373 |       setTimeout(() => toast.remove(), 300);
374 |     }, 3000);
375 |   }
376 | 
377 |   private getTimeAgo(timestamp: number): string {
378 |     const now = Date.now();
379 |     const diff = now - timestamp;
380 | 
381 |     const seconds = Math.floor(diff / 1000);
382 |     const minutes = Math.floor(seconds / 60);
383 |     const hours = Math.floor(minutes / 60);
384 | 
385 |     if (seconds < 60) return `${seconds}s ago`;
386 |     if (minutes < 60) return `${minutes}m ago`;
387 |     if (hours < 24) return `${hours}h ago`;
388 | 
389 |     return new Date(timestamp).toLocaleDateString();
390 |   }
391 | 
392 |   private truncateText(text: string, maxLength: number): string {
393 |     if (text.length <= maxLength) return text;
394 |     return text.substring(0, maxLength) + '...';
395 |   }
396 | 
397 |   private updateEmptyStateMessage(memoryStats: MemoryStats): void {
398 |     const hasData = memoryStats.conversations > 0 || memoryStats.analysisHistory > 0;
399 | 
400 |     if (!hasData) {
401 |       // Add notice to dashboard if no data
402 |       const dashboardContent = this.container.querySelector('.dashboard-content');
403 |       if (dashboardContent && !dashboardContent.querySelector('.dashboard-notice')) {
404 |         const notice = document.createElement('div');
405 |         notice.className = 'dashboard-notice';
406 |         notice.innerHTML = `
407 |           <div class="notice-content">
408 |             <h4>🤖 Agent Dashboard</h4>
409 |             <p>This dashboard will show agent performance metrics, memory usage, and analysis history once you start using the agent mode.</p>
410 |             <p><strong>To get started:</strong></p>
411 |             <ol>
412 |               <li>Enable Agent Mode in Settings (⚙️)</li>
413 |               <li>Ask questions about cryptocurrency markets</li>
414 |               <li>Watch the dashboard populate with data!</li>
415 |             </ol>
416 |           </div>
417 |         `;
418 |         dashboardContent.insertBefore(notice, dashboardContent.firstChild);
419 |       }
420 |     } else {
421 |       // Remove notice if data exists
422 |       const notice = this.container.querySelector('.dashboard-notice');
423 |       if (notice) {
424 |         notice.remove();
425 |       }
426 |     }
427 |   }
428 | 
429 |   public destroy(): void {
430 |     if (this.refreshInterval) {
431 |       clearInterval(this.refreshInterval);
432 |       this.refreshInterval = null;
433 |     }
434 |   }
435 | }
436 | 
```

--------------------------------------------------------------------------------
/webui/src/components/chat/DataCard.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * DataCard Component - Expandable cards for visualising tool response data
  3 |  *
  4 |  * Provides a clean, collapsible interface for displaying structured data
  5 |  * with embedded visualisations when expanded.
  6 |  */
  7 | 
  8 | export interface DataCardConfig {
  9 |   title: string;
 10 |   summary: string;
 11 |   data: any;
 12 |   dataType: 'kline' | 'rsi' | 'orderBlocks' | 'price' | 'volume' | 'unknown';
 13 |   expanded?: boolean;
 14 |   showChart?: boolean;
 15 | }
 16 | 
 17 | export class DataCard {
 18 |   private container: HTMLElement;
 19 |   private config: DataCardConfig;
 20 |   private isExpanded: boolean = false;
 21 |   private chartContainer?: HTMLElement;
 22 | 
 23 |   constructor(container: HTMLElement, config: DataCardConfig) {
 24 |     this.container = container;
 25 |     this.config = config;
 26 |     this.isExpanded = config.expanded || false;
 27 | 
 28 |     this.render();
 29 |     this.setupEventListeners();
 30 | 
 31 |     // If expanded by default, render chart after DOM is ready
 32 |     if (this.isExpanded) {
 33 |       setTimeout(() => {
 34 |         this.renderChart();
 35 |       }, 150);
 36 |     }
 37 |   }
 38 | 
 39 |   /**
 40 |    * Render the data card structure
 41 |    */
 42 |   private render(): void {
 43 |     this.container.innerHTML = `
 44 |       <div class="data-card ${this.isExpanded ? 'expanded' : 'collapsed'}" data-type="${this.config.dataType}">
 45 |         <div class="data-card-header" role="button" tabindex="0" aria-expanded="${this.isExpanded}">
 46 |           <div class="data-card-title">
 47 |             <span class="data-card-icon">${this.getDataTypeIcon()}</span>
 48 |             <h4>${this.config.title}</h4>
 49 |           </div>
 50 |           <div class="data-card-controls">
 51 |             <span class="data-card-summary">${this.config.summary}</span>
 52 |             <button class="expand-toggle" aria-label="${this.isExpanded ? 'Collapse' : 'Expand'} data card">
 53 |               <span class="expand-icon">${this.isExpanded ? '▼' : '▶'}</span>
 54 |             </button>
 55 |           </div>
 56 |         </div>
 57 |         <div class="data-card-content" ${this.isExpanded ? '' : 'style="display: none;"'}>
 58 |           <div class="data-card-details">
 59 |             ${this.renderDataSummary()}
 60 |           </div>
 61 |           ${this.config.showChart !== false ? '<div class="data-card-chart" id="chart-' + this.generateId() + '"></div>' : ''}
 62 |         </div>
 63 |       </div>
 64 |     `;
 65 | 
 66 |     // Store reference to chart container if it exists
 67 |     const chartElement = this.container.querySelector('.data-card-chart') as HTMLElement;
 68 |     if (chartElement) {
 69 |       this.chartContainer = chartElement;
 70 |     }
 71 |   }
 72 | 
 73 |   /**
 74 |    * Set up event listeners for card interactions
 75 |    */
 76 |   private setupEventListeners(): void {
 77 |     const header = this.container.querySelector('.data-card-header') as HTMLElement;
 78 |     const toggleButton = this.container.querySelector('.expand-toggle') as HTMLElement;
 79 | 
 80 |     if (header) {
 81 |       header.addEventListener('click', () => this.toggle());
 82 |       header.addEventListener('keydown', (e) => {
 83 |         if (e.key === 'Enter' || e.key === ' ') {
 84 |           e.preventDefault();
 85 |           this.toggle();
 86 |         }
 87 |       });
 88 |     }
 89 | 
 90 |     if (toggleButton) {
 91 |       toggleButton.addEventListener('click', (e) => {
 92 |         e.stopPropagation();
 93 |         this.toggle();
 94 |       });
 95 |     }
 96 |   }
 97 | 
 98 |   /**
 99 |    * Toggle card expanded/collapsed state
100 |    */
101 |   public toggle(): void {
102 |     this.isExpanded = !this.isExpanded;
103 |     this.updateExpandedState();
104 |   }
105 | 
106 |   /**
107 |    * Expand the card
108 |    */
109 |   public expand(): void {
110 |     if (!this.isExpanded) {
111 |       this.isExpanded = true;
112 |       this.updateExpandedState();
113 |     }
114 |   }
115 | 
116 |   /**
117 |    * Collapse the card
118 |    */
119 |   public collapse(): void {
120 |     if (this.isExpanded) {
121 |       this.isExpanded = false;
122 |       this.updateExpandedState();
123 |     }
124 |   }
125 | 
126 |   /**
127 |    * Update the visual state when expanded/collapsed
128 |    */
129 |   private updateExpandedState(): void {
130 |     const card = this.container.querySelector('.data-card') as HTMLElement;
131 |     const content = this.container.querySelector('.data-card-content') as HTMLElement;
132 |     const header = this.container.querySelector('.data-card-header') as HTMLElement;
133 |     const expandIcon = this.container.querySelector('.expand-icon') as HTMLElement;
134 | 
135 |     if (card && content && header && expandIcon) {
136 |       // Update classes
137 |       card.classList.toggle('expanded', this.isExpanded);
138 |       card.classList.toggle('collapsed', !this.isExpanded);
139 | 
140 |       // Update ARIA attributes
141 |       header.setAttribute('aria-expanded', this.isExpanded.toString());
142 | 
143 |       // Update expand icon
144 |       expandIcon.textContent = this.isExpanded ? '▼' : '▶';
145 | 
146 |       // Update button label
147 |       const toggleButton = this.container.querySelector('.expand-toggle') as HTMLElement;
148 |       if (toggleButton) {
149 |         toggleButton.setAttribute('aria-label', `${this.isExpanded ? 'Collapse' : 'Expand'} data card`);
150 |       }
151 | 
152 |       // Animate content visibility
153 |       if (this.isExpanded) {
154 |         content.style.display = 'block';
155 |         // Trigger chart rendering with a small delay to ensure DOM is ready
156 |         setTimeout(() => {
157 |           this.renderChart();
158 |         }, 100);
159 |       } else {
160 |         // Add a small delay to allow animation
161 |         setTimeout(() => {
162 |           if (!this.isExpanded) {
163 |             content.style.display = 'none';
164 |           }
165 |         }, 250);
166 |       }
167 |     }
168 |   }
169 | 
170 |   /**
171 |    * Get appropriate icon for data type
172 |    */
173 |   private getDataTypeIcon(): string {
174 |     switch (this.config.dataType) {
175 |       case 'kline': return '📈';
176 |       case 'rsi': return '📊';
177 |       case 'orderBlocks': return '🧱';
178 |       case 'price': return '💰';
179 |       case 'volume': return '📊';
180 |       default: return '📋';
181 |     }
182 |   }
183 | 
184 |   /**
185 |    * Render data summary in the expanded view
186 |    */
187 |   private renderDataSummary(): string {
188 |     // This will be enhanced based on data type
189 |     if (typeof this.config.data === 'object') {
190 |       return `<pre class="data-preview">${JSON.stringify(this.config.data, null, 2)}</pre>`;
191 |     }
192 |     return `<div class="data-preview">${this.config.data}</div>`;
193 |   }
194 | 
195 |   /**
196 |    * Render chart when card is expanded
197 |    */
198 |   private renderChart(): void {
199 |     if (!this.chartContainer || this.config.showChart === false) {
200 |       return;
201 |     }
202 | 
203 |     // Render different chart types based on data type
204 |     switch (this.config.dataType) {
205 |       case 'kline':
206 |         this.renderCandlestickChart();
207 |         break;
208 |       case 'rsi':
209 |         this.renderLineChart();
210 |         break;
211 |       case 'price':
212 |         this.renderPriceChart();
213 |         break;
214 |       default:
215 |         this.renderPlaceholder();
216 |         break;
217 |     }
218 |   }
219 | 
220 |   /**
221 |    * Format timestamp for X-axis labels
222 |    */
223 |   private formatTimestamp(timestamp: number, interval: string): string {
224 |     const date = new Date(timestamp);
225 | 
226 |     // For different intervals, show different levels of detail
227 |     switch (interval) {
228 |       case '1':  // 1 minute
229 |       case '5':  // 5 minutes
230 |       case '15': // 15 minutes
231 |       case '30': // 30 minutes
232 |         return date.toLocaleTimeString('en-US', {
233 |           hour: '2-digit',
234 |           minute: '2-digit',
235 |           hour12: false
236 |         });
237 |       case '60':  // 1 hour
238 |       case '240': // 4 hours
239 |         return date.toLocaleDateString('en-US', {
240 |           month: 'short',
241 |           day: 'numeric',
242 |           hour: '2-digit'
243 |         });
244 |       case 'D':   // Daily
245 |       case 'W':   // Weekly
246 |       default:
247 |         return date.toLocaleDateString('en-US', {
248 |           month: 'short',
249 |           day: 'numeric'
250 |         });
251 |     }
252 |   }
253 | 
254 |   /**
255 |    * Render candlestick chart for kline data
256 |    */
257 |   private renderCandlestickChart(): void {
258 |     if (!this.chartContainer) return;
259 | 
260 |     // Extract kline data
261 |     let klineData = this.config.data;
262 |     if (this.config.data.data && Array.isArray(this.config.data.data)) {
263 |       klineData = this.config.data.data;
264 |     }
265 | 
266 |     if (!Array.isArray(klineData) || klineData.length === 0) {
267 |       this.renderPlaceholder();
268 |       return;
269 |     }
270 | 
271 |     // Create canvas element with responsive sizing
272 |     const canvas = document.createElement('canvas');
273 |     const maxWidth = Math.min(this.chartContainer.clientWidth || 600, 800); // Cap at 800px
274 |     const containerWidth = Math.max(maxWidth - 40, 400); // Ensure minimum 400px with padding
275 |     canvas.width = containerWidth;
276 |     canvas.height = 350; // Increased height for X-axis labels
277 |     canvas.style.width = '100%';
278 |     canvas.style.maxWidth = `${containerWidth}px`;
279 |     canvas.style.height = '350px';
280 |     canvas.style.border = '1px solid #ddd';
281 |     canvas.style.display = 'block';
282 |     canvas.style.margin = '0 auto';
283 | 
284 |     this.chartContainer.innerHTML = '';
285 |     this.chartContainer.appendChild(canvas);
286 | 
287 |     const ctx = canvas.getContext('2d');
288 |     if (!ctx) return;
289 | 
290 |     // Parse and normalize data
291 |     const candles = klineData.slice(-50).map((item: any) => {
292 |       if (Array.isArray(item)) {
293 |         return {
294 |           timestamp: parseInt(item[0]),
295 |           open: parseFloat(item[1]),
296 |           high: parseFloat(item[2]),
297 |           low: parseFloat(item[3]),
298 |           close: parseFloat(item[4]),
299 |           volume: parseFloat(item[5] || 0)
300 |         };
301 |       } else if (typeof item === 'object') {
302 |         return {
303 |           timestamp: parseInt(item.timestamp || item.time || item.openTime || 0),
304 |           open: parseFloat(item.open || 0),
305 |           high: parseFloat(item.high || 0),
306 |           low: parseFloat(item.low || 0),
307 |           close: parseFloat(item.close || 0),
308 |           volume: parseFloat(item.volume || 0)
309 |         };
310 |       }
311 |       return null;
312 |     }).filter(Boolean);
313 | 
314 |     if (candles.length === 0) {
315 |       this.renderPlaceholder();
316 |       return;
317 |     }
318 | 
319 |     // Calculate price range
320 |     const prices = candles.flatMap(c => c ? [c.high, c.low] : []);
321 |     const minPrice = Math.min(...prices);
322 |     const maxPrice = Math.max(...prices);
323 |     const priceRange = maxPrice - minPrice;
324 |     const padding = priceRange * 0.1;
325 | 
326 |     // Chart dimensions - adjusted for X-axis labels
327 |     const chartWidth = canvas.width - 80;
328 |     const chartHeight = canvas.height - 90; // More space for X-axis
329 |     const chartX = 60;
330 |     const chartY = 20;
331 | 
332 |     // Clear canvas
333 |     ctx.fillStyle = '#ffffff';
334 |     ctx.fillRect(0, 0, canvas.width, canvas.height);
335 | 
336 |     // Draw background grid
337 |     ctx.strokeStyle = '#f0f0f0';
338 |     ctx.lineWidth = 1;
339 |     for (let i = 0; i <= 5; i++) {
340 |       const y = chartY + (chartHeight * i) / 5;
341 |       ctx.beginPath();
342 |       ctx.moveTo(chartX, y);
343 |       ctx.lineTo(chartX + chartWidth, y);
344 |       ctx.stroke();
345 |     }
346 | 
347 |     // Draw vertical grid lines for time
348 |     const timeSteps = Math.min(candles.length, 6);
349 |     for (let i = 0; i <= timeSteps; i++) {
350 |       const x = chartX + (chartWidth * i) / timeSteps;
351 |       ctx.beginPath();
352 |       ctx.moveTo(x, chartY);
353 |       ctx.lineTo(x, chartY + chartHeight);
354 |       ctx.stroke();
355 |     }
356 | 
357 |     // Draw price labels (Y-axis)
358 |     ctx.fillStyle = '#666';
359 |     ctx.font = '12px Arial';
360 |     ctx.textAlign = 'right';
361 |     for (let i = 0; i <= 5; i++) {
362 |       const price = maxPrice + padding - ((maxPrice + padding - (minPrice - padding)) * i) / 5;
363 |       const y = chartY + (chartHeight * i) / 5;
364 |       ctx.fillText(price.toFixed(4), chartX - 10, y + 4);
365 |     }
366 | 
367 |     // Draw time labels (X-axis)
368 |     ctx.textAlign = 'center';
369 |     const interval = this.config.data.interval || 'D';
370 |     for (let i = 0; i <= timeSteps; i++) {
371 |       const candleIndex = Math.floor((candles.length - 1) * i / timeSteps);
372 |       if (candles[candleIndex]) {
373 |         const x = chartX + (chartWidth * i) / timeSteps;
374 |         const timeLabel = this.formatTimestamp(candles[candleIndex].timestamp, interval);
375 |         ctx.fillText(timeLabel, x, chartY + chartHeight + 20);
376 |       }
377 |     }
378 | 
379 |     // Draw candlesticks
380 |     const candleWidth = Math.max(2, chartWidth / candles.length - 2);
381 |     candles.forEach((candle, index) => {
382 |       if (!candle) return;
383 | 
384 |       const x = chartX + (index * chartWidth) / candles.length + (chartWidth / candles.length - candleWidth) / 2;
385 | 
386 |       // Calculate y positions
387 |       const highY = chartY + ((maxPrice + padding - candle.high) / (maxPrice + padding - (minPrice - padding))) * chartHeight;
388 |       const lowY = chartY + ((maxPrice + padding - candle.low) / (maxPrice + padding - (minPrice - padding))) * chartHeight;
389 |       const openY = chartY + ((maxPrice + padding - candle.open) / (maxPrice + padding - (minPrice - padding))) * chartHeight;
390 |       const closeY = chartY + ((maxPrice + padding - candle.close) / (maxPrice + padding - (minPrice - padding))) * chartHeight;
391 | 
392 |       // Determine candle color
393 |       const isGreen = candle.close >= candle.open;
394 |       const bodyColor = isGreen ? '#22c55e' : '#ef4444';
395 |       const wickColor = '#666';
396 | 
397 |       // Draw wick (high-low line)
398 |       ctx.strokeStyle = wickColor;
399 |       ctx.lineWidth = 1;
400 |       ctx.beginPath();
401 |       ctx.moveTo(x + candleWidth / 2, highY);
402 |       ctx.lineTo(x + candleWidth / 2, lowY);
403 |       ctx.stroke();
404 | 
405 |       // Draw candle body
406 |       ctx.fillStyle = bodyColor;
407 |       const bodyTop = Math.min(openY, closeY);
408 |       const bodyHeight = Math.abs(closeY - openY) || 1;
409 |       ctx.fillRect(x, bodyTop, candleWidth, bodyHeight);
410 |     });
411 | 
412 |     // Add title
413 |     ctx.fillStyle = '#333';
414 |     ctx.font = 'bold 14px Arial';
415 |     ctx.textAlign = 'left';
416 |     const symbol = this.config.data.symbol || 'Symbol';
417 |     const intervalLabel = this.config.data.interval || '';
418 |     ctx.fillText(`${symbol} ${intervalLabel} Candlestick Chart`, chartX, 15);
419 | 
420 |     // Add current price info
421 |     const lastCandle = candles[candles.length - 1];
422 |     if (lastCandle) {
423 |       ctx.font = '12px Arial';
424 |       ctx.fillStyle = lastCandle.close >= lastCandle.open ? '#22c55e' : '#ef4444';
425 |       ctx.textAlign = 'right';
426 |       ctx.fillText(`Last: $${lastCandle.close.toFixed(4)}`, canvas.width - 10, 15);
427 |     }
428 |   }
429 | 
430 |   /**
431 |    * Render line chart for RSI and other indicators
432 |    */
433 |   private renderLineChart(): void {
434 |     if (!this.chartContainer) return;
435 | 
436 |     this.chartContainer.innerHTML = `
437 |       <div class="chart-placeholder">
438 |         <p>📊 Line chart for ${this.config.dataType} data</p>
439 |         <small>Line chart implementation coming soon</small>
440 |       </div>
441 |     `;
442 |   }
443 | 
444 |   /**
445 |    * Render simple price chart
446 |    */
447 |   private renderPriceChart(): void {
448 |     if (!this.chartContainer) return;
449 | 
450 |     this.chartContainer.innerHTML = `
451 |       <div class="chart-placeholder">
452 |         <p>💰 Price chart rendering</p>
453 |         <small>Price chart implementation coming soon</small>
454 |       </div>
455 |     `;
456 |   }
457 | 
458 |   /**
459 |    * Render placeholder for unsupported chart types
460 |    */
461 |   private renderPlaceholder(): void {
462 |     if (!this.chartContainer) return;
463 | 
464 |     this.chartContainer.innerHTML = `
465 |       <div class="chart-placeholder">
466 |         <p>Chart rendering for ${this.config.dataType} data</p>
467 |         <small>Chart component will be implemented next</small>
468 |       </div>
469 |     `;
470 |   }
471 | 
472 |   /**
473 |    * Generate unique ID for chart container
474 |    */
475 |   private generateId(): string {
476 |     return Math.random().toString(36).substr(2, 9);
477 |   }
478 | 
479 |   /**
480 |    * Update card configuration
481 |    */
482 |   public updateConfig(newConfig: Partial<DataCardConfig>): void {
483 |     this.config = { ...this.config, ...newConfig };
484 |     this.render();
485 |     this.setupEventListeners();
486 |   }
487 | 
488 |   /**
489 |    * Get current card state
490 |    */
491 |   public getState(): { expanded: boolean; dataType: string } {
492 |     return {
493 |       expanded: this.isExpanded,
494 |       dataType: this.config.dataType
495 |     };
496 |   }
497 | 
498 |   /**
499 |    * Destroy the card and clean up event listeners
500 |    */
501 |   public destroy(): void {
502 |     // Event listeners will be automatically removed when innerHTML is cleared
503 |     this.container.innerHTML = '';
504 |   }
505 | }
506 | 
```

--------------------------------------------------------------------------------
/src/tools/GetMarketStructure.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Tool, CallToolResult } from "@modelcontextprotocol/sdk/types.js"
  2 | import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js"
  3 | import { z } from "zod"
  4 | import { BaseToolImplementation } from "./BaseTool.js"
  5 | import { KlineData, calculateRSI, calculateVolatility } from "../utils/mathUtils.js"
  6 | import { 
  7 |   detectOrderBlocks,
  8 |   getActiveLevels,
  9 |   calculateOrderBlockStats,
 10 |   VolumeAnalysisConfig
 11 | } from "../utils/volumeAnalysis.js"
 12 | import { 
 13 |   applyKNNToRSI, 
 14 |   KNNConfig 
 15 | } from "../utils/knnAlgorithm.js"
 16 | import { GetKlineParamsV5, KlineIntervalV3 } from "bybit-api"
 17 | 
 18 | // Zod schema for input validation
 19 | const inputSchema = z.object({
 20 |   symbol: z.string()
 21 |     .min(1, "Symbol is required")
 22 |     .regex(/^[A-Z0-9]+$/, "Symbol must contain only uppercase letters and numbers"),
 23 |   category: z.enum(["spot", "linear", "inverse"]),
 24 |   interval: z.enum(["1", "3", "5", "15", "30", "60", "120", "240", "360", "720", "D", "W", "M"]),
 25 |   analysisDepth: z.number().min(100).max(500).optional().default(200),
 26 |   includeOrderBlocks: z.boolean().optional().default(true),
 27 |   includeMLRSI: z.boolean().optional().default(true),
 28 |   includeLiquidityZones: z.boolean().optional().default(true)
 29 | })
 30 | 
 31 | type ToolArguments = z.infer<typeof inputSchema>
 32 | 
 33 | interface LiquidityZone {
 34 |   price: number;
 35 |   strength: number;
 36 |   type: "support" | "resistance";
 37 | }
 38 | 
 39 | interface MarketStructureResponse {
 40 |   symbol: string;
 41 |   interval: string;
 42 |   marketRegime: "trending_up" | "trending_down" | "ranging" | "volatile";
 43 |   trendStrength: number;
 44 |   volatilityLevel: "low" | "medium" | "high";
 45 |   keyLevels: {
 46 |     support: number[];
 47 |     resistance: number[];
 48 |     liquidityZones: LiquidityZone[];
 49 |   };
 50 |   orderBlocks?: {
 51 |     bullishBlocks: any[];
 52 |     bearishBlocks: any[];
 53 |     activeBullishBlocks: number;
 54 |     activeBearishBlocks: number;
 55 |   };
 56 |   mlRsi?: {
 57 |     currentRsi: number;
 58 |     mlRsi: number;
 59 |     adaptiveOverbought: number;
 60 |     adaptiveOversold: number;
 61 |     trend: string;
 62 |     confidence: number;
 63 |   };
 64 |   recommendations: string[];
 65 |   metadata: {
 66 |     analysisDepth: number;
 67 |     calculationTime: number;
 68 |     confidence: number;
 69 |     dataQuality: "excellent" | "good" | "fair" | "poor";
 70 |   };
 71 | }
 72 | 
 73 | class GetMarketStructure extends BaseToolImplementation {
 74 |   name = "get_market_structure"
 75 |   toolDefinition: Tool = {
 76 |     name: this.name,
 77 |     description: "Advanced market structure analysis combining ML-RSI, order blocks, and liquidity zones. Provides comprehensive market regime detection and trading recommendations.",
 78 |     inputSchema: {
 79 |       type: "object",
 80 |       properties: {
 81 |         symbol: {
 82 |           type: "string",
 83 |           description: "Trading pair symbol (e.g., 'BTCUSDT')",
 84 |           pattern: "^[A-Z0-9]+$"
 85 |         },
 86 |         category: {
 87 |           type: "string",
 88 |           description: "Category of the instrument",
 89 |           enum: ["spot", "linear", "inverse"]
 90 |         },
 91 |         interval: {
 92 |           type: "string",
 93 |           description: "Kline interval",
 94 |           enum: ["1", "3", "5", "15", "30", "60", "120", "240", "360", "720", "D", "W", "M"]
 95 |         },
 96 |         analysisDepth: {
 97 |           type: "number",
 98 |           description: "How far back to analyse (default: 200)",
 99 |           minimum: 100,
100 |           maximum: 500
101 |         },
102 |         includeOrderBlocks: {
103 |           type: "boolean",
104 |           description: "Include order block analysis (default: true)"
105 |         },
106 |         includeMLRSI: {
107 |           type: "boolean",
108 |           description: "Include ML-RSI analysis (default: true)"
109 |         },
110 |         includeLiquidityZones: {
111 |           type: "boolean",
112 |           description: "Include liquidity analysis (default: true)"
113 |         }
114 |       },
115 |       required: ["symbol", "category", "interval"]
116 |     }
117 |   }
118 | 
119 |   async toolCall(request: z.infer<typeof CallToolRequestSchema>): Promise<CallToolResult> {
120 |     const startTime = Date.now()
121 |     
122 |     try {
123 |       this.logInfo("Starting get_market_structure tool call")
124 | 
125 |       // Parse and validate input
126 |       const validationResult = inputSchema.safeParse(request.params.arguments)
127 |       if (!validationResult.success) {
128 |         const errorDetails = validationResult.error.errors.map(err => ({
129 |           field: err.path.join('.'),
130 |           message: err.message,
131 |           code: err.code
132 |         }))
133 |         throw new Error(`Invalid input: ${JSON.stringify(errorDetails)}`)
134 |       }
135 | 
136 |       const args = validationResult.data
137 | 
138 |       // Fetch kline data
139 |       const klineData = await this.fetchKlineData(args)
140 |       
141 |       if (klineData.length < 50) {
142 |         throw new Error(`Insufficient data. Need at least 50 data points, got ${klineData.length}`)
143 |       }
144 | 
145 |       // Analyze market structure
146 |       const analysis = await this.analyzeMarketStructure(klineData, args)
147 | 
148 |       const calculationTime = Date.now() - startTime
149 | 
150 |       const response: MarketStructureResponse = {
151 |         symbol: args.symbol,
152 |         interval: args.interval,
153 |         marketRegime: analysis.marketRegime,
154 |         trendStrength: analysis.trendStrength,
155 |         volatilityLevel: analysis.volatilityLevel,
156 |         keyLevels: analysis.keyLevels,
157 |         orderBlocks: args.includeOrderBlocks ? analysis.orderBlocks : undefined,
158 |         mlRsi: args.includeMLRSI ? analysis.mlRsi : undefined,
159 |         recommendations: analysis.recommendations,
160 |         metadata: {
161 |           analysisDepth: args.analysisDepth,
162 |           calculationTime,
163 |           confidence: analysis.confidence,
164 |           dataQuality: analysis.dataQuality
165 |         }
166 |       }
167 | 
168 |       this.logInfo(`Market structure analysis completed in ${calculationTime}ms. Regime: ${analysis.marketRegime}, Confidence: ${analysis.confidence}%`)
169 |       return this.formatResponse(response)
170 | 
171 |     } catch (error) {
172 |       this.logInfo(`Market structure analysis failed: ${error instanceof Error ? error.message : String(error)}`)
173 |       return this.handleError(error)
174 |     }
175 |   }
176 | 
177 |   private async fetchKlineData(args: ToolArguments): Promise<KlineData[]> {
178 |     const params: GetKlineParamsV5 = {
179 |       category: args.category,
180 |       symbol: args.symbol,
181 |       interval: args.interval as KlineIntervalV3,
182 |       limit: args.analysisDepth
183 |     }
184 | 
185 |     const response = await this.executeRequest(() => this.client.getKline(params))
186 |     
187 |     if (!response.list || response.list.length === 0) {
188 |       throw new Error("No kline data received from API")
189 |     }
190 | 
191 |     // Convert API response to KlineData format
192 |     return response.list.map(kline => ({
193 |       timestamp: parseInt(kline[0]),
194 |       open: parseFloat(kline[1]),
195 |       high: parseFloat(kline[2]),
196 |       low: parseFloat(kline[3]),
197 |       close: parseFloat(kline[4]),
198 |       volume: parseFloat(kline[5])
199 |     })).reverse() // Reverse to get chronological order
200 |   }
201 | 
202 |   private async analyzeMarketStructure(klineData: KlineData[], args: ToolArguments) {
203 |     const closePrices = klineData.map(k => k.close)
204 |     const highs = klineData.map(k => k.high)
205 |     const lows = klineData.map(k => k.low)
206 |     
207 |     // Calculate basic indicators
208 |     const rsiValues = calculateRSI(closePrices, 14)
209 |     const volatility = calculateVolatility(closePrices, 20)
210 |     
211 |     // Determine market regime
212 |     const marketRegime = this.determineMarketRegime(klineData, rsiValues, volatility)
213 |     
214 |     // Calculate trend strength
215 |     const trendStrength = this.calculateTrendStrength(closePrices)
216 |     
217 |     // Determine volatility level
218 |     const volatilityLevel = this.determineVolatilityLevel(volatility)
219 |     
220 |     // Analyze order blocks if requested
221 |     let orderBlocks: any = undefined
222 |     if (args.includeOrderBlocks) {
223 |       const config: VolumeAnalysisConfig = {
224 |         volumePivotLength: 3, // Reduced for better detection
225 |         bullishBlocks: 5,
226 |         bearishBlocks: 5,
227 |         mitigationMethod: 'wick'
228 |       }
229 |       
230 |       const { bullishBlocks, bearishBlocks } = detectOrderBlocks(klineData, config)
231 |       const stats = calculateOrderBlockStats(bullishBlocks, bearishBlocks)
232 |       
233 |       orderBlocks = {
234 |         bullishBlocks,
235 |         bearishBlocks,
236 |         activeBullishBlocks: stats.activeBullishBlocks,
237 |         activeBearishBlocks: stats.activeBearishBlocks
238 |       }
239 |     }
240 |     
241 |     // Analyze ML-RSI if requested
242 |     let mlRsi: any = undefined
243 |     if (args.includeMLRSI && rsiValues.length > 0) {
244 |       const currentRsi = rsiValues[rsiValues.length - 1]
245 |       // Simplified ML-RSI for market structure analysis
246 |       mlRsi = {
247 |         currentRsi,
248 |         mlRsi: currentRsi, // Simplified for now
249 |         adaptiveOverbought: 70,
250 |         adaptiveOversold: 30,
251 |         trend: currentRsi > 50 ? "bullish" : "bearish",
252 |         confidence: 75
253 |       }
254 |     }
255 |     
256 |     // Identify key levels
257 |     const keyLevels = this.identifyKeyLevels(klineData, args.includeLiquidityZones)
258 |     
259 |     // Generate recommendations
260 |     const recommendations = this.generateRecommendations(marketRegime, trendStrength, volatilityLevel, mlRsi, orderBlocks)
261 |     
262 |     // Calculate overall confidence
263 |     const confidence = this.calculateConfidence(klineData.length, volatility)
264 |     
265 |     // Assess data quality
266 |     const dataQuality = this.assessDataQuality(klineData)
267 |     
268 |     return {
269 |       marketRegime,
270 |       trendStrength,
271 |       volatilityLevel,
272 |       keyLevels,
273 |       orderBlocks,
274 |       mlRsi,
275 |       recommendations,
276 |       confidence,
277 |       dataQuality
278 |     }
279 |   }
280 | 
281 |   private determineMarketRegime(klineData: KlineData[], rsiValues: number[], volatility: number[]): "trending_up" | "trending_down" | "ranging" | "volatile" {
282 |     const closePrices = klineData.map(k => k.close)
283 |     const recentPrices = closePrices.slice(-20) // Last 20 periods
284 |     
285 |     if (recentPrices.length < 10) return "ranging"
286 |     
287 |     const firstPrice = recentPrices[0]
288 |     const lastPrice = recentPrices[recentPrices.length - 1]
289 |     const priceChange = (lastPrice - firstPrice) / firstPrice
290 |     
291 |     const avgVolatility = volatility.length > 0 
292 |       ? volatility.slice(-10).reduce((sum, v) => sum + v, 0) / Math.min(10, volatility.length)
293 |       : 0
294 |     
295 |     const avgRsi = rsiValues.length > 0
296 |       ? rsiValues.slice(-10).reduce((sum, r) => sum + r, 0) / Math.min(10, rsiValues.length)
297 |       : 50
298 |     
299 |     // High volatility threshold
300 |     const highVolatilityThreshold = lastPrice * 0.02 // 2% of price
301 |     
302 |     if (avgVolatility > highVolatilityThreshold) {
303 |       return "volatile"
304 |     }
305 |     
306 |     if (priceChange > 0.03 && avgRsi > 45) { // 3% up move
307 |       return "trending_up"
308 |     } else if (priceChange < -0.03 && avgRsi < 55) { // 3% down move
309 |       return "trending_down"
310 |     } else {
311 |       return "ranging"
312 |     }
313 |   }
314 | 
315 |   private calculateTrendStrength(prices: number[]): number {
316 |     if (prices.length < 20) return 50
317 |     
318 |     const recent = prices.slice(-20)
319 |     const slope = this.calculateLinearRegressionSlope(recent)
320 |     const correlation = this.calculateCorrelation(recent)
321 |     
322 |     // Normalize slope and correlation to 0-100 scale
323 |     const normalizedSlope = Math.min(100, Math.max(0, (Math.abs(slope) * 1000) + 50))
324 |     const normalizedCorrelation = Math.abs(correlation) * 100
325 |     
326 |     return Math.round((normalizedSlope + normalizedCorrelation) / 2)
327 |   }
328 | 
329 |   private calculateLinearRegressionSlope(values: number[]): number {
330 |     const n = values.length
331 |     const x = Array.from({ length: n }, (_, i) => i)
332 |     
333 |     const sumX = x.reduce((sum, val) => sum + val, 0)
334 |     const sumY = values.reduce((sum, val) => sum + val, 0)
335 |     const sumXY = x.reduce((sum, val, idx) => sum + val * values[idx], 0)
336 |     const sumXX = x.reduce((sum, val) => sum + val * val, 0)
337 |     
338 |     return (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX)
339 |   }
340 | 
341 |   private calculateCorrelation(values: number[]): number {
342 |     const n = values.length
343 |     const x = Array.from({ length: n }, (_, i) => i)
344 |     
345 |     const meanX = x.reduce((sum, val) => sum + val, 0) / n
346 |     const meanY = values.reduce((sum, val) => sum + val, 0) / n
347 |     
348 |     const numerator = x.reduce((sum, val, idx) => sum + (val - meanX) * (values[idx] - meanY), 0)
349 |     const denomX = Math.sqrt(x.reduce((sum, val) => sum + Math.pow(val - meanX, 2), 0))
350 |     const denomY = Math.sqrt(values.reduce((sum, val) => sum + Math.pow(val - meanY, 2), 0))
351 |     
352 |     return denomX * denomY !== 0 ? numerator / (denomX * denomY) : 0
353 |   }
354 | 
355 |   private determineVolatilityLevel(volatility: number[]): "low" | "medium" | "high" {
356 |     if (volatility.length === 0) return "medium"
357 |     
358 |     const avgVolatility = volatility.slice(-10).reduce((sum, v) => sum + v, 0) / Math.min(10, volatility.length)
359 |     const maxVolatility = Math.max(...volatility.slice(-20))
360 |     
361 |     const relativeVolatility = avgVolatility / maxVolatility
362 |     
363 |     if (relativeVolatility < 0.3) return "low"
364 |     if (relativeVolatility > 0.7) return "high"
365 |     return "medium"
366 |   }
367 | 
368 |   private identifyKeyLevels(klineData: KlineData[], includeLiquidityZones: boolean) {
369 |     const highs = klineData.map(k => k.high)
370 |     const lows = klineData.map(k => k.low)
371 |     
372 |     // Find significant highs and lows
373 |     const resistance = this.findSignificantLevels(highs, 'high').slice(0, 5)
374 |     const support = this.findSignificantLevels(lows, 'low').slice(0, 5)
375 |     
376 |     const liquidityZones: LiquidityZone[] = []
377 |     
378 |     if (includeLiquidityZones) {
379 |       // Create liquidity zones around key levels
380 |       resistance.forEach(level => {
381 |         liquidityZones.push({
382 |           price: level,
383 |           strength: 75,
384 |           type: "resistance"
385 |         })
386 |       })
387 |       
388 |       support.forEach(level => {
389 |         liquidityZones.push({
390 |           price: level,
391 |           strength: 75,
392 |           type: "support"
393 |         })
394 |       })
395 |     }
396 |     
397 |     return {
398 |       support: support.sort((a, b) => b - a), // Descending
399 |       resistance: resistance.sort((a, b) => a - b), // Ascending
400 |       liquidityZones
401 |     }
402 |   }
403 | 
404 |   private findSignificantLevels(values: number[], type: 'high' | 'low'): number[] {
405 |     const levels: number[] = []
406 |     const lookback = 5
407 |     
408 |     for (let i = lookback; i < values.length - lookback; i++) {
409 |       const current = values[i]
410 |       let isSignificant = true
411 |       
412 |       // Check if current value is a local extreme
413 |       for (let j = i - lookback; j <= i + lookback; j++) {
414 |         if (j !== i) {
415 |           if (type === 'high' && values[j] >= current) {
416 |             isSignificant = false
417 |             break
418 |           } else if (type === 'low' && values[j] <= current) {
419 |             isSignificant = false
420 |             break
421 |           }
422 |         }
423 |       }
424 |       
425 |       if (isSignificant) {
426 |         levels.push(current)
427 |       }
428 |     }
429 |     
430 |     return levels
431 |   }
432 | 
433 |   private generateRecommendations(
434 |     marketRegime: string,
435 |     trendStrength: number,
436 |     volatilityLevel: string,
437 |     mlRsi: any,
438 |     orderBlocks: any
439 |   ): string[] {
440 |     const recommendations: string[] = []
441 |     
442 |     // Market regime recommendations
443 |     switch (marketRegime) {
444 |       case "trending_up":
445 |         recommendations.push("Market is in an uptrend - consider long positions on pullbacks")
446 |         if (trendStrength > 70) {
447 |           recommendations.push("Strong uptrend detected - momentum strategies may be effective")
448 |         }
449 |         break
450 |       case "trending_down":
451 |         recommendations.push("Market is in a downtrend - consider short positions on rallies")
452 |         if (trendStrength > 70) {
453 |           recommendations.push("Strong downtrend detected - avoid catching falling knives")
454 |         }
455 |         break
456 |       case "ranging":
457 |         recommendations.push("Market is ranging - consider mean reversion strategies")
458 |         recommendations.push("Look for support and resistance bounces")
459 |         break
460 |       case "volatile":
461 |         recommendations.push("High volatility detected - use smaller position sizes")
462 |         recommendations.push("Consider volatility-based strategies or wait for calmer conditions")
463 |         break
464 |     }
465 |     
466 |     // Volatility recommendations
467 |     if (volatilityLevel === "high") {
468 |       recommendations.push("High volatility - use wider stops and smaller positions")
469 |     } else if (volatilityLevel === "low") {
470 |       recommendations.push("Low volatility - potential for breakout moves")
471 |     }
472 |     
473 |     // RSI recommendations
474 |     if (mlRsi) {
475 |       if (mlRsi.currentRsi > 70) {
476 |         recommendations.push("RSI indicates overbought conditions - watch for potential reversal")
477 |       } else if (mlRsi.currentRsi < 30) {
478 |         recommendations.push("RSI indicates oversold conditions - potential buying opportunity")
479 |       }
480 |     }
481 |     
482 |     // Order block recommendations
483 |     if (orderBlocks && (orderBlocks.activeBullishBlocks > 0 || orderBlocks.activeBearishBlocks > 0)) {
484 |       recommendations.push("Active order blocks detected - watch for reactions at these levels")
485 |     }
486 |     
487 |     return recommendations
488 |   }
489 | 
490 |   private calculateConfidence(dataPoints: number, volatility: number[]): number {
491 |     let confidence = 50 // Base confidence
492 |     
493 |     // More data points = higher confidence
494 |     if (dataPoints > 150) confidence += 20
495 |     else if (dataPoints > 100) confidence += 10
496 |     
497 |     // Lower volatility = higher confidence in analysis
498 |     if (volatility.length > 0) {
499 |       const avgVolatility = volatility.slice(-10).reduce((sum, v) => sum + v, 0) / Math.min(10, volatility.length)
500 |       const maxVolatility = Math.max(...volatility)
501 |       const relativeVolatility = avgVolatility / maxVolatility
502 |       
503 |       if (relativeVolatility < 0.3) confidence += 15
504 |       else if (relativeVolatility > 0.7) confidence -= 10
505 |     }
506 |     
507 |     return Math.min(100, Math.max(0, confidence))
508 |   }
509 | 
510 |   private assessDataQuality(klineData: KlineData[]): "excellent" | "good" | "fair" | "poor" {
511 |     if (klineData.length > 200) return "excellent"
512 |     if (klineData.length > 150) return "good"
513 |     if (klineData.length > 100) return "fair"
514 |     return "poor"
515 |   }
516 | }
517 | 
518 | export default GetMarketStructure
519 | 
```

--------------------------------------------------------------------------------
/webui/src/components/ToolsManager.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tools Manager - Handles the MCP Tools tab functionality
  3 |  * Displays available tools, allows manual testing, and shows execution history
  4 |  */
  5 | 
  6 | import { mcpClient } from '@/services/mcpClient';
  7 | import type { MCPTool } from '@/types/mcp';
  8 | import { DataCard, type DataCardConfig } from './chat/DataCard';
  9 | import { detectDataType } from '../utils/dataDetection';
 10 | 
 11 | export class ToolsManager {
 12 |   private tools: MCPTool[] = [];
 13 |   private isInitialized = false;
 14 |   private executionHistory: Array<{
 15 |     id: string;
 16 |     tool: string;
 17 |     params: any;
 18 |     result: any;
 19 |     timestamp: number;
 20 |     success: boolean;
 21 |   }> = [];
 22 |   private dataCards: Map<string, DataCard> = new Map(); // Track DataCards by tool name
 23 | 
 24 |   constructor() {}
 25 | 
 26 |   /**
 27 |    * Initialize the tools manager
 28 |    */
 29 |   async initialize(): Promise<void> {
 30 |     if (this.isInitialized) return;
 31 | 
 32 |     try {
 33 |       console.log('🔧 Initializing Tools Manager...');
 34 | 
 35 |       // Load available tools
 36 |       await this.loadTools();
 37 | 
 38 |       // Render tools interface
 39 |       this.renderToolsInterface();
 40 | 
 41 |       this.isInitialized = true;
 42 |       console.log('✅ Tools Manager initialized');
 43 |     } catch (error) {
 44 |       console.error('❌ Failed to initialize Tools Manager:', error);
 45 |       this.showError('Failed to initialize tools');
 46 |     }
 47 |   }
 48 | 
 49 |   /**
 50 |    * Load available tools from MCP server
 51 |    */
 52 |   private async loadTools(): Promise<void> {
 53 |     try {
 54 |       this.tools = await mcpClient.listTools();
 55 |       console.log(`🔧 Loaded ${this.tools.length} tools`);
 56 |     } catch (error) {
 57 |       console.error('Failed to load tools:', error);
 58 |       this.tools = [];
 59 |     }
 60 |   }
 61 | 
 62 |   /**
 63 |    * Render the tools interface
 64 |    */
 65 |   private renderToolsInterface(): void {
 66 |     const container = document.getElementById('tools-grid');
 67 |     if (!container) return;
 68 | 
 69 |     if (this.tools.length === 0) {
 70 |       container.innerHTML = `
 71 |         <div class="tools-empty">
 72 |           <h3>No Tools Available</h3>
 73 |           <p>Unable to load MCP tools. Please check your connection.</p>
 74 |           <button onclick="location.reload()" class="retry-btn">Retry</button>
 75 |         </div>
 76 |       `;
 77 |       return;
 78 |     }
 79 | 
 80 |     // Create tools grid
 81 |     container.innerHTML = `
 82 |       <div class="tools-header">
 83 |         <h3>Available MCP Tools (${this.tools.length})</h3>
 84 |         <div class="tools-actions">
 85 |           <button id="refresh-tools" class="refresh-btn">Refresh</button>
 86 |           <button id="clear-history" class="clear-btn">Clear History</button>
 87 |         </div>
 88 |       </div>
 89 |       <div class="tools-list">
 90 |         ${this.tools.map(tool => this.renderToolCard(tool)).join('')}
 91 |       </div>
 92 |       <div class="execution-history">
 93 |         <h3>Execution History</h3>
 94 |         <div id="history-list" class="history-list">
 95 |           ${this.renderExecutionHistory()}
 96 |         </div>
 97 |       </div>
 98 |     `;
 99 | 
100 |     // Set up event listeners
101 |     this.setupEventListeners();
102 |   }
103 | 
104 |   /**
105 |    * Render a single tool card
106 |    */
107 |   private renderToolCard(tool: MCPTool): string {
108 |     const requiredParams = tool.inputSchema?.required || [];
109 |     const properties = tool.inputSchema?.properties || {};
110 | 
111 |     const html = `
112 |       <div class="tool-card" data-tool="${tool.name}">
113 |         <div class="tool-header">
114 |           <h4>${tool.name}</h4>
115 |           <button class="test-tool-btn" data-tool="${tool.name}">Test</button>
116 |         </div>
117 |         <p class="tool-description">${tool.description}</p>
118 |         <div class="tool-params">
119 |           <h5>Parameters:</h5>
120 |           ${Object.entries(properties).map(([key, param]: [string, any]) => {
121 |             // Determine default value
122 |             let defaultValue = '';
123 |             if (key === 'symbol') {
124 |               defaultValue = 'XRPUSDT';
125 |             } else if (key === 'category') {
126 |               defaultValue = 'spot';
127 |             } else if (key === 'interval') {
128 |               defaultValue = '15';
129 |             } else if (key === 'limit') {
130 |               defaultValue = '100';
131 |             }
132 | 
133 |             // Check if this field has enum options
134 |             if (param.enum && Array.isArray(param.enum)) {
135 |               // Use dropdown for enum fields
136 |               return `
137 |                 <div class="param-item">
138 |                   <label for="${tool.name}-${key}">
139 |                     ${key}${requiredParams.includes(key) ? ' *' : ''}
140 |                   </label>
141 |                   <select id="${tool.name}-${key}" class="param-select">
142 |                     ${param.enum.map((value: string) => `
143 |                       <option value="${value}" ${value === defaultValue ? 'selected' : ''}>
144 |                         ${value}
145 |                       </option>
146 |                     `).join('')}
147 |                   </select>
148 |                   ${param.description ? `<small class="param-description">${param.description}</small>` : ''}
149 |                 </div>
150 |               `;
151 |             } else {
152 |               // Use input for non-enum fields
153 |               return `
154 |                 <div class="param-item">
155 |                   <label for="${tool.name}-${key}">
156 |                     ${key}${requiredParams.includes(key) ? ' *' : ''}
157 |                   </label>
158 |                   <input
159 |                     type="text"
160 |                     id="${tool.name}-${key}"
161 |                     placeholder="${param.description || ''}"
162 |                     value="${defaultValue}"
163 |                     class="param-input"
164 |                   />
165 |                   ${param.description ? `<small class="param-description">${param.description}</small>` : ''}
166 |                 </div>
167 |               `;
168 |             }
169 |           }).join('')}
170 |         </div>
171 |         <div class="tool-result" id="result-${tool.name}" style="display: none;">
172 |           <div class="result-header">
173 |             <h5>Result</h5>
174 |             <button class="result-close" data-tool="${tool.name}" title="Close result">&times;</button>
175 |           </div>
176 |           <div class="result-content" id="result-content-${tool.name}"></div>
177 |         </div>
178 |       </div>
179 |     `;
180 | 
181 |     return html;
182 |   }
183 | 
184 |   /**
185 |    * Render execution history
186 |    */
187 |   private renderExecutionHistory(): string {
188 |     if (this.executionHistory.length === 0) {
189 |       return '<p class="history-empty">No executions yet</p>';
190 |     }
191 | 
192 |     return this.executionHistory
193 |       .slice(-10) // Show last 10 executions
194 |       .reverse()
195 |       .map(execution => `
196 |         <div class="history-item ${execution.success ? 'success' : 'error'}">
197 |           <div class="history-header">
198 |             <span class="tool-name">${execution.tool}</span>
199 |             <span class="timestamp">${new Date(execution.timestamp).toLocaleTimeString()}</span>
200 |           </div>
201 |           <div class="history-params">
202 |             <strong>Params:</strong> ${JSON.stringify(execution.params, null, 2)}
203 |           </div>
204 |           <div class="history-result">
205 |             <strong>Result:</strong>
206 |             <pre>${JSON.stringify(execution.result, null, 2)}</pre>
207 |           </div>
208 |         </div>
209 |       `).join('');
210 |   }
211 | 
212 |   /**
213 |    * Set up event listeners
214 |    */
215 |   private setupEventListeners(): void {
216 |     // Refresh tools button
217 |     const refreshBtn = document.getElementById('refresh-tools');
218 |     if (refreshBtn) {
219 |       refreshBtn.addEventListener('click', () => {
220 |         this.refreshTools();
221 |       });
222 |     }
223 | 
224 |     // Clear history button
225 |     const clearBtn = document.getElementById('clear-history');
226 |     if (clearBtn) {
227 |       clearBtn.addEventListener('click', () => {
228 |         this.clearHistory();
229 |       });
230 |     }
231 | 
232 |     // Test tool buttons
233 |     document.querySelectorAll('.test-tool-btn').forEach(btn => {
234 |       btn.addEventListener('click', (event) => {
235 |         const target = event.target as HTMLElement;
236 |         const toolName = target.dataset.tool;
237 |         if (toolName) {
238 |           this.testTool(toolName);
239 |         }
240 |       });
241 |     });
242 | 
243 |     // Result close buttons
244 |     document.querySelectorAll('.result-close').forEach(btn => {
245 |       btn.addEventListener('click', (event) => {
246 |         const target = event.target as HTMLElement;
247 |         const toolName = target.dataset.tool;
248 |         if (toolName) {
249 |           this.hideToolResult(toolName);
250 |         }
251 |       });
252 |     });
253 |   }
254 | 
255 |   /**
256 |    * Test a specific tool
257 |    */
258 |   private async testTool(toolName: string): Promise<void> {
259 |     const tool = this.tools.find(t => t.name === toolName);
260 |     if (!tool) return;
261 | 
262 |     // Collect parameters from form
263 |     const params: any = {};
264 |     const properties = tool.inputSchema?.properties || {};
265 | 
266 |     for (const [key] of Object.entries(properties)) {
267 |       const element = document.getElementById(`${toolName}-${key}`) as HTMLInputElement | HTMLSelectElement;
268 |       if (element && element.value) {
269 |         params[key] = element.value;
270 |       }
271 |     }
272 | 
273 |     try {
274 |       console.log(`🔧 Testing tool ${toolName} with params:`, params);
275 | 
276 |       // Show loading state
277 |       this.showToolLoading(toolName);
278 | 
279 |       // Execute tool
280 |       const result = await mcpClient.callTool(toolName as any, params);
281 | 
282 |       // Record execution
283 |       this.recordExecution(toolName, params, result, true);
284 | 
285 |       // Show result in tool card
286 |       this.showToolResult(toolName, result, true);
287 | 
288 |       // Update UI
289 |       this.hideToolLoading(toolName);
290 |       this.updateHistoryDisplay();
291 | 
292 |       console.log(`✅ Tool ${toolName} executed successfully:`, result);
293 | 
294 |     } catch (error) {
295 |       console.error(`❌ Tool ${toolName} execution failed:`, error);
296 | 
297 |       // Record failed execution
298 |       this.recordExecution(toolName, params, error, false);
299 | 
300 |       // Show error in tool card
301 |       this.showToolResult(toolName, error, false);
302 | 
303 |       this.hideToolLoading(toolName);
304 |       this.updateHistoryDisplay();
305 |     }
306 |   }
307 | 
308 |   /**
309 |    * Record tool execution
310 |    */
311 |   private recordExecution(tool: string, params: any, result: any, success: boolean): void {
312 |     this.executionHistory.push({
313 |       id: Date.now().toString(),
314 |       tool,
315 |       params,
316 |       result,
317 |       timestamp: Date.now(),
318 |       success,
319 |     });
320 | 
321 |     // Keep only last 50 executions
322 |     if (this.executionHistory.length > 50) {
323 |       this.executionHistory = this.executionHistory.slice(-50);
324 |     }
325 |   }
326 | 
327 |   /**
328 |    * Update history display
329 |    */
330 |   private updateHistoryDisplay(): void {
331 |     const historyContainer = document.getElementById('history-list');
332 |     if (historyContainer) {
333 |       historyContainer.innerHTML = this.renderExecutionHistory();
334 |     }
335 |   }
336 | 
337 |   /**
338 |    * Show tool loading state
339 |    */
340 |   private showToolLoading(toolName: string): void {
341 |     const btn = document.querySelector(`.test-tool-btn[data-tool="${toolName}"]`) as HTMLElement;
342 |     if (btn) {
343 |       btn.textContent = 'Testing...';
344 |       btn.setAttribute('disabled', 'true');
345 |     }
346 |   }
347 | 
348 |   /**
349 |    * Hide tool loading state
350 |    */
351 |   private hideToolLoading(toolName: string): void {
352 |     const btn = document.querySelector(`.test-tool-btn[data-tool="${toolName}"]`) as HTMLElement;
353 |     if (btn) {
354 |       btn.textContent = 'Test';
355 |       btn.removeAttribute('disabled');
356 |     }
357 |   }
358 | 
359 |   /**
360 |    * Show tool result in the tool card
361 |    */
362 |   private showToolResult(toolName: string, result: any, success: boolean): void {
363 |     const resultContainer = document.getElementById(`result-${toolName}`);
364 |     const resultContent = document.getElementById(`result-content-${toolName}`);
365 | 
366 |     if (!resultContainer || !resultContent) {
367 |       console.error(`❌ Could not find result DOM elements for ${toolName}`);
368 |       return;
369 |     }
370 | 
371 |     this.displayResult(resultContainer, resultContent, result, success);
372 |   }
373 | 
374 |   private displayResult(resultContainer: HTMLElement, resultContent: HTMLElement, result: any, success: boolean): void {
375 |     if (!success) {
376 |       // Handle error case with existing logic
377 |       this.displayErrorResult(resultContent, result);
378 |       resultContainer.style.display = 'block';
379 |       return;
380 |     }
381 | 
382 |     // Extract actual data from MCP content structure
383 |     const actualData = this.extractActualData(result);
384 | 
385 |     // Try to create a DataCard for visualisable data
386 |     const dataCardCreated = this.tryCreateDataCard(resultContainer, resultContent, actualData);
387 | 
388 |     if (!dataCardCreated) {
389 |       // Fall back to traditional JSON display
390 |       this.displayTraditionalResult(resultContent, actualData);
391 |     }
392 | 
393 |     // Show the result container
394 |     resultContainer.style.display = 'block';
395 | 
396 |     // Scroll result into view
397 |     resultContainer.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
398 |   }
399 | 
400 |   /**
401 |    * Extract actual data from MCP content structure
402 |    */
403 |   private extractActualData(result: any): any {
404 |     let actualData = result;
405 | 
406 |     // Check if this is an MCP content response
407 |     if (result && result.content && Array.isArray(result.content) && result.content.length > 0) {
408 |       const firstContent = result.content[0];
409 |       if (firstContent.type === 'text' && firstContent.text) {
410 |         try {
411 |           // Try to parse the text as JSON
412 |           actualData = JSON.parse(firstContent.text);
413 |         } catch {
414 |           // If parsing fails, use the text as-is
415 |           actualData = firstContent.text;
416 |         }
417 |       }
418 |     }
419 | 
420 |     return actualData;
421 |   }
422 | 
423 |   /**
424 |    * Try to create a DataCard for visualisable data
425 |    */
426 |   private tryCreateDataCard(resultContainer: HTMLElement, resultContent: HTMLElement, actualData: any): boolean {
427 |     try {
428 |       // Detect if the data is visualisable
429 |       const detection = detectDataType(actualData);
430 | 
431 |       if (!detection.visualisable || detection.confidence < 0.6) {
432 |         return false;
433 |       }
434 | 
435 |       // Get tool name from container
436 |       const toolName = this.getToolNameFromContainer(resultContainer);
437 |       if (!toolName) {
438 |         return false;
439 |       }
440 | 
441 |       // Clean up any existing DataCard for this tool
442 |       const existingCard = this.dataCards.get(toolName);
443 |       if (existingCard) {
444 |         existingCard.destroy();
445 |         this.dataCards.delete(toolName);
446 |       }
447 | 
448 |       // Create DataCard configuration
449 |       const cardConfig: DataCardConfig = {
450 |         title: this.generateToolCardTitle(toolName, detection),
451 |         summary: detection.summary,
452 |         data: actualData,
453 |         dataType: detection.dataType,
454 |         expanded: true, // Start expanded in tools tab for immediate visibility
455 |         showChart: true
456 |       };
457 | 
458 |       // Create container for DataCard
459 |       const cardContainer = document.createElement('div');
460 |       cardContainer.className = 'tool-result-datacard';
461 | 
462 |       // Create and store the DataCard
463 |       const dataCard = new DataCard(cardContainer, cardConfig);
464 |       this.dataCards.set(toolName, dataCard);
465 | 
466 |       // Add status and actions above the card
467 |       resultContent.innerHTML = `
468 |         <div class="result-status result-success">
469 |           ✅ Success - Data Visualisation Available
470 |         </div>
471 |         <div class="result-actions">
472 |           <button class="copy-result-btn" data-result="${encodeURIComponent(JSON.stringify(actualData, null, 2))}">
473 |             📋 Copy Raw Data
474 |           </button>
475 |           <button class="toggle-raw-btn">
476 |             📊 Show Raw JSON
477 |           </button>
478 |         </div>
479 |       `;
480 | 
481 |       // Append the DataCard
482 |       resultContent.appendChild(cardContainer);
483 | 
484 |       // Add toggle functionality for raw data
485 |       this.setupDataCardActions(resultContent, actualData);
486 | 
487 |       return true;
488 |     } catch (error) {
489 |       console.warn('Failed to create DataCard for tool result:', error);
490 |       return false;
491 |     }
492 |   }
493 | 
494 |   /**
495 |    * Set up actions for DataCard (copy, toggle raw data)
496 |    */
497 |   private setupDataCardActions(resultContent: HTMLElement, actualData: any): void {
498 |     const copyBtn = resultContent.querySelector('.copy-result-btn') as HTMLElement;
499 |     const toggleBtn = resultContent.querySelector('.toggle-raw-btn') as HTMLElement;
500 | 
501 |     if (copyBtn) {
502 |       copyBtn.addEventListener('click', () => {
503 |         const resultData = decodeURIComponent(copyBtn.dataset.result || '');
504 |         navigator.clipboard.writeText(resultData).then(() => {
505 |           copyBtn.textContent = '✅ Copied!';
506 |           setTimeout(() => {
507 |             copyBtn.textContent = '📋 Copy Raw Data';
508 |           }, 2000);
509 |         }).catch(() => {
510 |           copyBtn.textContent = '❌ Failed';
511 |           setTimeout(() => {
512 |             copyBtn.textContent = '📋 Copy Raw Data';
513 |           }, 2000);
514 |         });
515 |       });
516 |     }
517 | 
518 |     if (toggleBtn) {
519 |       let showingRaw = false;
520 |       toggleBtn.addEventListener('click', () => {
521 |         const cardContainer = resultContent.querySelector('.tool-result-datacard') as HTMLElement;
522 |         if (!cardContainer) return;
523 | 
524 |         if (showingRaw) {
525 |           // Show DataCard
526 |           cardContainer.style.display = 'block';
527 |           const rawDataDiv = resultContent.querySelector('.raw-data-display');
528 |           if (rawDataDiv) rawDataDiv.remove();
529 |           toggleBtn.textContent = '📊 Show Raw JSON';
530 |           showingRaw = false;
531 |         } else {
532 |           // Show raw JSON
533 |           cardContainer.style.display = 'none';
534 |           const rawDataDiv = document.createElement('div');
535 |           rawDataDiv.className = 'raw-data-display';
536 |           rawDataDiv.innerHTML = `<pre class="result-data">${JSON.stringify(actualData, null, 2)}</pre>`;
537 |           resultContent.appendChild(rawDataDiv);
538 |           toggleBtn.textContent = '🎴 Show DataCard';
539 |           showingRaw = true;
540 |         }
541 |       });
542 |     }
543 |   }
544 | 
545 |   /**
546 |    * Display traditional JSON result (fallback)
547 |    */
548 |   private displayTraditionalResult(resultContent: HTMLElement, actualData: any): void {
549 |     let formattedResult: string;
550 | 
551 |     if (typeof actualData === 'object') {
552 |       formattedResult = JSON.stringify(actualData, null, 2);
553 |     } else {
554 |       formattedResult = String(actualData);
555 |     }
556 | 
557 |     resultContent.innerHTML = `
558 |       <div class="result-status result-success">
559 |         ✅ Success
560 |       </div>
561 |       <pre class="result-data">${formattedResult}</pre>
562 |       <div class="result-actions">
563 |         <button class="copy-result-btn" data-result="${encodeURIComponent(formattedResult)}">
564 |           📋 Copy
565 |         </button>
566 |       </div>
567 |     `;
568 | 
569 |     // Add copy functionality
570 |     const copyBtn = resultContent.querySelector('.copy-result-btn') as HTMLElement;
571 |     if (copyBtn) {
572 |       copyBtn.addEventListener('click', () => {
573 |         const resultData = decodeURIComponent(copyBtn.dataset.result || '');
574 |         navigator.clipboard.writeText(resultData).then(() => {
575 |           copyBtn.textContent = '✅ Copied!';
576 |           setTimeout(() => {
577 |             copyBtn.textContent = '📋 Copy';
578 |           }, 2000);
579 |         }).catch(() => {
580 |           copyBtn.textContent = '❌ Failed';
581 |           setTimeout(() => {
582 |             copyBtn.textContent = '📋 Copy';
583 |           }, 2000);
584 |         });
585 |       });
586 |     }
587 |   }
588 | 
589 |   /**
590 |    * Display error result
591 |    */
592 |   private displayErrorResult(resultContent: HTMLElement, result: any): void {
593 |     let formattedResult: string;
594 | 
595 |     if (result instanceof Error) {
596 |       formattedResult = `Error: ${result.message}`;
597 |     } else {
598 |       formattedResult = `Error: ${String(result)}`;
599 |     }
600 | 
601 |     resultContent.innerHTML = `
602 |       <div class="result-status result-error">
603 |         ❌ Error
604 |       </div>
605 |       <pre class="result-data">${formattedResult}</pre>
606 |     `;
607 |   }
608 | 
609 |   /**
610 |    * Get tool name from result container
611 |    */
612 |   private getToolNameFromContainer(resultContainer: HTMLElement): string | null {
613 |     const id = resultContainer.id;
614 |     if (id && id.startsWith('result-')) {
615 |       return id.substring(7); // Remove 'result-' prefix
616 |     }
617 |     return null;
618 |   }
619 | 
620 |   /**
621 |    * Generate appropriate title for tool DataCard
622 |    */
623 |   private generateToolCardTitle(toolName: string, _detection: any): string {
624 |     const toolDisplayNames: Record<string, string> = {
625 |       'get_ticker': 'Ticker Data',
626 |       'get_kline_data': 'Kline Data',
627 |       'get_ml_rsi': 'ML-RSI Analysis',
628 |       'get_order_blocks': 'Order Blocks',
629 |       'get_market_structure': 'Market Structure'
630 |     };
631 | 
632 |     return toolDisplayNames[toolName] || toolName.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
633 |   }
634 | 
635 |   /**
636 |    * Hide tool result
637 |    */
638 |   private hideToolResult(toolName: string): void {
639 |     const resultContainer = document.getElementById(`result-${toolName}`);
640 |     if (resultContainer) {
641 |       resultContainer.style.display = 'none';
642 |     }
643 | 
644 |     // Clean up associated DataCard
645 |     const dataCard = this.dataCards.get(toolName);
646 |     if (dataCard) {
647 |       dataCard.destroy();
648 |       this.dataCards.delete(toolName);
649 |     }
650 |   }
651 | 
652 |   /**
653 |    * Refresh tools
654 |    */
655 |   private async refreshTools(): Promise<void> {
656 |     console.log('🔄 Refreshing tools...');
657 |     await this.loadTools();
658 |     this.renderToolsInterface();
659 |   }
660 | 
661 |   /**
662 |    * Clear execution history
663 |    */
664 |   private clearHistory(): void {
665 |     this.executionHistory = [];
666 |     this.updateHistoryDisplay();
667 |   }
668 | 
669 |   /**
670 |    * Show error message
671 |    */
672 |   private showError(message: string): void {
673 |     const container = document.getElementById('tools-grid');
674 |     if (container) {
675 |       container.innerHTML = `
676 |         <div class="tools-error">
677 |           <h3>❌ Error</h3>
678 |           <p>${message}</p>
679 |           <button onclick="location.reload()">Retry</button>
680 |         </div>
681 |       `;
682 |     }
683 |   }
684 | 
685 |   /**
686 |    * Get current state
687 |    */
688 |   getState(): { tools: MCPTool[]; history: any[] } {
689 |     return {
690 |       tools: [...this.tools],
691 |       history: [...this.executionHistory],
692 |     };
693 |   }
694 | 
695 |   /**
696 |    * Destroy tools manager
697 |    */
698 |   destroy(): void {
699 |     // Clean up all DataCards
700 |     this.dataCards.forEach(card => card.destroy());
701 |     this.dataCards.clear();
702 | 
703 |     this.isInitialized = false;
704 |     console.log('🗑️ Tools Manager destroyed');
705 |   }
706 | }
707 | 
708 | // Create singleton instance
709 | export const toolsManager = new ToolsManager();
710 | 
```

--------------------------------------------------------------------------------
/webui/src/main.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Main application entry point
  3 |  */
  4 | 
  5 | console.log('🚀 Main.ts loading...');
  6 | 
  7 | import './styles/main.css';
  8 | 
  9 | import { ChatApp } from './components/ChatApp';
 10 | import { DebugConsole } from './components/DebugConsole';
 11 | import { DataVerificationPanel } from './components/DataVerificationPanel';
 12 | import { AgentDashboard } from './components/AgentDashboard';
 13 | import { toolsManager } from './components/ToolsManager';
 14 | import { configService } from './services/configService';
 15 | import { agentConfigService } from './services/agentConfig';
 16 | import { mcpClient } from './services/mcpClient';
 17 | import { aiClient } from './services/aiClient';
 18 | import { multiStepAgent } from './services/multiStepAgent';
 19 | // Import logService to initialize console interception
 20 | import './services/logService';
 21 | 
 22 | class App {
 23 |   private chatApp?: ChatApp;
 24 |   private debugConsole?: DebugConsole;
 25 |   private verificationPanel?: DataVerificationPanel;
 26 |   private agentDashboard?: AgentDashboard;
 27 |   private isInitialized = false;
 28 |   private toolsInitialized = false;
 29 | 
 30 |   async initialize(): Promise<void> {
 31 |     if (this.isInitialized) return;
 32 | 
 33 |     try {
 34 |       // Show loading state
 35 |       this.showLoading();
 36 | 
 37 |       // Initialize services
 38 |       await this.initializeServices();
 39 | 
 40 |       // Initialize UI components
 41 |       this.initializeUI();
 42 | 
 43 |       // Initialize debug console
 44 |       this.initializeDebugConsole();
 45 | 
 46 |       // Initialize data verification panel
 47 |       this.initializeVerificationPanel();
 48 | 
 49 |       // Initialize agent dashboard
 50 |       this.initializeAgentDashboard();
 51 | 
 52 |       // Hide loading and show main app
 53 |       this.hideLoading();
 54 | 
 55 |       this.isInitialized = true;
 56 |       console.log('✅ Bybit MCP WebUI initialized successfully');
 57 |     } catch (error) {
 58 |       console.error('❌ Failed to initialize application:', error);
 59 |       this.showError('Failed to initialize application. Please check your configuration.');
 60 |     }
 61 |   }
 62 | 
 63 |   private async initializeServices(): Promise<void> {
 64 |     console.log('🚀 Initializing services...');
 65 | 
 66 |     // Get current configuration
 67 |     const aiConfig = configService.getAIConfig();
 68 |     const mcpConfig = configService.getMCPConfig();
 69 | 
 70 |     console.log('⚙️ AI Config:', {
 71 |       endpoint: aiConfig.endpoint,
 72 |       model: aiConfig.model,
 73 |       temperature: aiConfig.temperature,
 74 |       maxTokens: aiConfig.maxTokens
 75 |     });
 76 |     console.log('⚙️ MCP Config:', mcpConfig);
 77 | 
 78 |     // Note: MCP server should be started automatically with 'pnpm dev:full'
 79 |     console.log('💡 If MCP server is not running, use "pnpm dev:full" to start both services');
 80 | 
 81 |     // Update clients with current config
 82 |     aiClient.updateConfig(aiConfig);
 83 |     mcpClient.setBaseUrl(mcpConfig.endpoint);
 84 |     mcpClient.setTimeout(mcpConfig.timeout);
 85 | 
 86 |     // Test connections
 87 |     console.log('🔄 Testing connections...');
 88 |     const [aiConnected, mcpConnected] = await Promise.allSettled([
 89 |       aiClient.isConnected(),
 90 |       mcpClient.isConnected(),
 91 |     ]);
 92 | 
 93 |     console.log('📊 Connection results:', {
 94 |       ai: aiConnected.status === 'fulfilled' ? aiConnected.value : aiConnected.reason,
 95 |       mcp: mcpConnected.status === 'fulfilled' ? mcpConnected.value : mcpConnected.reason
 96 |     });
 97 | 
 98 |     // Initialize MCP client (fetch available tools)
 99 |     if (mcpConnected.status === 'fulfilled' && mcpConnected.value) {
100 |       try {
101 |         await mcpClient.initialize();
102 |         console.log('✅ MCP client initialized');
103 |       } catch (error) {
104 |         console.warn('⚠️ MCP client initialization failed:', error);
105 |       }
106 |     } else {
107 |       console.warn('⚠️ MCP server not reachable');
108 |     }
109 | 
110 |     // Log connection status
111 |     if (aiConnected.status === 'fulfilled' && aiConnected.value) {
112 |       console.log('✅ AI service connected');
113 |     } else {
114 |       console.warn('⚠️ AI service not reachable');
115 |     }
116 | 
117 |     // Initialize multi-step agent
118 |     try {
119 |       console.log('🤖 Initializing multi-step agent...');
120 |       await multiStepAgent.initialize();
121 |       console.log('✅ Multi-step agent initialized');
122 |     } catch (error) {
123 |       console.warn('⚠️ Multi-step agent initialization failed:', error);
124 |       console.log('💡 Falling back to legacy AI client');
125 |     }
126 | 
127 |     console.log('✅ Service initialization complete');
128 |   }
129 | 
130 |   private initializeUI(): void {
131 |     // Initialize chat application
132 |     this.chatApp = new ChatApp();
133 | 
134 |     // Set up global event listeners
135 |     this.setupGlobalEventListeners();
136 | 
137 |     // Set up theme toggle
138 |     this.setupThemeToggle();
139 | 
140 |     // Set up settings modal
141 |     this.setupSettingsModal();
142 |   }
143 | 
144 |   private setupGlobalEventListeners(): void {
145 |     // Handle keyboard shortcuts
146 |     document.addEventListener('keydown', (event) => {
147 |       // Ctrl/Cmd + K to focus chat input
148 |       if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
149 |         event.preventDefault();
150 |         const chatInput = document.getElementById('chat-input') as HTMLTextAreaElement;
151 |         if (chatInput) {
152 |           chatInput.focus();
153 |         }
154 |       }
155 | 
156 |       // Escape to close modals
157 |       if (event.key === 'Escape') {
158 |         this.closeAllModals();
159 |       }
160 |     });
161 | 
162 |     // Handle navigation
163 |     document.querySelectorAll('.nav-item').forEach(item => {
164 |       item.addEventListener('click', (event) => {
165 |         const target = event.currentTarget as HTMLElement;
166 |         const view = target.dataset.view;
167 |         if (view) {
168 |           this.switchView(view);
169 |         }
170 |       });
171 |     });
172 | 
173 |     // Handle example queries
174 |     document.querySelectorAll('.example-query').forEach(button => {
175 |       button.addEventListener('click', (event) => {
176 |         const target = event.currentTarget as HTMLElement;
177 |         const query = target.textContent?.trim();
178 |         if (query && this.chatApp) {
179 |           this.chatApp.sendMessage(query);
180 |         }
181 |       });
182 |     });
183 | 
184 |     // Agent settings button removed - now integrated into main settings modal
185 | 
186 |     // Handle agent mode toggle
187 |     const agentToggleBtn = document.getElementById('agent-toggle-btn');
188 |     if (agentToggleBtn && this.chatApp) {
189 |       agentToggleBtn.addEventListener('click', () => {
190 |         const isUsingAgent = this.chatApp!.isUsingAgent();
191 |         this.chatApp!.toggleAgentMode(!isUsingAgent);
192 |         agentToggleBtn.textContent = !isUsingAgent ? '🤖 Agent Mode' : '🔄 Legacy Mode';
193 |       });
194 |     }
195 |   }
196 | 
197 |   private setupThemeToggle(): void {
198 |     const themeToggle = document.getElementById('theme-toggle');
199 |     if (themeToggle) {
200 |       themeToggle.addEventListener('click', () => {
201 |         const settings = configService.getSettings();
202 |         const currentTheme = settings.ui.theme;
203 | 
204 |         let newTheme: 'light' | 'dark' | 'auto';
205 |         let icon: string;
206 | 
207 |         if (currentTheme === 'light') {
208 |           newTheme = 'dark';
209 |           icon = '☀️';
210 |         } else if (currentTheme === 'dark') {
211 |           newTheme = 'auto';
212 |           icon = '🌓';
213 |         } else {
214 |           newTheme = 'light';
215 |           icon = '🌙';
216 |         }
217 | 
218 |         configService.updateSettings({
219 |           ui: { ...settings.ui, theme: newTheme },
220 |         });
221 | 
222 |         // Update icon
223 |         const iconElement = themeToggle.querySelector('.theme-icon');
224 |         if (iconElement) {
225 |           iconElement.textContent = icon;
226 |         }
227 |       });
228 |     }
229 |   }
230 | 
231 |   private setupAgentDashboardButton(): void {
232 |     const agentDashboardBtn = document.getElementById('agent-dashboard-btn');
233 |     if (agentDashboardBtn && this.agentDashboard) {
234 |       agentDashboardBtn.addEventListener('click', () => {
235 |         this.agentDashboard!.toggleVisibility();
236 | 
237 |         // Update button appearance based on dashboard visibility
238 |         const isVisible = this.agentDashboard!.visible;
239 |         if (isVisible) {
240 |           agentDashboardBtn.classList.add('active');
241 |         } else {
242 |           agentDashboardBtn.classList.remove('active');
243 |         }
244 |       });
245 |     }
246 |   }
247 | 
248 |   private setupSettingsModal(): void {
249 |     const settingsBtn = document.getElementById('settings-btn');
250 |     const settingsModal = document.getElementById('settings-modal');
251 |     const closeSettings = document.getElementById('close-settings');
252 |     const saveSettings = document.getElementById('save-settings');
253 | 
254 |     if (settingsBtn && settingsModal) {
255 |       settingsBtn.addEventListener('click', () => {
256 |         this.openSettingsModal();
257 |       });
258 |     }
259 | 
260 |     if (closeSettings && settingsModal) {
261 |       closeSettings.addEventListener('click', () => {
262 |         settingsModal.classList.add('hidden');
263 |         settingsModal.classList.remove('active');
264 |       });
265 |     }
266 | 
267 |     if (saveSettings) {
268 |       saveSettings.addEventListener('click', () => {
269 |         this.saveSettingsFromModal();
270 |       });
271 |     }
272 | 
273 |     // Close modal when clicking backdrop
274 |     if (settingsModal) {
275 |       settingsModal.addEventListener('click', (event) => {
276 |         if (event.target === settingsModal) {
277 |           settingsModal.classList.add('hidden');
278 |           settingsModal.classList.remove('active');
279 |         }
280 |       });
281 |     }
282 |   }
283 | 
284 |   private initializeDebugConsole(): void {
285 |     // Create debug console container
286 |     const debugContainer = document.createElement('div');
287 |     debugContainer.id = 'debug-console-container';
288 |     document.body.appendChild(debugContainer);
289 | 
290 |     // Initialize debug console
291 |     this.debugConsole = new DebugConsole(debugContainer);
292 | 
293 |     // Add keyboard shortcut to toggle debug console (Ctrl+` or Cmd+`)
294 |     document.addEventListener('keydown', (e) => {
295 |       if ((e.ctrlKey || e.metaKey) && e.key === '`') {
296 |         e.preventDefault();
297 |         this.debugConsole?.toggle();
298 |       }
299 |     });
300 | 
301 |     console.log('🔍 Debug console initialized (Ctrl+` to toggle)');
302 |   }
303 | 
304 |   private initializeVerificationPanel(): void {
305 |     try {
306 |       // Initialize data verification panel
307 |       this.verificationPanel = new DataVerificationPanel('verification-panel-container');
308 |       console.log('📊 Data verification panel initialized (Ctrl+D to toggle)');
309 | 
310 |       // Make panel accessible for debugging
311 |       (window as any).verificationPanel = this.verificationPanel;
312 |     } catch (error) {
313 |       console.warn('⚠️ Failed to initialize verification panel:', error);
314 |     }
315 |   }
316 | 
317 |   private initializeAgentDashboard(): void {
318 |     try {
319 |       // Initialize agent dashboard with ChatApp reference
320 |       this.agentDashboard = new AgentDashboard('agent-dashboard-container', this.chatApp);
321 |       console.log('🤖 Agent dashboard initialized (Ctrl+M to toggle)');
322 | 
323 |       // Set up agent dashboard button now that dashboard is initialized
324 |       this.setupAgentDashboardButton();
325 | 
326 |       // Make dashboard accessible for debugging
327 |       (window as any).agentDashboard = this.agentDashboard;
328 |     } catch (error) {
329 |       console.warn('⚠️ Failed to initialize agent dashboard:', error);
330 |     }
331 |   }
332 | 
333 |   private openSettingsModal(): void {
334 |     const modal = document.getElementById('settings-modal');
335 |     if (!modal) return;
336 | 
337 |     // Populate current settings
338 |     const settings = configService.getSettings();
339 |     const agentConfig = agentConfigService.getConfig();
340 |     console.log('🔧 Opening settings modal with current settings:', settings, agentConfig);
341 | 
342 |     // AI Configuration
343 |     const aiEndpoint = document.getElementById('ai-endpoint') as HTMLInputElement;
344 |     const aiModel = document.getElementById('ai-model') as HTMLInputElement;
345 |     const mcpEndpoint = document.getElementById('mcp-endpoint') as HTMLInputElement;
346 | 
347 |     if (aiEndpoint) {
348 |       aiEndpoint.value = settings.ai.endpoint;
349 |       console.log('📝 Set AI endpoint field to:', settings.ai.endpoint);
350 |     }
351 |     if (aiModel) {
352 |       aiModel.value = settings.ai.model;
353 |       console.log('📝 Set AI model field to:', settings.ai.model);
354 |     }
355 |     if (mcpEndpoint) {
356 |       mcpEndpoint.value = settings.mcp.endpoint;
357 |       console.log('📝 Set MCP endpoint field to:', settings.mcp.endpoint);
358 |     }
359 | 
360 |     // Agent Configuration
361 |     const agentModeEnabled = document.getElementById('agent-mode-enabled') as HTMLInputElement;
362 |     const maxIterations = document.getElementById('max-iterations') as HTMLInputElement;
363 |     const toolTimeout = document.getElementById('tool-timeout') as HTMLInputElement;
364 |     const showWorkflowSteps = document.getElementById('show-workflow-steps') as HTMLInputElement;
365 |     const showToolCalls = document.getElementById('show-tool-calls') as HTMLInputElement;
366 |     const enableDebugMode = document.getElementById('enable-debug-mode') as HTMLInputElement;
367 | 
368 |     if (agentModeEnabled) {
369 |       agentModeEnabled.checked = this.chatApp?.isAgentModeEnabled() || false;
370 |     }
371 |     if (maxIterations) {
372 |       maxIterations.value = agentConfig.maxIterations.toString();
373 |     }
374 |     if (toolTimeout) {
375 |       toolTimeout.value = agentConfig.toolTimeout.toString();
376 |     }
377 |     if (showWorkflowSteps) {
378 |       showWorkflowSteps.checked = agentConfig.showWorkflowSteps;
379 |     }
380 |     if (showToolCalls) {
381 |       showToolCalls.checked = agentConfig.showToolCalls;
382 |     }
383 |     if (enableDebugMode) {
384 |       enableDebugMode.checked = agentConfig.enableDebugMode;
385 |     }
386 | 
387 |     modal.classList.remove('hidden');
388 |     modal.classList.add('active');
389 |   }
390 | 
391 |   private saveSettingsFromModal(): void {
392 |     const aiEndpoint = document.getElementById('ai-endpoint') as HTMLInputElement;
393 |     const aiModel = document.getElementById('ai-model') as HTMLInputElement;
394 |     const mcpEndpoint = document.getElementById('mcp-endpoint') as HTMLInputElement;
395 | 
396 |     // Agent Configuration elements
397 |     const agentModeEnabled = document.getElementById('agent-mode-enabled') as HTMLInputElement;
398 |     const maxIterations = document.getElementById('max-iterations') as HTMLInputElement;
399 |     const toolTimeout = document.getElementById('tool-timeout') as HTMLInputElement;
400 |     const showWorkflowSteps = document.getElementById('show-workflow-steps') as HTMLInputElement;
401 |     const showToolCalls = document.getElementById('show-tool-calls') as HTMLInputElement;
402 |     const enableDebugMode = document.getElementById('enable-debug-mode') as HTMLInputElement;
403 | 
404 |     console.log('💾 Saving settings from modal...');
405 |     console.log('AI Endpoint:', aiEndpoint?.value);
406 |     console.log('AI Model:', aiModel?.value);
407 |     console.log('MCP Endpoint:', mcpEndpoint?.value);
408 |     console.log('Agent Mode:', agentModeEnabled?.checked);
409 | 
410 |     const currentSettings = configService.getSettings();
411 |     const updates: Partial<typeof currentSettings> = {};
412 | 
413 |     // Build AI config updates
414 |     const aiUpdates: Partial<typeof currentSettings.ai> = {};
415 |     let hasAIUpdates = false;
416 | 
417 |     if (aiEndpoint?.value && aiEndpoint.value.trim() !== '') {
418 |       aiUpdates.endpoint = aiEndpoint.value.trim();
419 |       hasAIUpdates = true;
420 |     }
421 | 
422 |     if (aiModel?.value && aiModel.value.trim() !== '') {
423 |       aiUpdates.model = aiModel.value.trim();
424 |       hasAIUpdates = true;
425 |     }
426 | 
427 |     if (hasAIUpdates) {
428 |       updates.ai = { ...currentSettings.ai, ...aiUpdates };
429 |     }
430 | 
431 |     // Build MCP config updates
432 |     if (mcpEndpoint?.value && mcpEndpoint.value.trim() !== '') {
433 |       updates.mcp = { ...currentSettings.mcp, endpoint: mcpEndpoint.value.trim() };
434 |     }
435 | 
436 |     console.log('📝 Settings updates:', updates);
437 | 
438 |     if (Object.keys(updates).length > 0) {
439 |       configService.updateSettings(updates);
440 |       console.log('✅ Settings saved successfully');
441 | 
442 |       // Reinitialize services with new config
443 |       this.initializeServices().catch(console.error);
444 |     } else {
445 |       console.log('ℹ️ No settings changes to save');
446 |     }
447 | 
448 |     // Save agent configuration
449 |     const agentConfig = {
450 |       maxIterations: parseInt(maxIterations?.value || '5'),
451 |       toolTimeout: parseInt(toolTimeout?.value || '30000'),
452 |       showWorkflowSteps: showWorkflowSteps?.checked || false,
453 |       showToolCalls: showToolCalls?.checked || false,
454 |       enableDebugMode: enableDebugMode?.checked || false,
455 |       streamingEnabled: true // Always enabled
456 |     };
457 | 
458 |     console.log('🤖 Saving agent config:', agentConfig);
459 |     agentConfigService.updateConfig(agentConfig);
460 | 
461 |     // Update agent mode in chat app
462 |     if (this.chatApp && agentModeEnabled) {
463 |       this.chatApp.toggleAgentMode(agentModeEnabled.checked);
464 |     }
465 | 
466 |     // Close modal
467 |     const modal = document.getElementById('settings-modal');
468 |     if (modal) {
469 |       modal.classList.add('hidden');
470 |       modal.classList.remove('active');
471 |     }
472 |   }
473 | 
474 |   private switchView(viewName: string): void {
475 |     // Update navigation
476 |     document.querySelectorAll('.nav-item').forEach(item => {
477 |       item.classList.remove('active');
478 |     });
479 | 
480 |     const activeNavItem = document.querySelector(`[data-view="${viewName}"]`);
481 |     if (activeNavItem) {
482 |       activeNavItem.classList.add('active');
483 |     }
484 | 
485 |     // Update views
486 |     document.querySelectorAll('.view').forEach(view => {
487 |       view.classList.remove('active');
488 |     });
489 | 
490 |     const activeView = document.getElementById(`${viewName}-view`);
491 |     if (activeView) {
492 |       activeView.classList.add('active');
493 |     }
494 | 
495 |     // Initialize components when their views are accessed
496 |     if (viewName === 'tools' && !this.toolsInitialized) {
497 |       this.initializeTools();
498 |     }
499 | 
500 |     // Handle dashboard view - embed agent dashboard into the tab
501 |     if (viewName === 'dashboard' && this.agentDashboard) {
502 |       this.embedDashboardInTab();
503 |     }
504 |   }
505 | 
506 |   /**
507 |    * Initialize tools when tools tab is first accessed
508 |    */
509 |   private async initializeTools(): Promise<void> {
510 |     if (this.toolsInitialized) return;
511 | 
512 |     try {
513 |       console.log('🔧 Initializing tools...');
514 |       await toolsManager.initialize();
515 |       this.toolsInitialized = true;
516 |       console.log('✅ Tools initialized successfully');
517 |     } catch (error) {
518 |       console.error('❌ Failed to initialize tools:', error);
519 |     }
520 |   }
521 | 
522 |   /**
523 |    * Embed agent dashboard into the dashboard tab view
524 |    */
525 |   private embedDashboardInTab(): void {
526 |     if (!this.agentDashboard) return;
527 | 
528 |     const dashboardWrapper = document.getElementById('dashboard-content-wrapper');
529 |     const agentDashboardContainer = document.getElementById('agent-dashboard-container');
530 | 
531 |     if (dashboardWrapper && agentDashboardContainer) {
532 |       // Check if dashboard content already exists in the tab
533 |       if (dashboardWrapper.querySelector('.agent-dashboard')) {
534 |         return; // Already embedded
535 |       }
536 | 
537 |       // Get the dashboard content from the original container
538 |       const dashboardContent = agentDashboardContainer.querySelector('.agent-dashboard');
539 | 
540 |       if (dashboardContent) {
541 |         // Clone the dashboard content for the tab view
542 |         const clonedContent = dashboardContent.cloneNode(true) as HTMLElement;
543 | 
544 |         // Remove overlay-specific classes and styles
545 |         clonedContent.classList.remove('hidden');
546 |         clonedContent.classList.add('visible');
547 |         clonedContent.style.position = 'static';
548 |         clonedContent.style.zIndex = 'auto';
549 |         clonedContent.style.background = 'transparent';
550 |         clonedContent.style.boxShadow = 'none';
551 |         clonedContent.style.border = 'none';
552 |         clonedContent.style.borderRadius = '0';
553 |         clonedContent.style.width = '100%';
554 |         clonedContent.style.height = '100%';
555 |         clonedContent.style.maxWidth = 'none';
556 |         clonedContent.style.maxHeight = 'none';
557 |         clonedContent.style.transform = 'none';
558 |         clonedContent.style.top = 'auto';
559 |         clonedContent.style.left = 'auto';
560 |         clonedContent.style.right = 'auto';
561 |         clonedContent.style.bottom = 'auto';
562 | 
563 |         // Add the cloned content to the tab view
564 |         dashboardWrapper.innerHTML = '';
565 |         dashboardWrapper.appendChild(clonedContent);
566 | 
567 |         // Set up event listeners for the cloned content
568 |         this.setupTabDashboardEventListeners(clonedContent);
569 | 
570 |         // Debug: Check what data is available
571 |         console.log('🔍 Dashboard data check:');
572 |         console.log('Memory stats:', multiStepAgent.getMemoryStats());
573 |         console.log('Performance stats:', multiStepAgent.getPerformanceStats());
574 |         console.log('Analysis history:', multiStepAgent.getAnalysisHistory(undefined, 5));
575 | 
576 |         // Refresh the dashboard data
577 |         this.agentDashboard.show(); // This will trigger a refresh
578 |         this.agentDashboard.hide(); // Hide the overlay version
579 |       }
580 |     }
581 |   }
582 | 
583 |   /**
584 |    * Set up event listeners for the dashboard in tab view
585 |    */
586 |   private setupTabDashboardEventListeners(dashboardElement: HTMLElement): void {
587 |     // Refresh button
588 |     const refreshBtn = dashboardElement.querySelector('#dashboard-refresh') as HTMLButtonElement;
589 |     refreshBtn?.addEventListener('click', () => {
590 |       if (this.agentDashboard) {
591 |         // Trigger refresh and then update the tab view
592 |         this.agentDashboard.show();
593 |         this.agentDashboard.hide();
594 |         setTimeout(() => this.embedDashboardInTab(), 100);
595 |       }
596 |     });
597 | 
598 |     // Clear memory button
599 |     const clearMemoryBtn = dashboardElement.querySelector('#clear-memory') as HTMLButtonElement;
600 |     clearMemoryBtn?.addEventListener('click', () => {
601 |       if (confirm('Are you sure you want to clear all agent memory? This action cannot be undone.')) {
602 |         multiStepAgent.clearMemory();
603 |         // Refresh the tab view
604 |         setTimeout(() => this.embedDashboardInTab(), 100);
605 |         this.showToast('Memory cleared successfully!');
606 |       }
607 |     });
608 | 
609 |     // New conversation button
610 |     const newConversationBtn = dashboardElement.querySelector('#new-conversation') as HTMLButtonElement;
611 |     newConversationBtn?.addEventListener('click', () => {
612 |       // Clear agent memory
613 |       multiStepAgent.startNewConversation();
614 | 
615 |       // Clear chat UI if available
616 |       if (this.chatApp) {
617 |         this.chatApp.clearMessages();
618 |       }
619 | 
620 |       this.showToast('New conversation started!');
621 |     });
622 | 
623 |     // Export data button
624 |     const exportDataBtn = dashboardElement.querySelector('#export-data') as HTMLButtonElement;
625 |     exportDataBtn?.addEventListener('click', () => {
626 |       try {
627 |         const data = {
628 |           memoryStats: multiStepAgent.getMemoryStats(),
629 |           performanceStats: multiStepAgent.getPerformanceStats(),
630 |           recentAnalysis: multiStepAgent.getAnalysisHistory(undefined, 20),
631 |           exportedAt: new Date().toISOString()
632 |         };
633 | 
634 |         const dataStr = JSON.stringify(data, null, 2);
635 |         const blob = new Blob([dataStr], { type: 'application/json' });
636 |         const url = URL.createObjectURL(blob);
637 | 
638 |         const a = document.createElement('a');
639 |         a.href = url;
640 |         a.download = `agent-data-${new Date().toISOString().split('T')[0]}.json`;
641 |         document.body.appendChild(a);
642 |         a.click();
643 |         document.body.removeChild(a);
644 |         URL.revokeObjectURL(url);
645 | 
646 |         this.showToast('Data exported successfully!');
647 | 
648 |       } catch (error) {
649 |         console.error('Failed to export data:', error);
650 |         this.showToast('Failed to export data', 'error');
651 |       }
652 |     });
653 |   }
654 | 
655 |   private showToast(message: string, type: 'success' | 'error' = 'success'): void {
656 |     const toast = document.createElement('div');
657 |     toast.className = `dashboard-toast toast-${type}`;
658 |     toast.textContent = message;
659 | 
660 |     document.body.appendChild(toast);
661 | 
662 |     // Animate in
663 |     setTimeout(() => toast.classList.add('show'), 10);
664 | 
665 |     // Remove after 3 seconds
666 |     setTimeout(() => {
667 |       toast.classList.remove('show');
668 |       setTimeout(() => toast.remove(), 300);
669 |     }, 3000);
670 |   }
671 | 
672 |   private closeAllModals(): void {
673 |     document.querySelectorAll('.modal').forEach(modal => {
674 |       modal.classList.add('hidden');
675 |       modal.classList.remove('active');
676 |     });
677 |   }
678 | 
679 |   private showLoading(): void {
680 |     const loading = document.getElementById('loading');
681 |     const mainContainer = document.getElementById('main-container');
682 | 
683 |     if (loading) loading.classList.remove('hidden');
684 |     if (mainContainer) mainContainer.classList.add('hidden');
685 |   }
686 | 
687 |   private hideLoading(): void {
688 |     const loading = document.getElementById('loading');
689 |     const mainContainer = document.getElementById('main-container');
690 | 
691 |     if (loading) loading.classList.add('hidden');
692 |     if (mainContainer) mainContainer.classList.remove('hidden');
693 |   }
694 | 
695 |   private showError(message: string): void {
696 |     const loading = document.getElementById('loading');
697 |     if (loading) {
698 |       loading.innerHTML = `
699 |         <div class="loading-container">
700 |           <div style="color: var(--color-danger); text-align: center;">
701 |             <h2>❌ Error</h2>
702 |             <p>${message}</p>
703 |             <button onclick="location.reload()" style="
704 |               margin-top: 1rem;
705 |               padding: 0.5rem 1rem;
706 |               background: var(--color-primary);
707 |               color: white;
708 |               border: none;
709 |               border-radius: 0.5rem;
710 |               cursor: pointer;
711 |             ">Reload Page</button>
712 |           </div>
713 |         </div>
714 |       `;
715 |     }
716 |   }
717 | }
718 | 
719 | // Initialize application when DOM is ready
720 | document.addEventListener('DOMContentLoaded', () => {
721 |   const app = new App();
722 |   app.initialize().catch(console.error);
723 | });
724 | 
725 | // Handle unhandled errors
726 | window.addEventListener('error', (event) => {
727 |   console.error('Unhandled error:', event.error);
728 | });
729 | 
730 | window.addEventListener('unhandledrejection', (event) => {
731 |   console.error('Unhandled promise rejection:', event.reason);
732 | });
733 | 
```
Page 4/8FirstPrevNextLast