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:
12
go.sum
12
go.sum
@@ -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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:ts9VMZGj8C6688xIgBU9/Tyw2WBl55WfdVP2zG+EV98=
|
||||||
github.com/revrost/go-openrouter v0.2.4/go.mod h1:ZH/UdpnDEdMmJwq8tbSTX1S5I07ee8KMlEYN4jmegU0=
|
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=
|
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/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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
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 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
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.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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.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.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 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
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 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
@@ -404,6 +404,10 @@ body:not(.loading) #loading {
|
|||||||
color: #ed8796;
|
color: #ed8796;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message .text .error a {
|
||||||
|
color: #ee99a0;
|
||||||
|
}
|
||||||
|
|
||||||
.message.errored {
|
.message.errored {
|
||||||
border: 2px solid #ed8796;
|
border: 2px solid #ed8796;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,7 @@
|
|||||||
<script src="lib/marked.min.js"></script>
|
<script src="lib/marked.min.js"></script>
|
||||||
<script src="lib/morphdom.min.js"></script>
|
<script src="lib/morphdom.min.js"></script>
|
||||||
<script src="js/lib.js"></script>
|
<script src="js/lib.js"></script>
|
||||||
|
<script src="js/storage.js"></script>
|
||||||
<script src="js/markdown.js"></script>
|
<script src="js/markdown.js"></script>
|
||||||
<script src="js/dropdown.js"></script>
|
<script src="js/dropdown.js"></script>
|
||||||
<script src="js/chat.js"></script>
|
<script src="js/chat.js"></script>
|
||||||
|
|||||||
@@ -723,7 +723,7 @@
|
|||||||
data.collapsed = true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -868,14 +868,14 @@
|
|||||||
this.#save();
|
this.#save();
|
||||||
}
|
}
|
||||||
|
|
||||||
showError(error) {
|
setError(error) {
|
||||||
this.#error = error;
|
this.#error = error || "Something went wrong";
|
||||||
|
|
||||||
this.#_message.classList.add("errored");
|
this.#_message.classList.add("errored", "has-text");
|
||||||
|
|
||||||
const _err = make("div", "error");
|
const _err = make("div", "error");
|
||||||
|
|
||||||
_err.textContent = this.#error;
|
_err.innerHTML = renderInline(this.#error);
|
||||||
|
|
||||||
this.#_text.appendChild(_err);
|
this.#_text.appendChild(_err);
|
||||||
|
|
||||||
@@ -1226,7 +1226,7 @@
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
case "error":
|
case "error":
|
||||||
message.showError(chunk.text);
|
message.setError(chunk.text);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1326,7 +1326,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
const data = await json("/-/data");
|
const [_, data] = await Promise.all([connectDB(), json("/-/data")]);
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
notify("Failed to load data.", true);
|
notify("Failed to load data.", true);
|
||||||
@@ -1394,6 +1394,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function restore() {
|
function restore() {
|
||||||
|
const resizedHeight = loadValue("resized");
|
||||||
|
|
||||||
|
if (resizedHeight) {
|
||||||
|
$chat.style.height = `${resizedHeight}px`;
|
||||||
|
}
|
||||||
|
|
||||||
$message.value = loadValue("message", "");
|
$message.value = loadValue("message", "");
|
||||||
$role.value = loadValue("role", "user");
|
$role.value = loadValue("role", "user");
|
||||||
$model.value = loadValue("model", modelList.length ? modelList[0].id : "");
|
$model.value = loadValue("model", modelList.length ? modelList[0].id : "");
|
||||||
@@ -1424,13 +1430,13 @@
|
|||||||
loadValue("messages", []).forEach(message => {
|
loadValue("messages", []).forEach(message => {
|
||||||
const obj = new Message(message.role, message.reasoning, message.text, message.tool, message.files || [], message.images || [], message.tags || [], message.collapsed);
|
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) {
|
if (message.statistics) {
|
||||||
obj.setStatistics(message.statistics);
|
obj.setStatistics(message.statistics);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.error) {
|
||||||
|
obj.setError(message.error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
chatTitle = loadValue("title");
|
chatTitle = loadValue("title");
|
||||||
@@ -1919,12 +1925,6 @@
|
|||||||
dropdown($role);
|
dropdown($role);
|
||||||
dropdown($reasoningEffort);
|
dropdown($reasoningEffort);
|
||||||
|
|
||||||
const resizedHeight = loadValue("resized");
|
|
||||||
|
|
||||||
if (resizedHeight) {
|
|
||||||
$chat.style.height = `${resizedHeight}px`;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadData().then(() => {
|
loadData().then(() => {
|
||||||
restore();
|
restore();
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,5 @@
|
|||||||
/** biome-ignore-all lint/correctness/noUnusedVariables: utility */
|
/** 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) {
|
function schedule(cb) {
|
||||||
if (document.visibilityState === "visible") {
|
if (document.visibilityState === "visible") {
|
||||||
requestAnimationFrame(cb);
|
requestAnimationFrame(cb);
|
||||||
|
|||||||
@@ -158,4 +158,8 @@
|
|||||||
window.render = markdown => {
|
window.render = markdown => {
|
||||||
return marked.parse(markdown);
|
return marked.parse(markdown);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.renderInline = markdown => {
|
||||||
|
return marked.parseInline(markdown.trim());
|
||||||
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
156
static/js/storage.js
Normal file
156
static/js/storage.js
Normal 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);
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user