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();