diff --git a/api/score.go b/api/score.go index d28b870..2eeca1b 100755 --- a/api/score.go +++ b/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 } diff --git a/api/search.go b/api/search.go index 48b4bf0..fb5879e 100755 --- a/api/search.go +++ b/api/search.go @@ -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, } } diff --git a/api/types.go b/api/types.go index 807b635..a0085a0 100755 --- a/api/types.go +++ b/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 { diff --git a/app.go b/app.go index 284b72f..0fd4076 100755 --- a/app.go +++ b/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 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index dcf787b..fc94822 100755 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index 72318f5..1f38323 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 33d476b..ce0fe9e 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -05b5c44c752e5fa7c8860e80963ac683 \ No newline at end of file +1a36b753b2917c4d0b27ca4c30b27a95 \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 274c215..4624822 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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 (
-
- {state?.map((offer) => ( - - ))} +
+
+ {state?.map((offer) => ( + + ))} +
); diff --git a/frontend/src/components/Offer.tsx b/frontend/src/components/Offer.tsx index f010846..7779fbf 100755 --- a/frontend/src/components/Offer.tsx +++ b/frontend/src/components/Offer.tsx @@ -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
{scoredOffer.Score}
; + const copy = (text: string) => navigator.clipboard.writeText(text); + const [showDetails, setShowDetails] = useState(false); + const mb_to_gb = (mb: number) => Math.round(mb / 1024); + + return ( +
*]: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, + })} + > +
+ + {score >= 10 ? Math.round(score) : score.toFixed(1)} + + + {offer.num_gpus}x {offer.gpu_name}{" "} + {mb_to_gb(offer.gpu_ram)} GB + +
+
+ ${offer.search.totalHour.toFixed(2)}/hr + + mem{" "} + {mb_to_gb(offer.cpu_ram)}/{mb_to_gb(offer.cpu_ram / offer.gpu_frac)}GB + +
+
+ + + {Math.round(offer.duration / 60 / 60 / 24)} days + + {offer.verification} + +
+
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 ? ( + <> + + {reasons + .sort((a, _) => (a.IsMultiplier ? 1 : -1)) + .map((reason, i) => ( +
+ {reason.IsMultiplier ? ( + x{reason.Offset.toFixed(2)} + ) : ( + + {reason.Offset > 0 ? "+" : null} + {reason.Offset} + + )} + {reason.Reason} +
+ ))} + + ) : ( + "..." + )} +
+
+ ); } diff --git a/frontend/src/main.css b/frontend/src/main.css index 6e418dd..830be4c 100755 --- a/frontend/src/main.css +++ b/frontend/src/main.css @@ -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; + } +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 9ed916b..90dee8d 100755 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -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"; diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts new file mode 100755 index 0000000..f021764 --- /dev/null +++ b/frontend/src/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 21e157f..ad1695e 100755 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -18,7 +18,8 @@ "baseUrl": "./", "paths": { "@components/*": ["src/components/*"], - "@wails/*": ["./wailsjs/*"] + "@wails/*": ["./wailsjs/*"], + "@src/*": ["src/*"] } }, "include": ["src"], diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index bc2340a..2f5c234 100755 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -11,6 +11,7 @@ export default defineConfig({ alias: { "@components": rootPath + "src/components", "@wails": rootPath + "wailsjs", + "@src": rootPath + "src", }, }, }); diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 92b61de..b45aa20 100755 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -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 {