1341 lines
49 KiB
HTML
1341 lines
49 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>POE2 Trade Bot</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
|
background: #0d1117;
|
|
color: #e6edf3;
|
|
min-height: 100vh;
|
|
}
|
|
.container { max-width: 900px; margin: 0 auto; padding: 20px; }
|
|
|
|
header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 16px 0;
|
|
border-bottom: 1px solid #30363d;
|
|
margin-bottom: 20px;
|
|
}
|
|
header h1 { font-size: 20px; font-weight: 600; }
|
|
.header-right { display: flex; align-items: center; gap: 14px; }
|
|
.settings-btn {
|
|
background: none; border: none; cursor: pointer; padding: 4px;
|
|
color: #8b949e; transition: color 0.15s; line-height: 1;
|
|
}
|
|
.settings-btn:hover { background: none; color: #e6edf3; }
|
|
.settings-btn svg { display: block; }
|
|
.status-dot {
|
|
width: 10px; height: 10px; border-radius: 50%;
|
|
display: inline-block; margin-right: 8px;
|
|
}
|
|
.status-dot.running { background: #3fb950; box-shadow: 0 0 6px #3fb950; }
|
|
.status-dot.paused { background: #d29922; box-shadow: 0 0 6px #d29922; }
|
|
.status-dot.idle { background: #8b949e; }
|
|
|
|
.stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 12px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.stat-card {
|
|
background: #161b22;
|
|
border: 1px solid #30363d;
|
|
border-radius: 8px;
|
|
padding: 14px;
|
|
text-align: center;
|
|
}
|
|
.stat-card .value { font-size: 24px; font-weight: 700; color: #58a6ff; }
|
|
.stat-card .label { font-size: 11px; color: #8b949e; text-transform: uppercase; margin-top: 4px; }
|
|
|
|
.controls {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
}
|
|
button {
|
|
padding: 8px 20px;
|
|
border: 1px solid #30363d;
|
|
border-radius: 6px;
|
|
background: #21262d;
|
|
color: #e6edf3;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
transition: background 0.15s;
|
|
}
|
|
button:hover { background: #30363d; }
|
|
button.primary { background: #238636; border-color: #2ea043; }
|
|
button.primary:hover { background: #2ea043; }
|
|
button.danger { background: #da3633; border-color: #f85149; }
|
|
button.danger:hover { background: #f85149; }
|
|
button.warning { background: #9e6a03; border-color: #d29922; }
|
|
button.warning:hover { background: #d29922; }
|
|
|
|
.section { margin-bottom: 20px; }
|
|
.section-title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
margin-bottom: 10px;
|
|
color: #8b949e;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.add-link {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-bottom: 12px;
|
|
}
|
|
.add-link input {
|
|
flex: 1;
|
|
padding: 8px 12px;
|
|
background: #0d1117;
|
|
border: 1px solid #30363d;
|
|
border-radius: 6px;
|
|
color: #e6edf3;
|
|
font-size: 13px;
|
|
outline: none;
|
|
}
|
|
.add-link input:focus { border-color: #58a6ff; }
|
|
.add-link input::placeholder { color: #484f58; }
|
|
|
|
.links-list { display: flex; flex-direction: column; gap: 6px; }
|
|
.link-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
background: #161b22;
|
|
border: 1px solid #30363d;
|
|
border-radius: 6px;
|
|
padding: 10px 14px;
|
|
}
|
|
.link-left { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; }
|
|
.link-info { flex: 1; min-width: 0; }
|
|
.link-name { font-size: 13px; font-weight: 600; color: #e6edf3; }
|
|
.link-label { font-size: 12px; color: #8b949e; }
|
|
.link-url {
|
|
font-size: 11px; color: #484f58;
|
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
max-width: 600px;
|
|
}
|
|
.link-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
|
|
.link-item button { padding: 4px 12px; font-size: 12px; }
|
|
.link-item.inactive { opacity: 0.5; }
|
|
|
|
.mode-badge {
|
|
display: inline-block;
|
|
font-size: 10px;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
transition: background 0.15s;
|
|
}
|
|
.mode-badge.live { background: #1f6feb; color: #fff; }
|
|
.mode-badge.live:hover { background: #388bfd; }
|
|
.mode-badge.scrap { background: #9e6a03; color: #fff; }
|
|
.mode-badge.scrap:hover { background: #d29922; }
|
|
|
|
.mode-select {
|
|
padding: 6px 10px;
|
|
background: #0d1117;
|
|
border: 1px solid #30363d;
|
|
border-radius: 6px;
|
|
color: #e6edf3;
|
|
font-size: 13px;
|
|
outline: none;
|
|
}
|
|
.mode-select:focus { border-color: #58a6ff; }
|
|
|
|
/* Toggle switch */
|
|
.toggle { position: relative; width: 36px; height: 20px; cursor: pointer; flex-shrink: 0; }
|
|
.toggle input { opacity: 0; width: 0; height: 0; }
|
|
.toggle .slider {
|
|
position: absolute; inset: 0; background: #30363d;
|
|
border-radius: 20px; transition: background 0.2s;
|
|
}
|
|
.toggle .slider::before {
|
|
content: ''; position: absolute; left: 2px; top: 2px;
|
|
width: 16px; height: 16px; background: #8b949e;
|
|
border-radius: 50%; transition: transform 0.2s, background 0.2s;
|
|
}
|
|
.toggle input:checked + .slider { background: #238636; }
|
|
.toggle input:checked + .slider::before { transform: translateX(16px); background: #fff; }
|
|
|
|
.log-panel {
|
|
background: #161b22;
|
|
border: 1px solid #30363d;
|
|
border-radius: 8px;
|
|
height: 280px;
|
|
overflow-y: auto;
|
|
font-family: 'Cascadia Code', 'Fira Code', monospace;
|
|
font-size: 12px;
|
|
padding: 10px;
|
|
}
|
|
.log-line { padding: 2px 0; line-height: 1.5; }
|
|
.log-line .time { color: #484f58; }
|
|
.log-line.info .msg { color: #58a6ff; }
|
|
.log-line.warn .msg { color: #d29922; }
|
|
.log-line.error .msg { color: #f85149; }
|
|
.log-line.debug .msg { color: #8b949e; }
|
|
|
|
.empty-state {
|
|
color: #484f58;
|
|
text-align: center;
|
|
padding: 30px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.settings-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 10px;
|
|
}
|
|
.settings-grid.full { grid-template-columns: 1fr; }
|
|
.setting-row {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
.setting-row label {
|
|
font-size: 11px;
|
|
color: #8b949e;
|
|
text-transform: uppercase;
|
|
}
|
|
.setting-row input {
|
|
padding: 6px 10px;
|
|
background: #0d1117;
|
|
border: 1px solid #30363d;
|
|
border-radius: 6px;
|
|
color: #e6edf3;
|
|
font-size: 13px;
|
|
outline: none;
|
|
}
|
|
.setting-row input:focus { border-color: #58a6ff; }
|
|
.settings-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-top: 10px;
|
|
}
|
|
.saved-badge {
|
|
font-size: 12px;
|
|
color: #3fb950;
|
|
margin-right: 12px;
|
|
line-height: 32px;
|
|
opacity: 0;
|
|
transition: opacity 0.3s;
|
|
}
|
|
.saved-badge.show { opacity: 1; }
|
|
|
|
/* Modal overlay */
|
|
.modal-overlay {
|
|
display: none;
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.6);
|
|
z-index: 100;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
.modal-overlay.open { display: flex; }
|
|
.modal {
|
|
background: #161b22;
|
|
border: 1px solid #30363d;
|
|
border-radius: 10px;
|
|
width: 480px;
|
|
max-width: 90vw;
|
|
max-height: 85vh;
|
|
overflow-y: auto;
|
|
padding: 24px;
|
|
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4);
|
|
}
|
|
.modal-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 20px;
|
|
}
|
|
.modal-header h2 { font-size: 16px; font-weight: 600; }
|
|
.modal-close {
|
|
background: none; border: none; color: #8b949e;
|
|
cursor: pointer; padding: 4px; font-size: 18px; line-height: 1;
|
|
}
|
|
.modal-close:hover { background: none; color: #e6edf3; }
|
|
|
|
.debug-panel {
|
|
background: #161b22;
|
|
border: 1px solid #30363d;
|
|
border-radius: 8px;
|
|
padding: 14px;
|
|
}
|
|
.debug-row {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-bottom: 8px;
|
|
align-items: center;
|
|
}
|
|
.debug-row:last-of-type { margin-bottom: 0; }
|
|
.debug-row input {
|
|
padding: 6px 10px;
|
|
background: #0d1117;
|
|
border: 1px solid #30363d;
|
|
border-radius: 6px;
|
|
color: #e6edf3;
|
|
font-size: 13px;
|
|
outline: none;
|
|
}
|
|
.debug-row input:focus { border-color: #58a6ff; }
|
|
.debug-row input[type="text"] { flex: 1; }
|
|
.debug-result {
|
|
margin-top: 8px;
|
|
font-family: 'Cascadia Code', 'Fira Code', monospace;
|
|
font-size: 12px;
|
|
color: #8b949e;
|
|
white-space: pre-wrap;
|
|
}
|
|
.debug-result:empty { display: none; }
|
|
.grid-debug {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-top: 8px;
|
|
align-items: flex-start;
|
|
}
|
|
.grid-debug img {
|
|
border: 1px solid #30363d;
|
|
border-radius: 4px;
|
|
max-width: 280px;
|
|
max-height: 280px;
|
|
object-fit: contain;
|
|
}
|
|
.grid-view {
|
|
display: inline-grid;
|
|
gap: 1px;
|
|
background: #30363d;
|
|
border: 1px solid #30363d;
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
flex-shrink: 0;
|
|
}
|
|
.grid-cell {
|
|
width: 12px; height: 12px;
|
|
background: #0d1117;
|
|
}
|
|
.grid-cell.occupied {
|
|
background: #238636;
|
|
}
|
|
|
|
.detect-badge {
|
|
display: inline-block;
|
|
font-size: 10px;
|
|
padding: 1px 6px;
|
|
border-radius: 4px;
|
|
margin-left: 6px;
|
|
font-weight: 600;
|
|
}
|
|
.detect-badge.ok { background: #238636; color: #fff; }
|
|
.detect-badge.fallback { background: #9e6a03; color: #fff; }
|
|
|
|
/* Inventory grid */
|
|
.inv-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 10px;
|
|
}
|
|
.inv-free {
|
|
font-size: 12px;
|
|
color: #8b949e;
|
|
font-weight: 600;
|
|
}
|
|
.inventory-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(12, 1fr);
|
|
gap: 2px;
|
|
background: #161b22;
|
|
border: 1px solid #30363d;
|
|
border-radius: 8px;
|
|
padding: 10px;
|
|
}
|
|
.inv-cell {
|
|
aspect-ratio: 1;
|
|
border-radius: 3px;
|
|
background: #0d1117;
|
|
min-width: 0;
|
|
}
|
|
.inv-cell.occupied {
|
|
background: #238636;
|
|
}
|
|
.inv-cell.item-top { border-top: 2px solid #3fb950; }
|
|
.inv-cell.item-bottom { border-bottom: 2px solid #3fb950; }
|
|
.inv-cell.item-left { border-left: 2px solid #3fb950; }
|
|
.inv-cell.item-right { border-right: 2px solid #3fb950; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<h1>POE2 Trade Bot</h1>
|
|
<div class="header-right">
|
|
<div id="statusBadge">
|
|
<span class="status-dot idle"></span>
|
|
<span id="statusText">Connecting...</span>
|
|
</div>
|
|
<button class="settings-btn" onclick="openSettings()" title="Settings">
|
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="10" cy="10" r="3"/>
|
|
<path d="M10 1.5v2M10 16.5v2M3.4 3.4l1.4 1.4M15.2 15.2l1.4 1.4M1.5 10h2M16.5 10h2M3.4 16.6l1.4-1.4M15.2 4.8l1.4-1.4"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="stats">
|
|
<div class="stat-card">
|
|
<div class="value" id="stateValue">IDLE</div>
|
|
<div class="label">State</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="value" id="linksValue">0</div>
|
|
<div class="label">Active Links</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="value" id="completedValue">0</div>
|
|
<div class="label">Trades Done</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="value" id="failedValue">0</div>
|
|
<div class="label">Failed</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="controls" id="controls">
|
|
<button class="warning" id="pauseBtn" onclick="togglePause()">Pause</button>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="inv-header">
|
|
<div class="section-title" style="margin-bottom:0">Inventory</div>
|
|
<span class="inv-free" id="invFreeCount"></span>
|
|
</div>
|
|
<div class="inventory-grid" id="inventoryGrid">
|
|
<div class="empty-state" style="grid-column:1/-1">No active scrap session</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-title">Trade Links</div>
|
|
<div class="add-link">
|
|
<input type="text" id="nameInput" placeholder="Name (optional)" style="max-width:180px" />
|
|
<input type="text" id="urlInput" placeholder="Paste trade URL..." />
|
|
<select id="modeInput" class="mode-select" style="width:90px">
|
|
<option value="live">Live</option>
|
|
<option value="scrap">Scrap</option>
|
|
</select>
|
|
<button class="primary" onclick="addLink()">Add</button>
|
|
</div>
|
|
<div class="links-list" id="linksList">
|
|
<div class="empty-state">No trade links added yet</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-title">Debug Tools</div>
|
|
<div class="debug-panel">
|
|
<div class="debug-row">
|
|
<button onclick="openOcrSettings()" title="OCR Settings" style="display:flex;align-items:center;gap:4px">
|
|
<svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="10" cy="10" r="3"/><path d="M10 1.5v2M10 16.5v2M3.4 3.4l1.4 1.4M15.2 15.2l1.4 1.4M1.5 10h2M16.5 10h2M3.4 16.6l1.4-1.4M15.2 4.8l1.4-1.4"/></svg>
|
|
OCR Settings
|
|
</button>
|
|
<button onclick="debugScreenshot()">Screenshot</button>
|
|
<button onclick="debugOcr()">OCR Screen</button>
|
|
<button onclick="debugHideout()">Go Hideout</button>
|
|
</div>
|
|
<div class="debug-row">
|
|
<button onclick="debugFindAndClick('ANGE')">ANGE</button>
|
|
<button onclick="debugFindAndClick('STASH')">STASH</button>
|
|
<button onclick="debugFindAndClick('SALVAGE BENCH', true)">SALVAGE</button>
|
|
</div>
|
|
<div class="debug-row">
|
|
<button onclick="debugAngeOption('Currency')">Currency Exchange</button>
|
|
<button onclick="debugAngeOption('Manage')">Manage Shop</button>
|
|
<button onclick="debugAngeOption('Buy')">Buy or Sell</button>
|
|
<button onclick="debugAngeOption('Purchase')">Purchase Items</button>
|
|
</div>
|
|
<div class="debug-row">
|
|
<button onclick="debugGridScan('inventory')">Scan Inventory</button>
|
|
<button onclick="debugGridScan('stash12')">Scan Stash 12x12</button>
|
|
<button onclick="debugGridScan('stash24')">Scan Stash 24x24</button>
|
|
<button onclick="debugGridScan('seller')">Scan Seller</button>
|
|
<button onclick="debugGridScan('shop')">Scan Shop</button>
|
|
<button onclick="debugGridScan('vendor')">Scan Vendor</button>
|
|
</div>
|
|
<div class="debug-row">
|
|
<select id="matchLayout" style="padding:6px 10px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:13px">
|
|
<option value="shop">Shop</option>
|
|
<option value="seller">Seller</option>
|
|
<option value="stash12">Stash 12x12</option>
|
|
<option value="stash12_folder">Stash 12x12 (folder)</option>
|
|
<option value="inventory">Inventory</option>
|
|
<option value="vendor">Vendor</option>
|
|
</select>
|
|
<input type="number" id="matchRow" placeholder="Row" value="8" style="width:60px" />
|
|
<input type="number" id="matchCol" placeholder="Col" value="0" style="width:60px" />
|
|
<button class="primary" onclick="debugTestMatchHover()">Match & Hover</button>
|
|
</div>
|
|
<div class="debug-row">
|
|
<input type="text" id="debugTextInput" placeholder="Text to find (e.g. Stash, Ange)" />
|
|
<button onclick="debugFindText()">Find</button>
|
|
<button class="primary" onclick="debugFindAndClick()">Find & Click</button>
|
|
</div>
|
|
<div class="debug-row">
|
|
<input type="number" id="debugClickX" placeholder="X" style="width:80px" />
|
|
<input type="number" id="debugClickY" placeholder="Y" style="width:80px" />
|
|
<button onclick="debugClick()">Click At</button>
|
|
</div>
|
|
<div class="debug-result" id="debugResult"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-title">Activity Log</div>
|
|
<div class="log-panel" id="logPanel"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-overlay" id="settingsModal">
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
<h2>Settings</h2>
|
|
<button class="modal-close" onclick="closeSettings()">×</button>
|
|
</div>
|
|
<div class="settings-grid full">
|
|
<div class="setting-row">
|
|
<label>POE2 Client.txt Path</label>
|
|
<input type="text" id="settLogPath" />
|
|
</div>
|
|
</div>
|
|
<div class="settings-grid" style="margin-top:10px">
|
|
<div class="setting-row">
|
|
<label>Window Title</label>
|
|
<input type="text" id="settWindowTitle" />
|
|
</div>
|
|
<div class="setting-row">
|
|
<label>Travel Timeout (ms)</label>
|
|
<input type="number" id="settTravelTimeout" />
|
|
</div>
|
|
<div class="setting-row">
|
|
<label>Wait for More Items (ms)</label>
|
|
<input type="number" id="settWaitMore" />
|
|
</div>
|
|
<div class="setting-row">
|
|
<label>Delay Between Trades (ms)</label>
|
|
<input type="number" id="settTradeDelay" />
|
|
</div>
|
|
</div>
|
|
<div class="settings-actions">
|
|
<span class="saved-badge" id="savedBadge">Saved</span>
|
|
<button class="primary" onclick="saveSettings()">Save Settings</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-overlay" id="ocrSettingsModal">
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
<h2>OCR Settings</h2>
|
|
<button class="modal-close" onclick="closeOcrSettings()">×</button>
|
|
</div>
|
|
|
|
<div style="margin-bottom:16px">
|
|
<div class="section-title" style="margin-bottom:6px">Engine</div>
|
|
<select id="ocrEngine" class="mode-select" style="width:100%" onchange="toggleOcrSections()">
|
|
<option value="tesseract">Tesseract</option>
|
|
<option value="easyocr">EasyOCR</option>
|
|
<option value="paddleocr">PaddleOCR</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div style="margin-bottom:16px">
|
|
<div class="section-title" style="margin-bottom:6px">Screen OCR</div>
|
|
<select id="ocrScreenPreprocess" class="mode-select" style="width:100%" onchange="toggleOcrSections()">
|
|
<option value="none">None</option>
|
|
<option value="tophat">TopHat</option>
|
|
</select>
|
|
<div id="screenTophatParams" style="display:none;margin-top:8px">
|
|
<div class="settings-grid">
|
|
<div class="setting-row">
|
|
<label>Kernel Size</label>
|
|
<input type="number" id="ocrScreenKernel" value="41" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-bottom:16px">
|
|
<div class="section-title" style="margin-bottom:6px">Tooltip Method</div>
|
|
<select id="ocrTooltipMethod" class="mode-select" style="width:100%" onchange="toggleOcrSections()">
|
|
<option value="diff">Diff Detection</option>
|
|
<option value="edge">Edge Detection</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div style="margin-bottom:16px">
|
|
<div class="section-title" style="margin-bottom:6px">Tooltip Preprocess</div>
|
|
<select id="ocrTooltipPreprocess" class="mode-select" style="width:100%" onchange="toggleOcrSections()">
|
|
<option value="none">None</option>
|
|
<option value="bgsub">Background Subtraction</option>
|
|
<option value="tophat">TopHat</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div id="diffCropParams" style="margin-bottom:16px">
|
|
<div class="section-title" style="margin-bottom:6px">Crop Detection (Diff)</div>
|
|
<div class="settings-grid">
|
|
<div class="setting-row">
|
|
<label>Diff Threshold</label>
|
|
<input type="number" id="ocrDiffThresh" value="20" />
|
|
</div>
|
|
<div class="setting-row">
|
|
<label>Max Gap</label>
|
|
<input type="number" id="ocrMaxGap" value="20" />
|
|
</div>
|
|
<div class="setting-row">
|
|
<label>Trim Cutoff</label>
|
|
<input type="number" id="ocrTrimCutoff" value="0.4" step="0.05" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="edgeCropParams" style="margin-bottom:16px;display:none">
|
|
<div class="section-title" style="margin-bottom:6px">Crop Detection (Edge)</div>
|
|
<div class="settings-grid">
|
|
<div class="setting-row">
|
|
<label>Canny Low</label>
|
|
<input type="number" id="ocrCannyLow" value="50" />
|
|
</div>
|
|
<div class="setting-row">
|
|
<label>Canny High</label>
|
|
<input type="number" id="ocrCannyHigh" value="150" />
|
|
</div>
|
|
<div class="setting-row">
|
|
<label>Min Line Length</label>
|
|
<input type="number" id="ocrMinLineLength" value="100" />
|
|
</div>
|
|
<div class="setting-row">
|
|
<label>ROI Size</label>
|
|
<input type="number" id="ocrRoiSize" value="1400" />
|
|
</div>
|
|
<div class="setting-row">
|
|
<label>Density Threshold</label>
|
|
<input type="number" id="ocrDensityThreshold" value="0.15" step="0.01" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-bottom:16px">
|
|
<div class="section-title" style="margin-bottom:6px">OCR Processing</div>
|
|
<div class="settings-grid">
|
|
<div class="setting-row">
|
|
<label>Upscale</label>
|
|
<input type="number" id="ocrUpscale" value="2" />
|
|
</div>
|
|
<div class="setting-row">
|
|
<label>Merge Gap (px)</label>
|
|
<input type="number" id="ocrMergeGap" value="0" />
|
|
</div>
|
|
</div>
|
|
<div id="tooltipTophatParams" style="display:none;margin-top:8px">
|
|
<div class="settings-grid">
|
|
<div class="setting-row">
|
|
<label>Kernel Size</label>
|
|
<input type="number" id="ocrTooltipKernel" value="41" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="tooltipBgsubParams" style="display:none;margin-top:8px">
|
|
<div class="settings-grid">
|
|
<div class="setting-row">
|
|
<label>Dim Percentile</label>
|
|
<input type="number" id="ocrDimPercentile" value="40" />
|
|
</div>
|
|
<div class="setting-row">
|
|
<label>Text Threshold</label>
|
|
<input type="number" id="ocrTextThresh" value="60" />
|
|
</div>
|
|
</div>
|
|
<div style="display:flex;align-items:center;gap:8px;margin-top:8px">
|
|
<label class="toggle">
|
|
<input type="checkbox" id="ocrSoftThreshold" />
|
|
<span class="slider"></span>
|
|
</label>
|
|
<span style="font-size:12px;color:#8b949e">Soft Threshold</span>
|
|
</div>
|
|
</div>
|
|
<div id="easyocrParams" style="display:none;margin-top:8px">
|
|
<div class="settings-grid">
|
|
<div class="setting-row">
|
|
<label>Link Threshold</label>
|
|
<input type="number" id="ocrLinkThreshold" step="0.05" />
|
|
</div>
|
|
<div class="setting-row">
|
|
<label>Text Threshold</label>
|
|
<input type="number" id="ocrTextThreshold" step="0.05" />
|
|
</div>
|
|
<div class="setting-row">
|
|
<label>Low Text</label>
|
|
<input type="number" id="ocrLowText" step="0.05" />
|
|
</div>
|
|
<div class="setting-row">
|
|
<label>Width Threshold</label>
|
|
<input type="number" id="ocrWidthThs" step="0.05" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-bottom:16px">
|
|
<div class="section-title" style="margin-bottom:6px">Options</div>
|
|
<div style="display:flex;align-items:center;gap:8px">
|
|
<label class="toggle">
|
|
<input type="checkbox" id="ocrSaveDebugImages" checked />
|
|
<span class="slider"></span>
|
|
</label>
|
|
<span style="font-size:12px;color:#8b949e">Save debug images to disk</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="settings-actions">
|
|
<span class="saved-badge" id="ocrSavedBadge">Saved</span>
|
|
<button onclick="closeOcrSettings()" style="margin-right:8px">Close</button>
|
|
<button class="primary" onclick="saveOcrSettings()">Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let ws;
|
|
let status = { paused: false, state: 'IDLE', links: [], tradesCompleted: 0, tradesFailed: 0, uptime: 0, settings: {} };
|
|
let settingsLoaded = false;
|
|
|
|
function connect() {
|
|
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
ws = new WebSocket(`${proto}//${location.host}`);
|
|
ws.onmessage = (e) => {
|
|
const msg = JSON.parse(e.data);
|
|
if (msg.type === 'status') {
|
|
status = msg.data;
|
|
render();
|
|
} else if (msg.type === 'log') {
|
|
addLog(msg.data);
|
|
} else if (msg.type === 'debug') {
|
|
handleDebugResult(msg.data);
|
|
}
|
|
};
|
|
ws.onclose = () => {
|
|
addLog({ level: 'warn', message: 'Dashboard disconnected. Reconnecting...', time: new Date().toISOString() });
|
|
setTimeout(connect, 2000);
|
|
};
|
|
ws.onerror = () => {};
|
|
}
|
|
|
|
function render() {
|
|
// Status badge
|
|
const dot = document.querySelector('.status-dot');
|
|
const text = document.getElementById('statusText');
|
|
dot.className = 'status-dot ' + (status.paused ? 'paused' : status.state === 'IDLE' ? 'idle' : 'running');
|
|
text.textContent = status.paused ? 'Paused' : status.state;
|
|
|
|
// Stats
|
|
document.getElementById('stateValue').textContent = status.state;
|
|
document.getElementById('linksValue').textContent = status.links.length;
|
|
document.getElementById('completedValue').textContent = status.tradesCompleted;
|
|
document.getElementById('failedValue').textContent = status.tradesFailed;
|
|
|
|
// Pause button
|
|
const btn = document.getElementById('pauseBtn');
|
|
btn.textContent = status.paused ? 'Resume' : 'Pause';
|
|
btn.className = status.paused ? 'primary' : 'warning';
|
|
|
|
// Settings (populate once on first status)
|
|
if (status.settings) populateSettings(status.settings);
|
|
|
|
// Inventory grid
|
|
renderInventory();
|
|
|
|
// Active links count
|
|
document.getElementById('linksValue').textContent = status.links.filter(l => l.active).length;
|
|
|
|
// Links list
|
|
const list = document.getElementById('linksList');
|
|
if (status.links.length === 0) {
|
|
list.innerHTML = '<div class="empty-state">No trade links added yet</div>';
|
|
} else {
|
|
list.innerHTML = status.links.map(link => `
|
|
<div class="link-item${link.active ? '' : ' inactive'}">
|
|
<div class="link-left">
|
|
<label class="toggle" title="${link.active ? 'Active' : 'Inactive'}">
|
|
<input type="checkbox" ${link.active ? 'checked' : ''} onchange="toggleLink('${esc(link.id)}', this.checked)" />
|
|
<span class="slider"></span>
|
|
</label>
|
|
<span class="mode-badge ${link.mode || 'live'}" onclick="cycleMode('${esc(link.id)}', '${link.mode || 'live'}')" title="Click to change mode">${esc(link.mode || 'live')}</span>
|
|
<div class="link-info">
|
|
<div class="link-name" contenteditable="true" spellcheck="false"
|
|
onblur="renameLink('${esc(link.id)}', this.textContent)"
|
|
title="Click to edit name">${esc(link.name || link.label)}</div>
|
|
<div class="link-url">${esc(link.url)}</div>
|
|
</div>
|
|
</div>
|
|
<div class="link-actions">
|
|
<button class="danger" onclick="removeLink('${esc(link.id)}')">Remove</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
}
|
|
|
|
function renderInventory() {
|
|
const container = document.getElementById('inventoryGrid');
|
|
const freeLabel = document.getElementById('invFreeCount');
|
|
const grid = status.inventory ? status.inventory.grid : null;
|
|
const items = status.inventory ? status.inventory.items : [];
|
|
const free = status.inventory ? status.inventory.free : 60;
|
|
|
|
freeLabel.textContent = `${free}/60 free`;
|
|
|
|
let html = '';
|
|
for (let r = 0; r < 5; r++) {
|
|
for (let c = 0; c < 12; c++) {
|
|
const occupied = grid && grid[r] && grid[r][c] ? 'occupied' : '';
|
|
html += `<div class="inv-cell ${occupied}" data-r="${r}" data-c="${c}"></div>`;
|
|
}
|
|
}
|
|
container.innerHTML = html;
|
|
|
|
for (const item of items) {
|
|
for (let r = item.row; r < item.row + item.h; r++) {
|
|
for (let c = item.col; c < item.col + item.w; c++) {
|
|
const cell = container.querySelector(`[data-r="${r}"][data-c="${c}"]`);
|
|
if (cell) {
|
|
if (r === item.row) cell.classList.add('item-top');
|
|
if (r === item.row + item.h - 1) cell.classList.add('item-bottom');
|
|
if (c === item.col) cell.classList.add('item-left');
|
|
if (c === item.col + item.w - 1) cell.classList.add('item-right');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function addLog(data) {
|
|
const panel = document.getElementById('logPanel');
|
|
const line = document.createElement('div');
|
|
line.className = 'log-line ' + (data.level || 'info');
|
|
const t = new Date(data.time).toLocaleTimeString();
|
|
line.innerHTML = `<span class="time">${t}</span> <span class="msg">${esc(data.message)}</span>`;
|
|
panel.appendChild(line);
|
|
if (panel.children.length > 500) panel.removeChild(panel.firstChild);
|
|
panel.scrollTop = panel.scrollHeight;
|
|
}
|
|
|
|
// --- Handle debug results arriving via WebSocket ---
|
|
|
|
function handleDebugResult(data) {
|
|
switch (data.action) {
|
|
case 'screenshot':
|
|
if (data.error) { showDebugResult(`Error: ${data.error}`); break; }
|
|
showDebugResult(`Screenshots saved: ${(data.files || []).map(f => f.split(/[\\/]/).pop()).join(', ')}`);
|
|
break;
|
|
case 'ocr':
|
|
showDebugResult(data.error ? `Error: ${data.error}` : data.text);
|
|
break;
|
|
case 'find-text':
|
|
if (data.error) { showDebugResult(`Error: ${data.error}`); break; }
|
|
if (data.found) {
|
|
showDebugResult(`Found "${data.searchText}" at (${data.position.x}, ${data.position.y})`);
|
|
document.getElementById('debugClickX').value = data.position.x;
|
|
document.getElementById('debugClickY').value = data.position.y;
|
|
} else {
|
|
showDebugResult(`"${data.searchText}" not found on screen`);
|
|
}
|
|
break;
|
|
case 'find-and-click':
|
|
if (data.error) { showDebugResult(`Error: ${data.error}`); break; }
|
|
showDebugResult(data.found
|
|
? `Clicked "${data.searchText}" at (${data.position.x}, ${data.position.y})`
|
|
: `"${data.searchText}" not found on screen`);
|
|
break;
|
|
case 'click':
|
|
showDebugResult(data.error ? `Error: ${data.error}` : `Clicked at (${data.x}, ${data.y})`);
|
|
break;
|
|
case 'hideout':
|
|
showDebugResult(data.error ? `Error: ${data.error}` : 'Sent /hideout command');
|
|
break;
|
|
case 'click-then-click':
|
|
if (data.error) { showDebugResult(`Error: ${data.error}`); break; }
|
|
if (!data.found && data.step === 'first') showDebugResult(`"${data.first}" not found on screen`);
|
|
else if (!data.found) showDebugResult(`"${data.second}" not found after clicking "${data.first}" (timed out)`);
|
|
else showDebugResult(`Clicked "${data.second}" at (${data.position.x}, ${data.position.y})`);
|
|
break;
|
|
case 'grid-scan':
|
|
if (data.error) { showDebugResult(`Error: ${data.error}`); break; }
|
|
renderGridScanResult(data);
|
|
break;
|
|
case 'test-match-hover':
|
|
if (data.error) { showDebugResult(`Error: ${data.error}`); break; }
|
|
showDebugResult(`Done: ${data.itemSize} item, ${data.matchCount} matches, hovered ${data.hoveredCount} cells`);
|
|
break;
|
|
}
|
|
}
|
|
|
|
function renderGridScanResult(data) {
|
|
const layout = data.layout;
|
|
const hasTarget = data.targetRow != null && data.targetCol != null;
|
|
const targetRow = data.targetRow;
|
|
const targetCol = data.targetCol;
|
|
const el = document.getElementById('debugResult');
|
|
const count = data.occupied.length;
|
|
const items = data.items || [];
|
|
const matches = data.matches || [];
|
|
const r = data.region;
|
|
let html = `<b>${layout}</b> ${data.cols}x${data.rows}`;
|
|
html += ` — ${count} occupied, ${items.length} items`;
|
|
if (matches.length > 0) html += `, <span style="color:#f0883e;font-weight:bold">${matches.length} matches</span>`;
|
|
if (r) html += `<br><span style="color:#484f58">Region: (${r.x}, ${r.y}) ${r.width}x${r.height}</span>`;
|
|
if (items.length > 0) {
|
|
const sizes = {};
|
|
items.forEach(i => { const k = i.w + 'x' + i.h; sizes[k] = (sizes[k]||0) + 1; });
|
|
html += `<br><span style="color:#58a6ff">` + Object.entries(sizes).map(([k,v]) => `${v}x ${k}`).join(', ') + `</span>`;
|
|
}
|
|
if (hasTarget) {
|
|
html += `<br><span style="color:#f0883e">Target: (${targetRow},${targetCol})`;
|
|
if (matches.length > 0) html += ` → ${matches.map(m => `(${m.row},${m.col}) ${(m.similarity*100).toFixed(0)}%`).join(', ')}`;
|
|
html += `</span>`;
|
|
} else {
|
|
html += `<br><span style="color:#484f58">Click a cell to find matching items</span>`;
|
|
}
|
|
html += '<div class="grid-debug">';
|
|
if (data.image) {
|
|
html += `<img src="data:image/png;base64,${data.image}" alt="Grid capture" />`;
|
|
}
|
|
const matchSet = new Set(matches.map(m => m.row + ',' + m.col));
|
|
const targetKey = hasTarget ? targetRow + ',' + targetCol : null;
|
|
const itemMap = {};
|
|
const colors = ['#238636','#1f6feb','#8957e5','#da3633','#d29922','#3fb950','#388bfd','#a371f7','#f85149','#e3b341'];
|
|
items.forEach((item, idx) => {
|
|
const color = colors[idx % colors.length];
|
|
for (let dr = 0; dr < item.h; dr++)
|
|
for (let dc = 0; dc < item.w; dc++)
|
|
itemMap[(item.row+dr)+','+(item.col+dc)] = { item, color, isOrigin: dr===0 && dc===0 };
|
|
});
|
|
html += `<div class="grid-view" style="grid-template-columns:repeat(${data.cols},12px)">`;
|
|
const set = new Set(data.occupied.map(c => c.row + ',' + c.col));
|
|
for (let gr = 0; gr < data.rows; gr++) {
|
|
for (let gc = 0; gc < data.cols; gc++) {
|
|
const key = gr+','+gc;
|
|
const isTarget = key === targetKey;
|
|
const isMatch = matchSet.has(key);
|
|
const info = itemMap[key];
|
|
let bg;
|
|
if (isTarget) bg = '#f0883e';
|
|
else if (isMatch) bg = '#d29922';
|
|
else if (info) bg = info.color;
|
|
else if (set.has(key)) bg = '#238636';
|
|
else bg = '';
|
|
const outline = (isTarget || isMatch) ? 'outline:2px solid #f0883e;z-index:1;' : '';
|
|
const cursor = set.has(key) ? 'cursor:pointer;' : '';
|
|
const bgStyle = bg ? `background:${bg};` : '';
|
|
const style = (bgStyle || outline || cursor) ? ` style="${bgStyle}${outline}${cursor}"` : '';
|
|
let title = info ? `(${gr},${gc}) ${info.item.w}x${info.item.h}` : `(${gr},${gc})`;
|
|
if (isTarget) title += ' [TARGET]';
|
|
if (isMatch) { const m = matches.find(m => m.row===gr && m.col===gc); title += ` [MATCH ${(m.similarity*100).toFixed(0)}%]`; }
|
|
const onclick = set.has(key) ? ` onclick="debugGridScan('${layout}',${gr},${gc})"` : '';
|
|
html += `<div class="grid-cell${set.has(key) ? ' occupied' : ''}"${style}${onclick} title="${title}"></div>`;
|
|
}
|
|
}
|
|
html += '</div></div>';
|
|
el.innerHTML = html;
|
|
}
|
|
|
|
async function togglePause() {
|
|
const endpoint = status.paused ? '/api/resume' : '/api/pause';
|
|
await fetch(endpoint, { method: 'POST' });
|
|
}
|
|
|
|
async function addLink() {
|
|
const urlEl = document.getElementById('urlInput');
|
|
const nameEl = document.getElementById('nameInput');
|
|
const modeEl = document.getElementById('modeInput');
|
|
const url = urlEl.value.trim();
|
|
if (!url) return;
|
|
await fetch('/api/links', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ url, name: nameEl.value.trim(), mode: modeEl.value }),
|
|
});
|
|
urlEl.value = '';
|
|
nameEl.value = '';
|
|
}
|
|
|
|
async function removeLink(id) {
|
|
await fetch('/api/links/' + encodeURIComponent(id), { method: 'DELETE' });
|
|
}
|
|
|
|
async function toggleLink(id, active) {
|
|
await fetch('/api/links/' + encodeURIComponent(id) + '/toggle', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ active }),
|
|
});
|
|
}
|
|
|
|
let renameTimer = null;
|
|
async function renameLink(id, name) {
|
|
clearTimeout(renameTimer);
|
|
renameTimer = setTimeout(async () => {
|
|
await fetch('/api/links/' + encodeURIComponent(id) + '/name', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name: name.trim() }),
|
|
});
|
|
}, 300);
|
|
}
|
|
|
|
async function cycleMode(id, currentMode) {
|
|
const newMode = currentMode === 'live' ? 'scrap' : 'live';
|
|
await fetch('/api/links/' + encodeURIComponent(id) + '/mode', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ mode: newMode }),
|
|
});
|
|
}
|
|
|
|
function esc(s) {
|
|
const d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function populateSettings(s) {
|
|
if (!s || settingsLoaded) return;
|
|
settingsLoaded = true;
|
|
document.getElementById('settLogPath').value = s.poe2LogPath || '';
|
|
document.getElementById('settWindowTitle').value = s.poe2WindowTitle || '';
|
|
document.getElementById('settTravelTimeout').value = s.travelTimeoutMs || 15000;
|
|
document.getElementById('settWaitMore').value = s.waitForMoreItemsMs || 20000;
|
|
document.getElementById('settTradeDelay').value = s.betweenTradesDelayMs || 5000;
|
|
}
|
|
|
|
function openSettings() {
|
|
if (status.settings) {
|
|
const s = status.settings;
|
|
document.getElementById('settLogPath').value = s.poe2LogPath || '';
|
|
document.getElementById('settWindowTitle').value = s.poe2WindowTitle || '';
|
|
document.getElementById('settTravelTimeout').value = s.travelTimeoutMs || 15000;
|
|
document.getElementById('settWaitMore').value = s.waitForMoreItemsMs || 20000;
|
|
document.getElementById('settTradeDelay').value = s.betweenTradesDelayMs || 5000;
|
|
}
|
|
document.getElementById('settingsModal').classList.add('open');
|
|
}
|
|
|
|
function closeSettings() {
|
|
document.getElementById('settingsModal').classList.remove('open');
|
|
}
|
|
|
|
document.getElementById('settingsModal').addEventListener('click', (e) => {
|
|
if (e.target === e.currentTarget) closeSettings();
|
|
});
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') { closeSettings(); closeOcrSettings(); }
|
|
});
|
|
|
|
async function saveSettings() {
|
|
const body = {
|
|
poe2LogPath: document.getElementById('settLogPath').value,
|
|
poe2WindowTitle: document.getElementById('settWindowTitle').value,
|
|
travelTimeoutMs: parseInt(document.getElementById('settTravelTimeout').value) || 15000,
|
|
waitForMoreItemsMs: parseInt(document.getElementById('settWaitMore').value) || 20000,
|
|
betweenTradesDelayMs: parseInt(document.getElementById('settTradeDelay').value) || 5000,
|
|
};
|
|
await fetch('/api/settings', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
const badge = document.getElementById('savedBadge');
|
|
badge.classList.add('show');
|
|
setTimeout(() => badge.classList.remove('show'), 2000);
|
|
}
|
|
|
|
// --- Debug functions (fire-and-forget: POST returns instantly, results arrive via WS) ---
|
|
|
|
function debugScreenshot() {
|
|
showDebugResult('Taking screenshots...');
|
|
fetch('/api/debug/screenshot', { method: 'POST' });
|
|
}
|
|
|
|
function debugOcr() {
|
|
showDebugResult('Running OCR...');
|
|
fetch('/api/debug/ocr', { method: 'POST' });
|
|
}
|
|
|
|
function debugHideout() {
|
|
showDebugResult('Sending /hideout...');
|
|
fetch('/api/debug/hideout', { method: 'POST' });
|
|
}
|
|
|
|
function debugFindText() {
|
|
const text = document.getElementById('debugTextInput').value.trim();
|
|
if (!text) return;
|
|
showDebugResult(`Searching for "${text}"...`);
|
|
fetch('/api/debug/find-text', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ text }),
|
|
});
|
|
}
|
|
|
|
function debugFindAndClick(directText, fuzzy) {
|
|
const text = directText || document.getElementById('debugTextInput').value.trim();
|
|
if (!text) return;
|
|
showDebugResult(`Finding and clicking "${text}"${fuzzy ? ' (fuzzy)' : ''}...`);
|
|
fetch('/api/debug/find-and-click', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ text, fuzzy: !!fuzzy }),
|
|
});
|
|
}
|
|
|
|
function debugClick() {
|
|
const x = parseInt(document.getElementById('debugClickX').value);
|
|
const y = parseInt(document.getElementById('debugClickY').value);
|
|
if (isNaN(x) || isNaN(y)) return;
|
|
showDebugResult(`Clicking at (${x}, ${y})...`);
|
|
fetch('/api/debug/click', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ x, y }),
|
|
});
|
|
}
|
|
|
|
function debugAngeOption(option) {
|
|
showDebugResult(`Clicking ANGE → ${option}...`);
|
|
fetch('/api/debug/click-then-click', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ first: 'ANGE', second: option }),
|
|
});
|
|
}
|
|
|
|
function debugGridScan(layout, targetRow, targetCol) {
|
|
const hasTarget = targetRow != null && targetCol != null;
|
|
showDebugResult(hasTarget ? `Matching (${targetRow},${targetCol}) in ${layout}...` : `Scanning ${layout}...`);
|
|
const body = { layout };
|
|
if (hasTarget) { body.targetRow = targetRow; body.targetCol = targetCol; }
|
|
fetch('/api/debug/grid-scan', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
function debugTestMatchHover() {
|
|
const layout = document.getElementById('matchLayout').value;
|
|
const targetRow = parseInt(document.getElementById('matchRow').value);
|
|
const targetCol = parseInt(document.getElementById('matchCol').value);
|
|
if (isNaN(targetRow) || isNaN(targetCol)) { showDebugResult('Invalid row/col'); return; }
|
|
showDebugResult(`Scanning ${layout} and matching (${targetRow},${targetCol})...`);
|
|
fetch('/api/debug/test-match-hover', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ layout, targetRow, targetCol }),
|
|
});
|
|
}
|
|
|
|
function showDebugResult(text) {
|
|
document.getElementById('debugResult').textContent = text;
|
|
}
|
|
|
|
// Enter key handlers
|
|
document.getElementById('debugTextInput').addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') debugFindText();
|
|
});
|
|
|
|
document.getElementById('urlInput').addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') addLink();
|
|
});
|
|
|
|
// --- OCR Settings modal ---
|
|
|
|
function openOcrSettings() {
|
|
loadOcrSettings();
|
|
document.getElementById('ocrSettingsModal').classList.add('open');
|
|
}
|
|
|
|
function closeOcrSettings() {
|
|
document.getElementById('ocrSettingsModal').classList.remove('open');
|
|
}
|
|
|
|
document.getElementById('ocrSettingsModal').addEventListener('click', (e) => {
|
|
if (e.target === e.currentTarget) closeOcrSettings();
|
|
});
|
|
|
|
function toggleOcrSections() {
|
|
const screenPp = document.getElementById('ocrScreenPreprocess').value;
|
|
document.getElementById('screenTophatParams').style.display = screenPp === 'tophat' ? '' : 'none';
|
|
|
|
const method = document.getElementById('ocrTooltipMethod').value;
|
|
const isEdge = method === 'edge';
|
|
|
|
// Show/hide method-specific crop params
|
|
document.getElementById('diffCropParams').style.display = isEdge ? 'none' : '';
|
|
document.getElementById('edgeCropParams').style.display = isEdge ? '' : 'none';
|
|
|
|
// Disable bgsub when edge (no reference frame)
|
|
const ppSelect = document.getElementById('ocrTooltipPreprocess');
|
|
const bgsubOption = ppSelect.querySelector('option[value="bgsub"]');
|
|
if (isEdge) {
|
|
bgsubOption.disabled = true;
|
|
if (ppSelect.value === 'bgsub') ppSelect.value = 'tophat';
|
|
} else {
|
|
bgsubOption.disabled = false;
|
|
}
|
|
|
|
const tooltipPp = ppSelect.value;
|
|
document.getElementById('tooltipBgsubParams').style.display = tooltipPp === 'bgsub' ? '' : 'none';
|
|
document.getElementById('tooltipTophatParams').style.display = tooltipPp === 'tophat' ? '' : 'none';
|
|
|
|
const engine = document.getElementById('ocrEngine').value;
|
|
document.getElementById('easyocrParams').style.display = engine === 'easyocr' ? '' : 'none';
|
|
}
|
|
|
|
async function loadOcrSettings() {
|
|
try {
|
|
const res = await fetch('/api/debug/ocr-settings');
|
|
const data = await res.json();
|
|
if (!data.ok) return;
|
|
document.getElementById('ocrEngine').value = data.engine || 'easyocr';
|
|
document.getElementById('ocrScreenPreprocess').value = data.screenPreprocess || 'none';
|
|
document.getElementById('ocrTooltipMethod').value = data.tooltipMethod || 'diff';
|
|
document.getElementById('ocrTooltipPreprocess').value = data.tooltipPreprocess || 'tophat';
|
|
document.getElementById('ocrSaveDebugImages').checked = data.saveDebugImages !== false;
|
|
const tp = data.tooltipParams || {};
|
|
const crop = tp.crop || {};
|
|
const ocr = tp.ocr || {};
|
|
// Edge params
|
|
const ep = data.edgeParams || {};
|
|
const edgeCrop = ep.crop || {};
|
|
document.getElementById('ocrCannyLow').value = edgeCrop.cannyLow ?? 50;
|
|
document.getElementById('ocrCannyHigh').value = edgeCrop.cannyHigh ?? 150;
|
|
document.getElementById('ocrMinLineLength').value = edgeCrop.minLineLength ?? 100;
|
|
document.getElementById('ocrRoiSize').value = edgeCrop.roiSize ?? 1400;
|
|
document.getElementById('ocrDensityThreshold').value = edgeCrop.densityThreshold ?? 0.15;
|
|
document.getElementById('ocrDiffThresh').value = crop.diffThresh ?? 20;
|
|
document.getElementById('ocrMaxGap').value = crop.maxGap ?? 20;
|
|
document.getElementById('ocrTrimCutoff').value = crop.trimCutoff ?? 0.4;
|
|
document.getElementById('ocrUpscale').value = ocr.upscale ?? 2;
|
|
document.getElementById('ocrMergeGap').value = ocr.mergeGap ?? 0;
|
|
document.getElementById('ocrTooltipKernel').value = ocr.kernelSize ?? 41;
|
|
document.getElementById('ocrDimPercentile').value = ocr.dimPercentile ?? 40;
|
|
document.getElementById('ocrTextThresh').value = ocr.textThresh ?? 60;
|
|
document.getElementById('ocrSoftThreshold').checked = !!ocr.softThreshold;
|
|
document.getElementById('ocrLinkThreshold').value = ocr.linkThreshold ?? '';
|
|
document.getElementById('ocrTextThreshold').value = ocr.textThreshold ?? '';
|
|
document.getElementById('ocrLowText').value = ocr.lowText ?? '';
|
|
document.getElementById('ocrWidthThs').value = ocr.widthThs ?? '';
|
|
toggleOcrSections();
|
|
} catch {}
|
|
}
|
|
|
|
async function saveOcrSettings() {
|
|
const tooltipPp = document.getElementById('ocrTooltipPreprocess').value;
|
|
const screenPp = document.getElementById('ocrScreenPreprocess').value;
|
|
const engine = document.getElementById('ocrEngine').value;
|
|
|
|
const tooltipParams = {
|
|
crop: {
|
|
diffThresh: parseInt(document.getElementById('ocrDiffThresh').value) || 20,
|
|
maxGap: parseInt(document.getElementById('ocrMaxGap').value) || 20,
|
|
trimCutoff: parseFloat(document.getElementById('ocrTrimCutoff').value) || 0.4,
|
|
},
|
|
ocr: {
|
|
upscale: parseInt(document.getElementById('ocrUpscale').value) || 2,
|
|
useBackgroundSub: tooltipPp === 'bgsub',
|
|
},
|
|
};
|
|
|
|
const mg = parseInt(document.getElementById('ocrMergeGap').value);
|
|
if (mg > 0) tooltipParams.ocr.mergeGap = mg;
|
|
|
|
if (tooltipPp === 'tophat') {
|
|
tooltipParams.ocr.kernelSize = parseInt(document.getElementById('ocrTooltipKernel').value) || 21;
|
|
}
|
|
if (tooltipPp === 'bgsub') {
|
|
tooltipParams.ocr.dimPercentile = parseInt(document.getElementById('ocrDimPercentile').value) || 40;
|
|
tooltipParams.ocr.textThresh = parseInt(document.getElementById('ocrTextThresh').value) || 60;
|
|
tooltipParams.ocr.softThreshold = document.getElementById('ocrSoftThreshold').checked;
|
|
}
|
|
|
|
if (engine === 'easyocr') {
|
|
const lt = parseFloat(document.getElementById('ocrLinkThreshold').value);
|
|
const tt = parseFloat(document.getElementById('ocrTextThreshold').value);
|
|
const low = parseFloat(document.getElementById('ocrLowText').value);
|
|
const wt = parseFloat(document.getElementById('ocrWidthThs').value);
|
|
if (!isNaN(lt)) tooltipParams.ocr.linkThreshold = lt;
|
|
if (!isNaN(tt)) tooltipParams.ocr.textThreshold = tt;
|
|
if (!isNaN(low)) tooltipParams.ocr.lowText = low;
|
|
if (!isNaN(wt)) tooltipParams.ocr.widthThs = wt;
|
|
}
|
|
|
|
const tooltipMethod = document.getElementById('ocrTooltipMethod').value;
|
|
|
|
const edgeParams = {
|
|
crop: {
|
|
cannyLow: parseInt(document.getElementById('ocrCannyLow').value) || 50,
|
|
cannyHigh: parseInt(document.getElementById('ocrCannyHigh').value) || 150,
|
|
minLineLength: parseInt(document.getElementById('ocrMinLineLength').value) || 100,
|
|
roiSize: parseInt(document.getElementById('ocrRoiSize').value) || 1400,
|
|
densityThreshold: parseFloat(document.getElementById('ocrDensityThreshold').value) || 0.15,
|
|
},
|
|
ocr: {
|
|
upscale: parseInt(document.getElementById('ocrUpscale').value) || 2,
|
|
kernelSize: parseInt(document.getElementById('ocrTooltipKernel').value) || 21,
|
|
},
|
|
};
|
|
|
|
const body = {
|
|
engine,
|
|
screenPreprocess: screenPp,
|
|
tooltipMethod,
|
|
tooltipPreprocess: tooltipPp,
|
|
tooltipParams,
|
|
edgeParams,
|
|
saveDebugImages: document.getElementById('ocrSaveDebugImages').checked,
|
|
};
|
|
|
|
await fetch('/api/debug/ocr-settings', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
const badge = document.getElementById('ocrSavedBadge');
|
|
badge.classList.add('show');
|
|
setTimeout(() => badge.classList.remove('show'), 2000);
|
|
}
|
|
|
|
connect();
|
|
loadOcrSettings();
|
|
</script>
|
|
</body>
|
|
</html>
|