Skip to content
Merged
270 changes: 264 additions & 6 deletions prototype/ui-redesign/ACCESSIBILITY.md

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions prototype/ui-redesign/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,18 @@ const App: React.FC = () => {
return () => window.removeEventListener('hashchange', onHashChange);
}, []);

// Client-local in-app deep-link: components dispatch
// `lemonade:navigate` with { view } to switch views without involving lemond.
useEffect(() => {
const onAppNavigate = (e: Event) => {
const detail = (e as CustomEvent).detail as { view?: string } | undefined;
const target = detail?.view;
if (target && (VALID_VIEWS as string[]).includes(target)) setView(target as View);
};
window.addEventListener('lemonade:navigate', onAppNavigate as EventListener);
return () => window.removeEventListener('lemonade:navigate', onAppNavigate as EventListener);
}, [setView]);

useEffect(() => {
const unsubStatus = api.onStatusChange(setStatus);
const refreshGlobalModels = () => {
Expand Down
39 changes: 39 additions & 0 deletions prototype/ui-redesign/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ export interface ModelsData {
data: ModelInfo[];
}

/** Real disk usage for the model-storage drive (bytes). */
export interface StorageInfo {
usedBytes: number;
totalBytes: number;
}

export interface CloudProviderRow {
name: string;
base_url: string;
Expand Down Expand Up @@ -971,6 +977,39 @@ class LemonadeAPI {
return data;
}

/**
* Real used/total bytes of the drive where models are stored.
*
* POC LIMITATION (fl0rianr #2424): lemond is OFF LIMITS and exposes no
* disk-usage field today (verified: /system-info and /system-stats lack it).
* This probes system-info for a future `storage` / `disk` shape so the meter
* lights up the moment the backend team adds it — no client changes needed.
* Returns null when no real source is reachable, letting the UI fall back to a
* derived estimate instead of a hardcoded mock.
*/
async getStorageInfo(): Promise<StorageInfo | null> {
try {
const info = await this.systemInfo();
const candidates = [
(info as any).storage,
(info as any).disk,
(info as any).model_storage,
(info as any).Storage,
];
for (const c of candidates) {
if (!isObject(c)) continue;
const total = Number((c as any).total_bytes ?? (c as any).totalBytes ?? (c as any).total);
const used = Number((c as any).used_bytes ?? (c as any).usedBytes ?? (c as any).used);
if (Number.isFinite(total) && total > 0 && Number.isFinite(used) && used >= 0) {
return { usedBytes: used, totalBytes: total };
}
}
} catch {
/* unreachable in the browser-served POC — fall through to null */
}
return null;
}


async cloudProviders(): Promise<CloudProviderRow[]> {
const info = await this.systemInfo();
Expand Down
9 changes: 7 additions & 2 deletions prototype/ui-redesign/src/components/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export type IconName =
| 'search' | 'search-check' | 'edit' | 'download' | 'play' | 'pause' | 'trash' | 'rotate-ccw' | 'chevron-down' | 'chevron-right' | 'plug' | 'box' | 'alert' | 'clock'
| 'citrus' | 'scale' | 'scan-eye' | 'gem' | 'gauge' | 'timer' | 'pen-line' | 'library'
| 'hard-drive' | 'sliders-horizontal' | 'flame' | 'wrench' | 'brain' | 'rocket' | 'pin'
| 'speech' | 'book-open' | 'newspaper' | 'github' | 'discord';
| 'star' | 'hugging-face'
| 'speech' | 'book-open' | 'newspaper' | 'github' | 'discord' | 'funnel';

interface IconProps {
name: IconName;
Expand Down Expand Up @@ -81,11 +82,14 @@ export const Icon: React.FC<IconProps> = ({ name, size = 16, className, title })
case 'brain': return <><path d="M9 4a3 3 0 00-3 3v.4A3.5 3.5 0 003.5 11 3.5 3.5 0 006 14.4V17a3 3 0 003 3" /><path d="M15 4a3 3 0 013 3v.4a3.5 3.5 0 012.5 3.6 3.5 3.5 0 01-2.5 3.4V17a3 3 0 01-3 3" /><path d="M9 4v16M15 4v16M9 8h2M13 8h2M9 12h2M13 12h2M9 16h2M13 16h2" /></>;
case 'rocket': return <><path d="M5 19c1.5-.4 2.8-1.2 3.8-2.2" /><path d="M15 14l-5-5c1.8-3.2 4.8-5.3 9-6 0 4.2-2 7.2-5.2 9" /><path d="M9 15l-3 3" /><path d="M14 9h.01" /><path d="M7 11l-3 1 2-4 4-2" /><path d="M13 17l-1 3 4-2 2-4" /></>;
case 'pin': return <><path d="M12 17v5" /><path d="M5 17h14v-2l-4-4V5l2-2V2H7v1l2 2v6l-4 4v2z" /></>;
case 'star': return <path d="M12 2.5l2.9 6.1 6.6.7-4.9 4.5 1.3 6.5L12 17.8 6.1 20.8l1.3-6.5-4.9-4.5 6.6-.7z" />;
case 'hugging-face': return <><circle cx="12" cy="12" r="9" /><path d="M8.5 14a4 4 0 007 0" /><path d="M9 10h.01" /><path d="M15 10h.01" /></>;
case 'speech': return <><path d="M21 15a4 4 0 01-4 4H8l-5 3V7a4 4 0 014-4h10a4 4 0 014 4z" /><path d="M8 9h8" /><path d="M8 13h5" /></>;
case 'book-open': return <><path d="M12 7v14" /><path d="M3 5.5A2.5 2.5 0 015.5 3H12v18H5.5A2.5 2.5 0 013 18.5z" /><path d="M21 5.5A2.5 2.5 0 0018.5 3H12v18h6.5a2.5 2.5 0 002.5-2.5z" /></>;
case 'newspaper': return <><path d="M4 5h13a3 3 0 013 3v11H7a3 3 0 01-3-3z" /><path d="M4 16a3 3 0 003 3" /><path d="M8 8h6" /><path d="M8 12h8" /><path d="M8 15h5" /></>;
case 'github': return <><path d="M9 19c-4.5 1.4-4.5-2.2-6-2.7" /><path d="M15 22v-3.9a3.4 3.4 0 00-.9-2.6c3-.3 6.1-1.5 6.1-6.7a5.2 5.2 0 00-1.4-3.6 4.8 4.8 0 00-.1-3.6s-1.1-.4-3.7 1.4a12.8 12.8 0 00-6.7 0C5.7.2 4.6.6 4.6.6a4.8 4.8 0 00-.1 3.6A5.2 5.2 0 003.1 7.8c0 5.2 3.1 6.4 6.1 6.7a3 3 0 00-.8 1.8V22" /></>;
case 'discord': return <><path d="M8.6 7.5a11 11 0 016.8 0" /><path d="M7.2 18.5c-1.5-.4-2.8-1.1-4-2.2.4-4.2 1.5-7.5 3.4-10a12.9 12.9 0 013.5-1.1l.4.8a11.8 11.8 0 013 0l.4-.8a12.9 12.9 0 013.5 1.1c1.9 2.5 3 5.8 3.4 10a10.2 10.2 0 01-4 2.2l-.9-1.4a9.5 9.5 0 01-7.8 0z" /><circle cx="9.5" cy="12.5" r="1" /><circle cx="14.5" cy="12.5" r="1" /></>;
case 'funnel': return <path d="M3 4h18l-7 9v6l-4-2V13L3 4z" />;
default: return <><rect x="5" y="5" width="14" height="14" rx="3" /><path d="M9 9h6v6H9z" /></>;
}
})();
Expand All @@ -98,12 +102,13 @@ export const Icon: React.FC<IconProps> = ({ name, size = 16, className, title })
);
};

export type CapabilityIconTarget = ModelCapability | 'all' | 'vision' | 'code' | 'transcription' | 'popular' | 'tools' | 'reasoning' | 'mtp';
export type CapabilityIconTarget = ModelCapability | 'all' | 'vision' | 'code' | 'transcription' | 'popular' | 'tool' | 'tools' | 'reasoning' | 'mtp';

export function capabilityIconName(capability: CapabilityIconTarget): IconName {
switch (capability) {
case 'all': return 'globe';
case 'popular': return 'flame';
case 'tool': return 'wrench';
case 'tools': return 'wrench';
case 'reasoning': return 'brain';
case 'mtp': return 'rocket';
Expand Down
Loading
Loading