import { GoogleGenAI, Type } from "@google/genai"; // --- Types --- interface GoldPricePoint { time: string; price: number; } interface DrawingLine { id: string; x1: number; y1: number; x2: number; y2: number; color: string; } // --- State --- let priceData: GoldPricePoint[] = []; let lines: DrawingLine[] = []; let selectedLineId: string | null = null; let interactionMode: 'none' | 'drawing' | 'moving-handle-1' | 'moving-handle-2' | 'moving-line' = 'none'; let drawingStart = { x: 0, y: 0 }; let dragOffset = { dx: 0, dy: 0 }; let chartInstance: any = null; const ai = new GoogleGenAI({ apiKey: process.env.API_KEY || '' }); // --- DOM Elements --- const overlay = document.getElementById('drawing-overlay') as unknown as SVGSVGElement; const priceEl = document.getElementById('current-price')!; const lineCountEl = document.getElementById('line-count')!; const linesListEl = document.getElementById('lines-list')!; const aiContainer = document.getElementById('ai-result-container')!; const editIndicator = document.getElementById('edit-indicator')!; // --- Initialization --- async function init() { generateData(); initChart(); await loadLines(); renderLines(); setupEventListeners(); } function generateData() { let lastPrice = 2300; const now = Date.now(); for (let i = 24; i >= 0; i--) { const volatility = (Math.random() - 0.5) * 30; lastPrice += volatility; priceData.push({ time: new Date(now - i * 3600000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), price: parseFloat(lastPrice.toFixed(2)) }); } priceEl.textContent = `$${priceData[priceData.length - 1].price.toLocaleString()}`; } function initChart() { const ctx = (document.getElementById('gold-chart') as HTMLCanvasElement).getContext('2d'); chartInstance = new (window as any).Chart(ctx, { type: 'line', data: { labels: priceData.map(d => d.time), datasets: [{ label: 'Gold Price', data: priceData.map(d => d.price), borderColor: '#fbbf24', borderWidth: 3, pointRadius: 0, fill: false, tension: 0.4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { grid: { color: '#334155' }, ticks: { color: '#94a3b8' } }, y: { grid: { color: '#334155' }, ticks: { color: '#94a3b8' } } } } }); } // --- Drawing & Interaction --- function renderLines() { overlay.innerHTML = ''; lineCountEl.textContent = lines.length.toString(); linesListEl.innerHTML = lines.length === 0 ? '

No lines drawn yet.

' : ''; lines.forEach(line => { const isSelected = line.id === selectedLineId; const g = document.createElementNS("http://www.w3.org/2000/svg", "g"); const svgLine = document.createElementNS("http://www.w3.org/2000/svg", "line"); svgLine.setAttribute("x1", line.x1.toString()); svgLine.setAttribute("y1", line.y1.toString()); svgLine.setAttribute("x2", line.x2.toString()); svgLine.setAttribute("y2", line.y2.toString()); svgLine.setAttribute("stroke", isSelected ? "#3b82f6" : line.color); svgLine.setAttribute("stroke-width", isSelected ? "3" : "2"); if (!isSelected) svgLine.setAttribute("stroke-dasharray", "4 2"); svgLine.style.cursor = "pointer"; svgLine.style.pointerEvents = "auto"; g.appendChild(svgLine); if (isSelected) { const h1 = createHandle(line.x1, line.y1, line.id, 1); const h2 = createHandle(line.x2, line.y2, line.id, 2); g.appendChild(h1); g.appendChild(h2); editIndicator.classList.remove('hidden'); } overlay.appendChild(g); // Sidebar entry const entry = document.createElement('div'); entry.className = "group p-2 bg-slate-800/50 border border-slate-800 rounded hover:border-slate-600 transition flex items-center justify-between"; entry.innerHTML = `
ID: ${line.id.substring(0, 8)}
(${Math.round(line.x1)}, ${Math.round(line.y1)}) → (${Math.round(line.x2)}, ${Math.round(line.y2)})
`; entry.onclick = () => { selectedLineId = line.id; renderLines(); }; entry.querySelector('.btn-delete')?.addEventListener('click', (e) => { e.stopPropagation(); deleteLine(line.id); }); linesListEl.appendChild(entry); }); if (!selectedLineId) editIndicator.classList.add('hidden'); } function createHandle(cx: number, cy: number, lineId: string, handleNum: number) { const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle"); circle.setAttribute("cx", cx.toString()); circle.setAttribute("cy", cy.toString()); circle.setAttribute("r", "6"); circle.setAttribute("fill", "#3b82f6"); circle.setAttribute("data-handle", handleNum.toString()); circle.style.cursor = "move"; circle.style.pointerEvents = "auto"; return circle; } function setupEventListeners() { overlay.addEventListener('mousedown', (e: any) => { const coords = getCoords(e); const handleNum = e.target.getAttribute('data-handle'); if (handleNum) { interactionMode = handleNum === '1' ? 'moving-handle-1' : 'moving-handle-2'; return; } const clickedLine = lines.find(l => { const dist = Math.abs((l.y2 - l.y1) * coords.x - (l.x2 - l.x1) * coords.y + l.x2 * l.y1 - l.y2 * l.x1) / Math.sqrt(Math.pow(l.y2 - l.y1, 2) + Math.pow(l.x2 - l.x1, 2)); return dist < 10; }); if (clickedLine) { selectedLineId = clickedLine.id; interactionMode = 'moving-line'; dragOffset = { dx: coords.x - clickedLine.x1, dy: coords.y - clickedLine.y1 }; } else { selectedLineId = null; interactionMode = 'drawing'; drawingStart = coords; } renderLines(); }); window.addEventListener('mousemove', (e: any) => { if (interactionMode === 'none') return; const coords = getCoords(e); const line = lines.find(l => l.id === selectedLineId); if (interactionMode === 'drawing') { // Preview drawing renderLines(); const preview = document.createElementNS("http://www.w3.org/2000/svg", "line"); preview.setAttribute("x1", drawingStart.x.toString()); preview.setAttribute("y1", drawingStart.y.toString()); preview.setAttribute("x2", coords.x.toString()); preview.setAttribute("y2", coords.y.toString()); preview.setAttribute("stroke", "#fbbf24"); preview.setAttribute("stroke-width", "2"); preview.setAttribute("stroke-dasharray", "4 2"); preview.setAttribute("opacity", "0.6"); overlay.appendChild(preview); } else if (line) { if (interactionMode === 'moving-handle-1') { line.x1 = coords.x; line.y1 = coords.y; } else if (interactionMode === 'moving-handle-2') { line.x2 = coords.x; line.y2 = coords.y; } else if (interactionMode === 'moving-line') { const w = line.x2 - line.x1; const h = line.y2 - line.y1; line.x1 = coords.x - dragOffset.dx; line.y1 = coords.y - dragOffset.dy; line.x2 = line.x1 + w; line.y2 = line.y1 + h; } renderLines(); } }); window.addEventListener('mouseup', (e: any) => { if (interactionMode === 'drawing') { const coords = getCoords(e); const dist = Math.sqrt(Math.pow(coords.x - drawingStart.x, 2) + Math.pow(coords.y - drawingStart.y, 2)); if (dist > 5) { const newLine = { id: crypto.randomUUID(), x1: drawingStart.x, y1: drawingStart.y, x2: coords.x, y2: coords.y, color: '#fbbf24' }; lines.push(newLine); selectedLineId = newLine.id; } } interactionMode = 'none'; renderLines(); }); document.getElementById('btn-save')?.addEventListener('click', saveLines); document.getElementById('btn-clear')?.addEventListener('click', () => { lines = []; selectedLineId = null; renderLines(); }); document.getElementById('btn-ai')?.addEventListener('click', runAIAnalysis); } function getCoords(e: MouseEvent) { const rect = overlay.getBoundingClientRect(); return { x: e.clientX - rect.left, y: e.clientY - rect.top }; } // --- API Persistence --- async function loadLines() { try { const resp = await fetch('/api/lines'); if (resp.ok) lines = await resp.json(); } catch (err) { console.error("Load failed", err); } } async function saveLines() { const btn = document.getElementById('btn-save')!; const originalHtml = btn.innerHTML; btn.innerHTML = ' Saving...'; try { const resp = await fetch('/api/lines', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(lines) }); if (resp.ok) alert("Coordinates saved to Fiber backend!"); } catch (err) { alert("Save failed"); } btn.innerHTML = originalHtml; } function deleteLine(id: string) { lines = lines.filter(l => l.id !== id); if (selectedLineId === id) selectedLineId = null; renderLines(); } // --- AI Analysis --- async function runAIAnalysis() { const btn = document.getElementById('btn-ai')!; const originalHtml = btn.innerHTML; btn.innerHTML = ' AI Thinking...'; try { const prompt = `Analyze gold price: ${JSON.stringify(priceData)}. Provide sentiment, analysis, and prediction.`; const response = await ai.models.generateContent({ model: 'gemini-3-flash-preview', contents: prompt, config: { responseMimeType: "application/json", responseSchema: { type: Type.OBJECT, properties: { sentiment: { type: Type.STRING }, analysis: { type: Type.STRING }, prediction: { type: Type.STRING } }, required: ["sentiment", "analysis", "prediction"] } } }); const result = JSON.parse(response.text || '{}'); aiContainer.classList.remove('hidden'); document.getElementById('ai-sentiment')!.textContent = result.sentiment; document.getElementById('ai-sentiment')!.className = `text-xs font-bold px-2 py-1 rounded ${ result.sentiment === 'Bullish' ? 'bg-green-900/50 text-green-400' : 'bg-red-900/50 text-red-400' }`; document.getElementById('ai-analysis')!.textContent = result.analysis; document.getElementById('ai-prediction')!.textContent = result.prediction; } catch (err) { alert("AI analysis failed."); } btn.innerHTML = originalHtml; } init();