This commit is contained in:
+115
-104
@@ -26,9 +26,8 @@ const ollamaDownloadButton = document.getElementById("ollama-download");
|
||||
const ollamaInstallButton = document.getElementById("ollama-install");
|
||||
const ollamaLaunchButton = document.getElementById("ollama-launch");
|
||||
const ollamaPullButton = document.getElementById("ollama-pull");
|
||||
const codexLoginButton = document.getElementById("codex-login");
|
||||
const openaiModelsRefreshButton = document.getElementById("openai-models-refresh");
|
||||
const providerModelSelect = document.getElementById("provider-model-select");
|
||||
const providerModelLabel = document.getElementById("provider-model-label");
|
||||
const modelReasoningEffortSelect = document.getElementById("model-reasoning-effort");
|
||||
const ollamaStatusEl = document.getElementById("ollama-status");
|
||||
const ollamaMessageEl = document.getElementById("ollama-message");
|
||||
@@ -57,6 +56,7 @@ const updateModalReleases = document.getElementById("update-modal-releases");
|
||||
const plansRefreshButton = document.getElementById("plans-refresh");
|
||||
const plansCloseButton = document.getElementById("plans-close");
|
||||
const planForm = document.getElementById("plan-form");
|
||||
const planAutofillButton = document.getElementById("plan-autofill");
|
||||
const plansStatusEl = document.getElementById("plans-status");
|
||||
const plansDashboardEl = document.getElementById("plans-dashboard");
|
||||
const plansRailListEl = document.getElementById("plans-rail-list");
|
||||
@@ -164,6 +164,8 @@ function appendThinkingText(node, text) {
|
||||
steps.appendChild(item);
|
||||
}
|
||||
item.textContent += text;
|
||||
const thinking = node.querySelector(".thinking-log");
|
||||
if (thinking && !thinking.open) thinking.open = true;
|
||||
}
|
||||
|
||||
function createThinkTagParser(node) {
|
||||
@@ -564,7 +566,8 @@ function renderComposerImages() {
|
||||
function formatMetrics(event) {
|
||||
const read = formatTokenMetric(event.reading_tokens, event.reading_tokens_per_second);
|
||||
const wrote = formatTokenMetric(event.writing_tokens, event.writing_tokens_per_second);
|
||||
return [read && `read ${read}`, wrote && `wrote ${wrote}`].filter(Boolean).join(" | ");
|
||||
const cache = formatCacheMetric(event.cache_hit_tokens, event.cache_miss_tokens);
|
||||
return [read && `read ${read}`, wrote && `wrote ${wrote}`, cache].filter(Boolean).join(" | ");
|
||||
}
|
||||
|
||||
function formatTokenMetric(tokens, speed) {
|
||||
@@ -573,6 +576,13 @@ function formatTokenMetric(tokens, speed) {
|
||||
return `${tokens} tok${speedText}`;
|
||||
}
|
||||
|
||||
function formatCacheMetric(hitTokens, missTokens) {
|
||||
if (!hitTokens && !missTokens) return "";
|
||||
const hit = Number(hitTokens || 0).toLocaleString();
|
||||
const miss = Number(missTokens || 0).toLocaleString();
|
||||
return `cache ${hit} hit / ${miss} miss`;
|
||||
}
|
||||
|
||||
function setWarning(text) {
|
||||
warningEl.hidden = !text;
|
||||
warningEl.textContent = text || "";
|
||||
@@ -597,15 +607,13 @@ const configFieldIds = {
|
||||
|
||||
const ollamaFieldIds = {
|
||||
model_provider: "model-provider",
|
||||
deepseek_base_url: "deepseek-base-url",
|
||||
deepseek_api_key: "deepseek-api-key",
|
||||
deepseek_model: "deepseek-model",
|
||||
ollama_base_url: "ollama-base-url",
|
||||
ollama_model: "ollama-model",
|
||||
ollama_num_ctx: "ollama-num-ctx",
|
||||
openai_base_url: "openai-base-url",
|
||||
openai_api_key: "openai-api-key",
|
||||
openai_model: "openai-model",
|
||||
model_reasoning_effort: "model-reasoning-effort",
|
||||
codex_command: "codex-command",
|
||||
codex_model: "codex-model",
|
||||
};
|
||||
|
||||
async function refreshConfig() {
|
||||
@@ -667,7 +675,7 @@ async function saveConfig(event) {
|
||||
const result = await response.json();
|
||||
renderConfig(result);
|
||||
configStatusEl.textContent = result.message || "Saved";
|
||||
addMessage("assistant", "Config saved. Restart TraderAI for the new settings to fully apply.");
|
||||
addMessage("assistant", result.message || "Config saved.");
|
||||
} catch (error) {
|
||||
configStatusEl.textContent = `Config save failed: ${fetchErrorMessage(error)}`;
|
||||
}
|
||||
@@ -715,18 +723,18 @@ function renderOllamaStatus(status) {
|
||||
updateProviderFieldVisibility(status.provider || "ollama");
|
||||
const provider = providerDisplayName(status.provider);
|
||||
const models = status.models?.length ? status.models.join(", ") : "None detected";
|
||||
const isOpenAIProvider = status.provider === "openai";
|
||||
const isCodexProvider = status.provider === "codex";
|
||||
const ready = isOpenAIProvider
|
||||
const isDeepSeekProvider = status.provider === "deepseek";
|
||||
const isCloudProvider = isDeepSeekProvider;
|
||||
const ready = isCloudProvider
|
||||
? Boolean(status.online && status.model_available)
|
||||
: Boolean(status.installed && status.running && status.model_available);
|
||||
const pillClass = ready ? "status-pill" : "status-pill warning";
|
||||
const detailItems = [
|
||||
ollamaStatusItem("Provider", provider),
|
||||
ollamaStatusItem("Model", status.configured_model || ""),
|
||||
ollamaStatusItem(isCodexProvider ? "Command" : "URL", status.base_url || ""),
|
||||
ollamaStatusItem("URL", status.base_url || ""),
|
||||
];
|
||||
if (!isOpenAIProvider && !isCodexProvider) {
|
||||
if (!isCloudProvider) {
|
||||
detailItems.splice(1, 0, ollamaStatusItem("Installed", status.installed ? "Yes" : "No"));
|
||||
detailItems.splice(2, 0, ollamaStatusItem("Running", status.running ? "Yes" : "No"));
|
||||
detailItems.push(ollamaStatusItem("Pulled", status.model_available ? "Yes" : "No"));
|
||||
@@ -740,30 +748,22 @@ function renderOllamaStatus(status) {
|
||||
<div class="ollama-status-grid">
|
||||
${detailItems.join("")}
|
||||
</div>
|
||||
${ollamaStatusItem(isOpenAIProvider || isCodexProvider ? "Available Models" : "Installed Models", models)}
|
||||
${ollamaStatusItem(isCloudProvider ? "Available Models" : "Installed Models", models)}
|
||||
${status.detail ? ollamaStatusItem("Detail", status.detail) : ""}
|
||||
`;
|
||||
if (ollamaDownloadButton) ollamaDownloadButton.hidden = isOpenAIProvider || isCodexProvider;
|
||||
if (ollamaDownloadButton) ollamaDownloadButton.hidden = isCloudProvider;
|
||||
if (ollamaInstallButton) {
|
||||
ollamaInstallButton.hidden = isOpenAIProvider || isCodexProvider || !status.can_auto_install;
|
||||
ollamaInstallButton.hidden = isCloudProvider || !status.can_auto_install;
|
||||
ollamaInstallButton.disabled = Boolean(status.installed) || !status.can_auto_install;
|
||||
}
|
||||
if (ollamaLaunchButton) {
|
||||
ollamaLaunchButton.hidden = isOpenAIProvider || isCodexProvider;
|
||||
ollamaLaunchButton.hidden = isCloudProvider;
|
||||
ollamaLaunchButton.disabled = !status.installed || Boolean(status.running);
|
||||
}
|
||||
if (ollamaPullButton) {
|
||||
ollamaPullButton.hidden = isOpenAIProvider || isCodexProvider;
|
||||
ollamaPullButton.hidden = isCloudProvider;
|
||||
ollamaPullButton.disabled = !status.running || Boolean(status.model_available);
|
||||
}
|
||||
if (codexLoginButton) {
|
||||
codexLoginButton.hidden = !isCodexProvider;
|
||||
codexLoginButton.disabled = Boolean(status.online);
|
||||
}
|
||||
if (openaiModelsRefreshButton) {
|
||||
openaiModelsRefreshButton.hidden = false;
|
||||
openaiModelsRefreshButton.disabled = false;
|
||||
}
|
||||
renderProviderModelOptions(status.models || [], status);
|
||||
renderReasoningEffortOptions(status.reasoning_efforts || [], status.configured_reasoning_effort || "medium");
|
||||
updateOllamaAttention(status);
|
||||
@@ -808,18 +808,16 @@ function setOllamaButtonAttention(button, action, active) {
|
||||
function updateOllamaAttention(status = null) {
|
||||
const currentStatus = status || latestOllamaStatus;
|
||||
if (!currentStatus) return;
|
||||
const isOpenAIProvider = currentStatus.provider === "openai";
|
||||
const isCodexProvider = currentStatus.provider === "codex";
|
||||
const ready = isOpenAIProvider
|
||||
const isDeepSeekProvider = currentStatus.provider === "deepseek";
|
||||
const isCloudProvider = isDeepSeekProvider;
|
||||
const ready = isCloudProvider
|
||||
? Boolean(currentStatus.online && currentStatus.model_available)
|
||||
: Boolean(currentStatus.installed && currentStatus.running && currentStatus.model_available);
|
||||
ollamaToggle?.classList.toggle("attention-pulse", !ready);
|
||||
setOllamaButtonAttention(ollamaDownloadButton, "download", !isOpenAIProvider && !isCodexProvider && !currentStatus.installed);
|
||||
setOllamaButtonAttention(ollamaInstallButton, "install", !isOpenAIProvider && !isCodexProvider && !currentStatus.installed && currentStatus.can_auto_install);
|
||||
setOllamaButtonAttention(ollamaLaunchButton, "launch", !isOpenAIProvider && !isCodexProvider && currentStatus.installed && !currentStatus.running);
|
||||
setOllamaButtonAttention(ollamaPullButton, "pull", !isOpenAIProvider && !isCodexProvider && currentStatus.running && !currentStatus.model_available);
|
||||
setOllamaButtonAttention(codexLoginButton, "codex-login", isCodexProvider && !currentStatus.online);
|
||||
setOllamaButtonAttention(openaiModelsRefreshButton, "openai-models", isOpenAIProvider && !currentStatus.model_available);
|
||||
setOllamaButtonAttention(ollamaDownloadButton, "download", !isCloudProvider && !currentStatus.installed);
|
||||
setOllamaButtonAttention(ollamaInstallButton, "install", !isCloudProvider && !currentStatus.installed && currentStatus.can_auto_install);
|
||||
setOllamaButtonAttention(ollamaLaunchButton, "launch", !isCloudProvider && currentStatus.installed && !currentStatus.running);
|
||||
setOllamaButtonAttention(ollamaPullButton, "pull", !isCloudProvider && currentStatus.running && !currentStatus.model_available);
|
||||
if (ready) clickedOllamaActions.clear();
|
||||
}
|
||||
|
||||
@@ -830,7 +828,11 @@ function configuredOllamaModel() {
|
||||
function updateProviderFieldVisibility(provider) {
|
||||
for (const field of providerScopedFields) {
|
||||
const scope = field.dataset.providerScope;
|
||||
field.hidden = scope !== provider;
|
||||
const hiddenManualModel = field.dataset.manualModel === "true" && provider !== "ollama";
|
||||
field.hidden = scope !== provider || hiddenManualModel;
|
||||
}
|
||||
if (providerModelLabel) {
|
||||
providerModelLabel.textContent = provider === "ollama" ? "Available Models" : "Model";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -867,54 +869,6 @@ function renderProviderModelOptions(models, status = latestOllamaStatus) {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshOpenAIModels() {
|
||||
setOllamaMessage("Loading provider models");
|
||||
try {
|
||||
const provider = document.getElementById("model-provider")?.value || latestOllamaStatus?.provider || "openai";
|
||||
const response = await fetch(`/api/provider/models?provider=${encodeURIComponent(provider)}`);
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.detail || `HTTP ${response.status}`);
|
||||
renderProviderModelOptions(result.models || [], {
|
||||
provider: result.provider || provider,
|
||||
configured_model: configuredProviderModel(result.provider || provider),
|
||||
});
|
||||
setOllamaMessage(result.message || "Loaded provider models");
|
||||
await refreshOllamaStatus();
|
||||
} catch (error) {
|
||||
setOllamaMessage(`Provider model load failed: ${fetchErrorMessage(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function launchCodexLogin() {
|
||||
markOllamaActionClicked("codex-login");
|
||||
setOllamaMessage("Starting Codex sign-in");
|
||||
try {
|
||||
const response = await fetch("/api/codex/login", { method: "POST" });
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.detail || `HTTP ${response.status}`);
|
||||
if (result.auth_url) {
|
||||
window.open(result.auth_url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
setOllamaMessage(result.message || "Opened Codex sign-in in your browser. Waiting for completion...");
|
||||
await waitForCodexLogin();
|
||||
} catch (error) {
|
||||
setOllamaMessage(`Codex sign-in failed: ${fetchErrorMessage(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForCodexLogin() {
|
||||
for (let attempt = 0; attempt < 80; attempt += 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, attempt < 8 ? 1500 : 3000));
|
||||
await refreshOllamaStatus();
|
||||
const provider = latestOllamaStatus?.provider || "";
|
||||
if (provider === "codex" && latestOllamaStatus?.online) {
|
||||
setOllamaMessage("Codex sign-in complete.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
setOllamaMessage("Codex sign-in opened. If you completed it, click Load Provider Models or refresh provider status.");
|
||||
}
|
||||
|
||||
function renderReasoningEffortOptions(efforts, configured) {
|
||||
if (!modelReasoningEffortSelect) return;
|
||||
const options = [...new Set([...(efforts || []), configured || "medium"].filter(Boolean))];
|
||||
@@ -929,8 +883,7 @@ function renderReasoningEffortOptions(efforts, configured) {
|
||||
}
|
||||
|
||||
function configuredProviderModel(provider) {
|
||||
if (provider === "openai") return document.getElementById("openai-model")?.value || "";
|
||||
if (provider === "codex") return document.getElementById("codex-model")?.value || "";
|
||||
if (provider === "deepseek") return document.getElementById("deepseek-model")?.value || "";
|
||||
return document.getElementById("ollama-model")?.value || "";
|
||||
}
|
||||
|
||||
@@ -938,13 +891,8 @@ function syncSelectedProviderModel() {
|
||||
const provider = document.getElementById("model-provider")?.value || "ollama";
|
||||
const selectedModel = providerModelSelect?.value || "";
|
||||
if (!selectedModel) return;
|
||||
if (provider === "openai") {
|
||||
const field = document.getElementById("openai-model");
|
||||
if (field) field.value = selectedModel;
|
||||
return;
|
||||
}
|
||||
if (provider === "codex") {
|
||||
const field = document.getElementById("codex-model");
|
||||
if (provider === "deepseek") {
|
||||
const field = document.getElementById("deepseek-model");
|
||||
if (field) field.value = selectedModel;
|
||||
return;
|
||||
}
|
||||
@@ -953,8 +901,7 @@ function syncSelectedProviderModel() {
|
||||
}
|
||||
|
||||
function providerDisplayName(provider) {
|
||||
if (provider === "openai") return "OpenAI";
|
||||
if (provider === "codex") return "Codex";
|
||||
if (provider === "deepseek") return "DeepSeek";
|
||||
return "Local Ollama";
|
||||
}
|
||||
|
||||
@@ -1323,6 +1270,72 @@ function parsePlanItems(text) {
|
||||
});
|
||||
}
|
||||
|
||||
function formatPlanItems(items) {
|
||||
return (items || [])
|
||||
.map((item) => {
|
||||
const name = String(item.item_name || item.name || "").trim();
|
||||
if (!name) return "";
|
||||
const quantity = Number(item.desired_quantity || item.quantity || 1);
|
||||
const maxUnitPrice = item.max_unit_price ?? item.max_price;
|
||||
const parts = [name];
|
||||
if (Number.isFinite(quantity) && quantity > 1) parts.push(String(quantity));
|
||||
else if (maxUnitPrice !== null && maxUnitPrice !== undefined && maxUnitPrice !== "") parts.push("1");
|
||||
if (maxUnitPrice !== null && maxUnitPrice !== undefined && maxUnitPrice !== "") parts.push(String(maxUnitPrice));
|
||||
return parts.join(" | ");
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function applyPlanDraft(draft) {
|
||||
if (!draft) return;
|
||||
document.getElementById("plan-title").value = draft.title || "";
|
||||
document.getElementById("plan-objective").value = draft.objective || "";
|
||||
document.getElementById("plan-kind").value = draft.kind || "buying";
|
||||
document.getElementById("plan-tone").value = draft.constraints?.message_tone || "";
|
||||
document.getElementById("plan-instructions").value = draft.constraints?.instructions || "";
|
||||
document.getElementById("plan-cadence").value = draft.cadence || "";
|
||||
document.getElementById("plan-items").value = formatPlanItems(draft.items || []);
|
||||
}
|
||||
|
||||
async function autofillPlanDraft() {
|
||||
const title = document.getElementById("plan-title").value.trim();
|
||||
const objective = document.getElementById("plan-objective").value.trim();
|
||||
if (!title && !objective) {
|
||||
plansStatusEl.textContent = "Add at least a title or objective first";
|
||||
return;
|
||||
}
|
||||
const tone = document.getElementById("plan-tone").value.trim();
|
||||
const instructions = document.getElementById("plan-instructions").value.trim();
|
||||
const constraints = {};
|
||||
if (tone) constraints.message_tone = tone;
|
||||
if (instructions) constraints.instructions = instructions;
|
||||
const payload = {
|
||||
title,
|
||||
objective,
|
||||
kind: document.getElementById("plan-kind").value || "buying",
|
||||
constraints,
|
||||
items: parsePlanItems(document.getElementById("plan-items").value || ""),
|
||||
};
|
||||
plansStatusEl.textContent = "Drafting plan";
|
||||
if (planAutofillButton) planAutofillButton.disabled = true;
|
||||
try {
|
||||
const response = await fetch("/api/plans/draft", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.detail || `HTTP ${response.status}`);
|
||||
applyPlanDraft(result.draft || {});
|
||||
plansStatusEl.textContent = "Draft filled in. Review and edit anything you want.";
|
||||
} catch (error) {
|
||||
plansStatusEl.textContent = `Plan draft failed: ${fetchErrorMessage(error)}`;
|
||||
} finally {
|
||||
if (planAutofillButton) planAutofillButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createPlan(event) {
|
||||
event.preventDefault();
|
||||
const title = document.getElementById("plan-title").value.trim();
|
||||
@@ -1664,8 +1677,8 @@ async function checkHealth() {
|
||||
health = await fallbackResponse.json();
|
||||
}
|
||||
const provider = providerDisplayName(health.provider);
|
||||
const isOpenAIProvider = health.provider === "openai";
|
||||
const isCodexProvider = health.provider === "codex";
|
||||
const isDeepSeekProvider = health.provider === "deepseek";
|
||||
const isCloudProvider = isDeepSeekProvider;
|
||||
ollamaOnline = Boolean(health.online);
|
||||
if (!ollamaOnline) {
|
||||
statusEl.textContent = "Offline";
|
||||
@@ -1674,7 +1687,7 @@ async function checkHealth() {
|
||||
return false;
|
||||
}
|
||||
if (health.model_available === false) {
|
||||
const action = isOpenAIProvider ? "Load Provider Models." : isCodexProvider ? "Sign In to Codex." : "Install Model.";
|
||||
const action = isCloudProvider ? "Save a working DeepSeek model." : "Install Model.";
|
||||
setWarning(`${provider} needs the configured model "${health.model}". Open the model provider tab and use ${action}`);
|
||||
ollamaToggle?.classList.add("attention-pulse");
|
||||
} else {
|
||||
@@ -1890,6 +1903,7 @@ ollamaToggle?.addEventListener("click", () => toggleSidebarPanel("ollama"));
|
||||
plansRefreshButton?.addEventListener("click", () => refreshPlans());
|
||||
plansCloseButton?.addEventListener("click", closePlansPanel);
|
||||
planForm?.addEventListener("submit", createPlan);
|
||||
planAutofillButton?.addEventListener("click", autofillPlanDraft);
|
||||
ollamaForm?.addEventListener("submit", saveOllamaConfig);
|
||||
ollamaRefreshButton?.addEventListener("click", refreshOllamaStatus);
|
||||
ollamaDownloadButton?.addEventListener("click", () => {
|
||||
@@ -1908,17 +1922,12 @@ ollamaPullButton?.addEventListener("click", () => {
|
||||
markOllamaActionClicked("pull");
|
||||
postOllamaAction("/api/ollama/pull", { body: { model: configuredOllamaModel() } });
|
||||
});
|
||||
codexLoginButton?.addEventListener("click", launchCodexLogin);
|
||||
providerModelSelect?.addEventListener("change", syncSelectedProviderModel);
|
||||
document.getElementById("model-provider")?.addEventListener("change", () => {
|
||||
const provider = document.getElementById("model-provider")?.value || "ollama";
|
||||
updateProviderFieldVisibility(provider);
|
||||
renderProviderModelOptions(latestOllamaStatus?.models || [], { ...latestOllamaStatus, provider });
|
||||
});
|
||||
openaiModelsRefreshButton?.addEventListener("click", () => {
|
||||
markOllamaActionClicked("openai-models");
|
||||
refreshOpenAIModels();
|
||||
});
|
||||
updateCheckButton?.addEventListener("click", checkForUpdate);
|
||||
updateInstallButton?.addEventListener("click", installUpdate);
|
||||
updateOpenReleasesButton?.addEventListener("click", openReleasesPage);
|
||||
@@ -1979,6 +1988,8 @@ async function sendMessage() {
|
||||
const event = JSON.parse(line.slice(6));
|
||||
if (event.type === "status") {
|
||||
setMessageActivity(assistantNode, event.message, true);
|
||||
} else if (event.type === "reasoning") {
|
||||
appendThinkingText(assistantNode, event.content || "");
|
||||
} else if (event.type === "metrics") {
|
||||
setMessageMetrics(assistantNode, formatMetrics(event));
|
||||
} else if (event.type === "warning") {
|
||||
|
||||
Reference in New Issue
Block a user