Compare commits

..

1 Commits

Author SHA1 Message Date
Evan 523eff541e aaa 2026-02-18 16:57:07 +00:00
44 changed files with 251 additions and 1039 deletions
+2 -384
View File
@@ -16,8 +16,8 @@
/* Gotham-inspired accent colors */
--exo-grid: oklch(0.25 0 0);
--exo-scanline: oklch(0.15 0 0);
--exo-glow-yellow: oklch(0.85 0.18 85 / 0.3);
--exo-glow-yellow-strong: oklch(0.85 0.18 85 / 0.5);
--exo-glow-yellow: 0 0 20px oklch(0.85 0.18 85 / 0.3);
--exo-glow-yellow-strong: 0 0 40px oklch(0.85 0.18 85 / 0.5);
/* Theme Variables */
--radius: 0.375rem;
@@ -39,67 +39,6 @@
--border: oklch(0.22 0 0);
--input: oklch(0.22 0 0);
--ring: var(--exo-yellow);
/* Adaptive text colors (used via utility classes below) */
--text-primary: oklch(0.9 0 0);
--text-secondary: oklch(0.9 0 0 / 0.9);
--text-muted: oklch(0.9 0 0 / 0.7);
--text-faint: oklch(0.9 0 0 / 0.5);
--text-ghost: oklch(0.9 0 0 / 0.4);
--text-subtle: oklch(0.9 0 0 / 0.6);
--text-dim: oklch(0.9 0 0 / 0.3);
--text-whisper: oklch(0.9 0 0 / 0.8);
--exo-bg-hover: oklch(0.18 0 0);
}
/* ============================================================
LIGHT THEME - Applied when html does NOT have .dark class
============================================================ */
html:not(.dark) {
/* Override EXO brand colors for light mode */
--exo-black: oklch(0.97 0 0);
--exo-dark-gray: oklch(0.94 0 0);
--exo-medium-gray: oklch(0.88 0 0);
--exo-light-gray: oklch(0.45 0 0);
--exo-yellow: oklch(0.20 0.02 85);
--exo-yellow-darker: oklch(0.15 0.02 85);
--exo-yellow-glow: oklch(0.30 0.03 85);
--exo-grid: oklch(0.88 0 0);
--exo-scanline: oklch(0.92 0 0);
--exo-glow-yellow: oklch(0.20 0.02 85 / 0.08);
--exo-glow-yellow-strong: oklch(0.20 0.02 85 / 0.12);
/* Theme Variables */
--background: oklch(0.97 0 0);
--foreground: oklch(0.15 0 0);
--card: oklch(0.94 0 0);
--card-foreground: oklch(0.15 0 0);
--popover: oklch(0.96 0 0);
--popover-foreground: oklch(0.15 0 0);
--primary: oklch(0.20 0.02 85);
--primary-foreground: oklch(0.97 0 0);
--secondary: oklch(0.90 0 0);
--secondary-foreground: oklch(0.15 0 0);
--muted: oklch(0.92 0 0);
--muted-foreground: oklch(0.45 0 0);
--accent: oklch(0.90 0 0);
--accent-foreground: oklch(0.15 0 0);
--destructive: oklch(0.55 0.25 25);
--border: oklch(0.85 0 0);
--input: oklch(0.88 0 0);
--ring: oklch(0.20 0.02 85);
/* Adaptive text colors for light mode */
--text-primary: oklch(0.15 0 0);
--text-secondary: oklch(0.15 0 0 / 0.9);
--text-muted: oklch(0.15 0 0 / 0.7);
--text-faint: oklch(0.15 0 0 / 0.5);
--text-ghost: oklch(0.15 0 0 / 0.4);
--text-subtle: oklch(0.15 0 0 / 0.6);
--text-dim: oklch(0.15 0 0 / 0.3);
--text-whisper: oklch(0.15 0 0 / 0.8);
--exo-bg-hover: oklch(0.90 0 0);
}
@theme inline {
@@ -327,327 +266,6 @@ input:focus, textarea:focus {
box-shadow: none;
}
/* ============================================================
LIGHT MODE: Override hardcoded white/black text colors
Components use text-white/XX extensively - in light mode these
need to become dark text. We override at the CSS level so we
don't have to touch every single component.
============================================================ */
html:not(.dark) {
/* text-white variants → dark text */
& .text-white\/90,
& .text-white\/80,
& .text-white\/70,
& .text-white\/60 {
color: var(--foreground) !important;
}
/* Muted text-white variants → dark text with reduced alpha via color-mix */
& .text-white\/50 {
color: color-mix(in oklch, var(--foreground) 50%, transparent) !important;
}
& .text-white\/40 {
color: color-mix(in oklch, var(--foreground) 40%, transparent) !important;
}
& .text-white\/30 {
color: color-mix(in oklch, var(--foreground) 30%, transparent) !important;
}
/* bg-black variants → light bg */
& .bg-black\/40,
& .bg-black\/50,
& .bg-black\/60,
& .bg-black\/80 {
background-color: oklch(0.92 0 0 / 0.5) !important;
}
/* bg-exo-black opacity variants */
& [class*="bg-exo-black/"] {
background-color: oklch(0.92 0 0 / 0.5) !important;
}
/* shadow-black → softer shadow */
& [class*="shadow-black"] {
--tw-shadow-color: oklch(0 0 0 / 0.08) !important;
}
/* Scrollbar light mode */
& ::-webkit-scrollbar-track {
background: oklch(0.94 0 0) !important;
}
& ::-webkit-scrollbar-thumb {
background: oklch(0.78 0 0) !important;
}
& ::-webkit-scrollbar-thumb:hover {
background: oklch(0.40 0 0) !important;
}
/* Command panel light mode */
& .command-panel {
background: linear-gradient(
180deg,
oklch(0.96 0 0 / 0.95) 0%,
oklch(0.94 0 0 / 0.98) 100%
) !important;
border-color: oklch(0.85 0 0) !important;
box-shadow:
inset 0 1px 0 oklch(1 0 0 / 0.5),
0 4px 20px oklch(0 0 0 / 0.08) !important;
}
/* Glow text - no glow in light mode, just subtle shadow */
& .glow-text {
text-shadow:
0 0 10px oklch(0 0 0 / 0.06),
0 0 20px oklch(0 0 0 / 0.03) !important;
}
/* Grid bg */
& .grid-bg {
background-image:
linear-gradient(oklch(0.85 0 0 / 0.3) 1px, transparent 1px),
linear-gradient(90deg, oklch(0.85 0 0 / 0.3) 1px, transparent 1px) !important;
}
/* Scanlines - very subtle in light mode */
& .scanlines::before {
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
oklch(0 0 0 / 0.015) 2px,
oklch(0 0 0 / 0.015) 4px
) !important;
}
/* CRT screen effect */
& .crt-screen {
background: radial-gradient(
ellipse at center,
oklch(0.96 0 0) 0%,
oklch(0.94 0 0) 50%,
oklch(0.92 0 0) 100%
) !important;
box-shadow:
inset 0 0 100px oklch(0 0 0 / 0.05),
0 0 50px oklch(0 0 0 / 0.03) !important;
}
/* Graph links */
& .graph-link {
stroke: oklch(0.35 0 0 / 0.5) !important;
filter: none !important;
}
& .graph-link-active {
stroke: oklch(0.20 0 0 / 0.8) !important;
filter: none !important;
}
/* Shooting stars - hide in light mode */
& .shooting-stars {
display: none !important;
}
/* Logo: invert white→black + subtle dark shadow */
& img[alt="EXO"] {
filter: invert(1) drop-shadow(0 0 10px oklch(0 0 0 / 0.15)) !important;
}
/* Status indicator colors */
& .bg-exo-yellow.rounded-full {
box-shadow: 0 0 6px oklch(0 0 0 / 0.15);
}
/* Red/green/blue semantic colors stay mostly the same but darken slightly */
& .text-red-400 {
color: oklch(0.55 0.22 25) !important;
}
& .text-green-400 {
color: oklch(0.50 0.18 155) !important;
}
& .text-blue-200 {
color: oklch(0.50 0.16 250) !important;
}
& .text-blue-300 {
color: oklch(0.50 0.17 250) !important;
}
& .text-blue-400 {
color: oklch(0.50 0.18 250) !important;
}
/* bg-red variants */
& .bg-red-500\/10 {
background-color: oklch(0.55 0.22 25 / 0.08) !important;
}
& .bg-red-500\/20 {
background-color: oklch(0.55 0.22 25 / 0.12) !important;
}
& .bg-red-500\/30 {
background-color: oklch(0.55 0.22 25 / 0.15) !important;
}
/* Input/textarea backgrounds */
& textarea,
& input[type="text"] {
color: var(--foreground) !important;
}
& textarea::placeholder,
& input::placeholder {
color: oklch(0.45 0 0 / 0.5) !important;
}
/* KaTeX / Math colors in light mode */
& .katex,
& .katex .mord,
& .katex .minner,
& .katex .mop,
& .katex .mbin,
& .katex .mrel,
& .katex .mpunct {
color: oklch(0.15 0 0) !important;
}
& .katex .frac-line,
& .katex .overline-line,
& .katex .underline-line,
& .katex .hline,
& .katex .rule {
border-color: oklch(0.25 0 0) !important;
background: oklch(0.25 0 0) !important;
}
& .katex .sqrt-line {
border-color: oklch(0.25 0 0) !important;
}
& .katex svg {
fill: oklch(0.25 0 0) !important;
stroke: oklch(0.25 0 0) !important;
}
& .katex svg path {
stroke: oklch(0.25 0 0) !important;
}
& .katex .delimsizing,
& .katex .delim-size1,
& .katex .delim-size2,
& .katex .delim-size3,
& .katex .delim-size4,
& .katex .mopen,
& .katex .mclose {
color: oklch(0.35 0 0) !important;
}
/* Code blocks in light mode */
& .hljs {
color: oklch(0.25 0 0) !important;
}
& .code-block-wrapper,
& .math-display-wrapper {
background: oklch(0.96 0 0) !important;
border-color: oklch(0.85 0 0) !important;
}
& .code-block-header,
& .math-display-header {
background: oklch(0.93 0 0) !important;
border-color: oklch(0.87 0 0) !important;
}
/* Inline code */
& .inline-code {
background: oklch(0.90 0 0) !important;
color: oklch(0.25 0 0) !important;
}
/* Blockquote */
& blockquote {
background: oklch(0.94 0 0) !important;
}
/* Table */
& th {
background: oklch(0.92 0 0) !important;
border-color: oklch(0.82 0 0) !important;
}
& td {
border-color: oklch(0.85 0 0) !important;
}
& hr {
border-color: oklch(0.85 0 0) !important;
}
/* hljs syntax colors for light mode */
& .hljs-keyword,
& .hljs-selector-tag,
& .hljs-literal,
& .hljs-section,
& .hljs-link {
color: oklch(0.50 0.25 300) !important;
}
& .hljs-string,
& .hljs-title,
& .hljs-name,
& .hljs-type,
& .hljs-attribute,
& .hljs-symbol,
& .hljs-bullet,
& .hljs-addition,
& .hljs-variable,
& .hljs-template-tag,
& .hljs-template-variable {
color: oklch(0.45 0.12 55) !important;
}
& .hljs-comment,
& .hljs-quote,
& .hljs-deletion,
& .hljs-meta {
color: oklch(0.55 0 0) !important;
}
& .hljs-number,
& .hljs-regexp,
& .hljs-built_in {
color: oklch(0.45 0.16 160) !important;
}
& .hljs-function,
& .hljs-class .hljs-title {
color: oklch(0.45 0.18 250) !important;
}
/* LaTeX proof/theorem blocks */
& .latex-proof {
background: oklch(0.97 0 0) !important;
border-left-color: oklch(0.70 0 0) !important;
}
& .latex-proof-header {
color: oklch(0.25 0 0) !important;
}
& .latex-proof-content {
color: oklch(0.15 0 0) !important;
}
& .latex-proof-content::after {
color: oklch(0.45 0 0) !important;
}
& .latex-theorem {
background: oklch(0.95 0 0) !important;
border-color: oklch(0.82 0 0) !important;
}
& .latex-diagram-placeholder {
background: oklch(0.97 0 0) !important;
border-color: oklch(0.82 0 0) !important;
color: oklch(0.35 0 0) !important;
}
}
/* Shooting Stars Animation */
.shooting-stars {
position: fixed;
+1 -1
View File
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en" class="dark">
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
@@ -39,7 +39,7 @@
<div class="flex flex-wrap gap-2 mb-3 px-1">
{#each files as file (file.id)}
<div
class="group relative flex items-center gap-2 bg-exo-dark-gray/80 border border-exo-yellow/30 rounded px-2.5 py-1.5 text-xs font-mono transition-all hover:border-exo-yellow/50 hover:shadow-[0_0_10px_var(--exo-glow-yellow)]"
class="group relative flex items-center gap-2 bg-exo-dark-gray/80 border border-exo-yellow/30 rounded px-2.5 py-1.5 text-xs font-mono transition-all hover:border-exo-yellow/50 hover:shadow-[0_0_10px_rgba(255,215,0,0.1)]"
>
<!-- File preview or icon -->
{#if file.preview && getFileCategory(file.type, file.name) === "image"}
+1 -1
View File
@@ -659,7 +659,7 @@
class="px-2.5 sm:px-4 py-1.5 sm:py-2 rounded text-xs sm:text-xs tracking-[0.1em] sm:tracking-[0.15em] uppercase font-medium transition-all duration-200 whitespace-nowrap
{!canSend || loading || isEditOnlyWithoutImage
? 'bg-exo-medium-gray/50 text-exo-light-gray cursor-not-allowed'
: 'bg-exo-yellow text-exo-black hover:bg-exo-yellow-darker hover:shadow-[0_0_20px_var(--exo-glow-yellow)]'}"
: 'bg-exo-yellow text-exo-black hover:bg-exo-yellow-darker hover:shadow-[0_0_20px_rgba(255,215,0,0.3)]'}"
aria-label={shouldShowEditMode
? "Edit image"
: isImageModel()
@@ -271,7 +271,7 @@
<!-- Assistant message header -->
<div class="flex items-center gap-1.5 sm:gap-2 mb-1.5 sm:mb-2">
<div
class="w-1.5 h-1.5 sm:w-2 sm:h-2 bg-exo-yellow rounded-full shadow-[0_0_10px_var(--exo-glow-yellow)]"
class="w-1.5 h-1.5 sm:w-2 sm:h-2 bg-exo-yellow rounded-full shadow-[0_0_10px_rgba(255,215,0,0.5)]"
></div>
<span
class="text-sm sm:text-xs text-exo-yellow tracking-[0.15em] sm:tracking-[0.2em] uppercase font-medium"
+7 -62
View File
@@ -1,22 +1,11 @@
<script lang="ts">
import { browser } from "$app/environment";
import { toggleMode, mode } from "mode-watcher";
let {
showHome = true,
onHome = null,
showSidebarToggle = false,
sidebarVisible = true,
onToggleSidebar = null,
}: {
showHome?: boolean;
onHome?: (() => void) | null;
showSidebarToggle?: boolean;
sidebarVisible?: boolean;
onToggleSidebar?: (() => void) | null;
} = $props();
const currentMode = $derived(mode.current);
export let showHome = true;
export let onHome: (() => void) | null = null;
export let showSidebarToggle = false;
export let sidebarVisible = true;
export let onToggleSidebar: (() => void) | null = null;
function handleHome(): void {
if (onHome) {
@@ -86,58 +75,14 @@
<img
src="/exo-logo.png"
alt="EXO"
class="h-18 drop-shadow-[0_0_20px_color-mix(in_oklch,var(--exo-yellow)_50%,transparent)]"
class="h-18 drop-shadow-[0_0_20px_rgba(255,215,0,0.5)]"
/>
</button>
<!-- Right: Theme Toggle + Home + Downloads -->
<!-- Right: Home + Downloads -->
<div
class="absolute right-6 top-1/2 -translate-y-1/2 flex items-center gap-4"
>
<!-- Theme Toggle Button -->
<button
onclick={() => toggleMode()}
class="p-2 rounded border border-exo-medium-gray/40 hover:border-exo-yellow/50 transition-colors cursor-pointer"
title={currentMode === "dark"
? "Switch to light mode"
: "Switch to dark mode"}
aria-label={currentMode === "dark"
? "Switch to light mode"
: "Switch to dark mode"}
>
{#if currentMode === "dark"}
<!-- Sun icon - click to go light -->
<svg
class="w-4 h-4 text-exo-light-gray hover:text-exo-yellow transition-colors"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="5" />
<path
stroke-linecap="round"
d="M12 1v2m0 18v2m-9-11H1m22 0h-2m-2.636-6.364l-1.414 1.414M6.05 6.05L4.636 4.636m0 14.728l1.414-1.414m11.314 1.414l1.414 1.414"
/>
</svg>
{:else}
<!-- Moon icon - click to go dark -->
<svg
class="w-4 h-4 text-exo-light-gray hover:text-exo-yellow transition-colors"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21 12.79A9 9 0 1111.21 3a7 7 0 009.79 9.79z"
/>
</svg>
{/if}
</button>
{#if showHome}
<button
onclick={handleHome}
@@ -59,13 +59,14 @@
}
const sizeOptions: ImageGenerationParams["size"][] = [
"auto",
"512x512",
"768x768",
"1024x1024",
"1024x768",
"768x1024",
"1024x1365",
"1365x1024",
"1024x1536",
"1536x1024",
];
const qualityOptions: ImageGenerationParams["quality"][] = [
@@ -176,92 +177,90 @@
<div class="border-b border-exo-medium-gray/30 px-3 py-2">
<!-- Basic params row -->
<div class="flex items-center gap-3 flex-wrap">
<!-- Size (hidden in edit mode - output size comes from input image) -->
{#if !isEditMode}
<div class="flex items-center gap-1.5">
<span class="text-xs text-exo-light-gray uppercase tracking-wider"
>SIZE:</span
<!-- Size -->
<div class="flex items-center gap-1.5">
<span class="text-xs text-exo-light-gray uppercase tracking-wider"
>SIZE:</span
>
<div class="relative">
<button
bind:this={sizeButtonRef}
type="button"
onclick={() => (isSizeDropdownOpen = !isSizeDropdownOpen)}
class="bg-exo-medium-gray/50 border border-exo-yellow/30 rounded pl-2 pr-6 py-1 text-xs font-mono text-exo-yellow cursor-pointer transition-all duration-200 hover:border-exo-yellow/50 focus:outline-none focus:border-exo-yellow/70 {isSizeDropdownOpen
? 'border-exo-yellow/70'
: ''}"
>
<div class="relative">
<button
bind:this={sizeButtonRef}
type="button"
onclick={() => (isSizeDropdownOpen = !isSizeDropdownOpen)}
class="bg-exo-medium-gray/50 border border-exo-yellow/30 rounded pl-2 pr-6 py-1 text-xs font-mono text-exo-yellow cursor-pointer transition-all duration-200 hover:border-exo-yellow/50 focus:outline-none focus:border-exo-yellow/70 {isSizeDropdownOpen
? 'border-exo-yellow/70'
: ''}"
{params.size.toUpperCase()}
</button>
<div
class="absolute right-1.5 top-1/2 -translate-y-1/2 pointer-events-none transition-transform duration-200 {isSizeDropdownOpen
? 'rotate-180'
: ''}"
>
<svg
class="w-3 h-3 text-exo-yellow/60"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
{params.size}
</button>
<div
class="absolute right-1.5 top-1/2 -translate-y-1/2 pointer-events-none transition-transform duration-200 {isSizeDropdownOpen
? 'rotate-180'
: ''}"
>
<svg
class="w-3 h-3 text-exo-yellow/60"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
</div>
{#if isSizeDropdownOpen}
<!-- Backdrop to close dropdown -->
<button
type="button"
class="fixed inset-0 z-[9998] cursor-default"
onclick={() => (isSizeDropdownOpen = false)}
aria-label="Close dropdown"
></button>
<!-- Dropdown Panel - fixed positioning to escape overflow:hidden -->
<div
class="fixed bg-exo-dark-gray border border-exo-yellow/30 rounded shadow-lg shadow-black/50 z-[9999] max-h-48 overflow-y-auto overflow-x-hidden min-w-max"
style="bottom: calc(100vh - {sizeDropdownPosition()
.top}px + 4px); left: {sizeDropdownPosition().left}px;"
>
<div class="py-1">
{#each sizeOptions as size}
<button
type="button"
onclick={() => selectSize(size)}
class="w-full px-3 py-1.5 text-left text-xs font-mono tracking-wide transition-colors duration-100 flex items-center gap-2 {params.size ===
size
? 'bg-transparent text-exo-yellow'
: 'text-exo-light-gray hover:text-exo-yellow'}"
>
{#if params.size === size}
<svg
class="w-3 h-3 flex-shrink-0"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
/>
</svg>
{:else}
<span class="w-3"></span>
{/if}
<span>{size.toUpperCase()}</span>
</button>
{/each}
</div>
</div>
{#if isSizeDropdownOpen}
<!-- Backdrop to close dropdown -->
<button
type="button"
class="fixed inset-0 z-[9998] cursor-default"
onclick={() => (isSizeDropdownOpen = false)}
aria-label="Close dropdown"
></button>
<!-- Dropdown Panel - fixed positioning to escape overflow:hidden -->
<div
class="fixed bg-exo-dark-gray border border-exo-yellow/30 rounded shadow-lg shadow-black/50 z-[9999] max-h-48 overflow-y-auto min-w-max"
style="bottom: calc(100vh - {sizeDropdownPosition()
.top}px + 4px); left: {sizeDropdownPosition().left}px;"
>
<div class="py-1">
{#each sizeOptions as size}
<button
type="button"
onclick={() => selectSize(size)}
class="w-full px-3 py-1.5 text-left text-xs font-mono tracking-wide transition-colors duration-100 flex items-center gap-2 {params.size ===
size
? 'bg-transparent text-exo-yellow'
: 'text-exo-light-gray hover:text-exo-yellow'}"
>
{#if params.size === size}
<svg
class="w-3 h-3 flex-shrink-0"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
/>
</svg>
{:else}
<span class="w-3"></span>
{/if}
<span>{size}</span>
</button>
{/each}
</div>
</div>
{/if}
</div>
{/if}
{/if}
</div>
<!-- Quality -->
<div class="flex items-center gap-1.5">
@@ -311,7 +310,7 @@
<!-- Dropdown Panel - fixed positioning to escape overflow:hidden -->
<div
class="fixed bg-exo-dark-gray border border-exo-yellow/30 rounded shadow-lg shadow-black/50 z-[9999] max-h-48 overflow-y-auto min-w-max"
class="fixed bg-exo-dark-gray border border-exo-yellow/30 rounded shadow-lg shadow-black/50 z-[9999] max-h-48 overflow-y-auto overflow-x-hidden min-w-max"
style="bottom: calc(100vh - {qualityDropdownPosition()
.top}px + 4px); left: {qualityDropdownPosition().left}px;"
>
@@ -669,7 +668,7 @@
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--exo-yellow);
background: #ffd700;
cursor: pointer;
border: none;
}
@@ -678,7 +677,7 @@
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--exo-yellow);
background: #ffd700;
cursor: pointer;
border: none;
}
@@ -1051,231 +1051,4 @@
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* ============================================
LIGHT MODE OVERRIDES
All dark-mode-specific hardcoded colors are
overridden below for light backgrounds.
============================================ */
/* --- Inline code --- */
:global(html:not(.dark)) .markdown-content :global(.inline-code) {
background: rgba(0, 0, 0, 0.06);
color: var(--exo-yellow, #b8860b);
}
/* --- Blockquote --- */
:global(html:not(.dark)) .markdown-content :global(blockquote) {
background: rgba(0, 0, 0, 0.03);
}
/* --- Tables --- */
:global(html:not(.dark)) .markdown-content :global(th) {
background: rgba(0, 0, 0, 0.05);
border: 1px solid rgba(0, 0, 0, 0.12);
}
:global(html:not(.dark)) .markdown-content :global(td) {
border: 1px solid rgba(0, 0, 0, 0.1);
}
/* --- Horizontal rule --- */
:global(html:not(.dark)) .markdown-content :global(hr) {
border-top: 1px solid rgba(0, 0, 0, 0.12);
}
/* --- Code block wrapper --- */
:global(html:not(.dark)) .markdown-content :global(.code-block-wrapper) {
border: 1px solid rgba(0, 0, 0, 0.12);
background: rgba(0, 0, 0, 0.03);
}
:global(html:not(.dark)) .markdown-content :global(.code-block-header) {
background: rgba(0, 0, 0, 0.04);
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
}
/* --- Syntax highlighting - light theme --- */
:global(html:not(.dark)) .markdown-content :global(.hljs) {
color: #1f2937;
}
:global(html:not(.dark)) .markdown-content :global(.hljs-keyword),
:global(html:not(.dark)) .markdown-content :global(.hljs-selector-tag),
:global(html:not(.dark)) .markdown-content :global(.hljs-literal),
:global(html:not(.dark)) .markdown-content :global(.hljs-section),
:global(html:not(.dark)) .markdown-content :global(.hljs-link) {
color: #7c3aed;
}
:global(html:not(.dark)) .markdown-content :global(.hljs-string),
:global(html:not(.dark)) .markdown-content :global(.hljs-title),
:global(html:not(.dark)) .markdown-content :global(.hljs-name),
:global(html:not(.dark)) .markdown-content :global(.hljs-type),
:global(html:not(.dark)) .markdown-content :global(.hljs-attribute),
:global(html:not(.dark)) .markdown-content :global(.hljs-symbol),
:global(html:not(.dark)) .markdown-content :global(.hljs-bullet),
:global(html:not(.dark)) .markdown-content :global(.hljs-addition),
:global(html:not(.dark)) .markdown-content :global(.hljs-variable),
:global(html:not(.dark)) .markdown-content :global(.hljs-template-tag),
:global(html:not(.dark)) .markdown-content :global(.hljs-template-variable) {
color: #b45309;
}
:global(html:not(.dark)) .markdown-content :global(.hljs-comment),
:global(html:not(.dark)) .markdown-content :global(.hljs-quote),
:global(html:not(.dark)) .markdown-content :global(.hljs-deletion),
:global(html:not(.dark)) .markdown-content :global(.hljs-meta) {
color: #9ca3af;
}
:global(html:not(.dark)) .markdown-content :global(.hljs-number),
:global(html:not(.dark)) .markdown-content :global(.hljs-regexp),
:global(html:not(.dark)) .markdown-content :global(.hljs-literal),
:global(html:not(.dark)) .markdown-content :global(.hljs-built_in) {
color: #059669;
}
:global(html:not(.dark)) .markdown-content :global(.hljs-function),
:global(html:not(.dark)) .markdown-content :global(.hljs-class .hljs-title) {
color: #2563eb;
}
/* --- KaTeX math - light mode --- */
:global(html:not(.dark)) .markdown-content :global(.katex) {
color: oklch(0.2 0 0);
}
:global(html:not(.dark)) .markdown-content :global(.katex .mord),
:global(html:not(.dark)) .markdown-content :global(.katex .minner),
:global(html:not(.dark)) .markdown-content :global(.katex .mop),
:global(html:not(.dark)) .markdown-content :global(.katex .mbin),
:global(html:not(.dark)) .markdown-content :global(.katex .mrel),
:global(html:not(.dark)) .markdown-content :global(.katex .mpunct) {
color: oklch(0.2 0 0);
}
:global(html:not(.dark)) .markdown-content :global(.katex .frac-line),
:global(html:not(.dark)) .markdown-content :global(.katex .overline-line),
:global(html:not(.dark)) .markdown-content :global(.katex .underline-line),
:global(html:not(.dark)) .markdown-content :global(.katex .hline),
:global(html:not(.dark)) .markdown-content :global(.katex .rule) {
border-color: oklch(0.25 0 0) !important;
background: oklch(0.25 0 0);
}
:global(html:not(.dark)) .markdown-content :global(.katex .sqrt-line) {
border-color: oklch(0.25 0 0) !important;
}
:global(html:not(.dark)) .markdown-content :global(.katex svg) {
fill: oklch(0.25 0 0);
stroke: oklch(0.25 0 0);
}
:global(html:not(.dark)) .markdown-content :global(.katex svg path) {
stroke: oklch(0.25 0 0);
}
:global(html:not(.dark)) .markdown-content :global(.katex .delimsizing),
:global(html:not(.dark)) .markdown-content :global(.katex .delim-size1),
:global(html:not(.dark)) .markdown-content :global(.katex .delim-size2),
:global(html:not(.dark)) .markdown-content :global(.katex .delim-size3),
:global(html:not(.dark)) .markdown-content :global(.katex .delim-size4),
:global(html:not(.dark)) .markdown-content :global(.katex .mopen),
:global(html:not(.dark)) .markdown-content :global(.katex .mclose) {
color: oklch(0.35 0 0);
}
/* --- Display math wrapper --- */
:global(html:not(.dark)) .markdown-content :global(.math-display-wrapper) {
border: 1px solid rgba(0, 0, 0, 0.1);
background: rgba(0, 0, 0, 0.02);
}
:global(html:not(.dark))
.markdown-content
:global(.math-display-wrapper:hover) {
border-color: rgba(0, 0, 0, 0.18);
box-shadow: 0 0 12px rgba(0, 0, 0, 0.05);
}
:global(html:not(.dark)) .markdown-content :global(.math-display-header) {
background: rgba(0, 0, 0, 0.03);
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
:global(html:not(.dark)) .markdown-content :global(.math-label) {
color: rgba(0, 0, 0, 0.5);
}
/* --- Math scrollbar (light mode) --- */
:global(html:not(.dark))
.markdown-content
:global(.math-display-content::-webkit-scrollbar-track) {
background: rgba(0, 0, 0, 0.04);
}
:global(html:not(.dark))
.markdown-content
:global(.math-display-content::-webkit-scrollbar-thumb) {
background: rgba(0, 0, 0, 0.15);
}
:global(html:not(.dark))
.markdown-content
:global(.math-display-content::-webkit-scrollbar-thumb:hover) {
background: rgba(0, 0, 0, 0.25);
}
/* --- Inline math hover --- */
:global(html:not(.dark)) .markdown-content :global(.math-inline:hover) {
background: rgba(0, 0, 0, 0.04);
}
/* --- LaTeX proof environment --- */
:global(html:not(.dark)) .markdown-content :global(.latex-proof) {
background: rgba(0, 0, 0, 0.02);
border-left: 3px solid rgba(0, 0, 0, 0.25);
}
:global(html:not(.dark)) .markdown-content :global(.latex-proof-header) {
color: oklch(0.25 0 0);
}
:global(html:not(.dark)) .markdown-content :global(.latex-proof-content) {
color: oklch(0.2 0 0);
}
:global(html:not(.dark))
.markdown-content
:global(.latex-proof-content::after) {
color: oklch(0.4 0 0);
}
/* --- LaTeX theorem-like environments --- */
:global(html:not(.dark)) .markdown-content :global(.latex-theorem) {
background: rgba(0, 0, 0, 0.02);
border: 1px solid rgba(0, 0, 0, 0.1);
}
:global(html:not(.dark)) .markdown-content :global(.latex-theorem-content) {
color: oklch(0.2 0 0);
}
/* --- LaTeX diagram/figure placeholder --- */
:global(html:not(.dark))
.markdown-content
:global(.latex-diagram-placeholder) {
background: rgba(0, 0, 0, 0.02);
border: 1px dashed rgba(0, 0, 0, 0.2);
color: rgba(0, 0, 0, 0.45);
}
/* --- Math error (light mode) --- */
:global(html:not(.dark)) .markdown-content :global(.math-error) {
color: #dc2626;
background: rgba(220, 38, 38, 0.08);
border: 1px solid rgba(220, 38, 38, 0.15);
}
</style>
+33 -64
View File
@@ -6,42 +6,6 @@
TopologyEdge,
} from "$lib/stores/app.svelte";
import { debugMode, topologyData } from "$lib/stores/app.svelte";
import { browser } from "$app/environment";
// Theme detection
let isDark = $state(true);
$effect(() => {
if (!browser) return;
const update = () => {
isDark = document.documentElement.classList.contains("dark");
};
update();
const observer = new MutationObserver(update);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
return () => observer.disconnect();
});
// Theme-aware colors
let tc = $derived({
accent: isDark ? "#FFD700" : "#1a1a1a",
accentDim: isDark ? "rgba(255,215,0,0.1)" : "rgba(26,26,26,0.06)",
accentGlow: isDark ? "rgba(255,215,0,0.1)" : "rgba(0,0,0,0.03)",
outlineActive: isDark ? "#FFD700" : "#374151",
outlineInactive: isDark ? "#4B5563" : "#d1d5db",
strokeActive: isDark ? "#FFD700" : "#374151",
strokeInactive: isDark ? "#4B5563" : "#d1d5db",
caseFill: isDark ? "#0a0a0a" : "#f3f4f6",
caseDetail: isDark ? "#374151" : "#d1d5db",
errorDot: isDark ? "#f87171" : "#dc2626",
statusInactive: isDark ? "#4B5563" : "#9ca3af",
connGood: isDark ? "rgba(255,255,255,0.85)" : "rgba(30,30,30,0.85)",
connBad: isDark ? "rgba(248,113,113,0.85)" : "rgba(220,38,38,0.85)",
scanlineColor: isDark ? "rgba(255,215,0,0.02)" : "rgba(0,0,0,0.01)",
});
interface Props {
model: { id: string; name?: string; storage_size_megabytes?: number };
@@ -527,7 +491,7 @@
<div
class="bg-exo-dark-gray/60 border {canFit
? 'border-exo-yellow/20 group-hover:border-exo-yellow/40'
: 'border-red-500/20'} p-3 transition-all duration-200 group-hover:shadow-[0_0_15px_var(--exo-glow-yellow)]"
: 'border-red-500/20'} p-3 transition-all duration-200 group-hover:shadow-[0_0_15px_rgba(255,215,0,0.1)]"
>
<!-- Model Name & Memory Required -->
<div class="flex items-start justify-between gap-2 mb-2">
@@ -625,8 +589,7 @@
>
<!-- Scanline effect -->
<div
style="background: repeating-linear-gradient(0deg, transparent, transparent 2px, {tc.scanlineColor} 2px, {tc.scanlineColor} 4px)"
class="absolute inset-0 pointer-events-none"
class="absolute inset-0 bg-[repeating-linear-gradient(0deg,transparent,transparent_2px,rgba(255,215,0,0.02)_2px,rgba(255,215,0,0.02)_4px)] pointer-events-none"
></div>
<svg
@@ -707,9 +670,7 @@
y1={node.y}
x2={node2.x}
y2={node2.y}
stroke={node.isUsed && node2.isUsed
? tc.strokeActive
: tc.strokeInactive}
stroke={node.isUsed && node2.isUsed ? "#FFD700" : "#374151"}
stroke-width="1"
stroke-dasharray={node.isUsed && node2.isUsed ? "4,2" : "2,4"}
opacity={node.isUsed && node2.isUsed ? 0.4 : 0.15}
@@ -745,7 +706,9 @@
dominant-baseline="hanging"
font-size="6"
font-family="SF Mono, Monaco, monospace"
fill={conn.iface ? tc.connGood : tc.connBad}
fill={conn.iface
? "rgba(255,255,255,0.85)"
: "rgba(248,113,113,0.85)"}
>
{conn.arrow}
{isRdma
@@ -762,7 +725,9 @@
dominant-baseline="hanging"
font-size="6"
font-family="SF Mono, Monaco, monospace"
fill={conn.iface ? tc.connGood : tc.connBad}
fill={conn.iface
? "rgba(255,255,255,0.85)"
: "rgba(248,113,113,0.85)"}
>
{conn.arrow}
{isRdma
@@ -781,7 +746,9 @@
dominant-baseline="auto"
font-size="6"
font-family="SF Mono, Monaco, monospace"
fill={conn.iface ? tc.connGood : tc.connBad}
fill={conn.iface
? "rgba(255,255,255,0.85)"
: "rgba(248,113,113,0.85)"}
>
{conn.arrow}
{isRdma
@@ -800,7 +767,9 @@
dominant-baseline="auto"
font-size="6"
font-family="SF Mono, Monaco, monospace"
fill={conn.iface ? tc.connGood : tc.connBad}
fill={conn.iface
? "rgba(255,255,255,0.85)"
: "rgba(248,113,113,0.85)"}
>
{conn.arrow}
{isRdma
@@ -832,7 +801,7 @@
height={node.iconSize * 0.65}
rx="2"
fill="none"
stroke={node.isUsed ? tc.strokeActive : tc.outlineInactive}
stroke={node.isUsed ? "#FFD700" : "#4B5563"}
stroke-width="1.5"
/>
<!-- Screen area (memory fill container) -->
@@ -841,7 +810,7 @@
y="2"
width={node.iconSize - 8}
height={node.screenHeight}
fill={tc.caseFill}
fill="#0a0a0a"
/>
<!-- Current memory fill (gray) -->
<rect
@@ -849,7 +818,7 @@
y={2 + node.screenHeight - node.currentFillHeight}
width={node.iconSize - 8}
height={node.currentFillHeight}
fill={tc.caseDetail}
fill="#374151"
/>
<!-- New model memory fill (glowing yellow) -->
{#if node.modelUsageGB > 0 && node.isUsed}
@@ -861,7 +830,7 @@
node.modelFillHeight}
width={node.iconSize - 8}
height={node.modelFillHeight}
fill={tc.accent}
fill="#FFD700"
filter="url(#memGlow-{filterId})"
class="animate-pulse-slow"
/>
@@ -873,7 +842,7 @@
0.68} L {node.iconSize - 2} {node.iconSize *
0.78} L 2 {node.iconSize * 0.78} Z"
fill="none"
stroke={node.isUsed ? tc.strokeActive : tc.outlineInactive}
stroke={node.isUsed ? "#FFD700" : "#4B5563"}
stroke-width="1.5"
/>
</g>
@@ -890,7 +859,7 @@
height={node.iconSize - 4}
rx="4"
fill="none"
stroke={node.isUsed ? tc.strokeActive : tc.outlineInactive}
stroke={node.isUsed ? "#FFD700" : "#4B5563"}
stroke-width="1.5"
/>
<!-- Memory fill background -->
@@ -899,7 +868,7 @@
y="4"
width={node.iconSize - 8}
height={node.iconSize - 8}
fill={tc.caseFill}
fill="#0a0a0a"
/>
<!-- Current memory fill -->
<rect
@@ -908,7 +877,7 @@
(node.iconSize - 8) * (1 - node.currentPercent / 100)}
width={node.iconSize - 8}
height={(node.iconSize - 8) * (node.currentPercent / 100)}
fill={tc.caseDetail}
fill="#374151"
/>
<!-- New model memory fill -->
{#if node.modelUsageGB > 0 && node.isUsed}
@@ -918,7 +887,7 @@
width={node.iconSize - 8}
height={(node.iconSize - 8) *
((node.newPercent - node.currentPercent) / 100)}
fill={tc.accent}
fill="#FFD700"
filter="url(#memGlow-{filterId})"
class="animate-pulse-slow"
/>
@@ -937,7 +906,7 @@
height={node.iconSize * 0.4}
rx="3"
fill="none"
stroke={node.isUsed ? tc.strokeActive : tc.outlineInactive}
stroke={node.isUsed ? "#FFD700" : "#4B5563"}
stroke-width="1.5"
/>
<!-- Memory fill background -->
@@ -946,7 +915,7 @@
y={node.iconSize * 0.32}
width={node.iconSize - 8}
height={node.iconSize * 0.36}
fill={tc.caseFill}
fill="#0a0a0a"
/>
<!-- Current memory fill -->
<rect
@@ -955,7 +924,7 @@
node.iconSize * 0.36 * (1 - node.currentPercent / 100)}
width={node.iconSize - 8}
height={node.iconSize * 0.36 * (node.currentPercent / 100)}
fill={tc.caseDetail}
fill="#374151"
/>
<!-- New model memory fill -->
{#if node.modelUsageGB > 0 && node.isUsed}
@@ -967,7 +936,7 @@
height={node.iconSize *
0.36 *
((node.newPercent - node.currentPercent) / 100)}
fill={tc.accent}
fill="#FFD700"
filter="url(#memGlow-{filterId})"
class="animate-pulse-slow"
/>
@@ -986,8 +955,8 @@
0.75} {node.iconSize /
2},{node.iconSize} 0,{node.iconSize *
0.75} 0,{node.iconSize * 0.25}"
fill={node.isUsed ? tc.accentDim : tc.caseFill}
stroke={node.isUsed ? tc.strokeActive : tc.outlineInactive}
fill={node.isUsed ? "rgba(255,215,0,0.1)" : "#0a0a0a"}
stroke={node.isUsed ? "#FFD700" : "#4B5563"}
stroke-width="1.5"
/>
</g>
@@ -1001,9 +970,9 @@
font-family="SF Mono, Monaco, monospace"
fill={node.isUsed
? node.newPercent > 90
? tc.errorDot
: tc.accent
: tc.statusInactive}
? "#f87171"
: "#FFD700"
: "#4B5563"}
>
{node.newPercent.toFixed(0)}%
</text>
+48 -127
View File
@@ -36,84 +36,6 @@
const rdmaCtlData = $derived(nodeRdmaCtl());
const identitiesData = $derived(nodeIdentities());
// Theme-aware colors - read from CSS custom properties
function getThemeColors() {
if (typeof document === "undefined") return getDefaultColors();
const style = getComputedStyle(document.documentElement);
const isDark = document.documentElement.classList.contains("dark");
return {
accent:
style.getPropertyValue("--exo-yellow").trim() ||
(isDark ? "oklch(0.85 0.18 85)" : "oklch(0.20 0.02 85)"),
accentRgb: isDark ? "255,215,0" : "30,28,20",
deviceCase: isDark ? "#1a1a1a" : "#e8e8e8",
deviceCaseDark: isDark ? "#2c2c2c" : "#d4d4d4",
deviceScreen: isDark ? "#0a0a12" : "#f0f0f5",
deviceScreenFill: isDark ? "rgba(0,20,40,0.9)" : "rgba(240,242,248,0.95)",
labelWhite: isDark ? "#FFFFFF" : "#1a1a1a",
labelMuted: isDark ? "rgba(179,179,179,0.9)" : "rgba(80,80,80,0.9)",
labelDim: isDark ? "rgba(179,179,179,0.7)" : "rgba(100,100,100,0.7)",
wireDefault: isDark ? "rgba(179,179,179,0.8)" : "rgba(120,120,120,0.6)",
wireBright: isDark ? "rgba(255,255,255,0.9)" : "rgba(30,30,30,0.9)",
wireFiltered: isDark ? "rgba(140,140,140,0.6)" : "rgba(160,160,160,0.5)",
gridStroke: isDark
? "var(--exo-light-gray, #B3B3B3)"
: "var(--exo-light-gray, #888888)",
errorText: isDark ? "rgba(248,113,113,0.9)" : "rgba(220,38,38,0.9)",
normalText: isDark ? "rgba(255,255,255,0.85)" : "rgba(30,30,30,0.85)",
gpuChip: isDark ? "rgba(80, 80, 90, 0.7)" : "rgba(180, 180, 190, 0.7)",
detailOverlay: isDark ? "rgba(0,0,0,0.35)" : "rgba(0,0,0,0.08)",
deviceShadow: isDark ? "rgba(0,0,0,0.2)" : "rgba(0,0,0,0.06)",
deviceHighlight: isDark
? "rgba(255,255,255,0.08)"
: "rgba(255,255,255,0.5)",
tbActive: isDark ? "rgba(234,179,8,0.9)" : "rgba(30,28,20,0.9)",
tbInactive: isDark ? "rgba(100,100,100,0.7)" : "rgba(160,160,160,0.7)",
deviceDetail: isDark ? "#374151" : "#c0c0c0",
};
}
function getDefaultColors() {
return {
accent: "oklch(0.85 0.18 85)",
accentRgb: "255,215,0",
deviceCase: "#1a1a1a",
deviceCaseDark: "#2c2c2c",
deviceScreen: "#0a0a12",
deviceScreenFill: "rgba(0,20,40,0.9)",
labelWhite: "#FFFFFF",
labelMuted: "rgba(179,179,179,0.9)",
labelDim: "rgba(179,179,179,0.7)",
wireDefault: "rgba(179,179,179,0.8)",
wireBright: "rgba(255,255,255,0.9)",
wireFiltered: "rgba(140,140,140,0.6)",
gridStroke: "var(--exo-light-gray, #B3B3B3)",
errorText: "rgba(248,113,113,0.9)",
normalText: "rgba(255,255,255,0.85)",
gpuChip: "rgba(80, 80, 90, 0.7)",
detailOverlay: "rgba(0,0,0,0.35)",
deviceShadow: "rgba(0,0,0,0.2)",
deviceHighlight: "rgba(255,255,255,0.08)",
tbActive: "rgba(234,179,8,0.9)",
tbInactive: "rgba(100,100,100,0.7)",
deviceDetail: "#374151",
};
}
let themeColors = $state(getThemeColors());
$effect(() => {
if (typeof document === "undefined") return;
const observer = new MutationObserver(() => {
themeColors = getThemeColors();
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
return () => observer.disconnect();
});
function getNodeLabel(nodeId: string): string {
const node = data?.nodes?.[nodeId];
return node?.friendly_name || nodeId.slice(0, 8);
@@ -205,7 +127,7 @@
function getTemperatureColor(temp: number): string {
// Default for N/A temp - light gray
if (isNaN(temp) || temp === null) return themeColors.wireDefault;
if (isNaN(temp) || temp === null) return "rgba(179, 179, 179, 0.8)";
const coolTemp = 45; // Temp for pure blue
const midTemp = 57.5; // Temp for pure yellow
@@ -286,7 +208,7 @@
.append("path")
.attr("d", "M 0 0 L 10 5 L 0 10")
.attr("fill", "none")
.attr("stroke", themeColors.gridStroke)
.attr("stroke", "var(--exo-light-gray, #B3B3B3)")
.attr("stroke-width", "1.6")
.attr("stroke-linecap", "round")
.attr("stroke-linejoin", "round")
@@ -299,7 +221,7 @@
.attr("y", centerY)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("fill", `rgba(${themeColors.accentRgb},0.4)`)
.attr("fill", "rgba(255,215,0,0.4)")
.attr("font-size", isMinimized ? 10 : 12)
.attr("font-family", "SF Mono, monospace")
.attr("letter-spacing", "0.1em")
@@ -583,8 +505,8 @@
.attr(
"fill",
conn.missingIface
? themeColors.errorText
: themeColors.normalText,
? "rgba(248,113,113,0.9)"
: "rgba(255,255,255,0.85)",
)
.text(label);
currentY += isTop ? lineHeight : -lineHeight;
@@ -643,22 +565,22 @@
// Holographic wireframe colors - bright yellow for filter, subtle yellow for hover, grey for filtered out
const wireColor = isInFilter
? `rgba(${themeColors.accentRgb},1)` // Bright yellow for filter selection
? "rgba(255,215,0,1)" // Bright yellow for filter selection
: isHovered
? `rgba(${themeColors.accentRgb},0.7)` // Subtle yellow for hover
? "rgba(255,215,0,0.7)" // Subtle yellow for hover
: isHighlighted
? `rgba(${themeColors.accentRgb},0.9)` // Yellow for instance highlight
? "rgba(255,215,0,0.9)" // Yellow for instance highlight
: isFilteredOut
? themeColors.wireFiltered // Grey for filtered out
: themeColors.wireDefault; // Default
const wireColorBright = themeColors.wireBright;
? "rgba(140,140,140,0.6)" // Grey for filtered out
: "rgba(179,179,179,0.8)"; // Default
const wireColorBright = "rgba(255,255,255,0.9)";
const fillColor = isInFilter
? `rgba(${themeColors.accentRgb},0.25)`
? "rgba(255,215,0,0.25)"
: isHovered
? `rgba(${themeColors.accentRgb},0.12)`
? "rgba(255,215,0,0.12)"
: isHighlighted
? `rgba(${themeColors.accentRgb},0.15)`
: `rgba(${themeColors.accentRgb},0.08)`;
? "rgba(255,215,0,0.15)"
: "rgba(255,215,0,0.08)";
const strokeWidth = isInFilter
? 3
: isHovered
@@ -666,8 +588,8 @@
: isHighlighted
? 2.5
: 1.5;
const screenFill = themeColors.deviceScreenFill;
const glowColor = `rgba(${themeColors.accentRgb},0.3)`;
const screenFill = "rgba(0,20,40,0.9)";
const glowColor = "rgba(255,215,0,0.3)";
const nodeG = nodesGroup
.append("g")
@@ -731,7 +653,7 @@
.attr("width", iconBaseWidth)
.attr("height", iconBaseHeight)
.attr("rx", cornerRadius)
.attr("fill", themeColors.deviceCase)
.attr("fill", "#1a1a1a")
.attr("stroke", wireColor)
.attr("stroke-width", strokeWidth);
@@ -749,12 +671,12 @@
)
.attr("width", iconBaseWidth)
.attr("height", memFillActualHeight)
.attr("fill", `rgba(${themeColors.accentRgb},0.75)`)
.attr("fill", "rgba(255,215,0,0.75)")
.attr("clip-path", `url(#${studioClipId})`);
}
// Front panel details - vertical slots
const detailColor = themeColors.detailOverlay;
const detailColor = "rgba(0,0,0,0.35)";
const slotHeight = iconBaseHeight * 0.14;
const vSlotWidth = iconBaseWidth * 0.05;
const vSlotY =
@@ -814,7 +736,7 @@
.attr("width", iconBaseWidth)
.attr("height", iconBaseHeight)
.attr("rx", cornerRadius)
.attr("fill", themeColors.deviceCase)
.attr("fill", "#1a1a1a")
.attr("stroke", wireColor)
.attr("stroke-width", strokeWidth);
@@ -832,12 +754,12 @@
)
.attr("width", iconBaseWidth)
.attr("height", memFillActualHeight)
.attr("fill", `rgba(${themeColors.accentRgb},0.75)`)
.attr("fill", "rgba(255,215,0,0.75)")
.attr("clip-path", `url(#${miniClipId})`);
}
// Front panel details - vertical slots (no horizontal slot for Mini)
const detailColor = themeColors.detailOverlay;
const detailColor = "rgba(0,0,0,0.35)";
const slotHeight = iconBaseHeight * 0.2;
const vSlotWidth = iconBaseWidth * 0.045;
const vSlotY =
@@ -892,7 +814,7 @@
.attr("width", screenWidth)
.attr("height", screenHeight)
.attr("rx", 3)
.attr("fill", themeColors.deviceCase)
.attr("fill", "#1a1a1a")
.attr("stroke", wireColor)
.attr("stroke-width", strokeWidth);
@@ -904,7 +826,7 @@
.attr("width", screenWidth - screenBezel * 2)
.attr("height", screenHeight - screenBezel * 2)
.attr("rx", 2)
.attr("fill", themeColors.deviceScreen);
.attr("fill", "#0a0a12");
// Memory fill on screen (fills from bottom up - classic style)
if (ramUsagePercent > 0) {
@@ -920,7 +842,7 @@
)
.attr("width", screenWidth - screenBezel * 2)
.attr("height", memFillActualHeight)
.attr("fill", `rgba(${themeColors.accentRgb},0.85)`)
.attr("fill", "rgba(255,215,0,0.85)")
.attr("clip-path", `url(#${screenClipId})`);
}
@@ -937,7 +859,7 @@
"transform",
`translate(${logoX}, ${logoY}) scale(${logoScale})`,
)
.attr("fill", themeColors.labelWhite)
.attr("fill", "#FFFFFF")
.attr("opacity", 0.9);
// Base (keyboard) - trapezoidal
@@ -953,7 +875,7 @@
"d",
`M ${baseTopX} ${baseY} L ${baseTopX + baseTopWidth} ${baseY} L ${baseBottomX + baseBottomWidth} ${baseY + baseHeight} L ${baseBottomX} ${baseY + baseHeight} Z`,
)
.attr("fill", themeColors.deviceCaseDark)
.attr("fill", "#2c2c2c")
.attr("stroke", wireColor)
.attr("stroke-width", 1);
@@ -968,7 +890,7 @@
.attr("y", keyboardY)
.attr("width", keyboardWidth)
.attr("height", keyboardHeight)
.attr("fill", themeColors.deviceShadow)
.attr("fill", "rgba(0,0,0,0.2)")
.attr("rx", 2);
// Trackpad
@@ -982,7 +904,7 @@
.attr("y", trackpadY)
.attr("width", trackpadWidth)
.attr("height", trackpadHeight)
.attr("fill", themeColors.deviceHighlight)
.attr("fill", "rgba(255,255,255,0.08)")
.attr("rx", 2);
} else {
// Default/Unknown - holographic hexagon
@@ -1020,7 +942,7 @@
.attr("y", gpuBarY)
.attr("width", gpuBarWidth)
.attr("height", gpuBarHeight)
.attr("fill", themeColors.gpuChip)
.attr("fill", "rgba(80, 80, 90, 0.7)")
.attr("rx", 2);
// GPU Bar Fill (from bottom up, colored by temperature)
@@ -1057,7 +979,7 @@
.attr("y", gpuTextY - lineSpacing)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("fill", themeColors.labelWhite)
.attr("fill", "#FFFFFF")
.attr("font-size", gpuTextFontSize)
.attr("font-weight", "700")
.attr("font-family", "SF Mono, Monaco, monospace")
@@ -1070,7 +992,7 @@
.attr("y", gpuTextY)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("fill", themeColors.labelWhite)
.attr("fill", "#FFFFFF")
.attr("font-size", gpuTextFontSize)
.attr("font-weight", "700")
.attr("font-family", "SF Mono, Monaco, monospace")
@@ -1083,7 +1005,7 @@
.attr("y", gpuTextY + lineSpacing)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("fill", themeColors.labelWhite)
.attr("fill", "#FFFFFF")
.attr("font-size", gpuTextFontSize)
.attr("font-weight", "700")
.attr("font-family", "SF Mono, Monaco, monospace")
@@ -1111,7 +1033,7 @@
.attr("y", nameY)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("fill", themeColors.accent)
.attr("fill", "#FFD700")
.attr("font-size", fontSize)
.attr("font-weight", 500)
.attr("font-family", "SF Mono, Monaco, monospace")
@@ -1128,15 +1050,15 @@
.attr("font-family", "SF Mono, Monaco, monospace");
memText
.append("tspan")
.attr("fill", `rgba(${themeColors.accentRgb},0.9)`)
.attr("fill", "rgba(255,215,0,0.9)")
.text(`${formatBytes(ramUsed)}`);
memText
.append("tspan")
.attr("fill", themeColors.labelMuted)
.attr("fill", "rgba(179,179,179,0.9)")
.text(`/${formatBytes(ramTotal)}`);
memText
.append("tspan")
.attr("fill", themeColors.labelDim)
.attr("fill", "rgba(179,179,179,0.7)")
.text(` (${ramUsagePercent.toFixed(0)}%)`);
} else if (showCompactLabels) {
// COMPACT MODE: Just name and basic info (4+ nodes)
@@ -1153,7 +1075,7 @@
.attr("x", nodeInfo.x)
.attr("y", nameY)
.attr("text-anchor", "middle")
.attr("fill", themeColors.accent)
.attr("fill", "#FFD700")
.attr("font-size", fontSize)
.attr("font-family", "SF Mono, Monaco, monospace")
.text(shortName);
@@ -1165,7 +1087,7 @@
.attr("x", nodeInfo.x)
.attr("y", statsY)
.attr("text-anchor", "middle")
.attr("fill", `rgba(${themeColors.accentRgb},0.7)`)
.attr("fill", "rgba(255,215,0,0.7)")
.attr("font-size", fontSize * 0.85)
.attr("font-family", "SF Mono, Monaco, monospace")
.text(
@@ -1186,7 +1108,7 @@
.attr("x", nodeInfo.x)
.attr("y", nameY)
.attr("text-anchor", "middle")
.attr("fill", themeColors.accent)
.attr("fill", "#FFD700")
.attr("font-size", fontSize)
.attr("font-weight", "500")
.attr("font-family", "SF Mono, Monaco, monospace")
@@ -1203,15 +1125,15 @@
.attr("font-family", "SF Mono, Monaco, monospace");
memTextMini
.append("tspan")
.attr("fill", `rgba(${themeColors.accentRgb},0.9)`)
.attr("fill", "rgba(255,215,0,0.9)")
.text(`${formatBytes(ramUsed)}`);
memTextMini
.append("tspan")
.attr("fill", themeColors.labelMuted)
.attr("fill", "rgba(179,179,179,0.9)")
.text(`/${formatBytes(ramTotal)}`);
memTextMini
.append("tspan")
.attr("fill", themeColors.labelDim)
.attr("fill", "rgba(179,179,179,0.7)")
.text(` (${ramUsagePercent.toFixed(0)}%)`);
}
@@ -1227,8 +1149,8 @@
const tbStatus = tbBridgeData[nodeInfo.id];
if (tbStatus) {
const tbColor = tbStatus.enabled
? themeColors.tbActive
: themeColors.tbInactive;
? "rgba(234,179,8,0.9)"
: "rgba(100,100,100,0.7)";
const tbText = tbStatus.enabled ? "TB:ON" : "TB:OFF";
nodeG
.append("text")
@@ -1246,7 +1168,7 @@
if (rdmaStatus !== undefined) {
const rdmaColor = rdmaStatus.enabled
? "rgba(74,222,128,0.9)"
: themeColors.tbInactive;
: "rgba(100,100,100,0.7)";
const rdmaText = rdmaStatus.enabled ? "RDMA:ON" : "RDMA:OFF";
nodeG
.append("text")
@@ -1267,7 +1189,7 @@
.attr("x", nodeInfo.x)
.attr("y", debugLabelY)
.attr("text-anchor", "middle")
.attr("fill", themeColors.labelDim)
.attr("fill", "rgba(179,179,179,0.7)")
.attr("font-size", debugFontSize)
.attr("font-family", "SF Mono, Monaco, monospace")
.text(
@@ -1284,7 +1206,6 @@
const _hoveredNodeId = hoveredNodeId;
const _filteredNodes = filteredNodes;
const _highlightedNodes = highlightedNodes;
const _themeColors = themeColors;
if (_data) {
renderGraph();
}
-3
View File
@@ -1,12 +1,9 @@
<script lang="ts">
import "../app.css";
import { ModeWatcher } from "mode-watcher";
let { children } = $props();
</script>
<ModeWatcher defaultMode="dark" />
<svelte:head>
<title>EXO</title>
<meta name="description" content="EXO - Distributed AI Cluster Dashboard" />
+3 -3
View File
@@ -2299,7 +2299,7 @@
<!-- Panel Header -->
<div class="flex items-center gap-2 mb-4">
<div
class="w-2 h-2 bg-exo-yellow rounded-full shadow-[0_0_8px_var(--exo-glow-yellow)] animate-pulse"
class="w-2 h-2 bg-exo-yellow rounded-full shadow-[0_0_8px_rgba(255,215,0,0.6)] animate-pulse"
></div>
<h3
class="text-xs text-exo-yellow font-mono tracking-[0.2em] uppercase"
@@ -2948,7 +2948,7 @@
<!-- Dot -->
<span
class="rounded-full transition-all {isSelected
? 'w-6 h-6 bg-exo-yellow shadow-[0_0_10px_var(--exo-glow-yellow)]'
? 'w-6 h-6 bg-exo-yellow shadow-[0_0_10px_rgba(255,215,0,0.6)]'
: isValid
? 'w-4 h-4 bg-exo-light-gray/70 mt-1'
: 'w-3 h-3 bg-exo-medium-gray/50 mt-1.5'}"
@@ -3114,7 +3114,7 @@
<!-- Panel Header -->
<div class="flex items-center gap-2 mb-4">
<div
class="w-2 h-2 bg-exo-yellow rounded-full shadow-[0_0_8px_var(--exo-glow-yellow)] animate-pulse"
class="w-2 h-2 bg-exo-yellow rounded-full shadow-[0_0_8px_rgba(255,215,0,0.6)] animate-pulse"
></div>
<h3
class="text-xs text-exo-yellow font-mono tracking-[0.2em] uppercase"
+1 -1
View File
@@ -382,7 +382,7 @@
class="group border-b border-exo-medium-gray/20 hover:bg-exo-medium-gray/10 transition-colors"
>
<td
class="sticky left-0 z-10 bg-exo-dark-gray group-hover:bg-[var(--exo-bg-hover)] transition-colors px-4 py-3 whitespace-nowrap border-r border-exo-medium-gray/20"
class="sticky left-0 z-10 bg-exo-dark-gray group-hover:bg-[oklch(0.18_0_0)] transition-colors px-4 py-3 whitespace-nowrap border-r border-exo-medium-gray/20"
>
<div class="flex items-center gap-2">
<div class="min-w-0">
-2
View File
@@ -6,8 +6,6 @@ readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"aiofiles>=24.1.0",
"aiohttp>=3.12.14",
"types-aiofiles>=24.1.0.20250708",
"pydantic>=2.11.7",
"fastapi>=0.116.1",
"filelock>=3.18.0",
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "deepseek"
quantization = "4bit"
base_model = "DeepSeek V3.1"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 405874409472
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "deepseek"
quantization = "8bit"
base_model = "DeepSeek V3.1"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 765577920512
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "glm"
quantization = "8bit"
base_model = "GLM 4.5 Air"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 122406567936
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "glm"
quantization = "bf16"
base_model = "GLM 4.5 Air"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 229780750336
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "glm"
quantization = "4bit"
base_model = "GLM 4.7"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 198556925568
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "glm"
quantization = "6bit"
base_model = "GLM 4.7"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 286737579648
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "glm"
quantization = "8bit"
base_model = "GLM 4.7"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 396963397248
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "glm"
quantization = "4bit"
base_model = "GLM 4.7 Flash"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 19327352832
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "glm"
quantization = "5bit"
base_model = "GLM 4.7 Flash"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 22548578304
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "glm"
quantization = "6bit"
base_model = "GLM 4.7 Flash"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 26843545600
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "glm"
quantization = "8bit"
base_model = "GLM 4.7 Flash"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 34359738368
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "kimi"
quantization = ""
base_model = "Kimi K2"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 706522120192
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "kimi"
quantization = ""
base_model = "Kimi K2.5"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 662498705408
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "minimax"
quantization = "3bit"
base_model = "MiniMax M2.1"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 100086644736
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "minimax"
quantization = "8bit"
base_model = "MiniMax M2.1"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 242986745856
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "qwen"
quantization = "4bit"
base_model = "Qwen3 0.6B"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 342884352
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "qwen"
quantization = "8bit"
base_model = "Qwen3 0.6B"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 698351616
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "qwen"
quantization = "4bit"
base_model = "Qwen3 235B"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 141733920768
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "qwen"
quantization = "8bit"
base_model = "Qwen3 235B"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 268435456000
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "qwen"
quantization = "4bit"
base_model = "Qwen3 30B"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 17612931072
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "qwen"
quantization = "8bit"
base_model = "Qwen3 30B"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 33279705088
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "qwen"
quantization = "4bit"
base_model = "Qwen3 Next 80B"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 47080074240
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "qwen"
quantization = "8bit"
base_model = "Qwen3 Next 80B"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 88814387200
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "step"
quantization = "4bit"
base_model = "Step 3.5 Flash"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 114572190076
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "step"
quantization = "6bit"
base_model = "Step 3.5 Flash"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 159039627774
@@ -6,7 +6,7 @@ tasks = ["TextGeneration"]
family = "step"
quantization = "8bit"
base_model = "Step 3.5 Flash"
capabilities = ["text", "thinking", "thinking_toggle"]
capabilities = ["text", "thinking"]
[storage_size]
in_bytes = 209082699847
+38 -41
View File
@@ -8,13 +8,13 @@ import traceback
from collections.abc import Awaitable
from datetime import timedelta
from pathlib import Path
from typing import Callable, Literal
from typing import Callable, Literal, cast
from urllib.parse import urljoin
import aiofiles
import aiofiles.os as aios
import aiohttp
import certifi
import httpx
from huggingface_hub import (
snapshot_download, # pyright: ignore[reportUnknownVariableType]
)
@@ -330,17 +330,17 @@ async def _fetch_file_list(
headers = await get_download_headers()
async with (
create_http_session(timeout_profile="short") as session,
session.get(url, headers=headers) as response,
):
if response.status in [401, 403]:
msg = await _build_auth_error_message(response.status, model_id)
response = await session.get(url, headers=headers)
if response.status_code in [401, 403]:
msg = await _build_auth_error_message(response.status_code, model_id)
raise HuggingFaceAuthenticationError(msg)
elif response.status == 429:
elif response.status_code == 429:
raise HuggingFaceRateLimitError(
f"Couldn't download {model_id} because of HuggingFace rate limit."
)
elif response.status == 200:
data_json = await response.text()
elif response.status_code == 200:
data_json = response.text
data = TypeAdapter(list[FileListEntry]).validate_json(data_json)
files: list[FileListEntry] = []
for item in data:
@@ -353,7 +353,7 @@ async def _fetch_file_list(
files.extend(subfiles)
return files
else:
raise Exception(f"Failed to fetch file list: {response.status}")
raise Exception(f"Failed to fetch file list: {response.status_code}")
async def get_download_headers() -> dict[str, str]:
@@ -361,34 +361,29 @@ async def get_download_headers() -> dict[str, str]:
def create_http_session(
auto_decompress: bool = False,
timeout_profile: Literal["short", "long"] = "long",
) -> aiohttp.ClientSession:
) -> httpx.AsyncClient:
if timeout_profile == "short":
total_timeout = 30
connect_timeout = 10
sock_read_timeout = 30
sock_connect_timeout = 10
read_timeout = 30
else:
total_timeout = 1800
connect_timeout = 60
sock_read_timeout = 60
sock_connect_timeout = 60
read_timeout = 60
ssl_context = ssl.create_default_context(
cafile=os.getenv("SSL_CERT_FILE") or certifi.where()
)
connector = aiohttp.TCPConnector(ssl=ssl_context)
return aiohttp.ClientSession(
auto_decompress=auto_decompress,
connector=connector,
proxy=os.getenv("HTTPS_PROXY") or os.getenv("HTTP_PROXY") or None,
timeout=aiohttp.ClientTimeout(
total=total_timeout,
# default here is to load env vars
return httpx.AsyncClient(
verify=ssl_context,
timeout=httpx.Timeout(
connect=connect_timeout,
sock_read=sock_read_timeout,
sock_connect=sock_connect_timeout,
read=read_timeout,
write=total_timeout,
pool=total_timeout,
),
)
@@ -415,26 +410,28 @@ async def file_meta(
headers = await get_download_headers()
async with (
create_http_session(timeout_profile="short") as session,
session.head(url, headers=headers) as r,
session.stream("HEAD", url, headers=headers) as r,
):
if r.status == 307:
if r.status_code == 307:
# On redirect, only trust Hugging Face's x-linked-* headers.
x_linked_size = r.headers.get("x-linked-size")
x_linked_etag = r.headers.get("x-linked-etag")
x_linked_size = cast(str | None, r.headers.get("x-linked-size"))
x_linked_etag = cast(str | None, r.headers.get("x-linked-etag"))
if x_linked_size and x_linked_etag:
content_length = int(x_linked_size)
etag = trim_etag(x_linked_etag)
return content_length, etag
# Otherwise, follow the redirect to get authoritative size/hash
redirected_location = r.headers.get("location")
redirected_location = cast(str | None, r.headers.get("location"))
return await file_meta(model_id, revision, path, redirected_location)
if r.status in [401, 403]:
msg = await _build_auth_error_message(r.status, model_id)
if r.status_code in [401, 403]:
msg = await _build_auth_error_message(r.status_code, model_id)
raise HuggingFaceAuthenticationError(msg)
content_length = int(
r.headers.get("x-linked-size") or r.headers.get("content-length") or 0
content_length = cast(
str | None,
r.headers.get("x-linked-size") or r.headers.get("content-length"),
)
etag = r.headers.get("x-linked-etag") or r.headers.get("etag")
content_length = 0 if content_length is None else int(content_length)
etag = cast(str | None, r.headers.get("x-linked-etag") or r.headers.get("etag"))
assert content_length > 0, f"No content length for {url}"
assert etag is not None, f"No remote hash for {url}"
etag = trim_etag(etag)
@@ -537,20 +534,20 @@ async def _download_file(
n_read = resume_byte_pos or 0
async with (
create_http_session(timeout_profile="long") as session,
session.get(url, headers=headers) as r,
session.stream("GET", url, headers=headers, follow_redirects=True) as r,
):
if r.status == 404:
if r.status_code == 404:
raise FileNotFoundError(f"File not found: {url}")
if r.status in [401, 403]:
msg = await _build_auth_error_message(r.status, model_id)
if r.status_code in [401, 403]:
msg = await _build_auth_error_message(r.status_code, model_id)
raise HuggingFaceAuthenticationError(msg)
assert r.status in [200, 206], (
f"Failed to download {path} from {url}: {r.status}"
assert r.status_code in [200, 206], (
f"Failed to download {path} from {url}: {r.status_code}"
)
async with aiofiles.open(
partial_path, "ab" if resume_byte_pos else "wb"
) as f:
while chunk := await r.content.read(8 * 1024 * 1024):
async for chunk in r.aiter_bytes(8 * 1024 * 1024):
n_read = n_read + (await f.write(chunk))
on_progress(n_read, length, False)
@@ -189,6 +189,7 @@ class ResumableShardDownloader(ShardDownloader):
try:
yield await task
except Exception as e:
task.cancel()
logger.warning(f"Error downloading shard: {type(e).__name__}")
async def get_shard_download_status_for_shard(
+2 -4
View File
@@ -1,10 +1,8 @@
import time
from typing import Generic, TypeVar
K = TypeVar("K")
from collections.abc import Hashable
class KeyedBackoff(Generic[K]):
class KeyedBackoff[K: Hashable]:
"""Tracks exponential backoff state per key."""
def __init__(self, base: float = 0.5, cap: float = 10.0):
Generated
-4
View File
@@ -367,7 +367,6 @@ version = "0.3.0"
source = { editable = "." }
dependencies = [
{ name = "aiofiles", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "exo-pyo3-bindings", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "fastapi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
@@ -389,7 +388,6 @@ dependencies = [
{ name = "rustworkx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "tiktoken", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "tomlkit", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "types-aiofiles", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "zstandard", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
]
@@ -406,7 +404,6 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "aiofiles", specifier = ">=24.1.0" },
{ name = "aiohttp", specifier = ">=3.12.14" },
{ name = "anyio", specifier = "==4.11.0" },
{ name = "exo-pyo3-bindings", editable = "rust/exo_pyo3_bindings" },
{ name = "fastapi", specifier = ">=0.116.1" },
@@ -428,7 +425,6 @@ requires-dist = [
{ name = "rustworkx", specifier = ">=0.17.1" },
{ name = "tiktoken", specifier = ">=0.12.0" },
{ name = "tomlkit", specifier = ">=0.14.0" },
{ name = "types-aiofiles", specifier = ">=24.1.0.20250708" },
{ name = "zstandard", specifier = ">=0.23.0" },
]