mirror of
https://github.com/Xevion/vastly.git
synced 2025-12-06 09:16:57 -06:00
major scoring implementation, major frontend work
This commit is contained in:
191
api/score.go
191
api/score.go
@@ -1,15 +1,198 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ScoreReason struct {
|
||||
Reason string
|
||||
Offset float64
|
||||
Value interface{}
|
||||
IsMultiplier bool
|
||||
}
|
||||
|
||||
type ScoredOffer struct {
|
||||
Offer Offer
|
||||
Score float64
|
||||
Offer Offer
|
||||
Score float64
|
||||
Reasons []ScoreReason
|
||||
}
|
||||
|
||||
var (
|
||||
sugar *zap.SugaredLogger
|
||||
)
|
||||
|
||||
func init() {
|
||||
logger, err := zap.NewDevelopment()
|
||||
sugar = logger.Sugar()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func AddReason(reasonList []ScoreReason, reason string, offset float64, multiplier bool, value interface{}) []ScoreReason {
|
||||
return append(reasonList, ScoreReason{Reason: reason, Offset: offset, IsMultiplier: multiplier, Value: fmt.Sprintf("%v", value)})
|
||||
}
|
||||
|
||||
func ScoreOffers(offers []Offer) []ScoredOffer {
|
||||
var scoredOffers = make([]ScoredOffer, 0, len(offers))
|
||||
|
||||
for _, offer := range offers {
|
||||
score := 100.0
|
||||
scoredOffers = append(scoredOffers, ScoredOffer{Offer: offer, Score: score})
|
||||
reasons := make([]ScoreReason, 0, 16)
|
||||
|
||||
// Judge cost
|
||||
targetCost := 0.42 // $/hour
|
||||
costMultiplier := targetCost / offer.Search.TotalHour // e.x $0.42 / $0.50 = x0.84
|
||||
// sugar.Debugw("Cost", "offer", offer.ID, "costMultiplier", costMultiplier, "targetCost", targetCost, "actualCost", offer.Search.TotalHour)
|
||||
if math.Abs(costMultiplier-1.0) > 0.05 {
|
||||
reasons = AddReason(reasons, "Cost Multiplier", costMultiplier, true, offer.Search.TotalHour)
|
||||
}
|
||||
|
||||
// Judge DLPerf
|
||||
targetDLPerf := 85.0
|
||||
dlPerfMultiplier := offer.DLPerf / targetDLPerf // e.x 100 / 85 = x1.18
|
||||
if math.Abs(dlPerfMultiplier-1.0) > 0.03 {
|
||||
if dlPerfMultiplier > 1.0 {
|
||||
dlPerfMultiplier = math.Sqrt(dlPerfMultiplier)
|
||||
} else {
|
||||
dlPerfMultiplier = math.Pow(dlPerfMultiplier, 2.0)
|
||||
}
|
||||
reasons = AddReason(reasons, "DLPerf Multiplier", dlPerfMultiplier, true, offer.DLPerf)
|
||||
}
|
||||
|
||||
// Judge Internet Download Speed
|
||||
if offer.InetDown < 150.0 {
|
||||
reasons = AddReason(reasons, "Very Poor Internet Download Speed", -7.0, false, offer.InetDown)
|
||||
} else if offer.InetDown < 300.0 {
|
||||
reasons = AddReason(reasons, "Poor Internet Download Speed", -3.0, false, offer.InetDown)
|
||||
} else if offer.InetDown < 500.0 {
|
||||
reasons = AddReason(reasons, "Decent Internet Download Speed", 1.0, false, offer.InetDown)
|
||||
} else if offer.InetDown < 1000.0 {
|
||||
reasons = AddReason(reasons, "Good Internet Download Speed", 3.0, false, offer.InetDown)
|
||||
} else if offer.InetDown >= 2000.0 {
|
||||
reasons = AddReason(reasons, "Great Internet Download Speed", 4.0, false, offer.InetDown)
|
||||
}
|
||||
|
||||
// Judge Internet Upload Speed
|
||||
if offer.InetUp < 50.0 {
|
||||
reasons = AddReason(reasons, "Extremely Poor Internet Upload Speed", -9.0, false, offer.InetUp)
|
||||
} else if offer.InetUp < 100.0 {
|
||||
reasons = AddReason(reasons, "Very Poor Internet Upload Speed", -5.0, false, offer.InetUp)
|
||||
} else if offer.InetUp < 200.0 {
|
||||
reasons = AddReason(reasons, "Poor Internet Upload Speed", -3.0, false, offer.InetUp)
|
||||
} else if offer.InetUp < 400.0 {
|
||||
reasons = AddReason(reasons, "Decent Internet Upload Speed", 1.0, false, offer.InetUp)
|
||||
} else if offer.InetUp >= 800.0 {
|
||||
reasons = AddReason(reasons, "Great Internet Upload Speed", 3.0, false, offer.InetUp)
|
||||
} else if offer.InetUp >= 1000.0 {
|
||||
reasons = AddReason(reasons, "Amazing Internet Upload Speed", 4.0, false, offer.InetUp)
|
||||
}
|
||||
|
||||
// Judge verification
|
||||
if offer.Verification == "verified" {
|
||||
reasons = AddReason(reasons, "Verified", 2.0, false, offer.Verification)
|
||||
} else {
|
||||
reasons = AddReason(reasons, "Not Verified", -5.0, false, offer.Verification)
|
||||
}
|
||||
|
||||
// Judge direct port count
|
||||
if offer.DirectPortCount < 8 {
|
||||
reasons = AddReason(reasons, "Very low direct port count", -2.0, false, offer.DirectPortCount)
|
||||
} else if offer.DirectPortCount >= 32 {
|
||||
reasons = AddReason(reasons, "Decent port count", 0.5, false, offer.DirectPortCount)
|
||||
} else if offer.DirectPortCount >= 100 {
|
||||
reasons = AddReason(reasons, "High port count", 1.0, false, offer.DirectPortCount)
|
||||
}
|
||||
|
||||
// Judge CPU memory
|
||||
if offer.CPURam < 16*1024 {
|
||||
reasons = AddReason(reasons, "Low CPU memory", -4.0, false, offer.CPURam)
|
||||
} else if offer.CPURam >= 31*1024 {
|
||||
reasons = AddReason(reasons, "High CPU memory", 2.0, false, offer.CPURam)
|
||||
} else if offer.CPURam >= 63*1024 {
|
||||
reasons = AddReason(reasons, "Very High CPU memory", 3.0, false, offer.CPURam)
|
||||
}
|
||||
|
||||
// Judge GPU count
|
||||
if offer.NumGPUs < 1 {
|
||||
reasons = AddReason(reasons, "No GPUs", -20.0, false, offer.NumGPUs)
|
||||
} else if offer.NumGPUs == 2 {
|
||||
reasons = AddReason(reasons, "Dual GPU", 1.0, false, offer.NumGPUs)
|
||||
} else if offer.NumGPUs >= 3 {
|
||||
reasons = AddReason(reasons, "Multi GPU", 2.0, false, offer.NumGPUs)
|
||||
}
|
||||
|
||||
// Judge CUDA version
|
||||
if offer.CudaMaxGood < 11.0 {
|
||||
reasons = AddReason(reasons, "CUDA version very outdated", -7.0, false, offer.CudaMaxGood)
|
||||
} else if offer.CudaMaxGood < 12.0 {
|
||||
reasons = AddReason(reasons, "CUDA version outdated", -5.0, false, offer.CudaMaxGood)
|
||||
} else if offer.CudaMaxGood >= 12.5 {
|
||||
reasons = AddReason(reasons, "CUDA version high", 5.0, false, offer.CudaMaxGood)
|
||||
} else if offer.CudaMaxGood >= 12.0 {
|
||||
reasons = AddReason(reasons, "CUDA version decent", 1.5, false, offer.CudaMaxGood)
|
||||
}
|
||||
|
||||
// Judge effective core count
|
||||
if offer.CPUCoresEffective < 4.0 {
|
||||
reasons = AddReason(reasons, "Low core count", -5.0, false, offer.CPUCoresEffective)
|
||||
} else if offer.CPUCoresEffective >= 8.0 {
|
||||
reasons = AddReason(reasons, "High core count", 5.0, false, offer.CPUCoresEffective)
|
||||
} else if offer.CPUCoresEffective >= 6.0 {
|
||||
reasons = AddReason(reasons, "Decent core count", 2.0, false, offer.CPUCoresEffective)
|
||||
}
|
||||
|
||||
// Judge disk space available
|
||||
if offer.DiskSpace < 100.0 {
|
||||
reasons = AddReason(reasons, "Low disk space", -5.0, false, offer.DiskSpace)
|
||||
} else if offer.DiskSpace >= 750.0 {
|
||||
reasons = AddReason(reasons, "Reasonable disk space", 1.0, false, offer.DiskSpace)
|
||||
} else if offer.DiskSpace >= 250.0 {
|
||||
reasons = AddReason(reasons, "Concerning disk space", -1.0, false, offer.DiskSpace)
|
||||
} else if offer.DiskSpace >= 2500.0 {
|
||||
reasons = AddReason(reasons, "High disk space", 2.0, false, offer.DiskSpace)
|
||||
}
|
||||
|
||||
// Judge GPU architecture
|
||||
if offer.GPUArch == "nvidia" {
|
||||
reasons = AddReason(reasons, "Nvidia Preference", 1.0, false, offer.GPUArch)
|
||||
} else {
|
||||
reasons = AddReason(reasons, "Unknown/Incompatible GPU Architecture", -10.0, false, offer.GPUArch)
|
||||
}
|
||||
|
||||
// Judge reliability
|
||||
if offer.Reliability2 < 0.98 {
|
||||
reasons = AddReason(reasons, "Low reliability", -5.0, false, offer.Reliability2)
|
||||
} else if offer.Reliability2 >= 0.999 {
|
||||
reasons = AddReason(reasons, "Very high reliability", 5.0, false, offer.Reliability2)
|
||||
} else if offer.Reliability2 >= 0.995 {
|
||||
reasons = AddReason(reasons, "High reliability", 2.0, false, offer.Reliability2)
|
||||
} else if offer.Reliability2 >= 0.99 {
|
||||
reasons = AddReason(reasons, "Decent reliability", 1.0, false, offer.Reliability2)
|
||||
}
|
||||
|
||||
// Calculate base score
|
||||
score := 0.0
|
||||
for _, reason := range reasons {
|
||||
if !reason.IsMultiplier {
|
||||
score += reason.Offset
|
||||
}
|
||||
}
|
||||
|
||||
// Apply multipliers
|
||||
multiplier := 1.0
|
||||
for _, reason := range reasons {
|
||||
if reason.IsMultiplier {
|
||||
multiplier *= reason.Offset
|
||||
}
|
||||
}
|
||||
newScore := score * multiplier
|
||||
// sugar.Infow("Multiplier Applied", "offer", offer.ID, "baseScore", score, "score", newScore, "multiplier", multiplier)
|
||||
score = newScore
|
||||
|
||||
scoredOffers = append(scoredOffers, ScoredOffer{Offer: offer, Score: score, Reasons: reasons})
|
||||
}
|
||||
return scoredOffers
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
)
|
||||
|
||||
type AdvancedSearch struct {
|
||||
Limit int `json:"limit,omitempty"`
|
||||
AllocatedStorage float64 `json:"allocated_storage,omitempty"`
|
||||
Verified *bool `json:"verified,omitempty"`
|
||||
ComputeCap *ComparableInteger `json:"compute_cap,omitempty"`
|
||||
DiskSpace *ComparableInteger `json:"disk_space,omitempty"`
|
||||
@@ -50,6 +52,7 @@ type AdvancedSearch struct {
|
||||
func NewSearch() *AdvancedSearch {
|
||||
return &AdvancedSearch{
|
||||
Rented: Pointer(false),
|
||||
Limit: 500,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
133
api/types.go
133
api/types.go
@@ -101,67 +101,78 @@ type Instance struct {
|
||||
}
|
||||
|
||||
type Offer struct {
|
||||
IsBid bool `json:"is_bid"`
|
||||
InetUpBilled *float64 `json:"inet_up_billed"`
|
||||
InetDownBilled *float64 `json:"inet_down_billed"`
|
||||
External bool `json:"external"`
|
||||
Webpage *string `json:"webpage"`
|
||||
Logo string `json:"logo"`
|
||||
Rentable bool `json:"rentable"`
|
||||
ComputeCap int `json:"compute_cap"`
|
||||
DriverVersion string `json:"driver_version"`
|
||||
CudaMaxGood float64 `json:"cuda_max_good"`
|
||||
MachineID int `json:"machine_id"`
|
||||
HostingType *float64 `json:"hosting_type"`
|
||||
PublicIPAddr string `json:"public_ipaddr"`
|
||||
Geolocation string `json:"geolocation"`
|
||||
Geocode *int64 `json:"geolocode"`
|
||||
FlopsPerDPHTotal float64 `json:"flops_per_dphtotal"`
|
||||
DLPerfPerDPHTotal float64 `json:"dlperf_per_dphtotal"`
|
||||
Reliability2 float64 `json:"reliability2"`
|
||||
HostRunTime int `json:"host_run_time"`
|
||||
HostID int `json:"host_id"`
|
||||
ID int `json:"id"`
|
||||
BundleID int `json:"bundle_id"`
|
||||
NumGPUs int `json:"num_gpus"`
|
||||
TotalFlops float64 `json:"total_flops"`
|
||||
MinBid float64 `json:"min_bid"`
|
||||
DPHBase float64 `json:"dph_base"`
|
||||
DPHTotal float64 `json:"dph_total"`
|
||||
GPUName string `json:"gpu_name"`
|
||||
GPURam int `json:"gpu_ram"`
|
||||
GPUDisplayActive bool `json:"gpu_display_active"`
|
||||
GPUMemBw float64 `json:"gpu_mem_bw"`
|
||||
BwNVLink float64 `json:"bw_nvlink"`
|
||||
DirectPortCount int `json:"direct_port_count"`
|
||||
GPULanes int `json:"gpu_lanes"`
|
||||
PCIeBw float64 `json:"pcie_bw"`
|
||||
PCIGen float64 `json:"pci_gen"`
|
||||
DLPerf float64 `json:"dlperf"`
|
||||
CPUName string `json:"cpu_name"`
|
||||
MoboName string `json:"mobo_name"`
|
||||
CPURam int `json:"cpu_ram"`
|
||||
CPUCores float64 `json:"cpu_cores"`
|
||||
CPUCoresEffective float64 `json:"cpu_cores_effective"`
|
||||
GPUFrac float64 `json:"gpu_frac"`
|
||||
HasAVX int `json:"has_avx"`
|
||||
DiskSpace float64 `json:"disk_space"`
|
||||
DiskName string `json:"disk_name"`
|
||||
DiskBw float64 `json:"disk_bw"`
|
||||
InetUp float64 `json:"inet_up"`
|
||||
InetDown float64 `json:"inet_down"`
|
||||
StartDate float64 `json:"start_date"`
|
||||
EndDate *float64 `json:"end_date"`
|
||||
Duration *float64 `json:"duration"`
|
||||
StorageCost float64 `json:"storage_cost"`
|
||||
InetUpCost float64 `json:"inet_up_cost"`
|
||||
InetDownCost float64 `json:"inet_down_cost"`
|
||||
StorageTotalCost float64 `json:"storage_total_cost"`
|
||||
Verification string `json:"verification"`
|
||||
Score float64 `json:"score"`
|
||||
Rented bool `json:"rented"`
|
||||
BundledResults int `json:"bundled_results"`
|
||||
PendingCount int `json:"pending_count"`
|
||||
IsBid bool `json:"is_bid"`
|
||||
InetUpBilled *float64 `json:"inet_up_billed"`
|
||||
InetDownBilled *float64 `json:"inet_down_billed"`
|
||||
External bool `json:"external"`
|
||||
Webpage *string `json:"webpage"`
|
||||
Logo string `json:"logo"`
|
||||
Rentable bool `json:"rentable"`
|
||||
ComputeCap int `json:"compute_cap"`
|
||||
DriverVersion string `json:"driver_version"`
|
||||
CudaMaxGood float64 `json:"cuda_max_good"`
|
||||
MachineID int `json:"machine_id"`
|
||||
HostingType *float64 `json:"hosting_type"`
|
||||
PublicIPAddr string `json:"public_ipaddr"`
|
||||
Geolocation string `json:"geolocation"`
|
||||
Geocode *int64 `json:"geolocode"`
|
||||
FlopsPerDPHTotal float64 `json:"flops_per_dphtotal"`
|
||||
DLPerfPerDPHTotal float64 `json:"dlperf_per_dphtotal"`
|
||||
Reliability2 float64 `json:"reliability2"`
|
||||
HostRunTime int `json:"host_run_time"`
|
||||
HostID int `json:"host_id"`
|
||||
ID int `json:"id"`
|
||||
BundleID int `json:"bundle_id"`
|
||||
NumGPUs int `json:"num_gpus"`
|
||||
TotalFlops float64 `json:"total_flops"`
|
||||
MinBid float64 `json:"min_bid"`
|
||||
DPHBase float64 `json:"dph_base"`
|
||||
DPHTotal float64 `json:"dph_total"`
|
||||
GPUName string `json:"gpu_name"`
|
||||
GPUArch string `json:"gpu_arch"`
|
||||
GPURam int `json:"gpu_ram"`
|
||||
GPUDisplayActive bool `json:"gpu_display_active"`
|
||||
GPUMemBw float64 `json:"gpu_mem_bw"`
|
||||
BwNVLink float64 `json:"bw_nvlink"`
|
||||
DirectPortCount int `json:"direct_port_count"`
|
||||
GPULanes int `json:"gpu_lanes"`
|
||||
PCIeBw float64 `json:"pcie_bw"`
|
||||
PCIGen float64 `json:"pci_gen"`
|
||||
DLPerf float64 `json:"dlperf"`
|
||||
CPUName string `json:"cpu_name"`
|
||||
MoboName string `json:"mobo_name"`
|
||||
CPURam int `json:"cpu_ram"`
|
||||
CPUCores float64 `json:"cpu_cores"`
|
||||
CPUCoresEffective float64 `json:"cpu_cores_effective"`
|
||||
GPUFrac float64 `json:"gpu_frac"`
|
||||
HasAVX int `json:"has_avx"`
|
||||
DiskSpace float64 `json:"disk_space"`
|
||||
DiskName string `json:"disk_name"`
|
||||
DiskBw float64 `json:"disk_bw"`
|
||||
InetUp float64 `json:"inet_up"`
|
||||
InetDown float64 `json:"inet_down"`
|
||||
StartDate float64 `json:"start_date"`
|
||||
EndDate *float64 `json:"end_date"`
|
||||
Duration *float64 `json:"duration"`
|
||||
StorageCost float64 `json:"storage_cost"`
|
||||
InetUpCost float64 `json:"inet_up_cost"`
|
||||
InetDownCost float64 `json:"inet_down_cost"`
|
||||
StorageTotalCost float64 `json:"storage_total_cost"`
|
||||
Verification string `json:"verification"`
|
||||
Score float64 `json:"score"`
|
||||
Rented bool `json:"rented"`
|
||||
BundledResults int `json:"bundled_results"`
|
||||
PendingCount int `json:"pending_count"`
|
||||
Search ExtendedOfferDetails `json:"search"`
|
||||
Instance ExtendedOfferDetails `json:"instance"`
|
||||
}
|
||||
|
||||
type ExtendedOfferDetails struct {
|
||||
GPUCostPerHour float64 `json:"gpuCostPerHour"`
|
||||
DiskHour float64 `json:"diskHour"`
|
||||
TotalHour float64 `json:"totalHour"`
|
||||
DiscountTotalHour float64 `json:"discountTotalHour"`
|
||||
DiscountedTotalPerHour float64 `json:"discountPerHour"`
|
||||
}
|
||||
|
||||
func (o *Offer) String() string {
|
||||
|
||||
3
app.go
3
app.go
@@ -52,6 +52,9 @@ func (a *App) Search() []api.ScoredOffer {
|
||||
|
||||
// Create search
|
||||
search := api.NewSearch()
|
||||
search.AllocatedStorage = 39.94657756485159
|
||||
search.Limit = 1000
|
||||
// search.Rentable = api.Pointer(true)
|
||||
// search.CPUCores = api.Ge(8)
|
||||
|
||||
// Perform search
|
||||
|
||||
62
frontend/package-lock.json
generated
62
frontend/package-lock.json
generated
@@ -8,8 +8,11 @@
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"react-tooltip": "^5.28.0",
|
||||
"tailwind-merge": "^2.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.17",
|
||||
@@ -394,6 +397,28 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.6.8",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz",
|
||||
"integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.6.12",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz",
|
||||
"integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.6.0",
|
||||
"@floating-ui/utils": "^0.2.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
|
||||
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig=="
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
@@ -772,6 +797,19 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -1985,6 +2023,19 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-tooltip": {
|
||||
"version": "5.28.0",
|
||||
"resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.28.0.tgz",
|
||||
"integrity": "sha512-R5cO3JPPXk6FRbBHMO0rI9nkUG/JKfalBSQfZedZYzmqaZQgq7GLzF8vcCWx6IhUCKg0yPqJhXIzmIO5ff15xg==",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.6.1",
|
||||
"classnames": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.14.0",
|
||||
"react-dom": ">=16.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@@ -2267,6 +2318,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.5.tgz",
|
||||
"integrity": "sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/dcastil"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.16",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.16.tgz",
|
||||
|
||||
@@ -9,8 +9,11 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"react-tooltip": "^5.28.0",
|
||||
"tailwind-merge": "^2.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.17",
|
||||
|
||||
@@ -1 +1 @@
|
||||
05b5c44c752e5fa7c8860e80963ac683
|
||||
1a36b753b2917c4d0b27ca4c30b27a95
|
||||
@@ -9,6 +9,7 @@ function App() {
|
||||
async function invoke() {
|
||||
const offers = await Search();
|
||||
console.log({ offer: offers[0] });
|
||||
offers.sort((a, b) => b.Score - a.Score);
|
||||
setState(offers);
|
||||
}
|
||||
|
||||
@@ -18,10 +19,12 @@ function App() {
|
||||
|
||||
return (
|
||||
<div id="App">
|
||||
<div className="p-4" onClick={invoke}>
|
||||
{state?.map((offer) => (
|
||||
<Offer offer={offer} />
|
||||
))}
|
||||
<div className="p-4">
|
||||
<div className="space-y-3 flex flex-col justify-items-center">
|
||||
{state?.map((offer) => (
|
||||
<Offer offer={offer} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,102 @@
|
||||
import { api } from "@wails/go/models";
|
||||
import { useState } from "react";
|
||||
import { cn } from "@src/utils";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
|
||||
export default function Offer({
|
||||
offer: scoredOffer,
|
||||
offer: { Offer: offer, Score: score, Reasons: reasons },
|
||||
}: {
|
||||
offer: api.ScoredOffer;
|
||||
}) {
|
||||
return <div className="p-4">{scoredOffer.Score}</div>;
|
||||
const copy = (text: string) => navigator.clipboard.writeText(text);
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const mb_to_gb = (mb: number) => Math.round(mb / 1024);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn({
|
||||
"flex [&>*]:px-2 flex-col relative bg-zinc-700/90 rounded max-w-md overflow-hidden":
|
||||
true,
|
||||
"h-24": !showDetails,
|
||||
"min-h-24 max-h-48": showDetails,
|
||||
})}
|
||||
>
|
||||
<div className="flex">
|
||||
<span className="text-4xl font-bold pr-2">
|
||||
{score >= 10 ? Math.round(score) : score.toFixed(1)}
|
||||
</span>
|
||||
<span className="relative text-xl top-2.5">
|
||||
{offer.num_gpus}x {offer.gpu_name}{" "}
|
||||
<span className="text-sm">{mb_to_gb(offer.gpu_ram)} GB</span>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>${offer.search.totalHour.toFixed(2)}/hr</span>
|
||||
<span className="pl-3 text-sm">
|
||||
<span className="text-xs select-none">mem</span>{" "}
|
||||
{mb_to_gb(offer.cpu_ram)}/{mb_to_gb(offer.cpu_ram / offer.gpu_frac)}GB
|
||||
</span>
|
||||
</div>
|
||||
<div className="select-none [>button]:select-auto [>button]:text-blue-500 w-full left-1 text-xs space-x-1">
|
||||
<button onClick={() => copy(offer.machine_id.toString())}>
|
||||
m{offer.machine_id}
|
||||
</button>
|
||||
<button onClick={() => copy(offer.host_id.toString())}>
|
||||
h{offer.host_id}
|
||||
</button>
|
||||
<span>{Math.round(offer.duration / 60 / 60 / 24)} days</span>
|
||||
<span
|
||||
className={
|
||||
offer.verification != "verified" ? "text-orange-400/90" : ""
|
||||
}
|
||||
>
|
||||
{offer.verification}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
className={cn({
|
||||
"px-0 w-full bg-zinc-900/70 border-t cursor-pointer border-zinc-600/80 text-center":
|
||||
true,
|
||||
"select-none h-3 leading-[0.2rem] text-zinc-100 absolute bottom-0":
|
||||
!showDetails,
|
||||
"h-40 overflow-y-auto text-sm": showDetails,
|
||||
})}
|
||||
>
|
||||
{showDetails ? (
|
||||
<>
|
||||
<Tooltip id="reason" />
|
||||
{reasons
|
||||
.sort((a, _) => (a.IsMultiplier ? 1 : -1))
|
||||
.map((reason, i) => (
|
||||
<div
|
||||
data-tooltip-id="reason"
|
||||
data-tooltip-content={reason.Value}
|
||||
key={i}
|
||||
className={cn(
|
||||
"space-x-2",
|
||||
(reason.IsMultiplier && reason.Offset < 1) ||
|
||||
(!reason.IsMultiplier && reason.Offset < 0)
|
||||
? ""
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
{reason.IsMultiplier ? (
|
||||
<span>x{reason.Offset.toFixed(2)}</span>
|
||||
) : (
|
||||
<span>
|
||||
{reason.Offset > 0 ? "+" : null}
|
||||
{reason.Offset}
|
||||
</span>
|
||||
)}
|
||||
<span>{reason.Reason}</span>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
"..."
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,3 +5,37 @@
|
||||
html {
|
||||
@apply bg-zinc-800 p-0 m-0 h-screen w-screen text-zinc-200 overflow-x-hidden;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
/* @apply border-border; */
|
||||
}
|
||||
body {
|
||||
/* @apply bg-background text-foreground; */
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
list-style: revert;
|
||||
}
|
||||
/* NEW CODE */
|
||||
/* width */
|
||||
::-webkit-scrollbar {
|
||||
@apply w-2 rounded-lg;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-zinc-700 rounded-lg;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-zinc-500 rounded-xl;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
@apply bg-zinc-400 rounded-lg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "react-tooltip/dist/react-tooltip.css";
|
||||
import "./main.css";
|
||||
import App from "./App";
|
||||
|
||||
|
||||
6
frontend/src/utils.ts
Executable file
6
frontend/src/utils.ts
Executable file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -18,7 +18,8 @@
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@components/*": ["src/components/*"],
|
||||
"@wails/*": ["./wailsjs/*"]
|
||||
"@wails/*": ["./wailsjs/*"],
|
||||
"@src/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
|
||||
@@ -11,6 +11,7 @@ export default defineConfig({
|
||||
alias: {
|
||||
"@components": rootPath + "src/components",
|
||||
"@wails": rootPath + "wailsjs",
|
||||
"@src": rootPath + "src",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
export namespace api {
|
||||
|
||||
export class ExtendedOfferDetails {
|
||||
gpuCostPerHour: number;
|
||||
diskHour: number;
|
||||
totalHour: number;
|
||||
discountTotalHour: number;
|
||||
discountPerHour: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new ExtendedOfferDetails(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.gpuCostPerHour = source["gpuCostPerHour"];
|
||||
this.diskHour = source["diskHour"];
|
||||
this.totalHour = source["totalHour"];
|
||||
this.discountTotalHour = source["discountTotalHour"];
|
||||
this.discountPerHour = source["discountPerHour"];
|
||||
}
|
||||
}
|
||||
export class Offer {
|
||||
is_bid: boolean;
|
||||
inet_up_billed?: number;
|
||||
@@ -29,6 +49,7 @@ export namespace api {
|
||||
dph_base: number;
|
||||
dph_total: number;
|
||||
gpu_name: string;
|
||||
gpu_arch: string;
|
||||
gpu_ram: number;
|
||||
gpu_display_active: boolean;
|
||||
gpu_mem_bw: number;
|
||||
@@ -62,6 +83,8 @@ export namespace api {
|
||||
rented: boolean;
|
||||
bundled_results: number;
|
||||
pending_count: number;
|
||||
search: ExtendedOfferDetails;
|
||||
instance: ExtendedOfferDetails;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Offer(source);
|
||||
@@ -97,6 +120,7 @@ export namespace api {
|
||||
this.dph_base = source["dph_base"];
|
||||
this.dph_total = source["dph_total"];
|
||||
this.gpu_name = source["gpu_name"];
|
||||
this.gpu_arch = source["gpu_arch"];
|
||||
this.gpu_ram = source["gpu_ram"];
|
||||
this.gpu_display_active = source["gpu_display_active"];
|
||||
this.gpu_mem_bw = source["gpu_mem_bw"];
|
||||
@@ -130,11 +154,50 @@ export namespace api {
|
||||
this.rented = source["rented"];
|
||||
this.bundled_results = source["bundled_results"];
|
||||
this.pending_count = source["pending_count"];
|
||||
this.search = this.convertValues(source["search"], ExtendedOfferDetails);
|
||||
this.instance = this.convertValues(source["instance"], ExtendedOfferDetails);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class ScoreReason {
|
||||
Reason: string;
|
||||
Offset: number;
|
||||
Value: any;
|
||||
IsMultiplier: boolean;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new ScoreReason(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.Reason = source["Reason"];
|
||||
this.Offset = source["Offset"];
|
||||
this.Value = source["Value"];
|
||||
this.IsMultiplier = source["IsMultiplier"];
|
||||
}
|
||||
}
|
||||
export class ScoredOffer {
|
||||
Offer: Offer;
|
||||
Score: number;
|
||||
Reasons: ScoreReason[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new ScoredOffer(source);
|
||||
@@ -144,6 +207,7 @@ export namespace api {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.Offer = this.convertValues(source["Offer"], Offer);
|
||||
this.Score = source["Score"];
|
||||
this.Reasons = this.convertValues(source["Reasons"], ScoreReason);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
|
||||
Reference in New Issue
Block a user