Compare commits

..

2 Commits

Author SHA1 Message Date
Joseph c8e2ca9d4f Merge remote-tracking branch 'origin/main' 2026-03-26 10:08:22 +07:00
Joseph 63c0c8590b update 2026-03-26 10:07:33 +07:00
4 changed files with 994 additions and 163 deletions

57
HUONG_DAN.md Normal file
View File

@ -0,0 +1,57 @@
# Hướng Dẫn Sử Dụng - eBay Deep Scan
Công cụ này giúp bạn tìm kiếm, lọc và phân tích các mặt hàng trên eBay dựa trên các bộ từ khóa và cấu hình linh hoạt.
## 1. Cấu Hình Ban Đầu
Trước khi bắt đầu, bạn cần thiết lập các thông số môi trường trong file `.env`:
1. Copy `.env.example` thành `.env`.
2. Điền các mã API của eBay (`EBAY_CLIENT_ID`, `EBAY_CLIENT_SECRET`).
3. Điền `OPENAI_API_KEY` để sử dụng tính năng phân tích bằng AI.
## 2. Quản Lý Hồ Sơ Thu Quét (Scan Profiles)
Mỗi "Profile" đại diện cho một nhóm mặt hàng (ví dụ: SSD, RAM, GPU).
1. Truy cập Dashboard (thông qua `http://localhost:4000`).
2. Nhấn **Manage Profiles**.
3. **Thêm Profile mới**: Điền tên và **Price Ratio** (tỷ lệ giá mục tiêu so với giá thị trường, ví dụ `0.85`).
4. **Common Keywords (Keyword Chung)**: Đây là những từ khóa sẽ được nối vào cuối mọi truy vấn tìm kiếm trong profile đó (ví dụ `-cable -adapter` để loại bỏ dây cáp và đầu chuyển).
## 3. Nhập Từ Khóa (Import Keywords)
Bạn có thể nhập từ khóa theo phương pháp Bulk Import (nhập hàng loạt).
1. Chọn Profile bạn muốn làm việc.
2. Nhấn **Bulk Import**.
3. Định dạng nhập: `Mã Sản Phẩm | Từ Khóa 1, Từ Khóa 2 | Giá Mục Tiêu`
- Ví dụ: `"SSDSC2BX200" | "200GB", "2.5" | 14.00`
- **Lưu ý**: Nếu bạn để mã sản phẩm trong ngoặc kép `"Mã"`, hệ thống sẽ tìm kiếm chính xác mã đó trên eBay.
## 4. Thực Hiện Quét (Scanning)
1. Nhấn nút **Deep Scan** ở góc trên bên phải Dashboard.
2. Một thanh tiến trình (Progress Bar) sẽ hiện ra cho biết hệ thống đang tìm kiếm từ khóa nào và hoàn thành được bao nhiêu phần trăm.
3. Kết quả sẽ được tự động lưu vào database và hiển thị trên bảng.
## 5. Xem Kết Quả và Phân Tích AI
- **Bảng Kết Quả**: Các mặt hàng có giá tốt (vượt qua bộ lọc giá) sẽ hiện lên trên dashboard.
- **AI Analysis**: Hệ thống tự động gửi thông tin chi tiết của các mặt hàng "PASS" lên AI để phân tích xem sản phẩm đó có đúng là món hời hay có rủi ro gì không.
- **Popup Chi Tiết**: Nhấn vào tên sản phẩm để xem ảnh phóng to, mô tả đầy đủ, thông tin người bán và các chỉ số giá (Avg Price/Unit).
## 6. Các Phím Tắt và Tiện Ích
- **ESC**: Đóng các cửa sổ popup.
- **Dọn dẹp database**: Nếu muốn xóa sạch danh sách mặt hàng đã quét, hãy chạy lệnh:
```bash
node truncate_items.js
```
## 7. Lưu Ý
- Hệ thống tự động bỏ qua các mặt hàng từ Trung Quốc (CN) và các sản phẩm bị lỗi/hỏng (defective).
- Các mặt hàng được đấu giá (Auction) cũng được hỗ trợ nhưng sẽ được phân tích dựa trên giá hiện tại cộng phí ship.
Chúc bạn săn được những món hời nhất trên eBay!

326
RAM.kw
View File

@ -1,165 +1,165 @@
M321R8GA0BB0-CQK | "Samsung" "64GB" "2Rx4" "4800" "RDIMM"| 1425
HMCG94AGBQA181N | "Hynix" "64GB" "2Rx4" "5600" "RDIMM"| 1425
MTC40F2046S1RC56BG1 | "Micron" "64GB" "2Rx4" "5600" "RDIMM"| 1325
HMCG94AGBRA181N | "Hynix" "64GB" "2Rx4" "5600" "RDIMM"| 1425
M321R8GA0EB2-CCP | "Samsung" "64GB" "2Rx4" "6400" "RDIMM"| 1570
HMCG84MEBRA17N | "Hynix" "32GB" "1Rx4" "4800" "RDIMM"| 640
M321R4GA3BB0-CQK | "Samsung" "32GB" "2Rx8" "4800" "RDIMM"| 985
M321R4GA3BB6-CQK | "Samsung" "32GB" "2Rx8" "4800" "RDIMM"| 640
MTC20F2085S1RC52BAW | "Micron" "32GB" "2Rx8" "5200" "RDIMM"| 570
M321R4GA3PB0-CWM | "Samsung" "32GB" "2Rx8" "5600" "RDIMM"| 740
HMCG88AGBRA191N-BB | "Hynix" "32GB" "2Rx8" "5600" "RDIMM"| 715
HMCG88AGBRA190N | "Hynix" "32GB" "2Rx8" "5600" "RDIMM"| 715
M321R4GA0EB2-CCP | "Samsung" "32GB" "1Rx4" "6400" "RDIMM"| 715
M321R2GA3BB0-CQK | "Samsung" "16GB" "1Rx8" "4800" "RDIMM"| 315
MTC10F1084S1RC48BA1 | "Micron" "16GB" "1Rx8" "4800" "RDIMM"| 285
M386AAK40B40-CWD | "Samsung" "128GB" "2S4Rx4" "2666" "LRDIMM"| 530
M393AAG40M3B-CYF | "Samsung" "128GB" "2S2Rx4" "2933" "LRDIMM"| 580
HMABAGL7ABR4N-XN | "Hynix" "128GB" "4DRx4" "3200" "LRDIMM"| 470
M386A8K40BM1-CRC | "Samsung" "64GB" "4DRx4" "2400" "LRDIMM"| 287.36
M386A8K40BMB-CRC | "Samsung" "64GB" "4DRx4" "2400" "LRDIMM"| 287.36
HMAA8GL7MMR4N-UH | "Hynix" "64GB" "4DRx4" "2400" "LRDIMM"| 284.9
MTA72ASS8G72LZ-2G3 | "Micron" "64GB" "4DRx4" "2400" "LRDIMM"| 240
M386A8K40DM2-CTD | "Samsung" "64GB" "4DRx4" "2666" "LRDIMM"| 428
M386A8K40BM2-CTD | "Samsung" "64GB" "4DRx4" "2666" "LRDIMM"| 420.5
M393A8K40B22-CWD | "Samsung" "64GB" "2S2Rx4" "2666" "LRDIMM"| 400
M386A8K40CM2-CTD | "Samsung" "64GB" "4DRx4" "2666" "LRDIMM"| 428
HMAA8GL7MMR4N-VK | "Hynix" "64GB" "4DRx4" "2666" "LRDIMM"| 334.3
HMAA8GL7CPR4N-VK | "Hynix" "64GB" "4DRx4" "2666" "LRDIMM"| 334.3
M386A8K40DM2-CVF | "Samsung" "64GB" "4DRx4" "2933" "LRDIMM"| 552.33
HMAA8GL7CPR4N-WM | "Hynix" "64GB" "4DRx4" "2933" "LRDIMM"| 558
MTA72ASS8G72LZ-2G9 | "Micron" "64GB" "4DRx4" "2933" "LRDIMM"| 539
M393A8G40AB2-CVF | "Samsung" "64GB" "2Rx4" "2933" "RDIMM"| 605
M393A8G40MB2-CVF | "Samsung" "64GB" "2Rx4" "2933" "RDIMM"| 512
HMAA8GR7AJR4N-WM | "Hynix" "64GB" "2Rx4" "2933" "RDIMM"| 750.5
HMAA8GR7MJR4N-WM | "Hynix" "64GB" "2Rx4" "2933" "RDIMM"| 536
MTA36ASF8G72PZ-2G9 | "Micron" "64GB" "2Rx4" "2933" "RDIMM"| 595
M393A8G40AB2-CWE | "Samsung" "64GB" "2Rx4" "3200" "RDIMM"| 872
M393A8G40MB2-CWE | "Samsung" "64GB" "2Rx4" "3200" "RDIMM"| 760
M393A8G40CB4-CWE | "Samsung" "64GB" "2Rx4" "3200" "RDIMM"| 740
HMAA8GR7AJR4N-XN | "Hynix" "64GB" "2Rx4" "3200" "RDIMM"| 600
HMAA8GR7CJR4N-XN | "Hynix" "64GB" "2Rx4" "3200" "RDIMM"| 836
M386B4G70DM0-YH9 | "Samsung" "32GB" "4Rx4" "1333" "LRDIMM"| 28.37
M386B4G70DM0-YK | "Samsung" "32GB" "4DRx4" "1600" "LRDIMM"| 28.37
HMA84GL7AMR4N-TF | "Hynix" "32GB" "4DRx4" "2133" "LRDIMM"| 123.5
MTA72ASS4G72LZ-2G1 | "Micron" "32GB" "4DRx4" "2133" "LRDIMM"| 92.59
M386A4G40DM0-CPB | "Samsung" "32GB" "4DRx4" "2133" "LRDIMM"| 152
M386A4G40DM1-CRC | "Samsung" "32GB" "4DRx4" "2400" "LRDIMM"| 156.75
M393A4K40BB0-CPB | "Samsung" "32GB" "2Rx4" "2133" "RDIMM"| 207
HMA84GR7MFR4N-TF | "Hynix" "32GB" "2Rx4" "2133" "RDIMM"| 174.17
MTA36ASF4G72PZ-2G1 | "Micron" "32GB" "2Rx4" "2133" "RDIMM"| 133
M386A4K40BB0-CRC | "Samsung" "32GB" "2Rx4" "2400" "LRDIMM"| 172.56
M393A4K40BB1-CRC | "Samsung" "32GB" "2Rx4" "2400" "RDIMM"| 251
M393A4K40CB1-CRC | "Samsung" "32GB" "2Rx4" "2400" "RDIMM"| 251
HMA84GR7MFR4N-UH | "Hynix" "32GB" "2Rx4" "2400" "RDIMM"| 207
HMA84GR7AFR4N-UH | "Hynix" "32GB" "2Rx4" "2400" "RDIMM"| 181.16
HMA84GL7MFR4N-UH | "Hynix" "32GB" "2Rx4" "2400" "RDIMM"| 181.16
MTA36ASF4G72PZ-2G3 | "Micron" "32GB" "2Rx4" "2400" "RDIMM"| 192.85
M393A4K40BB2-CTD | "Samsung" "32GB" "2Rx4" "2666" "RDIMM"| 280
M393A4K40CB2-CTD | "Samsung" "32GB" "2Rx4" "2666" "RDIMM"| 266
HMA84GR7CJR4N-VK | "Hynix" "32GB" "2Rx4" "2666" "RDIMM"| 248
HMA84GR7AFR4N-VK | "Hynix" "32GB" "2Rx4" "2666" "RDIMM"| 248
HMA84GR7JJR4N-VK | "Hynix" "32GB" "2Rx4" "2666" "RDIMM"| 248
MTA36ASF4G72PZ-2G6 | "Micron" "32GB" "2Rx4" "2666" "RDIMM"| 213
MTA36ASF4G72LZ-2G6 | "Micron" "32GB" "2Rx4" "2666" "RDIMM"| 213
M393A4K40DB2-CVF | "Samsung" "32GB" "2Rx4" "2933" "RDIMM"| 228
M393A4K40CB2-CVF | "Samsung" "32GB" "2Rx4" "2933" "RDIMM"| 380
HMA84GR7JJR4N-WM | "Hynix" "32GB" "2Rx4" "2933" "RDIMM"| 413
HMA84GR7DJR4N-WM | "Hynix" "32GB" "2Rx4" "2933" "RDIMM"| 413
HMA84GR7CJR4N-WM | "Hynix" "32GB" "2Rx4" "2933" "RDIMM"| 413
MTA36ASF4G72PZ-2G9 | "Micron" "32GB" "2Rx4" "2933" "RDIMM"| 302.82
M393A4G40BB3-CWE | "Samsung" "32GB" "1Rx4" "3200" "RDIMM"| 436.05
M393A4K40EB3-CWE | "Samsung" "32GB" "2Rx4" "3200" "RDIMM"| 503
M393A4K40DB3-CWE | "Samsung" "32GB" "2Rx4" "3200" "RDIMM"| 503
HMA84GR7CJR4N-XN | "Hynix" "32GB" "2Rx4" "3200" "RDIMM"| 475
HMA84GR7DJR4N-XN | "Hynix" "32GB" "2Rx4" "3200" "RDIMM"| 475
MTA36ASF4G72PZ-3G2 | "Micron" "32GB" "2Rx4" "3200" "RDIMM"| 361
M393A4G43AB3-CWE | "Samsung" "32GB" "2Rx8" "3200" "RDIMM"| 425
HMAA4GR7CJR8N-XN | "Hynix" "32GB" "2Rx8" "3200" "RDIMM"| 362.32
HMT42GR7BMR4C-G7 | "Hynix" "16GB" "4Rx4" "1066" "RDIMM"| 21.13
MT36KSF2G72PZ-1G4 | "Micron" "16GB" "2Rx4" "1333" "RDIMM"| 16.88
M393B2G70EB0-YK0 | "Samsung" "16GB" "2Rx4" "1600" "RDIMM"| 19.69
M393B2G70DB0-YK0 | "Samsung" "16GB" "2Rx4" "1600" "RDIMM"| 19.69
M393B2G70BH0-YK0 | "Samsung" "16GB" "2Rx4" "1600" "RDIMM"| 19.69
MT36KSF2G72PZ-1G6 | "Micron" "16GB" "2Rx4" "1600" "RDIMM"| 21.4
HMT42GR7BFR4A-PB | "Hynix" "16GB" "2Rx4" "1600" "RDIMM"| 19.69
HMT42GR7MFR4C-PB | "Hynix" "16GB" "2Rx4" "1600" "RDIMM"| 18.99
HMT42GR7AFR4C-RD | "Hynix" "16GB" "2Rx4" "1866" "RDIMM"| 17.02
M393A2G40EB1-CPB | "Samsung" "16GB" "2Rx4" "2133" "RDIMM"| 110.4
M393A2G40DB0-CPB | "Samsung" "16GB" "2Rx4" "2133" "RDIMM"| 110.4
HMA42GR7AFR4N-TF | "Hynix" "16GB" "2Rx4" "2133" "RDIMM"| 97.7
HMA42GR7MFR4N-TF | "Hynix" "16GB" "2Rx4" "2133" "RDIMM"| 60
MTA36ASF2G72PZ-2G1 | "Micron" "16GB" "2Rx4" "2133" "RDIMM"| 64.4
M393A2K40CB1-CRC | "Samsung" "16GB" "1Rx4" "2400" "RDIMM"| 10
M393A2K40BB1-CRC | "Samsung" "16GB" "1Rx4" "2400" "RDIMM"| 110
MTA18ASF2G72PZ-2G3 | "Micron" "16GB" "1Rx4" "2400" "RDIMM"| 95
M393A2G40EB1-CRC | "Samsung" "16GB" "2Rx4" "2400" "RDIMM"| 110.2
HMA42GR7AFR4N-UH | "Hynix" "16GB" "2Rx4" "2400" "RDIMM"| 108
HMA42GR7BJR4N-UH | "Hynix" "16GB" "2Rx4" "2400" "RDIMM"| 108
MTA36ASF2G72PZ-2G3 | "Micron" "16GB" "2Rx4" "2400" "RDIMM"| 85
HMA82GR7AFR4N-VK | "Hynix" "16GB" "1Rx4" "2666" "RDIMM"| 110
HMA82GR7CJR4N-VK | "Hynix" "16GB" "1Rx4" "2666" "RDIMM"| 110
M393A2K43BB1-CTD | "Samsung" "16GB" "2Rx8" "2666" "RDIMM"| 114.29
HMA82GR7AFR8N-VK | "Hynix" "16GB" "2Rx8" "2666" "RDIMM"| 107
HMA82GR7CJR8N-VK | "Hynix" "16GB" "2Rx8" "2666" "RDIMM"| 107
HMA82GR7JJR8N-VK | "Hynix" "16GB" "2Rx8" "2666" "RDIMM"| 107
MTA18ASF2G72PDZ-2G6 | "Micron" "16GB" "2Rx8" "2666" "RDIMM"| 83.33
M393A2K40CB2-CVF | "Samsung" "16GB" "1Rx4" "2933" "RDIMM"| 118.75
M393A2K40DB2-CVF | "Samsung" "16GB" "1Rx4" "2933" "RDIMM"| 118.75
M393A2K43DB2-CVF | "Samsung" "16GB" "2Rx8" "2933" "RDIMM"| 100.57
HMA82GR7CJR8N-WM | "Hynix" "16GB" "2Rx8" "2933" "RDIMM"| 78.65
HMA82GR7JJR8N-WM | "Hynix" "16GB" "2Rx8" "2933" "RDIMM"| 102.56
M393A2K43DB3-CWE | "Samsung" "16GB" "2Rx8" "3200" "RDIMM"| 171
HMA82GR7DJR8N-XN | "Hynix" "16GB" "2Rx8" "3200" "RDIMM"| 114.29
HMA82GR7CJR8N-XN | "Hynix" "16GB" "2Rx8" "3200" "RDIMM"| 114.29
MTA18ASF2G72PDZ-3G2 | "Micron" "16GB" "2Rx8" "3200" "RDIMM"| 85.47
M393B1K70DH0-YH9 | "Samsung" "8GB" "2Rx4" "1333" "RDIMM"| 6.05
M393B1K70CH0-CH9 | "Samsung" "8GB" "2Rx4" "1333" "RDIMM"| 6.05
MTA18ASF1G72PZ-2G1 | "Micron" "8GB" "1Rx4" "2133" "RDIMM"| 23.98
HMA41GR7AFR8N-TF | "Hynix" "8GB" "1Rx8" "2133" "RDIMM"| 30.24
M393A1G40DB0-CPB | "Samsung" "8GB" "1Rx8" "2133" "RDIMM"| 45
MTA18ASF1G72PZ-2G1 | "Micron" "8GB" "1Rx8" "2133" "RDIMM"| 23.91
HMA41GR7AFR8N-TF | "Hynix" "8GB" "2Rx8" "2133" "RDIMM"| 28.13
M393A1G43DB0-CPB | "Samsung" "8GB" "2Rx8" "2133" "RDIMM"| 28.13
MTA18ASF1G72PZ-2G1 | "Micron" "8GB" "2Rx8" "2133" "RDIMM"| 23.91
HMA41GR7AFR4N-UH | "Hynix" "8GB" "1Rx4" "2400" "RDIMM"| 28
HMA81GR7MFR8N-UH | "Hynix" "8GB" "1Rx8" "2400" "RDIMM"| 30.24
M393A1G40DB1-CRC | "Samsung" "8GB" "1Rx8" "2400" "RDIMM"| 30.24
MTA9ASF1G72PZ-2G3 | "Micron" "8GB" "1Rx8" "2400" "RDIMM"| 30.24
HMA41GR7AFR8N-UH | "Hynix" "8GB" "2Rx8" "2400" "RDIMM"| 39.6
M393A1G40DB1-CRC | "Samsung" "8GB" "2Rx8" "2400" "RDIMM"| 28.13
M393A1G43DB1-CRC | "Samsung" "8GB" "2Rx8" "2400" "RDIMM"| 39.6
MTA18ASF1G72PDZ-2G3 | "Micron" "8GB" "2Rx8" "2400" "RDIMM"| 28.13
HMA81GR7AFR8N-VK | "Hynix" "8GB" "1Rx8" "2666" "RDIMM"| 28.6
MTA9ASF1G72PZ-2G6 | "Micron" "8GB" "1Rx8" "2666" "RDIMM"| 28.6
HMA81GR7AFR8N-VK | "Hynix" "8GB" "2Rx8" "2666" "RDIMM"| 28.13
MTA9ASF1G72PZ-2G6 | "Micron" "8GB" "2Rx8" "2666" "RDIMM"| 28.13
HMA81GR7CJR8N-WM | "Hynix" "8GB" "1Rx8" "2933" "RDIMM"| 39.33
HMA81GR7CJR8N-WM | "Hynix" "8GB" "2Rx8" "2933" "RDIMM"| 41.49
M393A1K43DB1-CVF | "Samsung" "8GB" "2Rx8" "2933" "RDIMM"| 41.49
MTA9ASF1G72PZ-2G9 | "Micron" "8GB" "2Rx8" "2933" "RDIMM"| 30.24
HMA81GR7CJR8N-XN | "Hynix" "8GB" "1Rx8" "3200" "RDIMM"| 61.6
MTA9ASF1G72PZ-3G2 | "Micron" "8GB" "1Rx8" "3200" "RDIMM"| 30.24
HMA81GR7CJR8N-XN | "Hynix" "8GB" "2Rx8" "3200" "RDIMM"| 41.49
"M321R8GA0BB0-CQK" | "Samsung" "64GB" "2Rx4" "4800" "RDIMM"| 1425
"HMCG94AGBQA181N" | "Hynix" "64GB" "2Rx4" "5600" "RDIMM"| 1425
"MTC40F2046S1RC56BG1" | "Micron" "64GB" "2Rx4" "5600" "RDIMM"| 1325
"HMCG94AGBRA181N" | "Hynix" "64GB" "2Rx4" "5600" "RDIMM"| 1425
"M321R8GA0EB2-CCP" | "Samsung" "64GB" "2Rx4" "6400" "RDIMM"| 1570
"HMCG84MEBRA17N" | "Hynix" "32GB" "1Rx4" "4800" "RDIMM"| 640
"M321R4GA3BB0-CQK" | "Samsung" "32GB" "2Rx8" "4800" "RDIMM"| 985
"M321R4GA3BB6-CQK" | "Samsung" "32GB" "2Rx8" "4800" "RDIMM"| 640
"MTC20F2085S1RC52BAW" | "Micron" "32GB" "2Rx8" "5200" "RDIMM"| 570
"M321R4GA3PB0-CWM" | "Samsung" "32GB" "2Rx8" "5600" "RDIMM"| 740
"HMCG88AGBRA191N-BB" | "Hynix" "32GB" "2Rx8" "5600" "RDIMM"| 715
"HMCG88AGBRA190N" | "Hynix" "32GB" "2Rx8" "5600" "RDIMM"| 715
"M321R4GA0EB2-CCP" | "Samsung" "32GB" "1Rx4" "6400" "RDIMM"| 715
"M321R2GA3BB0-CQK" | "Samsung" "16GB" "1Rx8" "4800" "RDIMM"| 315
"MTC10F1084S1RC48BA1" | "Micron" "16GB" "1Rx8" "4800" "RDIMM"| 285
"M386AAK40B40-CWD" | "Samsung" "128GB" "2S4Rx4" "2666" "LRDIMM"| 530
"M393AAG40M3B-CYF" | "Samsung" "128GB" "2S2Rx4" "2933" "LRDIMM"| 580
"HMABAGL7ABR4N-XN" | "Hynix" "128GB" "4DRx4" "3200" "LRDIMM"| 470
"M386A8K40BM1-CRC" | "Samsung" "64GB" "4DRx4" "2400" "LRDIMM"| 287.36
"M386A8K40BMB-CRC" | "Samsung" "64GB" "4DRx4" "2400" "LRDIMM"| 287.36
"HMAA8GL7MMR4N-UH" | "Hynix" "64GB" "4DRx4" "2400" "LRDIMM"| 284.9
"MTA72ASS8G72LZ-2G3" | "Micron" "64GB" "4DRx4" "2400" "LRDIMM"| 240
"M386A8K40DM2-CTD" | "Samsung" "64GB" "4DRx4" "2666" "LRDIMM"| 428
"M386A8K40BM2-CTD" | "Samsung" "64GB" "4DRx4" "2666" "LRDIMM"| 420.5
"M393A8K40B22-CWD" | "Samsung" "64GB" "2S2Rx4" "2666" "LRDIMM"| 400
"M386A8K40CM2-CTD" | "Samsung" "64GB" "4DRx4" "2666" "LRDIMM"| 428
"HMAA8GL7MMR4N-VK" | "Hynix" "64GB" "4DRx4" "2666" "LRDIMM"| 334.3
"HMAA8GL7CPR4N-VK" | "Hynix" "64GB" "4DRx4" "2666" "LRDIMM"| 334.3
"M386A8K40DM2-CVF" | "Samsung" "64GB" "4DRx4" "2933" "LRDIMM"| 552.33
"HMAA8GL7CPR4N-WM" | "Hynix" "64GB" "4DRx4" "2933" "LRDIMM"| 558
"MTA72ASS8G72LZ-2G9" | "Micron" "64GB" "4DRx4" "2933" "LRDIMM"| 539
"M393A8G40AB2-CVF" | "Samsung" "64GB" "2Rx4" "2933" "RDIMM"| 605
"M393A8G40MB2-CVF" | "Samsung" "64GB" "2Rx4" "2933" "RDIMM"| 512
"HMAA8GR7AJR4N-WM" | "Hynix" "64GB" "2Rx4" "2933" "RDIMM"| 750.5
"HMAA8GR7MJR4N-WM" | "Hynix" "64GB" "2Rx4" "2933" "RDIMM"| 536
"MTA36ASF8G72PZ-2G9" | "Micron" "64GB" "2Rx4" "2933" "RDIMM"| 595
"M393A8G40AB2-CWE" | "Samsung" "64GB" "2Rx4" "3200" "RDIMM"| 872
"M393A8G40MB2-CWE" | "Samsung" "64GB" "2Rx4" "3200" "RDIMM"| 760
"M393A8G40CB4-CWE" | "Samsung" "64GB" "2Rx4" "3200" "RDIMM"| 740
"HMAA8GR7AJR4N-XN" | "Hynix" "64GB" "2Rx4" "3200" "RDIMM"| 600
"HMAA8GR7CJR4N-XN" | "Hynix" "64GB" "2Rx4" "3200" "RDIMM"| 836
"M386B4G70DM0-YH9" | "Samsung" "32GB" "4Rx4" "1333" "LRDIMM"| 28.37
"M386B4G70DM0-YK" | "Samsung" "32GB" "4DRx4" "1600" "LRDIMM"| 28.37
"HMA84GL7AMR4N-TF" | "Hynix" "32GB" "4DRx4" "2133" "LRDIMM"| 123.5
"MTA72ASS4G72LZ-2G1" | "Micron" "32GB" "4DRx4" "2133" "LRDIMM"| 92.59
"M386A4G40DM0-CPB" | "Samsung" "32GB" "4DRx4" "2133" "LRDIMM"| 152
"M386A4G40DM1-CRC" | "Samsung" "32GB" "4DRx4" "2400" "LRDIMM"| 156.75
"M393A4K40BB0-CPB" | "Samsung" "32GB" "2Rx4" "2133" "RDIMM"| 207
"HMA84GR7MFR4N-TF" | "Hynix" "32GB" "2Rx4" "2133" "RDIMM"| 174.17
"MTA36ASF4G72PZ-2G1" | "Micron" "32GB" "2Rx4" "2133" "RDIMM"| 133
"M386A4K40BB0-CRC" | "Samsung" "32GB" "2Rx4" "2400" "LRDIMM"| 172.56
"M393A4K40BB1-CRC" | "Samsung" "32GB" "2Rx4" "2400" "RDIMM"| 251
"M393A4K40CB1-CRC" | "Samsung" "32GB" "2Rx4" "2400" "RDIMM"| 251
"HMA84GR7MFR4N-UH" | "Hynix" "32GB" "2Rx4" "2400" "RDIMM"| 207
"HMA84GR7AFR4N-UH" | "Hynix" "32GB" "2Rx4" "2400" "RDIMM"| 181.16
"HMA84GL7MFR4N-UH" | "Hynix" "32GB" "2Rx4" "2400" "RDIMM"| 181.16
"MTA36ASF4G72PZ-2G3" | "Micron" "32GB" "2Rx4" "2400" "RDIMM"| 192.85
"M393A4K40BB2-CTD" | "Samsung" "32GB" "2Rx4" "2666" "RDIMM"| 280
"M393A4K40CB2-CTD" | "Samsung" "32GB" "2Rx4" "2666" "RDIMM"| 266
"HMA84GR7CJR4N-VK" | "Hynix" "32GB" "2Rx4" "2666" "RDIMM"| 248
"HMA84GR7AFR4N-VK" | "Hynix" "32GB" "2Rx4" "2666" "RDIMM"| 248
"HMA84GR7JJR4N-VK" | "Hynix" "32GB" "2Rx4" "2666" "RDIMM"| 248
"MTA36ASF4G72PZ-2G6" | "Micron" "32GB" "2Rx4" "2666" "RDIMM"| 213
"MTA36ASF4G72LZ-2G6" | "Micron" "32GB" "2Rx4" "2666" "RDIMM"| 213
"M393A4K40DB2-CVF" | "Samsung" "32GB" "2Rx4" "2933" "RDIMM"| 228
"M393A4K40CB2-CVF" | "Samsung" "32GB" "2Rx4" "2933" "RDIMM"| 380
"HMA84GR7JJR4N-WM" | "Hynix" "32GB" "2Rx4" "2933" "RDIMM"| 413
"HMA84GR7DJR4N-WM" | "Hynix" "32GB" "2Rx4" "2933" "RDIMM"| 413
"HMA84GR7CJR4N-WM" | "Hynix" "32GB" "2Rx4" "2933" "RDIMM"| 413
"MTA36ASF4G72PZ-2G9" | "Micron" "32GB" "2Rx4" "2933" "RDIMM"| 302.82
"M393A4G40BB3-CWE" | "Samsung" "32GB" "1Rx4" "3200" "RDIMM"| 436.05
"M393A4K40EB3-CWE" | "Samsung" "32GB" "2Rx4" "3200" "RDIMM"| 503
"M393A4K40DB3-CWE" | "Samsung" "32GB" "2Rx4" "3200" "RDIMM"| 503
"HMA84GR7CJR4N-XN" | "Hynix" "32GB" "2Rx4" "3200" "RDIMM"| 475
"HMA84GR7DJR4N-XN" | "Hynix" "32GB" "2Rx4" "3200" "RDIMM"| 475
"MTA36ASF4G72PZ-3G2" | "Micron" "32GB" "2Rx4" "3200" "RDIMM"| 361
"M393A4G43AB3-CWE" | "Samsung" "32GB" "2Rx8" "3200" "RDIMM"| 425
"HMAA4GR7CJR8N-XN" | "Hynix" "32GB" "2Rx8" "3200" "RDIMM"| 362.32
"HMT42GR7BMR4C-G7" | "Hynix" "16GB" "4Rx4" "1066" "RDIMM"| 21.13
"MT36KSF2G72PZ-1G4" | "Micron" "16GB" "2Rx4" "1333" "RDIMM"| 16.88
"M393B2G70EB0-YK0" | "Samsung" "16GB" "2Rx4" "1600" "RDIMM"| 19.69
"M393B2G70DB0-YK0" | "Samsung" "16GB" "2Rx4" "1600" "RDIMM"| 19.69
"M393B2G70BH0-YK0" | "Samsung" "16GB" "2Rx4" "1600" "RDIMM"| 19.69
"MT36KSF2G72PZ-1G6" | "Micron" "16GB" "2Rx4" "1600" "RDIMM"| 21.4
"HMT42GR7BFR4A-PB" | "Hynix" "16GB" "2Rx4" "1600" "RDIMM"| 19.69
"HMT42GR7MFR4C-PB" | "Hynix" "16GB" "2Rx4" "1600" "RDIMM"| 18.99
"HMT42GR7AFR4C-RD" | "Hynix" "16GB" "2Rx4" "1866" "RDIMM"| 17.02
"M393A2G40EB1-CPB" | "Samsung" "16GB" "2Rx4" "2133" "RDIMM"| 110.4
"M393A2G40DB0-CPB" | "Samsung" "16GB" "2Rx4" "2133" "RDIMM"| 110.4
"HMA42GR7AFR4N-TF" | "Hynix" "16GB" "2Rx4" "2133" "RDIMM"| 97.7
"HMA42GR7MFR4N-TF" | "Hynix" "16GB" "2Rx4" "2133" "RDIMM"| 60
"MTA36ASF2G72PZ-2G1" | "Micron" "16GB" "2Rx4" "2133" "RDIMM"| 64.4
"M393A2K40CB1-CRC" | "Samsung" "16GB" "1Rx4" "2400" "RDIMM"| 10
"M393A2K40BB1-CRC" | "Samsung" "16GB" "1Rx4" "2400" "RDIMM"| 110
"MTA18ASF2G72PZ-2G3" | "Micron" "16GB" "1Rx4" "2400" "RDIMM"| 95
"M393A2G40EB1-CRC" | "Samsung" "16GB" "2Rx4" "2400" "RDIMM"| 110.2
"HMA42GR7AFR4N-UH" | "Hynix" "16GB" "2Rx4" "2400" "RDIMM"| 108
"HMA42GR7BJR4N-UH" | "Hynix" "16GB" "2Rx4" "2400" "RDIMM"| 108
"MTA36ASF2G72PZ-2G3" | "Micron" "16GB" "2Rx4" "2400" "RDIMM"| 85
"HMA82GR7AFR4N-VK" | "Hynix" "16GB" "1Rx4" "2666" "RDIMM"| 110
"HMA82GR7CJR4N-VK" | "Hynix" "16GB" "1Rx4" "2666" "RDIMM"| 110
"M393A2K43BB1-CTD" | "Samsung" "16GB" "2Rx8" "2666" "RDIMM"| 114.29
"HMA82GR7AFR8N-VK" | "Hynix" "16GB" "2Rx8" "2666" "RDIMM"| 107
"HMA82GR7CJR8N-VK" | "Hynix" "16GB" "2Rx8" "2666" "RDIMM"| 107
"HMA82GR7JJR8N-VK" | "Hynix" "16GB" "2Rx8" "2666" "RDIMM"| 107
"MTA18ASF2G72PDZ-2G6" | "Micron" "16GB" "2Rx8" "2666" "RDIMM"| 83.33
"M393A2K40CB2-CVF" | "Samsung" "16GB" "1Rx4" "2933" "RDIMM"| 118.75
"M393A2K40DB2-CVF" | "Samsung" "16GB" "1Rx4" "2933" "RDIMM"| 118.75
"M393A2K43DB2-CVF" | "Samsung" "16GB" "2Rx8" "2933" "RDIMM"| 100.57
"HMA82GR7CJR8N-WM" | "Hynix" "16GB" "2Rx8" "2933" "RDIMM"| 78.65
"HMA82GR7JJR8N-WM" | "Hynix" "16GB" "2Rx8" "2933" "RDIMM"| 102.56
"M393A2K43DB3-CWE" | "Samsung" "16GB" "2Rx8" "3200" "RDIMM"| 171
"HMA82GR7DJR8N-XN" | "Hynix" "16GB" "2Rx8" "3200" "RDIMM"| 114.29
"HMA82GR7CJR8N-XN" | "Hynix" "16GB" "2Rx8" "3200" "RDIMM"| 114.29
"MTA18ASF2G72PDZ-3G2" | "Micron" "16GB" "2Rx8" "3200" "RDIMM"| 85.47
"M393B1K70DH0-YH9" | "Samsung" "8GB" "2Rx4" "1333" "RDIMM"| 6.05
"M393B1K70CH0-CH9" | "Samsung" "8GB" "2Rx4" "1333" "RDIMM"| 6.05
"MTA18ASF1G72PZ-2G1" | "Micron" "8GB" "1Rx4" "2133" "RDIMM"| 23.98
"HMA41GR7AFR8N-TF" | "Hynix" "8GB" "1Rx8" "2133" "RDIMM"| 30.24
"M393A1G40DB0-CPB" | "Samsung" "8GB" "1Rx8" "2133" "RDIMM"| 45
"MTA18ASF1G72PZ-2G1" | "Micron" "8GB" "1Rx8" "2133" "RDIMM"| 23.91
"HMA41GR7AFR8N-TF" | "Hynix" "8GB" "2Rx8" "2133" "RDIMM"| 28.13
"M393A1G43DB0-CPB" | "Samsung" "8GB" "2Rx8" "2133" "RDIMM"| 28.13
"MTA18ASF1G72PZ-2G1" | "Micron" "8GB" "2Rx8" "2133" "RDIMM"| 23.91
"HMA41GR7AFR4N-UH" | "Hynix" "8GB" "1Rx4" "2400" "RDIMM"| 28
"HMA81GR7MFR8N-UH" | "Hynix" "8GB" "1Rx8" "2400" "RDIMM"| 30.24
"M393A1G40DB1-CRC" | "Samsung" "8GB" "1Rx8" "2400" "RDIMM"| 30.24
"MTA9ASF1G72PZ-2G3" | "Micron" "8GB" "1Rx8" "2400" "RDIMM"| 30.24
"HMA41GR7AFR8N-UH" | "Hynix" "8GB" "2Rx8" "2400" "RDIMM"| 39.6
"M393A1G40DB1-CRC" | "Samsung" "8GB" "2Rx8" "2400" "RDIMM"| 28.13
"M393A1G43DB1-CRC" | "Samsung" "8GB" "2Rx8" "2400" "RDIMM"| 39.6
"MTA18ASF1G72PDZ-2G3" | "Micron" "8GB" "2Rx8" "2400" "RDIMM"| 28.13
"HMA81GR7AFR8N-VK" | "Hynix" "8GB" "1Rx8" "2666" "RDIMM"| 28.6
"MTA9ASF1G72PZ-2G6" | "Micron" "8GB" "1Rx8" "2666" "RDIMM"| 28.6
"HMA81GR7AFR8N-VK" | "Hynix" "8GB" "2Rx8" "2666" "RDIMM"| 28.13
"MTA9ASF1G72PZ-2G6" | "Micron" "8GB" "2Rx8" "2666" "RDIMM"| 28.13
"HMA81GR7CJR8N-WM" | "Hynix" "8GB" "1Rx8" "2933" "RDIMM"| 39.33
"HMA81GR7CJR8N-WM" | "Hynix" "8GB" "2Rx8" "2933" "RDIMM"| 41.49
"M393A1K43DB1-CVF" | "Samsung" "8GB" "2Rx8" "2933" "RDIMM"| 41.49
"MTA9ASF1G72PZ-2G9" | "Micron" "8GB" "2Rx8" "2933" "RDIMM"| 30.24
"HMA81GR7CJR8N-XN" | "Hynix" "8GB" "1Rx8" "3200" "RDIMM"| 61.6
"MTA9ASF1G72PZ-3G2" | "Micron" "8GB" "1Rx8" "3200" "RDIMM"| 30.24
"HMA81GR7CJR8N-XN" | "Hynix" "8GB" "2Rx8" "3200" "RDIMM"| 41.49
"" | "Samsung" "32GB" "2Rx8" "2133" "UDIMM"| 127.5
"" | "Samsung" "32GB" "2Rx8" "2400" "UDIMM"| 140
M378A4G43MB1-CTD | "Samsung" "32GB" "2Rx8" "2666" "UDIMM"| 164
M378A4G43AB2-CVF | "Samsung" "32GB" "2Rx8" "2933" "UDIMM"| 137.88
M378A4G43AB2-CWE | "Samsung" "32GB" "2Rx8" "3200" "UDIMM"| 180
HMAA4GU6AJR8N-XN | "Hynix" "32GB" "2Rx8" "3200" "UDIMM"| 242.25
M378A2K43BB1-CPB | "Samsung" "16GB" "2Rx8" "2133" "UDIMM"| 56.5
M378A2K43CB1-CRC | "Samsung" "16GB" "2Rx8" "2400" "UDIMM"| 110
MTA18ASF2G72AZ-2G3 | "Micron" "16GB" "2Rx8" "2400" "UDIMM"| 90.25
HMA82GU6JJR8N-VK | "Hynix" "16GB" "2Rx8" "2666" "UDIMM"| 90
M378A2K43DB1-CVF | "Samsung" "16GB" "2Rx8" "2933" "UDIMM"| 56.5
KVR32N22S8/16 | "Kingston" "16GB" "1RX8" "3200" "UDIMM"| 48.5
M378A1K43BB1-CPB | "Samsung" "8GB" "1RX8" "2133" "UDIMM"| 25
M378A1K43CB2-CRC | "Samsung" "8GB" "1RX8" "2400" "UDIMM"| 30
M474A2K43BB1-CRC | "Samsung" "16GB" "2Rx8" "2400" "SODIMM"| 90
HMAA2GS6CJR8N-XN | "Hynix" "16GB" "1Rx8" "3200" "SODIMM"| 72.67
M474A2K43EB1-CWE | "Samsung" "16GB" "2Rx8" "3200" "SODIMM"| 80
M471A4G43MB1-CTD | "Samsung" "32GB" "2Rx8" "2666" "SODIMM"| 125
M471A4G43BB1-CWE | "Samsung" "32GB" "2Rx8" "3200" "SODIMM"| 139.4
HMAA4GS6CJR8N | "Hynix" "32GB" "2Rx8" "3200" "SODIMM"| 123.55
M425R2GA3BB0-CWM | "Samsung" "16GB" "1Rx8" "5600" "SODIMM"| 130.62
M425R4GA3PB0-CWM | "Samsung" "32GB" "2Rx8" "5600" "SODIMM"| 246.73
"M378A4G43MB1-CTD" | "Samsung" "32GB" "2Rx8" "2666" "UDIMM"| 164
"M378A4G43AB2-CVF" | "Samsung" "32GB" "2Rx8" "2933" "UDIMM"| 137.88
"M378A4G43AB2-CWE" | "Samsung" "32GB" "2Rx8" "3200" "UDIMM"| 180
"HMAA4GU6AJR8N-XN" | "Hynix" "32GB" "2Rx8" "3200" "UDIMM"| 242.25
"M378A2K43BB1-CPB" | "Samsung" "16GB" "2Rx8" "2133" "UDIMM"| 56.5
"M378A2K43CB1-CRC" | "Samsung" "16GB" "2Rx8" "2400" "UDIMM"| 110
"MTA18ASF2G72AZ-2G3" | "Micron" "16GB" "2Rx8" "2400" "UDIMM"| 90.25
"HMA82GU6JJR8N-VK" | "Hynix" "16GB" "2Rx8" "2666" "UDIMM"| 90
"M378A2K43DB1-CVF" | "Samsung" "16GB" "2Rx8" "2933" "UDIMM"| 56.5
"KVR32N22S8/16" | "Kingston" "16GB" "1RX8" "3200" "UDIMM"| 48.5
"M378A1K43BB1-CPB" | "Samsung" "8GB" "1RX8" "2133" "UDIMM"| 25
"M378A1K43CB2-CRC" | "Samsung" "8GB" "1RX8" "2400" "UDIMM"| 30
"M474A2K43BB1-CRC" | "Samsung" "16GB" "2Rx8" "2400" "SODIMM"| 90
"HMAA2GS6CJR8N-XN" | "Hynix" "16GB" "1Rx8" "3200" "SODIMM"| 72.67
"M474A2K43EB1-CWE" | "Samsung" "16GB" "2Rx8" "3200" "SODIMM"| 80
"M471A4G43MB1-CTD" | "Samsung" "32GB" "2Rx8" "2666" "SODIMM"| 125
"M471A4G43BB1-CWE" | "Samsung" "32GB" "2Rx8" "3200" "SODIMM"| 139.4
"HMAA4GS6CJR8N" | "Hynix" "32GB" "2Rx8" "3200" "SODIMM"| 123.55
"M425R2GA3BB0-CWM" | "Samsung" "16GB" "1Rx8" "5600" "SODIMM"| 130.62
"M425R4GA3PB0-CWM" | "Samsung" "32GB" "2Rx8" "5600" "SODIMM"| 246.73

39
USER_GUIDE_VN.md Normal file
View File

@ -0,0 +1,39 @@
# Hướng Dẫn Sử Dụng Dashboard - eBay Deep Scan (Dành Cho Người Dùng)
Công cụ này giúp bạn tự động tìm kiếm và lọc ra những món hàng có giá hời trên eBay. Dưới đây là cách sử dụng dashboard đơn giản nhất.
---
### 1. Truy Cập Dashboard
Mở trình duyệt web và truy cập địa chỉ: `http://localhost:4000` (hoặc link server bạn được cung cấp).
### 2. Quản Lý Nhóm Sản Phẩm (Profiles)
Mỗi **Hồ Sơ (Profile)** giúp bạn quản lý một loại hàng riêng biệt (ví dụ: một cái cho SSD, một cái cho RAM).
1. Nhấn nút **Manage Profiles** trên thanh công cụ.
2. **Thêm mới**: Nhập tên nhóm hàng và **Tỷ lệ giá mục tiêu** (ví dụ: `0.85` tức là bạn muốn mua rẻ hơn thị trường 15%).
3. **Từ khóa chung (Common Keywords)**: Nếu bạn luôn muốn tránh một loại hàng nào đó, hãy nhập vào đây. Ví dụ: `-cable -adapter` để tự động lọc bỏ các loại dây cáp và đầu chuyển khỏi mọi tìm kiếm.
### 3. Nhập Danh Sách Sản Phẩm (Import Keywords)
Bạn có thể nhập danh sách hàng nghìn sản phẩm cùng lúc.
1. Nhấn **Bulk Import** trong phần quản lý từ khóa của Profile.
2. Nhập theo mẫu: `Mã sản phẩm | Tên hoặc thông số | Giá mua mong muốn`
- Ví dụ: `"SSDSC2BX200" | "200GB", "2.5" | 14.00`
- **Mẹo nhỏ**: Nếu bạn để mã sản phẩm trong dấu ngoặc kép `"Mã"`, eBay sẽ tìm kiếm chính xác mã đó, giúp kết quả chính xác hơn.
### 4. Bắt Đầu Tìm Kiếm (Deep Scan)
- Nhấn nút **Deep Scan** (màu xanh lá) ở góc trên bên phải.
- Một thanh tiến trình sẽ hiện ra cho biết hệ thống đang tìm kiếm đến đâu.
- **Lưu ý**: Hệ thống sẽ tự động bỏ qua các mặt hàng từ Trung Quốc và các máy bị hỏng/vỡ để bạn không mất thời gian xem.
### 5. Xem Kết Quả và Chốt Đơn
Hệ thống chỉ hiển thị những mặt hàng có giá thấp hơn mức bạn mong muốn.
- **Đánh giá AI (Robot)**: Xem nội dung phân tích tự động dưới cột **AI Analysis**. Nó sẽ cho bạn biết sản phẩm đó "đáng mua" hay có gì "nghi ngờ".
- **Xem Ảnh và Chi Tiết**: Nhấn vào **Tên sản phẩm**. Một cửa sổ lớn hiện ra cho phép bạn xem tất cả ảnh gốc, mô tả đầy đủ và giá trung bình thực tế.
- **Link eBay**: Nhấn nút **Open on eBay** để đến thẳng trang mua hàng.
- **Sắp xếp**: Sau khi xem xong, bạn có thể chọn trạng thái **Done** (Để đánh dấu đã mua/xem) hoặc **Skip** (Để bỏ qua) giúp bảng kết quả luôn sạch sẽ.
---
**Lưu ý**: Nếu bạn muốn xóa toàn bộ kết quả cũ để bắt đầu lại từ đầu, hãy nhờ kỹ thuật viên chạy file làm sạch dữ liệu.

735
test_OCR.js Normal file
View File

@ -0,0 +1,735 @@
const express = require("express");
const OpenAI = require("openai");
const cors = require("cors");
require("dotenv").config();
const app = express();
app.use(cors());
app.use(express.json({ limit: "10mb" }));
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// ---- API: analyze image ----
app.post("/analyze", async (req, res) => {
try {
const { imageUrl } = req.body;
if (!imageUrl) return res.status(400).json({ error: "imageUrl is required" });
const aiRes = await client.chat.completions.create({
model: "gpt-4o-mini", // upgraded: better vision accuracy
temperature: 0,
response_format: { type: "json_object" },
messages: [
{
role: "system",
content: "You are a RAM hardware OCR expert. Extract text from RAM module labels and return only valid JSON, no markdown."
},
{
role: "user",
content: [
{
type: "text",
text: `Read all text on this RAM module label and return JSON:
{
"modules": [
{
"brand": "<manufacturer name>",
"part_number": "<exact part number>",
"specs": "<capacity, rank, speed spec line>",
"raw_label": "<all visible text on label, space-separated>"
}
],
"total_modules": <integer>
}`
},
{
type: "image_url",
image_url: { url: imageUrl, detail: "high" } // request high detail
}
]
}
]
});
const raw = aiRes.choices[0].message.content;
const data = JSON.parse(raw);
res.json(data);
} catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
}
});
// ---- serve UI ----
app.get("/", (_req, res) => {
res.send(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>RAM OCR Inspector</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Syne:wght@400;600;800&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0a0c10;
--surface: #111318;
--surface2: #181c24;
--border: #252a35;
--border-glow: #2a7fff44;
--text: #e8eaf0;
--text-muted: #6b7280;
--accent: #2a7fff;
--accent-dim: #1a4f99;
--green: #00e5a0;
--yellow: #f5c518;
--red: #ff4d6a;
--mono: 'JetBrains Mono', monospace;
--sans: 'Syne', sans-serif;
}
body {
background: var(--bg);
color: var(--text);
font-family: var(--sans);
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* ── Header ── */
header {
padding: 18px 32px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 14px;
background: var(--surface);
}
.logo-icon {
width: 34px; height: 34px;
background: var(--accent);
border-radius: 8px;
display: grid; place-items: center;
font-size: 16px;
}
header h1 {
font-size: 18px;
font-weight: 800;
letter-spacing: -0.3px;
}
header span {
font-family: var(--mono);
font-size: 11px;
color: var(--text-muted);
background: var(--surface2);
border: 1px solid var(--border);
padding: 2px 8px;
border-radius: 4px;
margin-left: 6px;
}
/* ── Main layout ── */
main {
flex: 1;
display: grid;
grid-template-columns: 1fr 380px;
gap: 0;
overflow: hidden;
}
/* ── Left panel ── */
.left {
display: flex;
flex-direction: column;
padding: 24px;
gap: 20px;
overflow-y: auto;
border-right: 1px solid var(--border);
}
.input-row {
display: flex;
gap: 10px;
align-items: center;
}
.input-row input {
flex: 1;
background: var(--surface2);
border: 1px solid var(--border);
color: var(--text);
font-family: var(--mono);
font-size: 13px;
padding: 10px 14px;
border-radius: 8px;
outline: none;
transition: border-color .2s;
}
.input-row input:focus { border-color: var(--accent); }
.input-row input::placeholder { color: var(--text-muted); }
button#analyzeBtn {
background: var(--accent);
color: #fff;
border: none;
font-family: var(--sans);
font-weight: 700;
font-size: 13px;
padding: 10px 22px;
border-radius: 8px;
cursor: pointer;
transition: background .2s, transform .1s;
white-space: nowrap;
}
button#analyzeBtn:hover { background: #1a6fe8; }
button#analyzeBtn:active { transform: scale(.97); }
button#analyzeBtn:disabled { background: var(--accent-dim); cursor: not-allowed; }
/* ── Canvas wrapper ── */
.canvas-wrap {
position: relative;
display: inline-block;
line-height: 0;
border-radius: 10px;
overflow: hidden;
border: 1px solid var(--border);
align-self: flex-start;
max-width: 100%;
}
.canvas-wrap img {
display: block;
max-width: 100%;
height: auto;
}
.canvas-wrap canvas {
position: absolute;
top: 0; left: 0;
width: 100%;
height: 100%;
cursor: crosshair;
}
.placeholder {
border: 2px dashed var(--border);
border-radius: 10px;
height: 280px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
color: var(--text-muted);
font-family: var(--mono);
font-size: 13px;
}
.placeholder .icon { font-size: 36px; opacity: .4; }
/* ── Word list ── */
.word-list-wrap {
display: flex;
flex-direction: column;
gap: 6px;
}
.section-label {
font-family: var(--mono);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--text-muted);
padding-bottom: 4px;
border-bottom: 1px solid var(--border);
}
.word-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.chip {
font-family: var(--mono);
font-size: 12px;
padding: 4px 10px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 5px;
cursor: pointer;
transition: background .15s, border-color .15s, color .15s;
user-select: none;
}
.chip:hover { background: var(--accent); border-color: var(--accent); color: #fff; }
.chip.low-conf { border-color: var(--yellow); color: var(--yellow); }
.chip.dim { opacity: .45; }
/* ── Tooltip ── */
.tooltip {
position: fixed;
background: #fff;
color: #000;
font-family: var(--mono);
font-size: 12px;
padding: 4px 10px;
border-radius: 5px;
pointer-events: none;
opacity: 0;
transition: opacity .15s;
z-index: 999;
white-space: nowrap;
}
.tooltip.show { opacity: 1; }
/* ── Right panel ── */
.right {
display: flex;
flex-direction: column;
overflow: hidden;
}
.tabs {
display: flex;
border-bottom: 1px solid var(--border);
background: var(--surface);
}
.tab {
padding: 12px 20px;
font-family: var(--mono);
font-size: 12px;
color: var(--text-muted);
cursor: pointer;
border-bottom: 2px solid transparent;
transition: color .2s, border-color .2s;
user-select: none;
}
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
.tab:hover:not(.active) { color: var(--text); }
.tab-content {
flex: 1;
overflow-y: auto;
padding: 20px;
display: none;
}
.tab-content.active { display: block; }
/* ── Module cards ── */
.module-card {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 10px;
padding: 16px;
margin-bottom: 14px;
}
.module-card .brand {
font-size: 16px;
font-weight: 800;
color: var(--accent);
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.badge {
font-family: var(--mono);
font-size: 10px;
padding: 2px 7px;
border-radius: 4px;
background: var(--accent-dim);
color: var(--accent);
}
.kv-grid {
display: grid;
grid-template-columns: 110px 1fr;
gap: 6px 12px;
font-family: var(--mono);
font-size: 12px;
}
.kv-grid .k { color: var(--text-muted); }
.kv-grid .v { color: var(--text); word-break: break-all; }
.kv-grid .v.good { color: var(--green); }
/* ── JSON panel ── */
pre#jsonOut {
font-family: var(--mono);
font-size: 11.5px;
line-height: 1.7;
color: #a8b4cc;
white-space: pre-wrap;
word-break: break-all;
}
/* ── Status bar ── */
.status {
padding: 8px 24px;
background: var(--surface);
border-top: 1px solid var(--border);
font-family: var(--mono);
font-size: 11px;
color: var(--text-muted);
display: flex;
gap: 20px;
align-items: center;
}
.status .dot {
width: 7px; height: 7px;
border-radius: 50%;
background: var(--border);
display: inline-block;
}
.status .dot.ready { background: var(--green); box-shadow: 0 0 6px var(--green); }
.status .dot.loading { background: var(--yellow); animation: blink 1s infinite; }
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: .3; }
}
/* ── Copy toast ── */
.toast {
position: fixed;
bottom: 30px; right: 30px;
background: var(--green);
color: #000;
font-family: var(--mono);
font-size: 13px;
font-weight: 600;
padding: 10px 18px;
border-radius: 8px;
opacity: 0;
transform: translateY(8px);
transition: opacity .25s, transform .25s;
pointer-events: none;
z-index: 1000;
}
.toast.show { opacity: 1; transform: translateY(0); }
/* scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
</style>
</head>
<body>
<header>
<div class="logo-icon">🔍</div>
<h1>RAM OCR Inspector</h1>
<span>gpt-4o · high detail</span>
</header>
<main>
<!-- LEFT -->
<div class="left">
<div class="input-row">
<input id="url" type="url" placeholder="Paste image URL of RAM module…"/>
<button id="analyzeBtn" onclick="run()">Analyze</button>
</div>
<div id="imgArea">
<div class="placeholder">
<span class="icon">🖥</span>
<span>Image will appear here</span>
</div>
</div>
<div id="wordListArea"></div>
</div>
<!-- RIGHT -->
<div class="right">
<div class="tabs">
<div class="tab active" onclick="switchTab('modules')">Modules</div>
<div class="tab" onclick="switchTab('json')">Raw JSON</div>
</div>
<div id="tab-modules" class="tab-content active">
<p style="color:var(--text-muted); font-family:var(--mono); font-size:13px;">
Module info will appear after analysis.
</p>
</div>
<div id="tab-json" class="tab-content">
<pre id="jsonOut">// No data yet</pre>
</div>
</div>
</main>
<div class="status">
<span class="dot" id="statusDot"></span>
<span id="statusText">Ready</span>
<span id="wordCount"></span>
</div>
<div class="tooltip" id="tooltip"></div>
<div class="toast" id="toast"></div>
<script>
let currentWords = [];
let hoveredIdx = -1;
let imgEl = null;
let canvasEl = null;
function switchTab(name) {
document.querySelectorAll('.tab').forEach((t,i) => {
t.classList.toggle('active', ['modules','json'][i] === name);
});
document.querySelectorAll('.tab-content').forEach(c => {
c.classList.toggle('active', c.id === 'tab-' + name);
});
}
function setStatus(state, msg) {
const dot = document.getElementById('statusDot');
dot.className = 'dot ' + (state === 'loading' ? 'loading' : state === 'ready' ? 'ready' : '');
document.getElementById('statusText').textContent = msg;
}
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 1800);
}
async function run() {
const url = document.getElementById('url').value.trim();
if (!url) return;
const btn = document.getElementById('analyzeBtn');
btn.disabled = true;
btn.textContent = 'Analyzing…';
setStatus('loading', 'Sending to GPT-4o…');
document.getElementById('tab-modules').innerHTML = \`<p style="color:var(--text-muted);font-family:var(--mono);font-size:13px">Analyzing…</p>\`;
document.getElementById('jsonOut').textContent = '// Waiting…';
try {
const res = await fetch('/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ imageUrl: url })
});
const data = await res.json();
if (data.error) throw new Error(data.error);
currentWords = data.words || [];
document.getElementById('jsonOut').textContent = JSON.stringify(data, null, 2);
renderModules(data);
renderWordChips(currentWords);
drawImage(url, currentWords);
document.getElementById('wordCount').textContent =
\`\${currentWords.length} words · \${data.total_modules || 0} module(s)\`;
setStatus('ready', 'Done');
} catch (err) {
setStatus('', 'Error: ' + err.message);
document.getElementById('tab-modules').innerHTML =
\`<p style="color:var(--red);font-family:var(--mono);font-size:13px">\${err.message}</p>\`;
} finally {
btn.disabled = false;
btn.textContent = 'Analyze';
}
}
function renderModules(data) {
const panel = document.getElementById('tab-modules');
if (!data.modules || !data.modules.length) {
panel.innerHTML = '<p style="color:var(--text-muted);font-family:var(--mono);font-size:13px">No modules detected.</p>';
return;
}
panel.innerHTML = data.modules.map((m, i) => \`
<div class="module-card">
<div class="brand">
\${m.brand || 'Unknown'}
<span class="badge">Module \${i + 1}</span>
</div>
<div class="kv-grid">
\${row('Part №', m.part_number)}
\${row('Type', m.type, 'good')}
\${row('Capacity', m.capacity, 'good')}
\${row('Speed', m.speed)}
\${row('Voltage', m.voltage)}
\${row('Specs', m.specs)}
\${row('Label', m.raw_label)}
</div>
</div>
\`).join('');
}
function row(k, v, cls = '') {
if (!v) return '';
return \`<span class="k">\${k}</span><span class="v \${cls}">\${v}</span>\`;
}
function renderWordChips(words) {
const area = document.getElementById('wordListArea');
if (!words.length) { area.innerHTML = ''; return; }
area.innerHTML = \`
<div class="word-list-wrap">
<div class="section-label">Detected tokens click to highlight · click chip to copy</div>
<div class="word-chips">
\${words.map((w, i) => {
const conf = w.confidence ?? 1;
const cls = conf < 0.7 ? 'chip low-conf' : 'chip';
return \`<div class="\${cls}" id="chip-\${i}" onmouseenter="highlightWord(\${i})" onmouseleave="clearHighlight()" onclick="copyWord('\${w.text.replace(/'/g, "\\\\'")}', \${i})">\${w.text}</div>\`;
}).join('')}
</div>
</div>
\`;
}
function drawImage(url, words) {
const area = document.getElementById('imgArea');
area.innerHTML = \`<div class="canvas-wrap"><img id="theImg" crossorigin="anonymous"/><canvas id="theCanvas"></canvas></div>\`;
imgEl = document.getElementById('theImg');
canvasEl = document.getElementById('theCanvas');
imgEl.onload = () => {
const W = imgEl.naturalWidth;
const H = imgEl.naturalHeight;
canvasEl.width = W;
canvasEl.height = H;
drawBoxes(words, -1);
setupCanvasEvents(words);
};
imgEl.src = url;
}
function drawBoxes(words, activeIdx) {
if (!canvasEl || !imgEl) return;
const ctx = canvasEl.getContext('2d');
const W = canvasEl.width;
const H = canvasEl.height;
ctx.clearRect(0, 0, W, H);
words.forEach((w, i) => {
const [nx, ny, nw, nh] = w.bbox;
const x = nx * W, y = ny * H, bw = nw * W, bh = nh * H;
const conf = w.confidence ?? 1;
const isActive = i === activeIdx;
if (isActive) {
// Glow highlight
ctx.shadowColor = '#2a7fff';
ctx.shadowBlur = 14;
ctx.fillStyle = 'rgba(42,127,255,0.20)';
ctx.fillRect(x, y, bw, bh);
ctx.strokeStyle = '#2a7fff';
ctx.lineWidth = 2;
} else {
ctx.shadowBlur = 0;
if (conf < 0.7) {
ctx.strokeStyle = 'rgba(245,197,24,0.7)';
} else {
ctx.strokeStyle = 'rgba(0,229,160,0.65)';
}
ctx.lineWidth = 1;
}
ctx.strokeRect(x + .5, y + .5, bw, bh);
ctx.shadowBlur = 0;
// Label above box
if (isActive || (nw * W > 20 && nh * H > 10)) {
const label = w.text;
const fs = Math.max(9, Math.min(13, bh * 0.55));
ctx.font = \`600 \${fs}px JetBrains Mono, monospace\`;
const tw = ctx.measureText(label).width;
const lx = Math.min(x, W - tw - 4);
const ly = y > fs + 4 ? y - 3 : y + bh + fs + 2;
ctx.fillStyle = isActive ? '#2a7fff' : (conf < 0.7 ? '#f5c518' : '#00e5a0');
ctx.globalAlpha = isActive ? 1 : 0.75;
ctx.fillText(label, lx, ly);
ctx.globalAlpha = 1;
}
});
}
function setupCanvasEvents(words) {
const tooltip = document.getElementById('tooltip');
canvasEl.addEventListener('mousemove', (e) => {
const rect = canvasEl.getBoundingClientRect();
const scaleX = canvasEl.width / rect.width;
const scaleY = canvasEl.height / rect.height;
const cx = (e.clientX - rect.left) * scaleX;
const cy = (e.clientY - rect.top) * scaleY;
const W = canvasEl.width, H = canvasEl.height;
let found = -1;
words.forEach((w, i) => {
const [nx, ny, nw, nh] = w.bbox;
if (cx >= nx*W && cx <= (nx+nw)*W && cy >= ny*H && cy <= (ny+nh)*H) found = i;
});
if (found !== hoveredIdx) {
hoveredIdx = found;
drawBoxes(words, hoveredIdx);
}
if (found >= 0) {
const w = words[found];
const conf = ((w.confidence ?? 1) * 100).toFixed(0);
tooltip.textContent = \`\${w.text} · conf \${conf}%\`;
tooltip.style.left = (e.clientX + 14) + 'px';
tooltip.style.top = (e.clientY - 24) + 'px';
tooltip.classList.add('show');
} else {
tooltip.classList.remove('show');
}
});
canvasEl.addEventListener('mouseleave', () => {
hoveredIdx = -1;
drawBoxes(words, -1);
tooltip.classList.remove('show');
});
canvasEl.addEventListener('click', (e) => {
const rect = canvasEl.getBoundingClientRect();
const scaleX = canvasEl.width / rect.width;
const scaleY = canvasEl.height / rect.height;
const cx = (e.clientX - rect.left) * scaleX;
const cy = (e.clientY - rect.top) * scaleY;
const W = canvasEl.width, H = canvasEl.height;
words.forEach((w) => {
const [nx, ny, nw, nh] = w.bbox;
if (cx >= nx*W && cx <= (nx+nw)*W && cy >= ny*H && cy <= (ny+nh)*H) {
navigator.clipboard.writeText(w.text).then(() => showToast('Copied: ' + w.text));
}
});
});
}
function highlightWord(i) {
hoveredIdx = i;
drawBoxes(currentWords, i);
}
function clearHighlight() {
hoveredIdx = -1;
drawBoxes(currentWords, -1);
}
function copyWord(text, i) {
navigator.clipboard.writeText(text).then(() => showToast('Copied: ' + text));
}
</script>
</body>
</html>`);
});
app.listen(3000, () => console.log("Server running at http://localhost:3000"));