add cross-platform reset scripts, portable server manager, and frontend scaffolding

This commit is contained in:
techjarves committed 2026-06-26 07:00:17 +05:30
1 parent 1d28391243
commit 349b9e167f
6 files changed
+258 -22

No files matched your search

+1
View File
@@ -17,6 +17,7 @@ app/config/
app/cache/
app/dist/
app/outputs/
app/chat-history/
app/transcriptions/
app/tts-outputs/
app/tts-cache/
+57 -20
View File
@@ -7,7 +7,7 @@ import Settings from "./components/Settings";
import TextChat from "./components/TextChat";
import SpeechTranscriber from "./components/SpeechTranscriber";
import TextToSpeech from "./components/TextToSpeech";
import { cleanupCandidates, formatBytes, getCleanupCandidates, getDiagnostics, getHardwareSpecs, getHealth, getTelemetry, getBackendOptions, getBackendStatus, listGeneratedOutputs, listSpeechTranscriptions, deleteSpeechTranscription, listTtsOutputs, deleteTtsOutput, stopServer } from "./services/api";
import { cleanupCandidates, formatBytes, getCleanupCandidates, getDiagnostics, getHardwareSpecs, getHealth, getTelemetry, getBackendOptions, getBackendStatus, listGeneratedOutputs, listLlmConversations, saveLlmConversation, deleteLlmConversation, listSpeechTranscriptions, deleteSpeechTranscription, listTtsOutputs, deleteTtsOutput, stopServer } from "./services/api";
import "./App.css";
function App() {
@@ -240,17 +240,6 @@ function App() {
const [selectedTtsOutput, setSelectedTtsOutput] = useState(null);
const [showTtsHistory, setShowTtsHistory] = useState(false);
useEffect(() => {
const saved = localStorage.getItem("chat_conversations");
if (saved) {
try {
setConversations(JSON.parse(saved));
} catch (_) {
localStorage.removeItem("chat_conversations");
}
}
}, []);
const sanitizeMessageForStorage = (message) => {
if (!Array.isArray(message?.content)) return message;
return {
@@ -272,37 +261,83 @@ function App() {
: [],
});
const persistConversations = (list) => {
const compactList = list.map(sanitizeConversationForStorage);
const persistConversation = (conversation) => {
const compactConversation = sanitizeConversationForStorage(conversation);
try {
localStorage.setItem("chat_conversations", JSON.stringify(compactList));
saveLlmConversation(compactConversation).catch((err) => {
console.warn("Could not save chat history to app folder:", err);
});
} catch (err) {
console.warn("Could not save chat history:", err);
}
};
useEffect(() => {
let cancelled = false;
async function loadConversations() {
let legacyConversations = [];
const legacySaved = localStorage.getItem("chat_conversations");
if (legacySaved) {
try {
legacyConversations = JSON.parse(legacySaved);
} catch (_) {
localStorage.removeItem("chat_conversations");
}
}
try {
const diskConversations = await listLlmConversations();
const diskIds = new Set(diskConversations.map((conversation) => conversation.id));
const missingLegacy = legacyConversations
.map(sanitizeConversationForStorage)
.filter((conversation) => conversation.id && !diskIds.has(conversation.id));
if (missingLegacy.length > 0) {
await Promise.all(missingLegacy.map((conversation) => saveLlmConversation(conversation)));
}
const merged = [...diskConversations, ...missingLegacy]
.sort((a, b) => Number(b.timestamp || 0) - Number(a.timestamp || 0));
if (!cancelled) setConversations(merged);
if (legacySaved) localStorage.removeItem("chat_conversations");
} catch (err) {
console.warn("Could not load chat history from app folder:", err);
if (!cancelled && legacyConversations.length > 0) {
setConversations(legacyConversations.map(sanitizeConversationForStorage));
}
}
}
loadConversations();
return () => {
cancelled = true;
};
}, []);
const saveConversationState = useCallback((id, msgs, modelName, newTitle = null) => {
setConversations((prev) => {
const list = [...prev];
const idx = list.findIndex(c => c.id === id);
let conversation;
if (idx !== -1) {
list[idx] = {
conversation = {
...list[idx],
messages: msgs,
timestamp: Date.now(),
model: modelName,
...(newTitle ? { title: newTitle } : {})
};
list[idx] = conversation;
} else {
list.unshift({
conversation = {
id,
title: newTitle || "Chat Session",
model: modelName,
messages: msgs,
timestamp: Date.now()
});
};
list.unshift(conversation);
}
persistConversations(list);
persistConversation(conversation);
return list;
});
}, []);
@@ -311,9 +346,11 @@ function App() {
if (e) e.stopPropagation();
setConversations((prev) => {
const filtered = prev.filter((c) => c.id !== id);
persistConversations(filtered);
return filtered;
});
deleteLlmConversation(id).catch((err) => {
console.warn("Could not delete chat history from app folder:", err);
});
if (activeConversationId === id) {
setActiveConversationId(null);
}
+25
View File
@@ -365,6 +365,31 @@ export async function listLlmModels() {
return (data.models || []).map(normalizeModel);
}
export async function listLlmConversations() {
const res = await fetch("/api/llm/conversations");
const data = await readJsonResponse(res, "The local server returned invalid chat history.");
return data.conversations || [];
}
export async function saveLlmConversation(conversation) {
const res = await fetch("/api/llm/save-conversation", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ conversation }),
});
const data = await readJsonResponse(res, "The local server returned an invalid chat save response.");
return data.conversation;
}
export async function deleteLlmConversation(id) {
const res = await fetch("/api/llm/delete-conversation", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id }),
});
return await readJsonResponse(res, "The local server returned an invalid chat delete response.");
}
export async function searchHuggingFaceModels(query = "", filters = [], page = 1) {
const params = new URLSearchParams();
if (query.trim()) params.set("query", query.trim());
+13 -1
View File
@@ -78,6 +78,18 @@ if (Test-Path $ttsModelsDir) {
Write-Host " >> Preserving TTS models in app/tts-models." -ForegroundColor Cyan
}
# Preserve generated image outputs
$outputsDir = Join-Path $appDir "outputs"
if (Test-Path $outputsDir) {
Write-Host " >> Preserving generated image outputs in app/outputs." -ForegroundColor Cyan
}
# Preserve chat history
$chatHistoryDir = Join-Path $appDir "chat-history"
if (Test-Path $chatHistoryDir) {
Write-Host " >> Preserving chat history in app/chat-history." -ForegroundColor Cyan
}
# Preserve transcriptions
$transcriptionsDir = Join-Path $appDir "transcriptions"
if (Test-Path $transcriptionsDir) {
@@ -125,7 +137,7 @@ if (Test-Path $lockFile) {
Write-Host ""
Write-Host " ============================================================" -ForegroundColor Green
Write-Host " Reset complete. Image models, text models, speech models, TTS models, transcripts, TTS outputs, OpenVINO models, and generated outputs were preserved." -ForegroundColor Green
Write-Host " Reset complete. Models, generated images, chat history, transcripts, TTS outputs, TTS cache, and OpenVINO models were preserved." -ForegroundColor Green
Write-Host " ============================================================" -ForegroundColor Green
Write-Host ""
Read-Host " Press Enter to close..."
+11 -1
View File
@@ -72,6 +72,16 @@ if [[ -d "$APP_DIR/tts-models" ]]; then
echo " >> Preserving TTS models in app/tts-models."
fi
# Preserve generated image outputs
if [[ -d "$APP_DIR/outputs" ]]; then
echo " >> Preserving generated image outputs in app/outputs."
fi
# Preserve chat history
if [[ -d "$APP_DIR/chat-history" ]]; then
echo " >> Preserving chat history in app/chat-history."
fi
# Preserve transcriptions
if [[ -d "$APP_DIR/transcriptions" ]]; then
echo " >> Preserving speech transcripts in app/transcriptions."
@@ -111,7 +121,7 @@ fi
echo ""
echo " ============================================================"
echo " Reset complete. Image models, text models, speech models, TTS models, transcripts, TTS outputs, OpenVINO models, and generated outputs were preserved."
echo " Reset complete. Models, generated images, chat history, transcripts, TTS outputs, TTS cache, and OpenVINO models were preserved."
echo " ============================================================"
echo ""
read -rp " Press Enter to close..."
+151
View File
@@ -94,6 +94,10 @@ const LLM_MODELS = path.join(ROOT, "app", "llm-models");
if (!fs.existsSync(LLM_MODELS)) {
fs.mkdirSync(LLM_MODELS, { recursive: true });
}
const CHAT_HISTORY = path.join(ROOT, "app", "chat-history");
if (!fs.existsSync(CHAT_HISTORY)) {
fs.mkdirSync(CHAT_HISTORY, { recursive: true });
}
const LLM_BACKEND_PATHS = {
winCuda: path.join(ROOT, "app", "llm-backend", "win", "cuda", "llama-server.exe"),
winVulkan: path.join(ROOT, "app", "llm-backend", "win", "vulkan", "llama-server.exe"),
@@ -2706,6 +2710,9 @@ function getSetupPaths() {
cpuBackend: BACKEND_PATHS.cpu,
models: MODELS,
outputs: OUTPUTS,
chatHistory: CHAT_HISTORY,
transcriptions: TRANSCRIPTIONS,
ttsOutputs: TTS_OUTPUTS,
};
} else if (osPlatform === "darwin") {
return {
@@ -2715,6 +2722,9 @@ function getSetupPaths() {
macBackend: BACKEND_PATHS.mac,
models: MODELS,
outputs: OUTPUTS,
chatHistory: CHAT_HISTORY,
transcriptions: TRANSCRIPTIONS,
ttsOutputs: TTS_OUTPUTS,
};
} else {
// Linux / WSL
@@ -2727,6 +2737,9 @@ function getSetupPaths() {
linuxRocmBackend: BACKEND_PATHS.linuxRocm,
models: MODELS,
outputs: OUTPUTS,
chatHistory: CHAT_HISTORY,
transcriptions: TRANSCRIPTIONS,
ttsOutputs: TTS_OUTPUTS,
};
}
}
@@ -2739,6 +2752,9 @@ async function getHealth() {
getPathInfo("Frontend build", paths.distIndex),
getDirInfo("Models folder", paths.models),
getDirInfo("Outputs folder", paths.outputs),
getDirInfo("Chat history folder", paths.chatHistory),
getDirInfo("Transcriptions folder", paths.transcriptions),
getDirInfo("TTS outputs folder", paths.ttsOutputs),
];
if (osPlatform === "win32") {
@@ -2811,6 +2827,16 @@ async function getHealth() {
totalBytes: getPathSize(OUTPUTS),
totalSize: formatBytes(getPathSize(OUTPUTS)),
},
chat: {
count: fs.existsSync(CHAT_HISTORY) ? fs.readdirSync(CHAT_HISTORY).filter((file) => file.toLowerCase().endsWith(".json")).length : 0,
totalBytes: getPathSize(CHAT_HISTORY),
totalSize: formatBytes(getPathSize(CHAT_HISTORY)),
},
speech: {
transcriptions: fs.existsSync(TRANSCRIPTIONS) ? fs.readdirSync(TRANSCRIPTIONS).filter((file) => file.toLowerCase().endsWith(".json")).length : 0,
totalBytes: getPathSize(TRANSCRIPTIONS),
totalSize: formatBytes(getPathSize(TRANSCRIPTIONS)),
},
tts: {
models: fs.existsSync(TTS_MODELS) ? fs.readdirSync(TTS_MODELS).filter((file) => file.toLowerCase().endsWith(".json")).length : 0,
outputs: fs.existsSync(TTS_OUTPUTS) ? fs.readdirSync(TTS_OUTPUTS).filter((file) => file.toLowerCase().endsWith(".json")).length : 0,
@@ -2827,6 +2853,8 @@ function addCleanupCandidate(candidates, id, targetPath, reason, options = {}) {
if (!options.allowUserData && (
pathInside(targetPath, MODELS) ||
pathInside(targetPath, OUTPUTS) ||
pathInside(targetPath, CHAT_HISTORY) ||
pathInside(targetPath, TRANSCRIPTIONS) ||
pathInside(targetPath, TTS_MODELS) ||
pathInside(targetPath, TTS_OUTPUTS)
)) return;
@@ -5402,6 +5430,101 @@ function safeOutputName(value) {
.slice(0, 120);
}
function sanitizeChatMessageForStorage(message) {
if (!message || typeof message !== "object") return null;
const role = String(message.role || "").trim();
if (!["system", "user", "assistant"].includes(role)) return null;
let content = message.content;
if (Array.isArray(content)) {
content = content.map((item) => {
if (!item || typeof item !== "object") return item;
if (item.type === "image_url") {
return { type: "text", text: "[Attached image omitted from saved chat history]" };
}
return item;
});
} else if (typeof content !== "string") {
content = content === undefined || content === null ? "" : String(content);
}
return {
...message,
role,
content,
};
}
function sanitizeChatConversationForStorage(conversation = {}) {
const now = Date.now();
const id = safeOutputName(conversation.id || `chat_${now}`) || `chat_${now}`;
const messages = Array.isArray(conversation.messages)
? conversation.messages.map(sanitizeChatMessageForStorage).filter(Boolean)
: [];
const timestamp = Number(conversation.timestamp) || now;
return {
id,
title: String(conversation.title || "Chat Session").slice(0, 160),
model: String(conversation.model || ""),
messages,
timestamp,
createdAt: conversation.createdAt || new Date(timestamp).toISOString(),
updatedAt: new Date().toISOString(),
};
}
function getChatConversationPath(id) {
const safeId = safeOutputName(id);
if (!safeId) throw new Error("A chat id is required.");
const filePath = path.join(CHAT_HISTORY, `${safeId}.json`);
if (!pathInside(filePath, CHAT_HISTORY)) {
throw new Error("Invalid chat id.");
}
return filePath;
}
function saveChatConversation(conversation = {}) {
const saved = sanitizeChatConversationForStorage(conversation);
const filePath = getChatConversationPath(saved.id);
fs.writeFileSync(filePath, JSON.stringify(saved, null, 2), "utf8");
return saved;
}
function listChatConversations() {
try {
return fs.readdirSync(CHAT_HISTORY)
.filter((file) => file.toLowerCase().endsWith(".json"))
.map((file) => {
try {
const filePath = path.join(CHAT_HISTORY, file);
if (!pathInside(filePath, CHAT_HISTORY)) return null;
const conversation = JSON.parse(fs.readFileSync(filePath, "utf8"));
const stat = fs.statSync(filePath);
return {
...conversation,
filename: file,
modifiedAt: stat.mtime.toISOString(),
sizeBytes: stat.size,
size: formatBytes(stat.size),
};
} catch (_) {
return null;
}
})
.filter(Boolean)
.sort((a, b) => Number(b.timestamp || 0) - Number(a.timestamp || 0));
} catch (_) {
return [];
}
}
function deleteChatConversation(id) {
const filePath = getChatConversationPath(id);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
return true;
}
return false;
}
function saveGeneratedOutput(imageDataUrl, metadata = {}) {
const match = String(imageDataUrl || "").match(/^data:(image\/(?:png|jpeg|jpg|webp));base64,(.+)$/);
if (!match) {
@@ -5699,6 +5822,34 @@ const server = http.createServer(async (req, res) => {
return json(res, 200, { ok: true, model: filename, settings: updated });
}
if (req.url === "/api/llm/conversations" && req.method === "GET") {
return json(res, 200, { ok: true, conversations: listChatConversations() });
}
if (req.url === "/api/llm/save-conversation" && req.method === "POST") {
const body = await readJsonBody(req, res);
if (!body) return;
try {
const conversation = saveChatConversation(body.conversation || body);
return json(res, 200, { ok: true, conversation });
} catch (err) {
console.error(" [api] Failed to save chat conversation:", err);
return json(res, 500, { ok: false, error: err.message || String(err) });
}
}
if (req.url === "/api/llm/delete-conversation" && req.method === "POST") {
const body = await readJsonBody(req, res);
if (!body) return;
try {
const deleted = deleteChatConversation(body.id);
return json(res, 200, { ok: true, deleted });
} catch (err) {
console.error(" [api] Failed to delete chat conversation:", err);
return json(res, 500, { ok: false, error: err.message || String(err) });
}
}
if (req.url === "/api/llm/models" && req.method === "GET") {
return json(res, 200, { models: getLlmModels() });
}