mirror of
https://github.com/Xevion/vastly.git
synced 2025-12-06 01:16:50 -06:00
major scoring implementation, major frontend work
This commit is contained in:
187
api/score.go
187
api/score.go
@@ -1,15 +1,198 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ScoreReason struct {
|
||||||
|
Reason string
|
||||||
|
Offset float64
|
||||||
|
Value interface{}
|
||||||
|
IsMultiplier bool
|
||||||
|
}
|
||||||
|
|
||||||
type ScoredOffer struct {
|
type ScoredOffer struct {
|
||||||
Offer Offer
|
Offer Offer
|
||||||
Score float64
|
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 {
|
func ScoreOffers(offers []Offer) []ScoredOffer {
|
||||||
var scoredOffers = make([]ScoredOffer, 0, len(offers))
|
var scoredOffers = make([]ScoredOffer, 0, len(offers))
|
||||||
|
|
||||||
for _, offer := range offers {
|
for _, offer := range offers {
|
||||||
score := 100.0
|
reasons := make([]ScoreReason, 0, 16)
|
||||||
scoredOffers = append(scoredOffers, ScoredOffer{Offer: offer, Score: score})
|
|
||||||
|
// 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
|
return scoredOffers
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type AdvancedSearch struct {
|
type AdvancedSearch struct {
|
||||||
|
Limit int `json:"limit,omitempty"`
|
||||||
|
AllocatedStorage float64 `json:"allocated_storage,omitempty"`
|
||||||
Verified *bool `json:"verified,omitempty"`
|
Verified *bool `json:"verified,omitempty"`
|
||||||
ComputeCap *ComparableInteger `json:"compute_cap,omitempty"`
|
ComputeCap *ComparableInteger `json:"compute_cap,omitempty"`
|
||||||
DiskSpace *ComparableInteger `json:"disk_space,omitempty"`
|
DiskSpace *ComparableInteger `json:"disk_space,omitempty"`
|
||||||
@@ -50,6 +52,7 @@ type AdvancedSearch struct {
|
|||||||
func NewSearch() *AdvancedSearch {
|
func NewSearch() *AdvancedSearch {
|
||||||
return &AdvancedSearch{
|
return &AdvancedSearch{
|
||||||
Rented: Pointer(false),
|
Rented: Pointer(false),
|
||||||
|
Limit: 500,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
api/types.go
11
api/types.go
@@ -129,6 +129,7 @@ type Offer struct {
|
|||||||
DPHBase float64 `json:"dph_base"`
|
DPHBase float64 `json:"dph_base"`
|
||||||
DPHTotal float64 `json:"dph_total"`
|
DPHTotal float64 `json:"dph_total"`
|
||||||
GPUName string `json:"gpu_name"`
|
GPUName string `json:"gpu_name"`
|
||||||
|
GPUArch string `json:"gpu_arch"`
|
||||||
GPURam int `json:"gpu_ram"`
|
GPURam int `json:"gpu_ram"`
|
||||||
GPUDisplayActive bool `json:"gpu_display_active"`
|
GPUDisplayActive bool `json:"gpu_display_active"`
|
||||||
GPUMemBw float64 `json:"gpu_mem_bw"`
|
GPUMemBw float64 `json:"gpu_mem_bw"`
|
||||||
@@ -162,6 +163,16 @@ type Offer struct {
|
|||||||
Rented bool `json:"rented"`
|
Rented bool `json:"rented"`
|
||||||
BundledResults int `json:"bundled_results"`
|
BundledResults int `json:"bundled_results"`
|
||||||
PendingCount int `json:"pending_count"`
|
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 {
|
func (o *Offer) String() string {
|
||||||
|
|||||||
3
app.go
3
app.go
@@ -52,6 +52,9 @@ func (a *App) Search() []api.ScoredOffer {
|
|||||||
|
|
||||||
// Create search
|
// Create search
|
||||||
search := api.NewSearch()
|
search := api.NewSearch()
|
||||||
|
search.AllocatedStorage = 39.94657756485159
|
||||||
|
search.Limit = 1000
|
||||||
|
// search.Rentable = api.Pointer(true)
|
||||||
// search.CPUCores = api.Ge(8)
|
// search.CPUCores = api.Ge(8)
|
||||||
|
|
||||||
// Perform search
|
// Perform search
|
||||||
|
|||||||
62
frontend/package-lock.json
generated
62
frontend/package-lock.json
generated
@@ -8,8 +8,11 @@
|
|||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"react": "^18.2.0",
|
"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": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.0.17",
|
"@types/react": "^18.0.17",
|
||||||
@@ -394,6 +397,28 @@
|
|||||||
"node": ">=12"
|
"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": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
@@ -772,6 +797,19 @@
|
|||||||
"node": ">= 6"
|
"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": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -1985,6 +2023,19 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@@ -2267,6 +2318,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "3.4.16",
|
"version": "3.4.16",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.16.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.16.tgz",
|
||||||
|
|||||||
@@ -9,8 +9,11 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"react": "^18.2.0",
|
"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": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.0.17",
|
"@types/react": "^18.0.17",
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
05b5c44c752e5fa7c8860e80963ac683
|
1a36b753b2917c4d0b27ca4c30b27a95
|
||||||
@@ -9,6 +9,7 @@ function App() {
|
|||||||
async function invoke() {
|
async function invoke() {
|
||||||
const offers = await Search();
|
const offers = await Search();
|
||||||
console.log({ offer: offers[0] });
|
console.log({ offer: offers[0] });
|
||||||
|
offers.sort((a, b) => b.Score - a.Score);
|
||||||
setState(offers);
|
setState(offers);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,12 +19,14 @@ function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="App">
|
<div id="App">
|
||||||
<div className="p-4" onClick={invoke}>
|
<div className="p-4">
|
||||||
|
<div className="space-y-3 flex flex-col justify-items-center">
|
||||||
{state?.map((offer) => (
|
{state?.map((offer) => (
|
||||||
<Offer offer={offer} />
|
<Offer offer={offer} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,102 @@
|
|||||||
import { api } from "@wails/go/models";
|
import { api } from "@wails/go/models";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { cn } from "@src/utils";
|
||||||
|
import { Tooltip } from "react-tooltip";
|
||||||
|
|
||||||
export default function Offer({
|
export default function Offer({
|
||||||
offer: scoredOffer,
|
offer: { Offer: offer, Score: score, Reasons: reasons },
|
||||||
}: {
|
}: {
|
||||||
offer: api.ScoredOffer;
|
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 {
|
html {
|
||||||
@apply bg-zinc-800 p-0 m-0 h-screen w-screen text-zinc-200 overflow-x-hidden;
|
@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 React from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
|
import "react-tooltip/dist/react-tooltip.css";
|
||||||
import "./main.css";
|
import "./main.css";
|
||||||
import App from "./App";
|
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": "./",
|
"baseUrl": "./",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@components/*": ["src/components/*"],
|
"@components/*": ["src/components/*"],
|
||||||
"@wails/*": ["./wailsjs/*"]
|
"@wails/*": ["./wailsjs/*"],
|
||||||
|
"@src/*": ["src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default defineConfig({
|
|||||||
alias: {
|
alias: {
|
||||||
"@components": rootPath + "src/components",
|
"@components": rootPath + "src/components",
|
||||||
"@wails": rootPath + "wailsjs",
|
"@wails": rootPath + "wailsjs",
|
||||||
|
"@src": rootPath + "src",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
export namespace api {
|
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 {
|
export class Offer {
|
||||||
is_bid: boolean;
|
is_bid: boolean;
|
||||||
inet_up_billed?: number;
|
inet_up_billed?: number;
|
||||||
@@ -29,6 +49,7 @@ export namespace api {
|
|||||||
dph_base: number;
|
dph_base: number;
|
||||||
dph_total: number;
|
dph_total: number;
|
||||||
gpu_name: string;
|
gpu_name: string;
|
||||||
|
gpu_arch: string;
|
||||||
gpu_ram: number;
|
gpu_ram: number;
|
||||||
gpu_display_active: boolean;
|
gpu_display_active: boolean;
|
||||||
gpu_mem_bw: number;
|
gpu_mem_bw: number;
|
||||||
@@ -62,6 +83,8 @@ export namespace api {
|
|||||||
rented: boolean;
|
rented: boolean;
|
||||||
bundled_results: number;
|
bundled_results: number;
|
||||||
pending_count: number;
|
pending_count: number;
|
||||||
|
search: ExtendedOfferDetails;
|
||||||
|
instance: ExtendedOfferDetails;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new Offer(source);
|
return new Offer(source);
|
||||||
@@ -97,6 +120,7 @@ export namespace api {
|
|||||||
this.dph_base = source["dph_base"];
|
this.dph_base = source["dph_base"];
|
||||||
this.dph_total = source["dph_total"];
|
this.dph_total = source["dph_total"];
|
||||||
this.gpu_name = source["gpu_name"];
|
this.gpu_name = source["gpu_name"];
|
||||||
|
this.gpu_arch = source["gpu_arch"];
|
||||||
this.gpu_ram = source["gpu_ram"];
|
this.gpu_ram = source["gpu_ram"];
|
||||||
this.gpu_display_active = source["gpu_display_active"];
|
this.gpu_display_active = source["gpu_display_active"];
|
||||||
this.gpu_mem_bw = source["gpu_mem_bw"];
|
this.gpu_mem_bw = source["gpu_mem_bw"];
|
||||||
@@ -130,11 +154,50 @@ export namespace api {
|
|||||||
this.rented = source["rented"];
|
this.rented = source["rented"];
|
||||||
this.bundled_results = source["bundled_results"];
|
this.bundled_results = source["bundled_results"];
|
||||||
this.pending_count = source["pending_count"];
|
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 {
|
export class ScoredOffer {
|
||||||
Offer: Offer;
|
Offer: Offer;
|
||||||
Score: number;
|
Score: number;
|
||||||
|
Reasons: ScoreReason[];
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new ScoredOffer(source);
|
return new ScoredOffer(source);
|
||||||
@@ -144,6 +207,7 @@ export namespace api {
|
|||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.Offer = this.convertValues(source["Offer"], Offer);
|
this.Offer = this.convertValues(source["Offer"], Offer);
|
||||||
this.Score = source["Score"];
|
this.Score = source["Score"];
|
||||||
|
this.Reasons = this.convertValues(source["Reasons"], ScoreReason);
|
||||||
}
|
}
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
|
|||||||
Reference in New Issue
Block a user