Initial ForgeLM import
This commit is contained in:
commit
a392b9dc60
4296 files changed
+1212675
No files matched your search
@@ -0,0 +1,541 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { bootstrapRuntime, getRuntimeSnapshot, saveRuntimeConfig, scanRuntimeModels, startRuntime, stopRuntime } from "./api";
|
||||
import {
|
||||
createDefaultRuntimeSnapshot,
|
||||
createRuntimeLifecycleResult,
|
||||
getRuntimeApplyImpact,
|
||||
getRuntimeConnection,
|
||||
getRuntimeHealthSummary,
|
||||
getRuntimeWorkspaceSummary,
|
||||
validateModelFile,
|
||||
validateModelsDirectory,
|
||||
validateSourceMode,
|
||||
} from "./runtime-utils";
|
||||
import { createRuntimeDraftFieldState, createRuntimeDraftState } from "./types";
|
||||
import type {
|
||||
RuntimeApplyImpact,
|
||||
RuntimeConfig,
|
||||
RuntimeDraftFieldKey,
|
||||
RuntimeDraftFieldMeta,
|
||||
RuntimeDraftState,
|
||||
RuntimeLifecycleCommand,
|
||||
RuntimeLifecycleResult,
|
||||
RuntimeModelFile,
|
||||
RuntimeSnapshot,
|
||||
RuntimeWorkspaceHealth,
|
||||
RuntimeWorkspaceSummary,
|
||||
} from "./types";
|
||||
|
||||
function areConfigsEqual(left: RuntimeConfig, right: RuntimeConfig): boolean {
|
||||
return JSON.stringify(left) === JSON.stringify(right);
|
||||
}
|
||||
|
||||
function cloneConfig(config: RuntimeConfig): RuntimeConfig {
|
||||
return {
|
||||
...config,
|
||||
extra_model_paths: [...config.extra_model_paths],
|
||||
remote_presets: config.remote_presets.map((preset) => ({ ...preset })),
|
||||
};
|
||||
}
|
||||
|
||||
function applyOperationalSnapshot(current: RuntimeSnapshot, next: RuntimeSnapshot): RuntimeSnapshot {
|
||||
return {
|
||||
...current,
|
||||
status: next.status,
|
||||
ready: next.ready,
|
||||
process_running: next.process_running,
|
||||
resolved_url: next.resolved_url,
|
||||
binary_path: next.binary_path,
|
||||
last_error: next.last_error,
|
||||
active_command: next.active_command,
|
||||
last_command_result: next.last_command_result,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSnapshot(snapshot: RuntimeSnapshot): RuntimeSnapshot {
|
||||
return {
|
||||
...createDefaultRuntimeSnapshot(),
|
||||
...snapshot,
|
||||
config: {
|
||||
...createDefaultRuntimeSnapshot().config,
|
||||
...snapshot.config,
|
||||
},
|
||||
last_command_result:
|
||||
snapshot.last_command_result ?? createRuntimeLifecycleResult(snapshot.active_command ?? "idle"),
|
||||
active_command: snapshot.active_command ?? "idle",
|
||||
};
|
||||
}
|
||||
|
||||
function reconcileDraftState(current: RuntimeDraftState, next: RuntimeSnapshot): RuntimeDraftState {
|
||||
const normalizedNext = normalizeSnapshot(next);
|
||||
|
||||
if (current.isDirty) {
|
||||
return {
|
||||
...current,
|
||||
liveSnapshot: applyOperationalSnapshot(current.liveSnapshot, normalizedNext),
|
||||
};
|
||||
}
|
||||
|
||||
// Run source-mode validation when reconciling to baseline
|
||||
const validation = validateSourceMode(normalizedNext.config);
|
||||
const updatedFields = createRuntimeDraftFieldState();
|
||||
for (const [fieldName, fieldValidation] of Object.entries(validation.fieldValidations)) {
|
||||
updatedFields[fieldName as RuntimeDraftFieldKey] = {
|
||||
invalid: !fieldValidation.valid,
|
||||
pending: false,
|
||||
message: fieldValidation.message,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...current,
|
||||
liveSnapshot: normalizedNext,
|
||||
savedBaseline: cloneConfig(normalizedNext.config),
|
||||
draftConfig: cloneConfig(normalizedNext.config),
|
||||
fields: updatedFields,
|
||||
// Preserve scanned models state during reconciliation
|
||||
scannedModels: current.scannedModels,
|
||||
isScanning: current.isScanning,
|
||||
};
|
||||
}
|
||||
|
||||
function blockWhileLifecycleBusy(
|
||||
snapshot: RuntimeSnapshot,
|
||||
command: RuntimeLifecycleCommand,
|
||||
): RuntimeSnapshot | null {
|
||||
if (snapshot.active_command === "idle") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const blockedBy = snapshot.active_command;
|
||||
const message =
|
||||
blockedBy === "start"
|
||||
? "Local API start already in progress."
|
||||
: blockedBy === "stop"
|
||||
? "Local API stop already in progress."
|
||||
: blockedBy === "save"
|
||||
? "Runtime settings save already in progress."
|
||||
: "Runtime save and start already in progress.";
|
||||
|
||||
return {
|
||||
...snapshot,
|
||||
last_command_result: createRuntimeLifecycleResult(command, message, "blocked"),
|
||||
};
|
||||
}
|
||||
|
||||
function startLifecycleSnapshot(
|
||||
snapshot: RuntimeSnapshot,
|
||||
command: RuntimeLifecycleCommand,
|
||||
status: RuntimeSnapshot["status"],
|
||||
): RuntimeSnapshot {
|
||||
return {
|
||||
...snapshot,
|
||||
status,
|
||||
process_running: command === "start" ? true : snapshot.process_running,
|
||||
ready: false,
|
||||
last_error: null,
|
||||
active_command: command,
|
||||
};
|
||||
}
|
||||
|
||||
function finalizeLifecycleSnapshot(
|
||||
fallbackCommand: RuntimeLifecycleCommand,
|
||||
fallbackMessage: string,
|
||||
next: RuntimeSnapshot,
|
||||
): RuntimeSnapshot {
|
||||
const normalizedNext = normalizeSnapshot(next);
|
||||
if (normalizedNext.last_command_result.command !== "idle") {
|
||||
return normalizedNext;
|
||||
}
|
||||
|
||||
return {
|
||||
...normalizedNext,
|
||||
last_command_result: createRuntimeLifecycleResult(fallbackCommand, fallbackMessage),
|
||||
};
|
||||
}
|
||||
|
||||
export function useRuntimeManager() {
|
||||
const [draftState, setDraftState] = useState<RuntimeDraftState>(() =>
|
||||
createRuntimeDraftState(createDefaultRuntimeSnapshot()),
|
||||
);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
const next = await getRuntimeSnapshot();
|
||||
setDraftState((current) => reconcileDraftState(current, next));
|
||||
return next;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
|
||||
bootstrapRuntime()
|
||||
.then((next) => {
|
||||
if (active) {
|
||||
setDraftState((current) => reconcileDraftState(current, next));
|
||||
setLoading(false);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (active) {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
const id = window.setInterval(() => {
|
||||
refresh().catch(() => undefined);
|
||||
}, 3000);
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
window.clearInterval(id);
|
||||
};
|
||||
}, [refresh]);
|
||||
|
||||
const openDraft = useCallback(() => {
|
||||
setDraftState((current) => {
|
||||
const draftConfig = current.isDirty ? current.draftConfig : cloneConfig(current.savedBaseline);
|
||||
|
||||
// Run source-mode validation when opening draft
|
||||
const validation = validateSourceMode(draftConfig);
|
||||
|
||||
// Initialize field validations based on validation results
|
||||
const updatedFields = { ...current.fields };
|
||||
for (const [fieldName, fieldValidation] of Object.entries(validation.fieldValidations)) {
|
||||
updatedFields[fieldName as RuntimeDraftFieldKey] = {
|
||||
invalid: !fieldValidation.valid,
|
||||
pending: false,
|
||||
message: fieldValidation.message,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...current,
|
||||
isOpen: true,
|
||||
draftConfig,
|
||||
fields: updatedFields,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const closeDraft = useCallback(() => {
|
||||
setDraftState((current) => ({
|
||||
...current,
|
||||
isOpen: false,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const resetDraft = useCallback(() => {
|
||||
setDraftState((current) => {
|
||||
const resetConfig = cloneConfig(current.savedBaseline);
|
||||
|
||||
// Run source-mode validation when resetting
|
||||
const validation = validateSourceMode(resetConfig);
|
||||
|
||||
// Initialize field validations based on validation results
|
||||
const updatedFields = createRuntimeDraftFieldState();
|
||||
for (const [fieldName, fieldValidation] of Object.entries(validation.fieldValidations)) {
|
||||
updatedFields[fieldName as RuntimeDraftFieldKey] = {
|
||||
invalid: !fieldValidation.valid,
|
||||
pending: false,
|
||||
message: fieldValidation.message,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...current,
|
||||
draftConfig: resetConfig,
|
||||
isDirty: false,
|
||||
fields: updatedFields,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const updateDraft = useCallback((patch: Partial<RuntimeConfig>) => {
|
||||
setDraftState((current) => {
|
||||
const nextDraft = {
|
||||
...current.draftConfig,
|
||||
...patch,
|
||||
};
|
||||
|
||||
return {
|
||||
...current,
|
||||
isOpen: true,
|
||||
isDirty: !areConfigsEqual(nextDraft, current.savedBaseline),
|
||||
draftConfig: nextDraft,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const updateDraftField = useCallback(<K extends RuntimeDraftFieldKey>(field: K, value: RuntimeConfig[K]) => {
|
||||
setDraftState((current) => {
|
||||
const nextDraft = {
|
||||
...current.draftConfig,
|
||||
[field]: value,
|
||||
};
|
||||
|
||||
// Run source-mode validation
|
||||
const validation = validateSourceMode(nextDraft);
|
||||
|
||||
// Update field validations based on validation results
|
||||
const updatedFields = { ...current.fields };
|
||||
for (const [fieldName, fieldValidation] of Object.entries(validation.fieldValidations)) {
|
||||
updatedFields[fieldName as RuntimeDraftFieldKey] = {
|
||||
invalid: !fieldValidation.valid,
|
||||
pending: false,
|
||||
message: fieldValidation.message,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...current,
|
||||
isOpen: true,
|
||||
isDirty: !areConfigsEqual(nextDraft, current.savedBaseline),
|
||||
draftConfig: nextDraft,
|
||||
fields: updatedFields,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const updateDraftFieldMeta = useCallback(
|
||||
(field: RuntimeDraftFieldKey, patch: Partial<RuntimeDraftFieldMeta>) => {
|
||||
setDraftState((current) => ({
|
||||
...current,
|
||||
fields: {
|
||||
...current.fields,
|
||||
[field]: {
|
||||
...current.fields[field],
|
||||
invalid: false,
|
||||
pending: false,
|
||||
message: null,
|
||||
...patch,
|
||||
},
|
||||
},
|
||||
}));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Phase 4: Model scanning for validation
|
||||
const scanModels = useCallback(async (dirPath: string | null) => {
|
||||
if (!dirPath) {
|
||||
setDraftState((current) => ({
|
||||
...current,
|
||||
scannedModels: [],
|
||||
isScanning: false,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Set scanning state first
|
||||
setDraftState((current) => ({
|
||||
...current,
|
||||
isScanning: true,
|
||||
}));
|
||||
|
||||
try {
|
||||
const models = await scanRuntimeModels(dirPath);
|
||||
|
||||
// Now update with results
|
||||
setDraftState((current) => {
|
||||
const modelPathInvalid = current.draftConfig.model_path
|
||||
? !models.some((m) => m.path === current.draftConfig.model_path)
|
||||
: false;
|
||||
const modelPathMessage = current.draftConfig.model_path
|
||||
? models.some((m) => m.path === current.draftConfig.model_path)
|
||||
? null
|
||||
: "Model file not found in current models directory"
|
||||
: null;
|
||||
|
||||
return {
|
||||
...current,
|
||||
scannedModels: models,
|
||||
isScanning: false,
|
||||
// Validate model_path against scanned models
|
||||
fields: {
|
||||
...current.fields,
|
||||
model_path: {
|
||||
invalid: modelPathInvalid,
|
||||
pending: false,
|
||||
message: modelPathMessage,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
setDraftState((current) => ({
|
||||
...current,
|
||||
scannedModels: [],
|
||||
isScanning: false,
|
||||
fields: {
|
||||
...current.fields,
|
||||
models_dir: {
|
||||
invalid: true,
|
||||
pending: false,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const commitSavedSnapshot = useCallback((next: RuntimeSnapshot, keepOpen: boolean) => {
|
||||
setDraftState((current) => ({
|
||||
...current,
|
||||
liveSnapshot: next,
|
||||
savedBaseline: cloneConfig(next.config),
|
||||
draftConfig: cloneConfig(next.config),
|
||||
isDirty: false,
|
||||
isOpen: keepOpen,
|
||||
fields: createRuntimeDraftFieldState(),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const saveDraft = useCallback(async () => {
|
||||
const blockedSnapshot = blockWhileLifecycleBusy(draftState.liveSnapshot, "save");
|
||||
if (blockedSnapshot) {
|
||||
setDraftState((current) => ({ ...current, liveSnapshot: blockedSnapshot }));
|
||||
return blockedSnapshot;
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
setDraftState((current) => ({
|
||||
...current,
|
||||
liveSnapshot: startLifecycleSnapshot(current.liveSnapshot, "save", current.liveSnapshot.status),
|
||||
}));
|
||||
try {
|
||||
const next = await saveRuntimeConfig(draftState.draftConfig);
|
||||
const resolvedNext = finalizeLifecycleSnapshot("save", "Save completed.", next);
|
||||
commitSavedSnapshot(resolvedNext, true);
|
||||
return resolvedNext;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [commitSavedSnapshot, draftState.draftConfig]);
|
||||
|
||||
const saveConfig = useCallback(async (patch: Partial<RuntimeConfig>) => {
|
||||
const blockedSnapshot = blockWhileLifecycleBusy(draftState.liveSnapshot, "save");
|
||||
if (blockedSnapshot) {
|
||||
setDraftState((current) => ({ ...current, liveSnapshot: blockedSnapshot }));
|
||||
return blockedSnapshot;
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
setDraftState((current) => ({
|
||||
...current,
|
||||
liveSnapshot: startLifecycleSnapshot(current.liveSnapshot, "save", current.liveSnapshot.status),
|
||||
}));
|
||||
try {
|
||||
const next = await saveRuntimeConfig({ ...draftState.draftConfig, ...patch });
|
||||
const resolvedNext = finalizeLifecycleSnapshot("save", "Save completed.", next);
|
||||
commitSavedSnapshot(resolvedNext, draftState.isOpen);
|
||||
return resolvedNext;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [commitSavedSnapshot, draftState.draftConfig, draftState.isOpen]);
|
||||
|
||||
const startLocal = useCallback(async () => {
|
||||
const blockedSnapshot = blockWhileLifecycleBusy(draftState.liveSnapshot, "start");
|
||||
if (blockedSnapshot) {
|
||||
setDraftState((current) => ({ ...current, liveSnapshot: blockedSnapshot }));
|
||||
return blockedSnapshot;
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
setDraftState((current) => ({
|
||||
...current,
|
||||
liveSnapshot: startLifecycleSnapshot(current.liveSnapshot, "start", "starting"),
|
||||
}));
|
||||
try {
|
||||
const next = await startRuntime();
|
||||
const resolvedNext = finalizeLifecycleSnapshot("start", "Local API started.", next);
|
||||
setDraftState((current) => reconcileDraftState(current, resolvedNext));
|
||||
return resolvedNext;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [draftState.liveSnapshot]);
|
||||
|
||||
const stopLocal = useCallback(async () => {
|
||||
const blockedSnapshot = blockWhileLifecycleBusy(draftState.liveSnapshot, "stop");
|
||||
if (blockedSnapshot) {
|
||||
setDraftState((current) => ({ ...current, liveSnapshot: blockedSnapshot }));
|
||||
return blockedSnapshot;
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
setDraftState((current) => ({
|
||||
...current,
|
||||
liveSnapshot: startLifecycleSnapshot(current.liveSnapshot, "stop", "stopping"),
|
||||
}));
|
||||
try {
|
||||
const next = await stopRuntime();
|
||||
const resolvedNext = finalizeLifecycleSnapshot("stop", "Local API stopped.", next);
|
||||
setDraftState((current) => reconcileDraftState(current, resolvedNext));
|
||||
return resolvedNext;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [draftState.liveSnapshot]);
|
||||
|
||||
const connection = useMemo(() => getRuntimeConnection(draftState.liveSnapshot), [draftState.liveSnapshot]);
|
||||
const activeLifecycleCommand = draftState.liveSnapshot.active_command;
|
||||
const lastLifecycleResult: RuntimeLifecycleResult = draftState.liveSnapshot.last_command_result;
|
||||
const lifecycleBusy = activeLifecycleCommand !== "idle";
|
||||
const applyImpact: RuntimeApplyImpact = useMemo(
|
||||
() =>
|
||||
getRuntimeApplyImpact({
|
||||
savedBaseline: draftState.savedBaseline,
|
||||
draftConfig: draftState.draftConfig,
|
||||
processRunning: draftState.liveSnapshot.process_running,
|
||||
}),
|
||||
[draftState.draftConfig, draftState.liveSnapshot.process_running, draftState.savedBaseline],
|
||||
);
|
||||
|
||||
// Phase 3: Unified workspace contract
|
||||
const workspaceSummary: RuntimeWorkspaceSummary = useMemo(
|
||||
() => getRuntimeWorkspaceSummary(draftState.liveSnapshot),
|
||||
[draftState.liveSnapshot],
|
||||
);
|
||||
const workspaceHealth: RuntimeWorkspaceHealth = useMemo(
|
||||
() => getRuntimeHealthSummary(draftState.liveSnapshot),
|
||||
[draftState.liveSnapshot],
|
||||
);
|
||||
|
||||
return {
|
||||
snapshot: draftState.liveSnapshot,
|
||||
liveSnapshot: draftState.liveSnapshot,
|
||||
savedBaseline: draftState.savedBaseline,
|
||||
draftConfig: draftState.draftConfig,
|
||||
draftDirty: draftState.isDirty,
|
||||
draftOpen: draftState.isOpen,
|
||||
draftFields: draftState.fields,
|
||||
connection,
|
||||
loading,
|
||||
busy,
|
||||
activeLifecycleCommand,
|
||||
lastLifecycleResult,
|
||||
lifecycleBusy,
|
||||
applyImpact,
|
||||
// Phase 3: Unified workspace contract
|
||||
workspaceSummary,
|
||||
workspaceHealth,
|
||||
refresh,
|
||||
openDraft,
|
||||
closeDraft,
|
||||
resetDraft,
|
||||
updateDraft,
|
||||
updateDraftField,
|
||||
updateDraftFieldMeta,
|
||||
saveDraft,
|
||||
saveConfig,
|
||||
startLocal,
|
||||
stopLocal,
|
||||
// Phase 4: Model scanning for validation
|
||||
scannedModels: draftState.scannedModels,
|
||||
isScanningModels: draftState.isScanning,
|
||||
scanModels,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user