versioning: 0.0.6, ux: move buttons, feat: add cloud providers, feat: increese tool call limit
Build Release EXE / build-windows-exe (release) Successful in 51s
Build Release EXE / build-windows-exe (release) Successful in 51s
This commit is contained in:
+224
-34
@@ -1,5 +1,6 @@
|
||||
const form = document.getElementById("chat-form");
|
||||
const input = document.getElementById("message-input");
|
||||
const composerImagesEl = document.getElementById("composer-images");
|
||||
const messages = document.getElementById("messages");
|
||||
const statusEl = document.getElementById("status");
|
||||
const pendingEl = document.getElementById("pending-actions");
|
||||
@@ -25,6 +26,7 @@ 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 openaiModelsRefreshButton = document.getElementById("openai-models-refresh");
|
||||
const ollamaStatusEl = document.getElementById("ollama-status");
|
||||
const ollamaMessageEl = document.getElementById("ollama-message");
|
||||
const updateCheckButton = document.getElementById("update-check");
|
||||
@@ -61,25 +63,53 @@ let latestUpdate = null;
|
||||
let currentThreadId = "default";
|
||||
let currentNegotiationId = null;
|
||||
let latestOllamaStatus = null;
|
||||
let composerImages = [];
|
||||
const clickedOllamaActions = new Set();
|
||||
|
||||
if (window.lucide) {
|
||||
window.lucide.createIcons();
|
||||
}
|
||||
|
||||
function addMessage(role, text) {
|
||||
function addMessage(role, text, options = {}) {
|
||||
const node = document.createElement("div");
|
||||
node.className = `message ${role}`;
|
||||
setMessageMarkdown(node, text);
|
||||
setMessageMarkdown(node, text, options);
|
||||
messages.appendChild(node);
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
return node;
|
||||
}
|
||||
|
||||
function setMessageMarkdown(node, text) {
|
||||
function setMessageMarkdown(node, text, options = {}) {
|
||||
const body = node.querySelector(".message-body") || node;
|
||||
body.innerHTML = renderMarkdown(text);
|
||||
enhanceNegotiationLinks(body);
|
||||
body.innerHTML = "";
|
||||
const attachedImages = options.images || [];
|
||||
if (attachedImages.length) {
|
||||
body.appendChild(renderImageGallery(attachedImages));
|
||||
}
|
||||
if (text) {
|
||||
const markdown = document.createElement("div");
|
||||
markdown.innerHTML = renderMarkdown(text);
|
||||
body.appendChild(markdown);
|
||||
enhanceNegotiationLinks(markdown);
|
||||
}
|
||||
}
|
||||
|
||||
function renderImageGallery(images) {
|
||||
const gallery = document.createElement("div");
|
||||
gallery.className = "message-images";
|
||||
for (const image of images) {
|
||||
const card = document.createElement("div");
|
||||
card.className = "message-image";
|
||||
const preview = document.createElement("img");
|
||||
preview.src = image.preview_url || `data:${image.content_type || "image/png"};base64,${image.image_data}`;
|
||||
preview.alt = image.name || "Attached image";
|
||||
const label = document.createElement("span");
|
||||
label.className = "message-image-label";
|
||||
label.textContent = image.name || "Attached image";
|
||||
card.append(preview, label);
|
||||
gallery.appendChild(card);
|
||||
}
|
||||
return gallery;
|
||||
}
|
||||
|
||||
function setMessageActivity(node, text, active = false) {
|
||||
@@ -459,6 +489,74 @@ function escapeHtml(text) {
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function composerImageId() {
|
||||
if (window.crypto?.randomUUID) return window.crypto.randomUUID();
|
||||
return `image-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
}
|
||||
|
||||
function readFileAsDataUrl(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(String(reader.result || ""));
|
||||
reader.onerror = () => reject(reader.error || new Error(`Could not read ${file.name || "image"}`));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
async function addComposerImages(files) {
|
||||
const additions = [];
|
||||
for (const file of files) {
|
||||
if (!file || !String(file.type || "").startsWith("image/")) continue;
|
||||
const previewUrl = await readFileAsDataUrl(file);
|
||||
const [, imageData = ""] = previewUrl.split(",", 2);
|
||||
if (!imageData) continue;
|
||||
additions.push({
|
||||
id: composerImageId(),
|
||||
name: file.name || `pasted-image-${composerImages.length + additions.length + 1}.png`,
|
||||
content_type: file.type || "image/png",
|
||||
image_data: imageData,
|
||||
preview_url: previewUrl,
|
||||
});
|
||||
}
|
||||
if (!additions.length) return;
|
||||
composerImages = [...composerImages, ...additions];
|
||||
renderComposerImages();
|
||||
}
|
||||
|
||||
function removeComposerImage(imageId) {
|
||||
composerImages = composerImages.filter((image) => image.id !== imageId);
|
||||
renderComposerImages();
|
||||
}
|
||||
|
||||
function clearComposerImages() {
|
||||
composerImages = [];
|
||||
renderComposerImages();
|
||||
}
|
||||
|
||||
function renderComposerImages() {
|
||||
if (!composerImagesEl) return;
|
||||
composerImagesEl.innerHTML = "";
|
||||
composerImagesEl.hidden = !composerImages.length;
|
||||
for (const image of composerImages) {
|
||||
const card = document.createElement("div");
|
||||
card.className = "composer-image";
|
||||
const preview = document.createElement("img");
|
||||
preview.src = image.preview_url;
|
||||
preview.alt = image.name || "Pasted image";
|
||||
const remove = document.createElement("button");
|
||||
remove.type = "button";
|
||||
remove.className = "composer-image-remove";
|
||||
remove.textContent = "×";
|
||||
remove.title = "Remove image";
|
||||
remove.addEventListener("click", () => removeComposerImage(image.id));
|
||||
const label = document.createElement("span");
|
||||
label.className = "composer-image-name";
|
||||
label.textContent = image.name || "Pasted image";
|
||||
card.append(preview, remove, label);
|
||||
composerImagesEl.appendChild(card);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -494,9 +592,13 @@ const configFieldIds = {
|
||||
};
|
||||
|
||||
const ollamaFieldIds = {
|
||||
model_provider: "model-provider",
|
||||
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",
|
||||
};
|
||||
|
||||
async function refreshConfig() {
|
||||
@@ -527,7 +629,12 @@ function renderConfig(config) {
|
||||
for (const [key, id] of Object.entries(ollamaFieldIds)) {
|
||||
const field = document.getElementById(id);
|
||||
if (!field) continue;
|
||||
field.value = values[key] ?? "";
|
||||
if (field.type === "password") {
|
||||
field.value = "";
|
||||
field.placeholder = secretsConfigured[key] ? "Configured" : "";
|
||||
} else {
|
||||
field.value = values[key] ?? "";
|
||||
}
|
||||
}
|
||||
configPathsEl.textContent = `App data: ${config.app_data_dir}\nConfig: ${config.config_path}\nLog: ${config.log_path}\nEdge profile: ${config.edge_profile_dir}`;
|
||||
configStatusEl.textContent = "";
|
||||
@@ -565,7 +672,7 @@ async function saveOllamaConfig(event) {
|
||||
if (!field) continue;
|
||||
values[key] = field.value;
|
||||
}
|
||||
setOllamaMessage("Saving Ollama config");
|
||||
setOllamaMessage("Saving provider config");
|
||||
try {
|
||||
const response = await fetch("/api/config", {
|
||||
method: "POST",
|
||||
@@ -577,46 +684,71 @@ async function saveOllamaConfig(event) {
|
||||
setOllamaMessage(result.message || "Saved");
|
||||
await refreshOllamaStatus();
|
||||
} catch (error) {
|
||||
setOllamaMessage(`Ollama config save failed: ${fetchErrorMessage(error)}`);
|
||||
setOllamaMessage(`Provider config save failed: ${fetchErrorMessage(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshOllamaStatus() {
|
||||
if (!ollamaStatusEl) return;
|
||||
ollamaStatusEl.textContent = "Checking Ollama";
|
||||
ollamaStatusEl.textContent = "Checking provider";
|
||||
try {
|
||||
const response = await fetch("/api/ollama/status");
|
||||
const status = await response.json();
|
||||
renderOllamaStatus(status);
|
||||
} catch (error) {
|
||||
ollamaStatusEl.textContent = `Ollama status failed: ${error.message}`;
|
||||
ollamaStatusEl.textContent = `Provider status failed: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderOllamaStatus(status) {
|
||||
if (!ollamaStatusEl) return;
|
||||
latestOllamaStatus = status;
|
||||
const provider = status.provider === "openai" ? "OpenAI" : "Ollama";
|
||||
const models = status.models?.length ? status.models.join(", ") : "None detected";
|
||||
const pillClass = status.installed && status.running && status.model_available ? "status-pill" : "status-pill warning";
|
||||
const ready = status.provider === "openai"
|
||||
? 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("URL", status.base_url || ""),
|
||||
];
|
||||
if (status.provider !== "openai") {
|
||||
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"));
|
||||
if (status.can_auto_install) detailItems.push(ollamaStatusItem("Auto Install", "Available"));
|
||||
if (status.num_ctx) detailItems.push(ollamaStatusItem("Context", status.num_ctx));
|
||||
} else {
|
||||
detailItems.splice(1, 0, ollamaStatusItem("Connected", status.online ? "Yes" : "No"));
|
||||
}
|
||||
ollamaStatusEl.innerHTML = `
|
||||
<div class="${pillClass}">${escapeHtml(status.message || "Unknown")}</div>
|
||||
<div class="ollama-status-grid">
|
||||
${ollamaStatusItem("Installed", status.installed ? "Yes" : "No")}
|
||||
${ollamaStatusItem("Running", status.running ? "Yes" : "No")}
|
||||
${ollamaStatusItem("Model", status.configured_model || "")}
|
||||
${ollamaStatusItem("Pulled", status.model_available ? "Yes" : "No")}
|
||||
${ollamaStatusItem("URL", status.base_url || "")}
|
||||
${status.can_auto_install ? ollamaStatusItem("Auto Install", "Available") : ""}
|
||||
${detailItems.join("")}
|
||||
</div>
|
||||
${ollamaStatusItem("Installed Models", models)}
|
||||
${ollamaStatusItem(status.provider === "openai" ? "Available Models" : "Installed Models", models)}
|
||||
${status.detail ? ollamaStatusItem("Detail", status.detail) : ""}
|
||||
`;
|
||||
if (ollamaDownloadButton) ollamaDownloadButton.hidden = status.provider === "openai";
|
||||
if (ollamaInstallButton) {
|
||||
ollamaInstallButton.hidden = !status.can_auto_install;
|
||||
ollamaInstallButton.hidden = status.provider === "openai" || !status.can_auto_install;
|
||||
ollamaInstallButton.disabled = Boolean(status.installed) || !status.can_auto_install;
|
||||
}
|
||||
if (ollamaLaunchButton) ollamaLaunchButton.disabled = !status.installed || Boolean(status.running);
|
||||
if (ollamaPullButton) ollamaPullButton.disabled = !status.running || Boolean(status.model_available);
|
||||
if (ollamaLaunchButton) {
|
||||
ollamaLaunchButton.hidden = status.provider === "openai";
|
||||
ollamaLaunchButton.disabled = !status.installed || Boolean(status.running);
|
||||
}
|
||||
if (ollamaPullButton) {
|
||||
ollamaPullButton.hidden = status.provider === "openai";
|
||||
ollamaPullButton.disabled = !status.running || Boolean(status.model_available);
|
||||
}
|
||||
if (openaiModelsRefreshButton) {
|
||||
openaiModelsRefreshButton.hidden = status.provider !== "openai";
|
||||
openaiModelsRefreshButton.disabled = false;
|
||||
}
|
||||
renderProviderModelOptions(status.models || []);
|
||||
updateOllamaAttention(status);
|
||||
}
|
||||
|
||||
@@ -659,12 +791,15 @@ function setOllamaButtonAttention(button, action, active) {
|
||||
function updateOllamaAttention(status = null) {
|
||||
const currentStatus = status || latestOllamaStatus;
|
||||
if (!currentStatus) return;
|
||||
const ready = Boolean(currentStatus.installed && currentStatus.running && currentStatus.model_available);
|
||||
const ready = currentStatus.provider === "openai"
|
||||
? Boolean(currentStatus.online && currentStatus.model_available)
|
||||
: Boolean(currentStatus.installed && currentStatus.running && currentStatus.model_available);
|
||||
ollamaToggle?.classList.toggle("attention-pulse", !ready);
|
||||
setOllamaButtonAttention(ollamaDownloadButton, "download", !currentStatus.installed);
|
||||
setOllamaButtonAttention(ollamaInstallButton, "install", !currentStatus.installed && currentStatus.can_auto_install);
|
||||
setOllamaButtonAttention(ollamaLaunchButton, "launch", currentStatus.installed && !currentStatus.running);
|
||||
setOllamaButtonAttention(ollamaPullButton, "pull", currentStatus.running && !currentStatus.model_available);
|
||||
setOllamaButtonAttention(ollamaDownloadButton, "download", currentStatus.provider !== "openai" && !currentStatus.installed);
|
||||
setOllamaButtonAttention(ollamaInstallButton, "install", currentStatus.provider !== "openai" && !currentStatus.installed && currentStatus.can_auto_install);
|
||||
setOllamaButtonAttention(ollamaLaunchButton, "launch", currentStatus.provider !== "openai" && currentStatus.installed && !currentStatus.running);
|
||||
setOllamaButtonAttention(ollamaPullButton, "pull", currentStatus.provider !== "openai" && currentStatus.running && !currentStatus.model_available);
|
||||
setOllamaButtonAttention(openaiModelsRefreshButton, "openai-models", currentStatus.provider === "openai" && !currentStatus.model_available);
|
||||
if (ready) clickedOllamaActions.clear();
|
||||
}
|
||||
|
||||
@@ -672,6 +807,31 @@ function configuredOllamaModel() {
|
||||
return document.getElementById("ollama-model")?.value || "";
|
||||
}
|
||||
|
||||
function renderProviderModelOptions(models) {
|
||||
const datalist = document.getElementById("provider-models");
|
||||
if (!datalist) return;
|
||||
datalist.innerHTML = "";
|
||||
for (const model of models) {
|
||||
const option = document.createElement("option");
|
||||
option.value = model;
|
||||
datalist.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshOpenAIModels() {
|
||||
setOllamaMessage("Loading OpenAI models");
|
||||
try {
|
||||
const response = await fetch("/api/openai/models");
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.detail || `HTTP ${response.status}`);
|
||||
renderProviderModelOptions(result.models || []);
|
||||
setOllamaMessage(result.message || "Loaded OpenAI models");
|
||||
await refreshOllamaStatus();
|
||||
} catch (error) {
|
||||
setOllamaMessage(`OpenAI models failed: ${fetchErrorMessage(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkForUpdate(promptUser = false) {
|
||||
if (!updateStatusEl) return;
|
||||
updateStatusEl.textContent = "Checking releases";
|
||||
@@ -1234,15 +1394,17 @@ async function checkHealth() {
|
||||
const response = await fetch("/api/health");
|
||||
const result = await response.json();
|
||||
const health = result.ollama || {};
|
||||
const provider = health.provider === "openai" ? "OpenAI" : "Ollama";
|
||||
ollamaOnline = Boolean(health.online);
|
||||
if (!ollamaOnline) {
|
||||
statusEl.textContent = "Offline";
|
||||
setWarning("Ollama needs attention. Open the Ollama tab and use the pulsing action button.");
|
||||
setWarning(`${provider} needs attention. Open the model provider tab and use the pulsing action button.`);
|
||||
ollamaToggle?.classList.add("attention-pulse");
|
||||
return false;
|
||||
}
|
||||
if (health.model_available === false) {
|
||||
setWarning(`Ollama needs the configured model "${health.model}". Open the Ollama tab and use Install Model.`);
|
||||
const action = health.provider === "openai" ? "Load OpenAI Models." : "Install Model.";
|
||||
setWarning(`${provider} needs the configured model "${health.model}". Open the model provider tab and use ${action}`);
|
||||
ollamaToggle?.classList.add("attention-pulse");
|
||||
} else {
|
||||
setWarning("");
|
||||
@@ -1253,7 +1415,7 @@ async function checkHealth() {
|
||||
} catch (error) {
|
||||
ollamaOnline = false;
|
||||
statusEl.textContent = "Offline";
|
||||
setWarning("Could not check Ollama health. Open the Ollama tab and use the pulsing action button.");
|
||||
setWarning("Could not check the active model provider. Open the model provider tab and use the pulsing action button.");
|
||||
ollamaToggle?.classList.add("attention-pulse");
|
||||
return false;
|
||||
}
|
||||
@@ -1426,6 +1588,23 @@ input.addEventListener("keydown", async (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener("paste", async (event) => {
|
||||
const clipboardItems = [...(event.clipboardData?.items || [])];
|
||||
const imageFiles = clipboardItems
|
||||
.filter((item) => item.kind === "file" && String(item.type || "").startsWith("image/"))
|
||||
.map((item) => item.getAsFile())
|
||||
.filter(Boolean);
|
||||
if (!imageFiles.length) return;
|
||||
if (!event.clipboardData?.getData("text/plain")) {
|
||||
event.preventDefault();
|
||||
}
|
||||
try {
|
||||
await addComposerImages(imageFiles);
|
||||
} catch (error) {
|
||||
setWarning(`Image paste failed: ${fetchErrorMessage(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
memoryRefreshButton?.addEventListener("click", refreshMemory);
|
||||
memoryClearButton?.addEventListener("click", clearMemory);
|
||||
configRefreshButton?.addEventListener("click", refreshConfig);
|
||||
@@ -1458,6 +1637,10 @@ ollamaPullButton?.addEventListener("click", () => {
|
||||
markOllamaActionClicked("pull");
|
||||
postOllamaAction("/api/ollama/pull", { body: { model: configuredOllamaModel() } });
|
||||
});
|
||||
openaiModelsRefreshButton?.addEventListener("click", () => {
|
||||
markOllamaActionClicked("openai-models");
|
||||
refreshOpenAIModels();
|
||||
});
|
||||
updateCheckButton?.addEventListener("click", checkForUpdate);
|
||||
updateInstallButton?.addEventListener("click", installUpdate);
|
||||
updateOpenReleasesButton?.addEventListener("click", openReleasesPage);
|
||||
@@ -1471,15 +1654,22 @@ updateModalInstall?.addEventListener("click", installUpdate);
|
||||
|
||||
async function sendMessage() {
|
||||
const message = input.value.trim();
|
||||
if (!message || input.disabled) return;
|
||||
const attachedImages = composerImages.map(({ name, content_type, image_data, preview_url }) => ({
|
||||
name,
|
||||
content_type,
|
||||
image_data,
|
||||
preview_url,
|
||||
}));
|
||||
if ((!message && !attachedImages.length) || input.disabled) return;
|
||||
const healthy = await checkHealth();
|
||||
if (!healthy) {
|
||||
addMessage("assistant warning-message", "Ollama needs attention before chat can continue. Open the Ollama tab and press the pulsing action button, then try again.");
|
||||
addMessage("assistant warning-message", "The active model provider needs attention before chat can continue. Open the model provider tab and press the pulsing action button, then try again.");
|
||||
return;
|
||||
}
|
||||
input.value = "";
|
||||
clearComposerImages();
|
||||
input.disabled = true;
|
||||
addMessage("user", message);
|
||||
addMessage("user", message, { images: attachedImages });
|
||||
const assistantNode = addMessage("assistant streaming", "");
|
||||
ensureStreamingChrome(assistantNode);
|
||||
let assistantText = "";
|
||||
@@ -1491,7 +1681,7 @@ async function sendMessage() {
|
||||
const response = await fetch("/api/chat/stream", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message, thread_id: currentThreadId }),
|
||||
body: JSON.stringify({ message, thread_id: currentThreadId, images: attachedImages }),
|
||||
});
|
||||
if (!response.ok || !response.body) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
@@ -1537,7 +1727,7 @@ async function sendMessage() {
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error.message.includes("503")
|
||||
? "Ollama needs attention before chat can continue. Open the Ollama tab and press the pulsing action button, then try again."
|
||||
? "The active model provider needs attention before chat can continue. Open the model provider tab and press the pulsing action button, then try again."
|
||||
: `Chat failed: ${error.message}`;
|
||||
setWarning(message);
|
||||
setMessageMarkdown(assistantNode, message);
|
||||
|
||||
+38
-24
@@ -57,12 +57,15 @@
|
||||
</header>
|
||||
<div class="warning" id="warning" hidden></div>
|
||||
<div class="messages" id="messages"></div>
|
||||
<div class="composer-wrap">
|
||||
<form class="composer" id="chat-form">
|
||||
<textarea id="message-input" rows="2" placeholder="Search listings, draft a reply, prepare an offer..."></textarea>
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="composer-wrap">
|
||||
<form class="composer" id="chat-form">
|
||||
<div class="composer-main">
|
||||
<textarea id="message-input" rows="2" placeholder="Search listings, draft a reply, prepare an offer..."></textarea>
|
||||
<div class="composer-images" id="composer-images" hidden></div>
|
||||
</div>
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<aside class="actions">
|
||||
<section class="side-section">
|
||||
@@ -70,21 +73,6 @@
|
||||
<div id="pending-actions" class="pending-empty">No pending actions</div>
|
||||
</section>
|
||||
<section class="side-section sidebar-tools">
|
||||
<div class="sidebar-tool-buttons" role="tablist" aria-label="Sidebar panels">
|
||||
<button class="sidebar-tool-button" id="settings-toggle" type="button" aria-expanded="false" aria-controls="settings-panel" title="Settings">
|
||||
<i data-lucide="settings" aria-hidden="true"></i>
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
<button class="sidebar-tool-button" id="memory-toggle" type="button" aria-expanded="false" aria-controls="memory-panel" title="Memory">
|
||||
<i data-lucide="brain" aria-hidden="true"></i>
|
||||
<span>Memory</span>
|
||||
</button>
|
||||
<button class="sidebar-tool-button" id="ollama-toggle" type="button" aria-expanded="false" aria-controls="ollama-panel" title="Ollama">
|
||||
<img class="sidebar-tool-image" src="/static/art/ollama-icon.svg" alt="" onerror="this.remove();">
|
||||
<i data-lucide="bot" aria-hidden="true"></i>
|
||||
<span>Ollama</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="sidebar-panel" id="settings-panel" hidden>
|
||||
<div class="section-title-row">
|
||||
<h2>Config</h2>
|
||||
@@ -131,14 +119,24 @@
|
||||
</div>
|
||||
<div class="sidebar-panel" id="ollama-panel" hidden>
|
||||
<div class="section-title-row">
|
||||
<h2>Ollama</h2>
|
||||
<h2>Model Provider</h2>
|
||||
<button class="secondary small-button" id="ollama-refresh" type="button">Refresh</button>
|
||||
</div>
|
||||
<form class="config-form" id="ollama-config-form">
|
||||
<label>Provider
|
||||
<select id="model-provider" name="model_provider">
|
||||
<option value="ollama">Ollama</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Ollama URL<input id="ollama-base-url" name="ollama_base_url" type="text"></label>
|
||||
<label>Model<input id="ollama-model" name="ollama_model" type="text"></label>
|
||||
<label>Ollama Model<input id="ollama-model" name="ollama_model" type="text" list="provider-models"></label>
|
||||
<label>Context Tokens<input id="ollama-num-ctx" name="ollama_num_ctx" type="number" min="1024" step="1024"></label>
|
||||
<button type="submit">Save Ollama Config</button>
|
||||
<label>OpenAI URL<input id="openai-base-url" name="openai_base_url" type="text"></label>
|
||||
<label>OpenAI API Key<input id="openai-api-key" name="openai_api_key" type="password" autocomplete="off"></label>
|
||||
<label>OpenAI Model<input id="openai-model" name="openai_model" type="text" list="provider-models"></label>
|
||||
<datalist id="provider-models"></datalist>
|
||||
<button type="submit">Save Provider Config</button>
|
||||
</form>
|
||||
<div class="ollama-status" id="ollama-status"></div>
|
||||
<div class="ollama-actions">
|
||||
@@ -146,9 +144,25 @@
|
||||
<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="openai-models-refresh" type="button">Load OpenAI Models</button>
|
||||
</div>
|
||||
<div class="config-status" id="ollama-message"></div>
|
||||
</div>
|
||||
<div class="sidebar-tool-buttons" role="tablist" aria-label="Sidebar panels">
|
||||
<button class="sidebar-tool-button" id="settings-toggle" type="button" aria-expanded="false" aria-controls="settings-panel" title="Settings">
|
||||
<i data-lucide="settings" aria-hidden="true"></i>
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
<button class="sidebar-tool-button" id="memory-toggle" type="button" aria-expanded="false" aria-controls="memory-panel" title="Memory">
|
||||
<i data-lucide="brain" aria-hidden="true"></i>
|
||||
<span>Memory</span>
|
||||
</button>
|
||||
<button class="sidebar-tool-button" id="ollama-toggle" type="button" aria-expanded="false" aria-controls="ollama-panel" title="Ollama">
|
||||
<img class="sidebar-tool-image" src="/static/art/ollama-icon.svg" alt="" onerror="this.remove();">
|
||||
<i data-lucide="bot" aria-hidden="true"></i>
|
||||
<span>Ollama</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
+98
-4
@@ -269,6 +269,8 @@ body::before {
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 28px;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
@@ -555,6 +557,38 @@ h2 {
|
||||
background: rgba(255, 250, 240, 0.96);
|
||||
}
|
||||
|
||||
.message-images {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.message-image {
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(88, 66, 47, 0.18);
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
|
||||
.message-image img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.message-image-label {
|
||||
display: block;
|
||||
padding: 8px 10px;
|
||||
color: #6d5b4e;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.message.warning-message {
|
||||
border-color: rgba(212, 175, 55, 0.6);
|
||||
background: #f5eac4;
|
||||
@@ -720,6 +754,60 @@ h2 {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.composer-main {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.composer-images {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.composer-image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(88, 66, 47, 0.16);
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
box-shadow: 0 12px 26px rgba(38, 58, 27, 0.08);
|
||||
}
|
||||
|
||||
.composer-image img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.composer-image-name {
|
||||
display: block;
|
||||
padding: 8px 10px 10px;
|
||||
color: #6d5b4e;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.composer-image-remove {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
min-height: 28px;
|
||||
padding: 0;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(88, 66, 47, 0.18);
|
||||
background: rgba(255, 250, 240, 0.92);
|
||||
color: var(--brown);
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 58px;
|
||||
@@ -1005,7 +1093,7 @@ button.secondary {
|
||||
}
|
||||
|
||||
.side-section {
|
||||
margin-bottom: 28px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.side-section + .side-section {
|
||||
@@ -1014,8 +1102,14 @@ button.secondary {
|
||||
}
|
||||
|
||||
.sidebar-tools {
|
||||
display: grid;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
margin-top: auto;
|
||||
position: sticky;
|
||||
bottom: -28px;
|
||||
padding-bottom: 28px;
|
||||
background: linear-gradient(180deg, rgba(247, 241, 220, 0) 0%, var(--cream) 22%, var(--cream) 100%);
|
||||
}
|
||||
|
||||
.sidebar-tool-buttons {
|
||||
@@ -1110,8 +1204,8 @@ button.secondary {
|
||||
}
|
||||
|
||||
.sidebar-panel {
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--line);
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.config-form {
|
||||
|
||||
Reference in New Issue
Block a user