feat: deepseek
Build Release EXE / build-windows-exe (release) Successful in 1m2s

This commit is contained in:
2026-06-08 23:41:46 -04:00
parent 00cf6f8747
commit 454bb57484
24 changed files with 1719 additions and 183 deletions
+115 -104
View File
@@ -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") {
+9 -11
View File
@@ -125,20 +125,17 @@
<form class="config-form" id="ollama-config-form">
<label>Provider
<select id="model-provider" name="model_provider">
<option value="deepseek">DeepSeek V4 (Recommended)</option>
<option value="ollama">Local Ollama</option>
<option value="openai">OpenAI</option>
<option value="codex">Codex</option>
</select>
</label>
<label data-provider-scope="deepseek">DeepSeek URL<input id="deepseek-base-url" name="deepseek_base_url" type="text"></label>
<label data-provider-scope="deepseek">DeepSeek API Key<input id="deepseek-api-key" name="deepseek_api_key" type="password" autocomplete="off"></label>
<label data-provider-scope="deepseek" data-manual-model="true">DeepSeek Model<input id="deepseek-model" name="deepseek_model" type="text" list="provider-models"></label>
<label data-provider-scope="ollama">Ollama URL<input id="ollama-base-url" name="ollama_base_url" type="text"></label>
<label data-provider-scope="ollama">Ollama Model<input id="ollama-model" name="ollama_model" type="text" list="provider-models"></label>
<label data-provider-scope="ollama">Context Tokens<input id="ollama-num-ctx" name="ollama_num_ctx" type="number" min="1024" step="1024"></label>
<label data-provider-scope="openai">OpenAI URL<input id="openai-base-url" name="openai_base_url" type="text"></label>
<label data-provider-scope="openai">OpenAI API Key<input id="openai-api-key" name="openai_api_key" type="password" autocomplete="off"></label>
<label data-provider-scope="openai">OpenAI Model<input id="openai-model" name="openai_model" type="text" list="provider-models"></label>
<label data-provider-scope="codex">Codex Command<input id="codex-command" name="codex_command" type="text"></label>
<label data-provider-scope="codex">Codex Model<input id="codex-model" name="codex_model" type="text" list="provider-models"></label>
<label>Available Models<select id="provider-model-select"></select></label>
<label><span id="provider-model-label">Model</span><select id="provider-model-select"></select></label>
<label>Reasoning Effort<select id="model-reasoning-effort" name="model_reasoning_effort"></select></label>
<datalist id="provider-models"></datalist>
<button type="submit">Save Provider Config</button>
@@ -149,8 +146,6 @@
<button class="secondary small-button" id="ollama-install" type="button">Auto Install</button>
<button class="secondary small-button" id="ollama-launch" type="button">Launch</button>
<button class="small-button" id="ollama-pull" type="button">Install Model</button>
<button class="secondary small-button" id="codex-login" type="button">Sign In to Codex</button>
<button class="secondary small-button" id="openai-models-refresh" type="button">Load Provider Models</button>
</div>
<div class="config-status" id="ollama-message"></div>
</div>
@@ -233,7 +228,10 @@
<span>Buying plans work best with item lines. Custom plans can run with just instructions.</span>
</div>
</div>
<button type="submit">Create Plan</button>
<div class="plan-form-actions">
<button id="plan-autofill" type="button">AI Fill</button>
<button type="submit">Create Plan</button>
</div>
<div class="config-status" id="plans-status"></div>
</form>
</div>
+9
View File
@@ -1302,6 +1302,15 @@ button.secondary {
min-height: 96px;
}
.plan-form-actions {
display: flex;
gap: 10px;
}
.plan-form-actions button {
flex: 1;
}
.plan-form-split {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));