Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 523eff541e |
+2
-384
@@ -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,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"}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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" },
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user