1
0
mirror of https://github.com/coalaura/whiskr.git synced 2025-12-02 20:22:52 +00:00

use index db & various fixes

This commit is contained in:
Laura
2025-09-12 20:52:10 +02:00
parent 2d6b618cf1
commit 57418ccbd4
7 changed files with 182 additions and 59 deletions

12
go.sum
View File

@@ -22,12 +22,6 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/revrost/go-openrouter v0.2.2 h1:7bOdLPKmw0iJB1AdpN+YaWUd2XC9cwfJKDY10iaSAzI=
github.com/revrost/go-openrouter v0.2.2/go.mod h1:ZH/UdpnDEdMmJwq8tbSTX1S5I07ee8KMlEYN4jmegU0=
github.com/revrost/go-openrouter v0.2.3 h1:ollIaPrgVWgqJyKbJGSX1jFs66eAWJs8Ojrxnd2i/E0=
github.com/revrost/go-openrouter v0.2.3/go.mod h1:ZH/UdpnDEdMmJwq8tbSTX1S5I07ee8KMlEYN4jmegU0=
github.com/revrost/go-openrouter v0.2.4-0.20250909110314-b8c4ee4c5861 h1:4XU64nIgj6l9659KJx+FOaABvdhM3YrytCgD8XoKu90=
github.com/revrost/go-openrouter v0.2.4-0.20250909110314-b8c4ee4c5861/go.mod h1:ZH/UdpnDEdMmJwq8tbSTX1S5I07ee8KMlEYN4jmegU0=
github.com/revrost/go-openrouter v0.2.4 h1:ts9VMZGj8C6688xIgBU9/Tyw2WBl55WfdVP2zG+EV98=
github.com/revrost/go-openrouter v0.2.4/go.mod h1:ZH/UdpnDEdMmJwq8tbSTX1S5I07ee8KMlEYN4jmegU0=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
@@ -35,20 +29,14 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -404,6 +404,10 @@ body:not(.loading) #loading {
color: #ed8796;
}
.message .text .error a {
color: #ee99a0;
}
.message.errored {
border: 2px solid #ed8796;
}

View File

@@ -129,6 +129,7 @@
<script src="lib/marked.min.js"></script>
<script src="lib/morphdom.min.js"></script>
<script src="js/lib.js"></script>
<script src="js/storage.js"></script>
<script src="js/markdown.js"></script>
<script src="js/dropdown.js"></script>
<script src="js/chat.js"></script>

View File

@@ -723,7 +723,7 @@
data.collapsed = true;
}
if (!data.images?.length && !data.files?.length && !data.reasoning && !data.text && !data.tool) {
if (!data.error && !data.images?.length && !data.files?.length && !data.reasoning && !data.text && !data.tool) {
return false;
}
@@ -868,14 +868,14 @@
this.#save();
}
showError(error) {
this.#error = error;
setError(error) {
this.#error = error || "Something went wrong";
this.#_message.classList.add("errored");
this.#_message.classList.add("errored", "has-text");
const _err = make("div", "error");
_err.textContent = this.#error;
_err.innerHTML = renderInline(this.#error);
this.#_text.appendChild(_err);
@@ -1226,7 +1226,7 @@
break;
case "error":
message.showError(chunk.text);
message.setError(chunk.text);
break;
}
@@ -1326,7 +1326,7 @@
}
async function loadData() {
const data = await json("/-/data");
const [_, data] = await Promise.all([connectDB(), json("/-/data")]);
if (!data) {
notify("Failed to load data.", true);
@@ -1394,6 +1394,12 @@
}
function restore() {
const resizedHeight = loadValue("resized");
if (resizedHeight) {
$chat.style.height = `${resizedHeight}px`;
}
$message.value = loadValue("message", "");
$role.value = loadValue("role", "user");
$model.value = loadValue("model", modelList.length ? modelList[0].id : "");
@@ -1424,13 +1430,13 @@
loadValue("messages", []).forEach(message => {
const obj = new Message(message.role, message.reasoning, message.text, message.tool, message.files || [], message.images || [], message.tags || [], message.collapsed);
if (message.error) {
obj.showError(message.error);
}
if (message.statistics) {
obj.setStatistics(message.statistics);
}
if (message.error) {
obj.setError(message.error);
}
});
chatTitle = loadValue("title");
@@ -1919,12 +1925,6 @@
dropdown($role);
dropdown($reasoningEffort);
const resizedHeight = loadValue("resized");
if (resizedHeight) {
$chat.style.height = `${resizedHeight}px`;
}
loadData().then(() => {
restore();

View File

@@ -1,35 +1,5 @@
/** biome-ignore-all lint/correctness/noUnusedVariables: utility */
function storeValue(key, value = false) {
if (value === null || value === undefined || value === false) {
localStorage.removeItem(key);
return;
}
localStorage.setItem(key, JSON.stringify(value));
}
function loadValue(key, fallback = false) {
const raw = localStorage.getItem(key);
if (raw === null) {
return fallback;
}
try {
const value = JSON.parse(raw);
if (value === null) {
throw new Error("no value");
}
return value;
} catch {}
return fallback;
}
function schedule(cb) {
if (document.visibilityState === "visible") {
requestAnimationFrame(cb);

View File

@@ -158,4 +158,8 @@
window.render = markdown => {
return marked.parse(markdown);
};
window.renderInline = markdown => {
return marked.parseInline(markdown.trim());
};
})();

156
static/js/storage.js Normal file
View File

@@ -0,0 +1,156 @@
/** biome-ignore-all lint/correctness/noUnusedVariables: utility */
(() => {
const DatabaseName = "whiskr",
StorageName = "chat";
function isNull(value) {
return value === null || value === false || value === undefined;
}
class Database {
#database;
#scheduled = new Map();
#writes = new Map();
#cache = new Map();
static async new() {
const db = new Database();
await db.#connect();
await db.#load();
return db;
}
#connect() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DatabaseName, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.#database = request.result;
resolve();
};
request.onupgradeneeded = event => {
const db = event.target.result;
if (db.objectStoreNames.contains(StorageName)) {
return;
}
db.createObjectStore(StorageName);
};
});
}
#load() {
return new Promise((resolve, reject) => {
const transaction = this.#database.transaction(StorageName, "readonly"),
store = transaction.objectStore(StorageName),
request = store.openCursor();
request.onerror = () => reject(request.error);
let total = 0;
request.onsuccess = event => {
const cursor = event.target.result;
if (cursor) {
if (!isNull(cursor.value)) {
this.#cache.set(cursor.key, cursor.value);
total++;
}
cursor.continue();
} else {
console.info(`Loaded ${total} items from IndexedDB`);
resolve();
}
};
});
}
#write(key, retry) {
if (this.#writes.has(key)) {
if (retry) {
this.#schedule(key);
}
return;
}
this.#writes.set(key, true);
try {
const transaction = this.#database.transaction(StorageName, "readwrite"),
store = transaction.objectStore(StorageName);
const value = this.#cache.get(key);
if (isNull(value)) {
store.delete(key);
} else {
store.put(value, key);
}
return new Promise((resolve, reject) => {
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
});
} catch (error) {
console.error(`Failed to write to IndexedDB: ${error}`);
} finally {
this.#writes.delete(key);
}
}
#schedule(key) {
if (this.#scheduled.has(key)) {
clearTimeout(this.#scheduled.get(key));
}
const timeout = setTimeout(() => {
this.#scheduled.delete(key);
this.#write(key, true);
}, 500);
this.#scheduled.set(key, timeout);
}
store(key, value = false) {
if (isNull(value)) {
this.#cache.delete(key);
} else {
this.#cache.set(key, value);
}
this.#schedule(key);
}
load(key, fallback = false) {
if (!this.#cache.has(key)) {
return fallback;
}
return this.#cache.get(key);
}
}
let db;
window.connectDB = async () => {
db = await Database.new();
};
window.storeValue = (key, value = false) => db.store(key, value);
window.loadValue = (key, fallback = false) => db.load(key, fallback);
})();