← back to Exo
fmt: add typescript formatting
383309e24e7627a62eed25f900f99ae0f5b5a3ea · 2026-01-08 13:06:00 +0000 · Jake Hillion
Add typescript auto formatting with Prettier and treefmt-nix. Added a
.prettierrc to useTabs, which isn't the default, to reduce churn. The
rest looks okay and will be checked by CI.
Test plan:
- CI
Files touched
A .prettierrcM dashboard/src/app.d.tsM dashboard/src/lib/components/index.tsM dashboard/src/lib/stores/app.svelte.tsM dashboard/src/lib/types/files.tsM dashboard/vite.config.tsM flake.nix
Diff
commit 383309e24e7627a62eed25f900f99ae0f5b5a3ea
Author: Jake Hillion <jake@hillion.co.uk>
Date: Thu Jan 8 13:06:00 2026 +0000
fmt: add typescript formatting
Add typescript auto formatting with Prettier and treefmt-nix. Added a
.prettierrc to useTabs, which isn't the default, to reduce churn. The
rest looks okay and will be checked by CI.
Test plan:
- CI
---
.prettierrc | 3 +
dashboard/src/app.d.ts | 1 -
dashboard/src/lib/components/index.ts | 15 +-
dashboard/src/lib/stores/app.svelte.ts | 725 ++++++++++++++++++++-------------
dashboard/src/lib/types/files.ts | 169 +++++---
dashboard/vite.config.ts | 19 +-
flake.nix | 4 +
7 files changed, 581 insertions(+), 355 deletions(-)
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 00000000..661f2536
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,3 @@
+{
+ "useTabs": true
+}
diff --git a/dashboard/src/app.d.ts b/dashboard/src/app.d.ts
index b111beb0..da08e6da 100644
--- a/dashboard/src/app.d.ts
+++ b/dashboard/src/app.d.ts
@@ -11,4 +11,3 @@ declare global {
}
export {};
-
diff --git a/dashboard/src/lib/components/index.ts b/dashboard/src/lib/components/index.ts
index fed749c4..5b35654e 100644
--- a/dashboard/src/lib/components/index.ts
+++ b/dashboard/src/lib/components/index.ts
@@ -1,8 +1,7 @@
-export { default as TopologyGraph } from './TopologyGraph.svelte';
-export { default as ChatForm } from './ChatForm.svelte';
-export { default as ChatMessages } from './ChatMessages.svelte';
-export { default as ChatAttachments } from './ChatAttachments.svelte';
-export { default as ChatSidebar } from './ChatSidebar.svelte';
-export { default as ModelCard } from './ModelCard.svelte';
-export { default as MarkdownContent } from './MarkdownContent.svelte';
-
+export { default as TopologyGraph } from "./TopologyGraph.svelte";
+export { default as ChatForm } from "./ChatForm.svelte";
+export { default as ChatMessages } from "./ChatMessages.svelte";
+export { default as ChatAttachments } from "./ChatAttachments.svelte";
+export { default as ChatSidebar } from "./ChatSidebar.svelte";
+export { default as ModelCard } from "./ModelCard.svelte";
+export { default as MarkdownContent } from "./MarkdownContent.svelte";
diff --git a/dashboard/src/lib/stores/app.svelte.ts b/dashboard/src/lib/stores/app.svelte.ts
index e0bfbad1..9ba5c546 100644
--- a/dashboard/src/lib/stores/app.svelte.ts
+++ b/dashboard/src/lib/stores/app.svelte.ts
@@ -1,23 +1,26 @@
/**
* AppStore - Central state management for the EXO dashboard
- *
+ *
* Manages:
* - Chat state (whether a conversation has started)
* - Topology data from the EXO server
* - UI state for the topology/chat transition
*/
-import { browser } from '$app/environment';
+import { browser } from "$app/environment";
// UUID generation fallback for browsers without crypto.randomUUID
function generateUUID(): string {
- if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
+ if (
+ typeof crypto !== "undefined" &&
+ typeof crypto.randomUUID === "function"
+ ) {
return crypto.randomUUID();
}
// Fallback implementation
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
- const r = Math.random() * 16 | 0;
- const v = c === 'x' ? r : (r & 0x3 | 0x8);
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
+ const r = (Math.random() * 16) | 0;
+ const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
@@ -102,7 +105,9 @@ interface RawTopologyNode {
interface RawTopologyConnection {
localNodeId: string;
sendBackNodeId: string;
- sendBackMultiaddr?: { multiaddr?: string; address?: string; ip_address?: string } | string;
+ sendBackMultiaddr?:
+ | { multiaddr?: string; address?: string; ip_address?: string }
+ | string;
}
interface RawTopology {
@@ -143,8 +148,8 @@ export interface ModelDownloadStatus {
// Placement preview from the API
export interface PlacementPreview {
model_id: string;
- sharding: 'Pipeline' | 'Tensor';
- instance_meta: 'MlxRing' | 'MlxIbv' | 'MlxJaccl';
+ sharding: "Pipeline" | "Tensor";
+ instance_meta: "MlxRing" | "MlxIbv" | "MlxJaccl";
instance: unknown | null;
memory_delta_by_node: Record<string, number> | null;
error: string | null;
@@ -156,14 +161,21 @@ export interface PlacementPreviewResponse {
interface RawStateResponse {
topology?: RawTopology;
- instances?: Record<string, { MlxRingInstance?: Instance; MlxIbvInstance?: Instance; MlxJacclInstance?: Instance }>;
+ instances?: Record<
+ string,
+ {
+ MlxRingInstance?: Instance;
+ MlxIbvInstance?: Instance;
+ MlxJacclInstance?: Instance;
+ }
+ >;
runners?: Record<string, unknown>;
downloads?: Record<string, unknown[]>;
nodeProfiles?: RawNodeProfiles;
}
export interface MessageAttachment {
- type: 'image' | 'text' | 'file';
+ type: "image" | "text" | "file";
name: string;
content?: string;
preview?: string;
@@ -172,13 +184,13 @@ export interface MessageAttachment {
export interface Message {
id: string;
- role: 'user' | 'assistant' | 'system';
+ role: "user" | "assistant" | "system";
content: string;
timestamp: number;
thinking?: string;
attachments?: MessageAttachment[];
- ttftMs?: number; // Time to first token in ms (for assistant messages)
- tps?: number; // Tokens per second (for assistant messages)
+ ttftMs?: number; // Time to first token in ms (for assistant messages)
+ tps?: number; // Tokens per second (for assistant messages)
}
export interface Conversation {
@@ -192,9 +204,12 @@ export interface Conversation {
instanceType: string | null;
}
-const STORAGE_KEY = 'exo-conversations';
+const STORAGE_KEY = "exo-conversations";
-function transformTopology(raw: RawTopology, profiles?: RawNodeProfiles): TopologyData {
+function transformTopology(
+ raw: RawTopology,
+ profiles?: RawNodeProfiles,
+): TopologyData {
const nodes: Record<string, NodeInfo> = {};
const edges: TopologyEdge[] = [];
@@ -205,58 +220,75 @@ function transformTopology(raw: RawTopology, profiles?: RawNodeProfiles): Topolo
const ramAvailable = profile?.memory?.ramAvailable?.inBytes ?? 0;
const ramUsage = Math.max(ramTotal - ramAvailable, 0);
- const networkInterfaces = (profile?.networkInterfaces || []).map((iface) => {
- const addresses: string[] = [];
- if (iface.ipAddress && typeof iface.ipAddress === 'string') {
- addresses.push(iface.ipAddress);
- }
- if (Array.isArray(iface.addresses)) {
- for (const addr of iface.addresses) {
- if (typeof addr === 'string') addresses.push(addr);
- else if (addr && typeof addr === 'object' && addr.address) addresses.push(addr.address);
+ const networkInterfaces = (profile?.networkInterfaces || []).map(
+ (iface) => {
+ const addresses: string[] = [];
+ if (iface.ipAddress && typeof iface.ipAddress === "string") {
+ addresses.push(iface.ipAddress);
}
- }
- if (Array.isArray(iface.ipAddresses)) {
- addresses.push(...iface.ipAddresses.filter((a): a is string => typeof a === 'string'));
- }
- if (Array.isArray(iface.ips)) {
- addresses.push(...iface.ips.filter((a): a is string => typeof a === 'string'));
- }
- if (iface.ipv4 && typeof iface.ipv4 === 'string') addresses.push(iface.ipv4);
- if (iface.ipv6 && typeof iface.ipv6 === 'string') addresses.push(iface.ipv6);
-
- return {
- name: iface.name,
- addresses: Array.from(new Set(addresses))
- };
- });
+ if (Array.isArray(iface.addresses)) {
+ for (const addr of iface.addresses) {
+ if (typeof addr === "string") addresses.push(addr);
+ else if (addr && typeof addr === "object" && addr.address)
+ addresses.push(addr.address);
+ }
+ }
+ if (Array.isArray(iface.ipAddresses)) {
+ addresses.push(
+ ...iface.ipAddresses.filter(
+ (a): a is string => typeof a === "string",
+ ),
+ );
+ }
+ if (Array.isArray(iface.ips)) {
+ addresses.push(
+ ...iface.ips.filter((a): a is string => typeof a === "string"),
+ );
+ }
+ if (iface.ipv4 && typeof iface.ipv4 === "string")
+ addresses.push(iface.ipv4);
+ if (iface.ipv6 && typeof iface.ipv6 === "string")
+ addresses.push(iface.ipv6);
+
+ return {
+ name: iface.name,
+ addresses: Array.from(new Set(addresses)),
+ };
+ },
+ );
const ipToInterface: Record<string, string> = {};
for (const iface of networkInterfaces) {
for (const addr of iface.addresses || []) {
- ipToInterface[addr] = iface.name ?? '';
+ ipToInterface[addr] = iface.name ?? "";
}
}
nodes[node.nodeId] = {
system_info: {
- model_id: profile?.modelId ?? 'Unknown',
+ model_id: profile?.modelId ?? "Unknown",
chip: profile?.chipId,
- memory: ramTotal
+ memory: ramTotal,
},
network_interfaces: networkInterfaces,
ip_to_interface: ipToInterface,
macmon_info: {
memory: {
ram_usage: ramUsage,
- ram_total: ramTotal
+ ram_total: ramTotal,
},
- temp: profile?.system?.temp !== undefined ? { gpu_temp_avg: profile.system.temp } : undefined,
- gpu_usage: profile?.system?.gpuUsage !== undefined ? [0, profile.system.gpuUsage] : undefined,
- sys_power: profile?.system?.sysPower
+ temp:
+ profile?.system?.temp !== undefined
+ ? { gpu_temp_avg: profile.system.temp }
+ : undefined,
+ gpu_usage:
+ profile?.system?.gpuUsage !== undefined
+ ? [0, profile.system.gpuUsage]
+ : undefined,
+ sys_power: profile?.system?.sysPower,
},
last_macmon_update: Date.now() / 1000,
- friendly_name: profile?.friendlyName
+ friendly_name: profile?.friendlyName,
};
}
@@ -268,17 +300,20 @@ function transformTopology(raw: RawTopology, profiles?: RawNodeProfiles): Topolo
let sendBackIp: string | undefined;
if (conn.sendBackMultiaddr) {
const multi = conn.sendBackMultiaddr;
- if (typeof multi === 'string') {
+ if (typeof multi === "string") {
sendBackIp = extractIpFromMultiaddr(multi);
} else {
- sendBackIp = multi.ip_address || extractIpFromMultiaddr(multi.multiaddr) || extractIpFromMultiaddr(multi.address);
+ sendBackIp =
+ multi.ip_address ||
+ extractIpFromMultiaddr(multi.multiaddr) ||
+ extractIpFromMultiaddr(multi.address);
}
}
edges.push({
source: conn.localNodeId,
target: conn.sendBackNodeId,
- sendBackIp
+ sendBackIp,
});
}
@@ -287,9 +322,9 @@ function transformTopology(raw: RawTopology, profiles?: RawNodeProfiles): Topolo
function extractIpFromMultiaddr(ma?: string): string | undefined {
if (!ma) return undefined;
- const parts = ma.split('/');
- const ip4Idx = parts.indexOf('ip4');
- const ip6Idx = parts.indexOf('ip6');
+ const parts = ma.split("/");
+ const ip4Idx = parts.indexOf("ip4");
+ const ip6Idx = parts.indexOf("ip6");
const idx = ip4Idx >= 0 ? ip4Idx : ip6Idx;
if (idx >= 0 && parts.length > idx + 1) {
return parts[idx + 1];
@@ -301,18 +336,18 @@ class AppStore {
// Conversation state
conversations = $state<Conversation[]>([]);
activeConversationId = $state<string | null>(null);
-
+
// Chat state
hasStartedChat = $state(false);
messages = $state<Message[]>([]);
- currentResponse = $state('');
+ currentResponse = $state("");
isLoading = $state(false);
-
+
// Performance metrics
- ttftMs = $state<number | null>(null); // Time to first token in ms
- tps = $state<number | null>(null); // Tokens per second
- totalTokens = $state<number>(0); // Total tokens in current response
-
+ ttftMs = $state<number | null>(null); // Time to first token in ms
+ tps = $state<number | null>(null); // Tokens per second
+ totalTokens = $state<number>(0); // Total tokens in current response
+
// Topology state
topologyData = $state<TopologyData | null>(null);
instances = $state<Record<string, unknown>>({});
@@ -322,14 +357,14 @@ class AppStore {
selectedPreviewModelId = $state<string | null>(null);
isLoadingPreviews = $state(false);
lastUpdate = $state<number | null>(null);
-
+
// UI state
isTopologyMinimized = $state(false);
isSidebarOpen = $state(false); // Hidden by default, shown when in chat mode
debugMode = $state(false);
topologyOnlyMode = $state(false);
chatSidebarVisible = $state(true); // Shown by default
-
+
private fetchInterval: ReturnType<typeof setInterval> | null = null;
private previewsInterval: ReturnType<typeof setInterval> | null = null;
private lastConversationPersistTs = 0;
@@ -354,17 +389,17 @@ class AppStore {
const parsed = JSON.parse(stored) as Array<Partial<Conversation>>;
this.conversations = parsed.map((conversation) => ({
id: conversation.id ?? generateUUID(),
- name: conversation.name ?? 'Chat',
+ name: conversation.name ?? "Chat",
messages: conversation.messages ?? [],
createdAt: conversation.createdAt ?? Date.now(),
updatedAt: conversation.updatedAt ?? Date.now(),
modelId: conversation.modelId ?? null,
sharding: conversation.sharding ?? null,
- instanceType: conversation.instanceType ?? null
+ instanceType: conversation.instanceType ?? null,
}));
}
} catch (error) {
- console.error('Failed to load conversations:', error);
+ console.error("Failed to load conversations:", error);
}
}
@@ -375,64 +410,70 @@ class AppStore {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.conversations));
} catch (error) {
- console.error('Failed to save conversations:', error);
+ console.error("Failed to save conversations:", error);
}
}
private loadDebugModeFromStorage() {
try {
- const stored = localStorage.getItem('exo-debug-mode');
+ const stored = localStorage.getItem("exo-debug-mode");
if (stored !== null) {
- this.debugMode = stored === 'true';
+ this.debugMode = stored === "true";
}
} catch (error) {
- console.error('Failed to load debug mode:', error);
+ console.error("Failed to load debug mode:", error);
}
}
private saveDebugModeToStorage() {
try {
- localStorage.setItem('exo-debug-mode', this.debugMode ? 'true' : 'false');
+ localStorage.setItem("exo-debug-mode", this.debugMode ? "true" : "false");
} catch (error) {
- console.error('Failed to save debug mode:', error);
+ console.error("Failed to save debug mode:", error);
}
}
private loadTopologyOnlyModeFromStorage() {
try {
- const stored = localStorage.getItem('exo-topology-only-mode');
+ const stored = localStorage.getItem("exo-topology-only-mode");
if (stored !== null) {
- this.topologyOnlyMode = stored === 'true';
+ this.topologyOnlyMode = stored === "true";
}
} catch (error) {
- console.error('Failed to load topology only mode:', error);
+ console.error("Failed to load topology only mode:", error);
}
}
private saveTopologyOnlyModeToStorage() {
try {
- localStorage.setItem('exo-topology-only-mode', this.topologyOnlyMode ? 'true' : 'false');
+ localStorage.setItem(
+ "exo-topology-only-mode",
+ this.topologyOnlyMode ? "true" : "false",
+ );
} catch (error) {
- console.error('Failed to save topology only mode:', error);
+ console.error("Failed to save topology only mode:", error);
}
}
private loadChatSidebarVisibleFromStorage() {
try {
- const stored = localStorage.getItem('exo-chat-sidebar-visible');
+ const stored = localStorage.getItem("exo-chat-sidebar-visible");
if (stored !== null) {
- this.chatSidebarVisible = stored === 'true';
+ this.chatSidebarVisible = stored === "true";
}
} catch (error) {
- console.error('Failed to load chat sidebar visibility:', error);
+ console.error("Failed to load chat sidebar visibility:", error);
}
}
private saveChatSidebarVisibleToStorage() {
try {
- localStorage.setItem('exo-chat-sidebar-visible', this.chatSidebarVisible ? 'true' : 'false');
+ localStorage.setItem(
+ "exo-chat-sidebar-visible",
+ this.chatSidebarVisible ? "true" : "false",
+ );
} catch (error) {
- console.error('Failed to save chat sidebar visibility:', error);
+ console.error("Failed to save chat sidebar visibility:", error);
}
}
@@ -473,22 +514,24 @@ class AppStore {
const conversation: Conversation = {
id,
- name: name || `Chat ${new Date(now).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}`,
+ name:
+ name ||
+ `Chat ${new Date(now).toLocaleString("en-US", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })}`,
messages: [],
createdAt: now,
updatedAt: now,
modelId: derivedModelId,
sharding: derivedSharding,
- instanceType: derivedInstanceType
+ instanceType: derivedInstanceType,
};
-
+
this.conversations.unshift(conversation);
this.activeConversationId = id;
this.messages = [];
this.hasStartedChat = true;
this.isTopologyMinimized = true;
this.isSidebarOpen = true; // Auto-open sidebar when chatting
-
+
this.saveConversationsToStorage();
return id;
}
@@ -497,16 +540,16 @@ class AppStore {
* Load a conversation by ID
*/
loadConversation(id: string): boolean {
- const conversation = this.conversations.find(c => c.id === id);
+ const conversation = this.conversations.find((c) => c.id === id);
if (!conversation) return false;
-
+
this.activeConversationId = id;
this.messages = [...conversation.messages];
this.hasStartedChat = true;
this.isTopologyMinimized = true;
this.isSidebarOpen = true; // Auto-open sidebar when chatting
this.refreshConversationModelFromInstances();
-
+
return true;
}
@@ -514,15 +557,15 @@ class AppStore {
* Delete a conversation by ID
*/
deleteConversation(id: string) {
- this.conversations = this.conversations.filter(c => c.id !== id);
-
+ this.conversations = this.conversations.filter((c) => c.id !== id);
+
if (this.activeConversationId === id) {
this.activeConversationId = null;
this.messages = [];
this.hasStartedChat = false;
this.isTopologyMinimized = false;
}
-
+
this.saveConversationsToStorage();
}
@@ -542,7 +585,7 @@ class AppStore {
* Rename a conversation
*/
renameConversation(id: string, newName: string) {
- const conversation = this.conversations.find(c => c.id === id);
+ const conversation = this.conversations.find((c) => c.id === id);
if (conversation) {
conversation.name = newName;
conversation.updatedAt = Date.now();
@@ -551,7 +594,7 @@ class AppStore {
}
private getTaggedValue(obj: unknown): [string | null, unknown] {
- if (!obj || typeof obj !== 'object') return [null, null];
+ if (!obj || typeof obj !== "object") return [null, null];
const keys = Object.keys(obj as Record<string, unknown>);
if (keys.length === 1) {
return [keys[0], (obj as Record<string, unknown>)[keys[0]]];
@@ -561,36 +604,50 @@ class AppStore {
private extractInstanceModelId(instanceWrapped: unknown): string | null {
const [, instance] = this.getTaggedValue(instanceWrapped);
- if (!instance || typeof instance !== 'object') return null;
+ if (!instance || typeof instance !== "object") return null;
const inst = instance as { shardAssignments?: { modelId?: string } };
return inst.shardAssignments?.modelId ?? null;
}
- private describeInstance(instanceWrapped: unknown): { sharding: string | null; instanceType: string | null } {
+ private describeInstance(instanceWrapped: unknown): {
+ sharding: string | null;
+ instanceType: string | null;
+ } {
const [instanceTag, instance] = this.getTaggedValue(instanceWrapped);
- if (!instance || typeof instance !== 'object') {
+ if (!instance || typeof instance !== "object") {
return { sharding: null, instanceType: null };
}
let instanceType: string | null = null;
- if (instanceTag === 'MlxRingInstance') instanceType = 'MLX Ring';
- else if (instanceTag === 'MlxIbvInstance' || instanceTag === 'MlxJacclInstance') instanceType = 'MLX RDMA';
+ if (instanceTag === "MlxRingInstance") instanceType = "MLX Ring";
+ else if (
+ instanceTag === "MlxIbvInstance" ||
+ instanceTag === "MlxJacclInstance"
+ )
+ instanceType = "MLX RDMA";
let sharding: string | null = null;
- const inst = instance as { shardAssignments?: { runnerToShard?: Record<string, unknown> } };
+ const inst = instance as {
+ shardAssignments?: { runnerToShard?: Record<string, unknown> };
+ };
const runnerToShard = inst.shardAssignments?.runnerToShard || {};
const firstShardWrapped = Object.values(runnerToShard)[0];
if (firstShardWrapped) {
const [shardTag] = this.getTaggedValue(firstShardWrapped);
- if (shardTag === 'PipelineShardMetadata') sharding = 'Pipeline';
- else if (shardTag === 'TensorShardMetadata') sharding = 'Tensor';
- else if (shardTag === 'PrefillDecodeShardMetadata') sharding = 'Prefill/Decode';
+ if (shardTag === "PipelineShardMetadata") sharding = "Pipeline";
+ else if (shardTag === "TensorShardMetadata") sharding = "Tensor";
+ else if (shardTag === "PrefillDecodeShardMetadata")
+ sharding = "Prefill/Decode";
}
return { sharding, instanceType };
}
- private buildConversationModelInfo(modelId: string): { modelId: string; sharding: string | null; instanceType: string | null } {
+ private buildConversationModelInfo(modelId: string): {
+ modelId: string;
+ sharding: string | null;
+ instanceType: string | null;
+ } {
let sharding: string | null = null;
let instanceType: string | null = null;
@@ -607,9 +664,15 @@ class AppStore {
return { modelId, sharding, instanceType };
}
- private applyConversationModelInfo(info: { modelId: string; sharding: string | null; instanceType: string | null }) {
+ private applyConversationModelInfo(info: {
+ modelId: string;
+ sharding: string | null;
+ instanceType: string | null;
+ }) {
if (!this.activeConversationId) return;
- const conversation = this.conversations.find(c => c.id === this.activeConversationId);
+ const conversation = this.conversations.find(
+ (c) => c.id === this.activeConversationId,
+ );
if (!conversation) return;
// Keep the first known modelId stable; only backfill if missing
@@ -622,21 +685,29 @@ class AppStore {
}
private getModelTail(modelId: string): string {
- const parts = modelId.split('/');
+ const parts = modelId.split("/");
return (parts[parts.length - 1] || modelId).toLowerCase();
}
- private isBetterModelId(currentId: string | null, candidateId: string | null): boolean {
+ private isBetterModelId(
+ currentId: string | null,
+ candidateId: string | null,
+ ): boolean {
if (!candidateId) return false;
if (!currentId) return true;
const currentTail = this.getModelTail(currentId);
const candidateTail = this.getModelTail(candidateId);
- return candidateTail.length > currentTail.length && candidateTail.startsWith(currentTail);
+ return (
+ candidateTail.length > currentTail.length &&
+ candidateTail.startsWith(currentTail)
+ );
}
private refreshConversationModelFromInstances() {
if (!this.activeConversationId) return;
- const conversation = this.conversations.find(c => c.id === this.activeConversationId);
+ const conversation = this.conversations.find(
+ (c) => c.id === this.activeConversationId,
+ );
if (!conversation) return;
// Prefer stored model; do not replace it once set. Only backfill when missing.
@@ -675,7 +746,9 @@ class AppStore {
}
const info = this.buildConversationModelInfo(preferredModelId);
- const hasNewInfo = Boolean(info.sharding || info.instanceType || !conversation.modelId);
+ const hasNewInfo = Boolean(
+ info.sharding || info.instanceType || !conversation.modelId,
+ );
if (hasNewInfo) {
this.applyConversationModelInfo(info);
}
@@ -690,28 +763,33 @@ class AppStore {
*/
private updateActiveConversation() {
if (!this.activeConversationId) return;
-
- const conversation = this.conversations.find(c => c.id === this.activeConversationId);
+
+ const conversation = this.conversations.find(
+ (c) => c.id === this.activeConversationId,
+ );
if (conversation) {
conversation.messages = [...this.messages];
conversation.updatedAt = Date.now();
-
+
// Auto-generate name from first user message if still has default name
- if (conversation.name.startsWith('Chat ')) {
- const firstUserMsg = conversation.messages.find(m => m.role === 'user' && m.content.trim());
+ if (conversation.name.startsWith("Chat ")) {
+ const firstUserMsg = conversation.messages.find(
+ (m) => m.role === "user" && m.content.trim(),
+ );
if (firstUserMsg) {
// Clean up the content - remove file context markers and whitespace
let content = firstUserMsg.content
- .replace(/\[File:.*?\][\s\S]*?```[\s\S]*?```/g, '') // Remove file attachments
+ .replace(/\[File:.*?\][\s\S]*?```[\s\S]*?```/g, "") // Remove file attachments
.trim();
-
+
if (content) {
const preview = content.slice(0, 50);
- conversation.name = preview.length < content.length ? preview + '...' : preview;
+ conversation.name =
+ preview.length < content.length ? preview + "..." : preview;
}
}
}
-
+
this.saveConversationsToStorage();
}
}
@@ -783,12 +861,12 @@ class AppStore {
async fetchState() {
try {
- const response = await fetch('/state');
+ const response = await fetch("/state");
if (!response.ok) {
throw new Error(`Failed to fetch state: ${response.status}`);
}
const data: RawStateResponse = await response.json();
-
+
if (data.topology) {
this.topologyData = transformTopology(data.topology, data.nodeProfiles);
}
@@ -804,27 +882,31 @@ class AppStore {
}
this.lastUpdate = Date.now();
} catch (error) {
- console.error('Error fetching state:', error);
+ console.error("Error fetching state:", error);
}
}
async fetchPlacementPreviews(modelId: string, showLoading = true) {
if (!modelId) return;
-
+
if (showLoading) {
this.isLoadingPreviews = true;
}
this.selectedPreviewModelId = modelId;
-
+
try {
- const response = await fetch(`/instance/previews?model_id=${encodeURIComponent(modelId)}`);
+ const response = await fetch(
+ `/instance/previews?model_id=${encodeURIComponent(modelId)}`,
+ );
if (!response.ok) {
- throw new Error(`Failed to fetch placement previews: ${response.status}`);
+ throw new Error(
+ `Failed to fetch placement previews: ${response.status}`,
+ );
}
const data: PlacementPreviewResponse = await response.json();
this.placementPreviews = data.previews;
} catch (error) {
- console.error('Error fetching placement previews:', error);
+ console.error("Error fetching placement previews:", error);
this.placementPreviews = [];
} finally {
if (showLoading) {
@@ -832,14 +914,14 @@ class AppStore {
}
}
}
-
+
startPreviewsPolling(modelId: string) {
// Stop any existing preview polling
this.stopPreviewsPolling();
-
+
// Fetch immediately
this.fetchPlacementPreviews(modelId);
-
+
// Then poll every 15 seconds (don't show loading spinner for subsequent fetches)
this.previewsInterval = setInterval(() => {
if (this.selectedPreviewModelId) {
@@ -847,14 +929,14 @@ class AppStore {
}
}, 15000);
}
-
+
stopPreviewsPolling() {
if (this.previewsInterval) {
clearInterval(this.previewsInterval);
this.previewsInterval = null;
}
}
-
+
selectPreviewModel(modelId: string | null) {
if (modelId) {
this.startPreviewsPolling(modelId);
@@ -885,12 +967,12 @@ class AppStore {
/**
* Add a message to the conversation
*/
- addMessage(role: 'user' | 'assistant', content: string) {
+ addMessage(role: "user" | "assistant", content: string) {
const message: Message = {
id: generateUUID(),
role,
content,
- timestamp: Date.now()
+ timestamp: Date.now(),
};
this.messages.push(message);
return message;
@@ -900,9 +982,9 @@ class AppStore {
* Delete a message and all subsequent messages
*/
deleteMessage(messageId: string) {
- const messageIndex = this.messages.findIndex(m => m.id === messageId);
+ const messageIndex = this.messages.findIndex((m) => m.id === messageId);
if (messageIndex === -1) return;
-
+
// Remove this message and all subsequent messages
this.messages = this.messages.slice(0, messageIndex);
this.updateActiveConversation();
@@ -912,9 +994,9 @@ class AppStore {
* Edit a user message content (does not regenerate response)
*/
editMessage(messageId: string, newContent: string) {
- const message = this.messages.find(m => m.id === messageId);
+ const message = this.messages.find((m) => m.id === messageId);
if (!message) return;
-
+
message.content = newContent;
message.timestamp = Date.now();
this.updateActiveConversation();
@@ -923,20 +1005,23 @@ class AppStore {
/**
* Edit a user message and regenerate the response
*/
- async editAndRegenerate(messageId: string, newContent: string): Promise<void> {
- const messageIndex = this.messages.findIndex(m => m.id === messageId);
+ async editAndRegenerate(
+ messageId: string,
+ newContent: string,
+ ): Promise<void> {
+ const messageIndex = this.messages.findIndex((m) => m.id === messageId);
if (messageIndex === -1) return;
-
+
const message = this.messages[messageIndex];
- if (message.role !== 'user') return;
-
+ if (message.role !== "user") return;
+
// Update the message content
message.content = newContent;
message.timestamp = Date.now();
-
+
// Remove all messages after this one (including the assistant response)
this.messages = this.messages.slice(0, messageIndex + 1);
-
+
// Regenerate the response
await this.regenerateLastResponse();
}
@@ -946,126 +1031,142 @@ class AppStore {
*/
async regenerateLastResponse(): Promise<void> {
if (this.isLoading) return;
-
+
// Find the last user message
let lastUserIndex = -1;
for (let i = this.messages.length - 1; i >= 0; i--) {
- if (this.messages[i].role === 'user') {
+ if (this.messages[i].role === "user") {
lastUserIndex = i;
break;
}
}
-
+
if (lastUserIndex === -1) return;
-
+
// Remove any messages after the user message
this.messages = this.messages.slice(0, lastUserIndex + 1);
-
+
// Resend the message to get a new response
this.isLoading = true;
- this.currentResponse = '';
-
+ this.currentResponse = "";
+
// Create placeholder for assistant message
- const assistantMessage = this.addMessage('assistant', '');
-
+ const assistantMessage = this.addMessage("assistant", "");
+
try {
const systemPrompt = {
- role: 'system' as const,
- content: 'You are a helpful AI assistant. Respond directly and concisely. Do not show your reasoning or thought process.'
+ role: "system" as const,
+ content:
+ "You are a helpful AI assistant. Respond directly and concisely. Do not show your reasoning or thought process.",
};
-
+
const apiMessages = [
systemPrompt,
...this.messages.slice(0, -1).map((m) => {
return { role: m.role, content: m.content };
- })
+ }),
];
-
+
// Determine which model to use
let modelToUse = this.selectedChatModel;
if (!modelToUse) {
const firstInstanceKey = Object.keys(this.instances)[0];
if (firstInstanceKey) {
- const instance = this.instances[firstInstanceKey] as Record<string, unknown> | undefined;
+ const instance = this.instances[firstInstanceKey] as
+ | Record<string, unknown>
+ | undefined;
if (instance) {
const keys = Object.keys(instance);
if (keys.length === 1) {
- const inst = instance[keys[0]] as { shardAssignments?: { modelId?: string } } | undefined;
- modelToUse = inst?.shardAssignments?.modelId || '';
+ const inst = instance[keys[0]] as
+ | { shardAssignments?: { modelId?: string } }
+ | undefined;
+ modelToUse = inst?.shardAssignments?.modelId || "";
}
}
}
}
-
+
if (!modelToUse) {
- const idx = this.messages.findIndex(m => m.id === assistantMessage.id);
+ const idx = this.messages.findIndex(
+ (m) => m.id === assistantMessage.id,
+ );
if (idx !== -1) {
- this.messages[idx].content = 'Error: No model available. Please launch an instance first.';
+ this.messages[idx].content =
+ "Error: No model available. Please launch an instance first.";
}
this.isLoading = false;
this.updateActiveConversation();
return;
}
-
- const response = await fetch('/v1/chat/completions', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
+
+ const response = await fetch("/v1/chat/completions", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: modelToUse,
messages: apiMessages,
- stream: true
- })
+ stream: true,
+ }),
});
-
+
if (!response.ok) {
const errorText = await response.text();
- const idx = this.messages.findIndex(m => m.id === assistantMessage.id);
+ const idx = this.messages.findIndex(
+ (m) => m.id === assistantMessage.id,
+ );
if (idx !== -1) {
- this.messages[idx].content = `Error: ${response.status} - ${errorText}`;
+ this.messages[idx].content =
+ `Error: ${response.status} - ${errorText}`;
}
this.isLoading = false;
this.updateActiveConversation();
return;
}
-
+
const reader = response.body?.getReader();
if (!reader) {
- const idx = this.messages.findIndex(m => m.id === assistantMessage.id);
+ const idx = this.messages.findIndex(
+ (m) => m.id === assistantMessage.id,
+ );
if (idx !== -1) {
- this.messages[idx].content = 'Error: No response stream available';
+ this.messages[idx].content = "Error: No response stream available";
}
this.isLoading = false;
this.updateActiveConversation();
return;
}
-
+
const decoder = new TextDecoder();
- let fullContent = '';
- let partialLine = '';
-
+ let fullContent = "";
+ let partialLine = "";
+
while (true) {
const { done, value } = await reader.read();
if (done) break;
-
+
const chunk = decoder.decode(value, { stream: true });
- const lines = (partialLine + chunk).split('\n');
- partialLine = lines.pop() || '';
-
+ const lines = (partialLine + chunk).split("\n");
+ partialLine = lines.pop() || "";
+
for (const line of lines) {
const trimmed = line.trim();
- if (!trimmed || trimmed === 'data: [DONE]') continue;
-
- if (trimmed.startsWith('data: ')) {
+ if (!trimmed || trimmed === "data: [DONE]") continue;
+
+ if (trimmed.startsWith("data: ")) {
try {
const json = JSON.parse(trimmed.slice(6));
const delta = json.choices?.[0]?.delta?.content;
if (delta) {
fullContent += delta;
- const { displayContent, thinkingContent } = this.stripThinkingTags(fullContent);
+ const { displayContent, thinkingContent } =
+ this.stripThinkingTags(fullContent);
this.currentResponse = displayContent;
-
+
// Update the assistant message in place (triggers Svelte reactivity)
- const idx = this.messages.findIndex(m => m.id === assistantMessage.id);
+ const idx = this.messages.findIndex(
+ (m) => m.id === assistantMessage.id,
+ );
if (idx !== -1) {
this.messages[idx].content = displayContent;
this.messages[idx].thinking = thinkingContent || undefined;
@@ -1078,25 +1179,26 @@ class AppStore {
}
}
}
-
+
// Final cleanup of the message
- const { displayContent, thinkingContent } = this.stripThinkingTags(fullContent);
- const idx = this.messages.findIndex(m => m.id === assistantMessage.id);
+ const { displayContent, thinkingContent } =
+ this.stripThinkingTags(fullContent);
+ const idx = this.messages.findIndex((m) => m.id === assistantMessage.id);
if (idx !== -1) {
this.messages[idx].content = displayContent;
this.messages[idx].thinking = thinkingContent || undefined;
}
this.persistActiveConversation();
-
} catch (error) {
- const idx = this.messages.findIndex(m => m.id === assistantMessage.id);
+ const idx = this.messages.findIndex((m) => m.id === assistantMessage.id);
if (idx !== -1) {
- this.messages[idx].content = `Error: ${error instanceof Error ? error.message : 'Unknown error'}`;
+ this.messages[idx].content =
+ `Error: ${error instanceof Error ? error.message : "Unknown error"}`;
}
this.persistActiveConversation();
} finally {
this.isLoading = false;
- this.currentResponse = '';
+ this.currentResponse = "";
this.updateActiveConversation();
}
}
@@ -1104,7 +1206,7 @@ class AppStore {
/**
* Selected model for chat (can be set by the UI)
*/
- selectedChatModel = $state('');
+ selectedChatModel = $state("");
/**
* Set the model to use for chat
@@ -1120,7 +1222,10 @@ class AppStore {
* Strip thinking tags from content for display.
* Handles both complete <think>...</think> blocks and in-progress <think>... blocks during streaming.
*/
- private stripThinkingTags(content: string): { displayContent: string; thinkingContent: string } {
+ private stripThinkingTags(content: string): {
+ displayContent: string;
+ thinkingContent: string;
+ } {
const extracted: string[] = [];
let displayContent = content;
@@ -1131,10 +1236,10 @@ class AppStore {
const inner = match[1]?.trim();
if (inner) extracted.push(inner);
}
- displayContent = displayContent.replace(completeBlockRegex, '');
+ displayContent = displayContent.replace(completeBlockRegex, "");
// Handle in-progress thinking block (has <think> but no closing </think> yet)
- const openTagIndex = displayContent.lastIndexOf('<think>');
+ const openTagIndex = displayContent.lastIndexOf("<think>");
if (openTagIndex !== -1) {
const inProgressThinking = displayContent.slice(openTagIndex + 7).trim();
if (inProgressThinking) {
@@ -1143,104 +1248,118 @@ class AppStore {
displayContent = displayContent.slice(0, openTagIndex);
}
- return { displayContent: displayContent.trim(), thinkingContent: extracted.join('\n\n') };
+ return {
+ displayContent: displayContent.trim(),
+ thinkingContent: extracted.join("\n\n"),
+ };
}
/**
* Send a message to the LLM and stream the response
*/
- async sendMessage(content: string, files?: { id: string; name: string; type: string; textContent?: string; preview?: string }[]): Promise<void> {
- if ((!content.trim() && (!files || files.length === 0)) || this.isLoading) return;
-
+ async sendMessage(
+ content: string,
+ files?: {
+ id: string;
+ name: string;
+ type: string;
+ textContent?: string;
+ preview?: string;
+ }[],
+ ): Promise<void> {
+ if ((!content.trim() && (!files || files.length === 0)) || this.isLoading)
+ return;
+
if (!this.hasStartedChat) {
this.startChat();
}
this.isLoading = true;
- this.currentResponse = '';
+ this.currentResponse = "";
this.ttftMs = null;
this.tps = null;
this.totalTokens = 0;
-
+
// Build attachments from files
const attachments: MessageAttachment[] = [];
- let fileContext = '';
-
+ let fileContext = "";
+
if (files && files.length > 0) {
for (const file of files) {
- const isImage = file.type.startsWith('image/');
-
+ const isImage = file.type.startsWith("image/");
+
if (isImage && file.preview) {
attachments.push({
- type: 'image',
+ type: "image",
name: file.name,
preview: file.preview,
- mimeType: file.type
+ mimeType: file.type,
});
} else if (file.textContent) {
attachments.push({
- type: 'text',
+ type: "text",
name: file.name,
content: file.textContent,
- mimeType: file.type
+ mimeType: file.type,
});
// Add text file content to the message context
fileContext += `\n\n[File: ${file.name}]\n\`\`\`\n${file.textContent}\n\`\`\``;
} else {
attachments.push({
- type: 'file',
+ type: "file",
name: file.name,
- mimeType: file.type
+ mimeType: file.type,
});
}
}
}
-
+
// Combine content with file context
const fullContent = content + fileContext;
-
+
// Add user message with attachments
const userMessage: Message = {
id: generateUUID(),
- role: 'user',
+ role: "user",
content: content, // Store original content for display
timestamp: Date.now(),
- attachments: attachments.length > 0 ? attachments : undefined
+ attachments: attachments.length > 0 ? attachments : undefined,
};
this.messages.push(userMessage);
-
+
// Create placeholder for assistant message
- const assistantMessage = this.addMessage('assistant', '');
+ const assistantMessage = this.addMessage("assistant", "");
this.updateActiveConversation();
-
+
try {
// Build the messages array for the API with system prompt
const systemPrompt = {
- role: 'system' as const,
- content: 'You are a helpful AI assistant. Respond directly and concisely. Do not show your reasoning or thought process. When files are shared with you, analyze them and respond helpfully.'
+ role: "system" as const,
+ content:
+ "You are a helpful AI assistant. Respond directly and concisely. Do not show your reasoning or thought process. When files are shared with you, analyze them and respond helpfully.",
};
-
+
// Build API messages - include file content for text files
const apiMessages = [
systemPrompt,
...this.messages.slice(0, -1).map((m) => {
// Build content including any text file attachments
let msgContent = m.content;
-
+
// Add text attachments as context
if (m.attachments) {
for (const attachment of m.attachments) {
- if (attachment.type === 'text' && attachment.content) {
+ if (attachment.type === "text" && attachment.content) {
msgContent += `\n\n[File: ${attachment.name}]\n\`\`\`\n${attachment.content}\n\`\`\``;
}
}
}
-
+
return {
role: m.role,
- content: msgContent
+ content: msgContent,
};
- })
+ }),
];
// Determine the model to use - prefer selectedChatModel, otherwise try to get from instances
@@ -1248,10 +1367,14 @@ class AppStore {
if (!modelToUse) {
// Try to get model from first running instance
for (const [, instanceWrapper] of Object.entries(this.instances)) {
- if (instanceWrapper && typeof instanceWrapper === 'object') {
- const keys = Object.keys(instanceWrapper as Record<string, unknown>);
+ if (instanceWrapper && typeof instanceWrapper === "object") {
+ const keys = Object.keys(
+ instanceWrapper as Record<string, unknown>,
+ );
if (keys.length === 1) {
- const instance = (instanceWrapper as Record<string, unknown>)[keys[0]] as { shardAssignments?: { modelId?: string } };
+ const instance = (instanceWrapper as Record<string, unknown>)[
+ keys[0]
+ ] as { shardAssignments?: { modelId?: string } };
if (instance?.shardAssignments?.modelId) {
modelToUse = instance.shardAssignments.modelId;
break;
@@ -1262,7 +1385,9 @@ class AppStore {
}
if (!modelToUse) {
- throw new Error('No model selected and no running instances available. Please launch an instance first.');
+ throw new Error(
+ "No model selected and no running instances available. Please launch an instance first.",
+ );
}
const conversationModelInfo = this.buildConversationModelInfo(modelToUse);
@@ -1272,18 +1397,18 @@ class AppStore {
const requestStartTime = performance.now();
let firstTokenTime: number | null = null;
let tokenCount = 0;
-
- const response = await fetch('/v1/chat/completions', {
- method: 'POST',
+
+ const response = await fetch("/v1/chat/completions", {
+ method: "POST",
headers: {
- 'Content-Type': 'application/json'
+ "Content-Type": "application/json",
},
body: JSON.stringify({
model: modelToUse,
messages: apiMessages,
temperature: 0.7,
- stream: true
- })
+ stream: true,
+ }),
});
if (!response.ok) {
@@ -1293,30 +1418,30 @@ class AppStore {
const reader = response.body?.getReader();
if (!reader) {
- throw new Error('No response body');
+ throw new Error("No response body");
}
const decoder = new TextDecoder();
- let fullContent = '';
- let buffer = '';
+ let fullContent = "";
+ let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
-
+
// Process complete lines
- const lines = buffer.split('\n');
- buffer = lines.pop() || ''; // Keep incomplete line in buffer
+ const lines = buffer.split("\n");
+ buffer = lines.pop() || ""; // Keep incomplete line in buffer
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
-
- if (trimmed.startsWith('data: ')) {
+
+ if (trimmed.startsWith("data: ")) {
const data = trimmed.slice(6);
- if (data === '[DONE]') continue;
+ if (data === "[DONE]") continue;
try {
const parsed = JSON.parse(data);
@@ -1327,25 +1452,28 @@ class AppStore {
firstTokenTime = performance.now();
this.ttftMs = firstTokenTime - requestStartTime;
}
-
+
// Count tokens (each SSE chunk is typically one token)
tokenCount += 1;
this.totalTokens = tokenCount;
-
+
// Update real-time TPS during streaming
if (firstTokenTime !== null && tokenCount > 1) {
const elapsed = performance.now() - firstTokenTime;
this.tps = (tokenCount / elapsed) * 1000;
}
-
+
fullContent += tokenContent;
-
+
// Strip thinking tags for display and extract thinking content
- const { displayContent, thinkingContent } = this.stripThinkingTags(fullContent);
+ const { displayContent, thinkingContent } =
+ this.stripThinkingTags(fullContent);
this.currentResponse = displayContent;
-
+
// Update the assistant message in place
- const idx = this.messages.findIndex(m => m.id === assistantMessage.id);
+ const idx = this.messages.findIndex(
+ (m) => m.id === assistantMessage.id,
+ );
if (idx !== -1) {
this.messages[idx].content = displayContent;
this.messages[idx].thinking = thinkingContent || undefined;
@@ -1358,11 +1486,11 @@ class AppStore {
}
}
}
-
+
// Process any remaining buffer
if (buffer.trim()) {
const trimmed = buffer.trim();
- if (trimmed.startsWith('data: ') && trimmed.slice(6) !== '[DONE]') {
+ if (trimmed.startsWith("data: ") && trimmed.slice(6) !== "[DONE]") {
try {
const parsed = JSON.parse(trimmed.slice(6));
const tokenContent = parsed.choices?.[0]?.delta?.content;
@@ -1375,16 +1503,17 @@ class AppStore {
}
}
}
-
+
// Calculate final TPS
if (firstTokenTime !== null && tokenCount > 1) {
const totalGenerationTime = performance.now() - firstTokenTime;
this.tps = (tokenCount / totalGenerationTime) * 1000; // tokens per second
}
-
+
// Final cleanup of the message
- const { displayContent, thinkingContent } = this.stripThinkingTags(fullContent);
- const idx = this.messages.findIndex(m => m.id === assistantMessage.id);
+ const { displayContent, thinkingContent } =
+ this.stripThinkingTags(fullContent);
+ const idx = this.messages.findIndex((m) => m.id === assistantMessage.id);
if (idx !== -1) {
this.messages[idx].content = displayContent;
this.messages[idx].thinking = thinkingContent || undefined;
@@ -1397,18 +1526,18 @@ class AppStore {
}
}
this.persistActiveConversation();
-
} catch (error) {
- console.error('Error sending message:', error);
+ console.error("Error sending message:", error);
// Update the assistant message with error
- const idx = this.messages.findIndex(m => m.id === assistantMessage.id);
+ const idx = this.messages.findIndex((m) => m.id === assistantMessage.id);
if (idx !== -1) {
- this.messages[idx].content = `Error: ${error instanceof Error ? error.message : 'Failed to get response'}`;
+ this.messages[idx].content =
+ `Error: ${error instanceof Error ? error.message : "Failed to get response"}`;
}
this.persistActiveConversation();
} finally {
this.isLoading = false;
- this.currentResponse = '';
+ this.currentResponse = "";
this.updateActiveConversation();
}
}
@@ -1421,7 +1550,7 @@ class AppStore {
this.messages = [];
this.hasStartedChat = false;
this.isTopologyMinimized = false;
- this.currentResponse = '';
+ this.currentResponse = "";
// Clear performance stats
this.ttftMs = null;
this.tps = null;
@@ -1432,7 +1561,9 @@ class AppStore {
*/
getActiveConversation(): Conversation | null {
if (!this.activeConversationId) return null;
- return this.conversations.find(c => c.id === this.activeConversationId) || null;
+ return (
+ this.conversations.find((c) => c.id === this.activeConversationId) || null
+ );
}
}
@@ -1462,33 +1593,53 @@ export const chatSidebarVisible = () => appStore.getChatSidebarVisible();
// Actions
export const startChat = () => appStore.startChat();
-export const sendMessage = (content: string, files?: { id: string; name: string; type: string; textContent?: string; preview?: string }[]) => appStore.sendMessage(content, files);
+export const sendMessage = (
+ content: string,
+ files?: {
+ id: string;
+ name: string;
+ type: string;
+ textContent?: string;
+ preview?: string;
+ }[],
+) => appStore.sendMessage(content, files);
export const clearChat = () => appStore.clearChat();
-export const setSelectedChatModel = (modelId: string) => appStore.setSelectedModel(modelId);
-export const selectPreviewModel = (modelId: string | null) => appStore.selectPreviewModel(modelId);
-export const deleteMessage = (messageId: string) => appStore.deleteMessage(messageId);
-export const editMessage = (messageId: string, newContent: string) => appStore.editMessage(messageId, newContent);
-export const editAndRegenerate = (messageId: string, newContent: string) => appStore.editAndRegenerate(messageId, newContent);
+export const setSelectedChatModel = (modelId: string) =>
+ appStore.setSelectedModel(modelId);
+export const selectPreviewModel = (modelId: string | null) =>
+ appStore.selectPreviewModel(modelId);
+export const deleteMessage = (messageId: string) =>
+ appStore.deleteMessage(messageId);
+export const editMessage = (messageId: string, newContent: string) =>
+ appStore.editMessage(messageId, newContent);
+export const editAndRegenerate = (messageId: string, newContent: string) =>
+ appStore.editAndRegenerate(messageId, newContent);
export const regenerateLastResponse = () => appStore.regenerateLastResponse();
// Conversation actions
export const conversations = () => appStore.conversations;
export const activeConversationId = () => appStore.activeConversationId;
-export const createConversation = (name?: string) => appStore.createConversation(name);
+export const createConversation = (name?: string) =>
+ appStore.createConversation(name);
export const loadConversation = (id: string) => appStore.loadConversation(id);
-export const deleteConversation = (id: string) => appStore.deleteConversation(id);
+export const deleteConversation = (id: string) =>
+ appStore.deleteConversation(id);
export const deleteAllConversations = () => appStore.deleteAllConversations();
-export const renameConversation = (id: string, name: string) => appStore.renameConversation(id, name);
+export const renameConversation = (id: string, name: string) =>
+ appStore.renameConversation(id, name);
export const getActiveConversation = () => appStore.getActiveConversation();
// Sidebar actions
export const isSidebarOpen = () => appStore.isSidebarOpen;
export const toggleSidebar = () => appStore.toggleSidebar();
export const toggleDebugMode = () => appStore.toggleDebugMode();
-export const setDebugMode = (enabled: boolean) => appStore.setDebugMode(enabled);
+export const setDebugMode = (enabled: boolean) =>
+ appStore.setDebugMode(enabled);
export const toggleTopologyOnlyMode = () => appStore.toggleTopologyOnlyMode();
-export const setTopologyOnlyMode = (enabled: boolean) => appStore.setTopologyOnlyMode(enabled);
-export const toggleChatSidebarVisible = () => appStore.toggleChatSidebarVisible();
-export const setChatSidebarVisible = (visible: boolean) => appStore.setChatSidebarVisible(visible);
+export const setTopologyOnlyMode = (enabled: boolean) =>
+ appStore.setTopologyOnlyMode(enabled);
+export const toggleChatSidebarVisible = () =>
+ appStore.toggleChatSidebarVisible();
+export const setChatSidebarVisible = (visible: boolean) =>
+ appStore.setChatSidebarVisible(visible);
export const refreshState = () => appStore.fetchState();
-
diff --git a/dashboard/src/lib/types/files.ts b/dashboard/src/lib/types/files.ts
index b92e269e..ecd9a41b 100644
--- a/dashboard/src/lib/types/files.ts
+++ b/dashboard/src/lib/types/files.ts
@@ -13,55 +13,124 @@ export interface ChatUploadedFile {
}
export interface ChatAttachment {
- type: 'image' | 'text' | 'pdf' | 'audio';
+ type: "image" | "text" | "pdf" | "audio";
name: string;
content?: string;
base64Url?: string;
mimeType?: string;
}
-export type FileCategory = 'image' | 'text' | 'pdf' | 'audio' | 'unknown';
+export type FileCategory = "image" | "text" | "pdf" | "audio" | "unknown";
-export const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'];
-export const IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'];
+export const IMAGE_EXTENSIONS = [
+ ".jpg",
+ ".jpeg",
+ ".png",
+ ".gif",
+ ".webp",
+ ".svg",
+];
+export const IMAGE_MIME_TYPES = [
+ "image/jpeg",
+ "image/png",
+ "image/gif",
+ "image/webp",
+ "image/svg+xml",
+];
export const TEXT_EXTENSIONS = [
- '.txt', '.md', '.json', '.xml', '.yaml', '.yml', '.csv', '.log',
- '.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.cpp', '.c', '.h',
- '.css', '.html', '.htm', '.sql', '.sh', '.bat', '.rs', '.go',
- '.rb', '.php', '.swift', '.kt', '.scala', '.r', '.dart', '.vue', '.svelte'
+ ".txt",
+ ".md",
+ ".json",
+ ".xml",
+ ".yaml",
+ ".yml",
+ ".csv",
+ ".log",
+ ".js",
+ ".ts",
+ ".jsx",
+ ".tsx",
+ ".py",
+ ".java",
+ ".cpp",
+ ".c",
+ ".h",
+ ".css",
+ ".html",
+ ".htm",
+ ".sql",
+ ".sh",
+ ".bat",
+ ".rs",
+ ".go",
+ ".rb",
+ ".php",
+ ".swift",
+ ".kt",
+ ".scala",
+ ".r",
+ ".dart",
+ ".vue",
+ ".svelte",
];
export const TEXT_MIME_TYPES = [
- 'text/plain', 'text/markdown', 'text/csv', 'text/html', 'text/css',
- 'application/json', 'application/xml', 'text/xml', 'application/javascript',
- 'text/javascript', 'application/typescript'
+ "text/plain",
+ "text/markdown",
+ "text/csv",
+ "text/html",
+ "text/css",
+ "application/json",
+ "application/xml",
+ "text/xml",
+ "application/javascript",
+ "text/javascript",
+ "application/typescript",
];
-export const PDF_EXTENSIONS = ['.pdf'];
-export const PDF_MIME_TYPES = ['application/pdf'];
+export const PDF_EXTENSIONS = [".pdf"];
+export const PDF_MIME_TYPES = ["application/pdf"];
-export const AUDIO_EXTENSIONS = ['.mp3', '.wav', '.ogg', '.m4a'];
-export const AUDIO_MIME_TYPES = ['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/mp4'];
+export const AUDIO_EXTENSIONS = [".mp3", ".wav", ".ogg", ".m4a"];
+export const AUDIO_MIME_TYPES = [
+ "audio/mpeg",
+ "audio/wav",
+ "audio/ogg",
+ "audio/mp4",
+];
/**
* Get file category based on MIME type and extension
*/
-export function getFileCategory(mimeType: string, fileName: string): FileCategory {
- const extension = fileName.toLowerCase().slice(fileName.lastIndexOf('.'));
-
- if (IMAGE_MIME_TYPES.includes(mimeType) || IMAGE_EXTENSIONS.includes(extension)) {
- return 'image';
+export function getFileCategory(
+ mimeType: string,
+ fileName: string,
+): FileCategory {
+ const extension = fileName.toLowerCase().slice(fileName.lastIndexOf("."));
+
+ if (
+ IMAGE_MIME_TYPES.includes(mimeType) ||
+ IMAGE_EXTENSIONS.includes(extension)
+ ) {
+ return "image";
}
if (PDF_MIME_TYPES.includes(mimeType) || PDF_EXTENSIONS.includes(extension)) {
- return 'pdf';
+ return "pdf";
}
- if (AUDIO_MIME_TYPES.includes(mimeType) || AUDIO_EXTENSIONS.includes(extension)) {
- return 'audio';
+ if (
+ AUDIO_MIME_TYPES.includes(mimeType) ||
+ AUDIO_EXTENSIONS.includes(extension)
+ ) {
+ return "audio";
}
- if (TEXT_MIME_TYPES.includes(mimeType) || TEXT_EXTENSIONS.includes(extension) || mimeType.startsWith('text/')) {
- return 'text';
+ if (
+ TEXT_MIME_TYPES.includes(mimeType) ||
+ TEXT_EXTENSIONS.includes(extension) ||
+ mimeType.startsWith("text/")
+ ) {
+ return "text";
}
- return 'unknown';
+ return "unknown";
}
/**
@@ -69,36 +138,36 @@ export function getFileCategory(mimeType: string, fileName: string): FileCategor
*/
export function getAcceptString(categories: FileCategory[]): string {
const accepts: string[] = [];
-
+
for (const category of categories) {
switch (category) {
- case 'image':
+ case "image":
accepts.push(...IMAGE_EXTENSIONS, ...IMAGE_MIME_TYPES);
break;
- case 'text':
+ case "text":
accepts.push(...TEXT_EXTENSIONS, ...TEXT_MIME_TYPES);
break;
- case 'pdf':
+ case "pdf":
accepts.push(...PDF_EXTENSIONS, ...PDF_MIME_TYPES);
break;
- case 'audio':
+ case "audio":
accepts.push(...AUDIO_EXTENSIONS, ...AUDIO_MIME_TYPES);
break;
}
}
-
- return accepts.join(',');
+
+ return accepts.join(",");
}
/**
* Format file size for display
*/
export function formatFileSize(bytes: number): string {
- if (bytes === 0) return '0 B';
+ if (bytes === 0) return "0 B";
const k = 1024;
- const sizes = ['B', 'KB', 'MB', 'GB'];
+ const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
- return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
}
/**
@@ -128,42 +197,44 @@ export function readFileAsText(file: File): Promise<string> {
/**
* Process uploaded files into ChatUploadedFile format
*/
-export async function processUploadedFiles(files: File[]): Promise<ChatUploadedFile[]> {
+export async function processUploadedFiles(
+ files: File[],
+): Promise<ChatUploadedFile[]> {
const results: ChatUploadedFile[] = [];
-
+
for (const file of files) {
- const id = Date.now().toString() + Math.random().toString(36).substring(2, 9);
+ const id =
+ Date.now().toString() + Math.random().toString(36).substring(2, 9);
const category = getFileCategory(file.type, file.name);
-
+
const base: ChatUploadedFile = {
id,
name: file.name,
size: file.size,
type: file.type,
- file
+ file,
};
-
+
try {
- if (category === 'image') {
+ if (category === "image") {
const preview = await readFileAsDataURL(file);
results.push({ ...base, preview });
- } else if (category === 'text' || category === 'unknown') {
+ } else if (category === "text" || category === "unknown") {
const textContent = await readFileAsText(file);
results.push({ ...base, textContent });
- } else if (category === 'pdf') {
+ } else if (category === "pdf") {
results.push(base);
- } else if (category === 'audio') {
+ } else if (category === "audio") {
const preview = await readFileAsDataURL(file);
results.push({ ...base, preview });
} else {
results.push(base);
}
} catch (error) {
- console.error('Error processing file:', file.name, error);
+ console.error("Error processing file:", file.name, error);
results.push(base);
}
}
-
+
return results;
}
-
diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts
index 1c0a2471..466c7c88 100644
--- a/dashboard/vite.config.ts
+++ b/dashboard/vite.config.ts
@@ -1,16 +1,15 @@
-import tailwindcss from '@tailwindcss/vite';
-import { sveltekit } from '@sveltejs/kit/vite';
-import { defineConfig } from 'vite';
+import tailwindcss from "@tailwindcss/vite";
+import { sveltekit } from "@sveltejs/kit/vite";
+import { defineConfig } from "vite";
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
server: {
proxy: {
- '/v1': 'http://localhost:52415',
- '/state': 'http://localhost:52415',
- '/models': 'http://localhost:52415',
- '/instance': 'http://localhost:52415'
- }
- }
+ "/v1": "http://localhost:52415",
+ "/state": "http://localhost:52415",
+ "/models": "http://localhost:52415",
+ "/instance": "http://localhost:52415",
+ },
+ },
});
-
diff --git a/flake.nix b/flake.nix
index 94d43688..e9ae54ca 100644
--- a/flake.nix
+++ b/flake.nix
@@ -52,6 +52,10 @@
enable = true;
package = (fenixToolchain system).rustfmt;
};
+ prettier = {
+ enable = true;
+ includes = [ "*.ts" ];
+ };
swift-format.enable = true;
};
};
← 55463a98 fmt: add swift formatting
·
back to Exo
·
Display RDMA debug info in macOS app. (#1072) ca680185 →