feat: fix and improve model list ui and features

This commit is contained in:
Shivering Ocelot committed 2026-06-01 13:56:10 -05:00
1 parent 9caf057a68
commit 7058cf0771
6 files changed
+228 -31

No files matched your search

+2 -2
View File
@@ -318,12 +318,12 @@ class App(ChatMixin):
print(f"Deleted model: {model_id}") print(f"Deleted model: {model_id}")
return None return None
else: else:
model = await db.Model.update_or_create( await db.Model.filter(id=model_id).update(
id=model_id,
name=name, name=name,
kind=kind, kind=kind,
value=value, value=value,
) )
model = await db.Model.get(id=model_id)
print(f"Saved model: {model.id} - {name}") print(f"Saved model: {model.id} - {name}")
return model return model
+1 -1
View File
@@ -184,7 +184,7 @@ async def start_server(app):
elif action == "get-models": elif action == "get-models":
await send({ await send({
'models': [{"id": model.id, "name": model.name, "value": model.value} for model in await app.get_models()], 'models': [{"id": model.id, "name": model.name, "value": model.value, "kind": model.kind} for model in await app.get_models()],
'nonce': nonce 'nonce': nonce
}) })
+4 -1
View File
@@ -354,8 +354,11 @@ export function invalidateCache({ chats=false, agents=false } = {}) {
// --- Connection --- // --- Connection ---
export function onConnect (callback) { export function onConnect (callback) {
// TODO: return cancel function
connectListeners.push(callback); connectListeners.push(callback);
if (socket && socket.readyState === WebSocket.OPEN) {
callback();
}
// TODO: return cancel function
} }
function reconnect() { function reconnect() {
+1 -1
View File
@@ -92,7 +92,7 @@ async function init() {
unsubscribeRunEvents = backend.onAction('run-event', updateRun); unsubscribeRunEvents = backend.onAction('run-event', updateRun);
} }
init(); backend.onConnect(init);
// Get all runs // Get all runs
export function getRuns() { export function getRuns() {
+219 -26
View File
@@ -10,23 +10,35 @@
import * as ui from '../ui'; import * as ui from '../ui';
const models = ref([]); const models = ref([]);
const expandedModelId = ref(new Set());
const createExpanded = ref(false); const createExpanded = ref(false);
const newKeyValue = ref(false);
const editModel = ref({kind: 'text', name: null, inputValue: '{"engine":"llama-cpp","model":"modelfile.gguf"}'}); function defaultModel () { return {kind: 'text', name: null, value: {engine:"llama-cpp", model:"modelfile.gguf"}}; }
const editModel = ref(defaultModel());
onMounted(async () => { onMounted(async () => {
let res = await backend.sendAction("get-models"); let res = await backend.sendAction("get-models");
models.value = res.models; models.value = res.models.map(entry => ({editing: {}, ...entry}));
}); });
const toggleModel = (id) => {
if (expandedModelId.value.has(id)) {
expandedModelId.value.delete(id);
} else {
expandedModelId.value.add(id);
}
};
async function createModel () { async function createModel () {
let current = Object.assign({}, editModel.value); let current = Object.assign({editing: {}}, editModel.value);
if (!current.name) { if (!current.name) {
ui.alert("Missing name"); ui.alert("Missing name");
return; return;
} }
/*
try { try {
current.value = JSON.parse(current.inputValue); current.value = JSON.parse(current.inputValue);
delete current.inputValue; delete current.inputValue;
@@ -34,24 +46,61 @@
ui.alert("Bad value: " + exception.toString()); ui.alert("Bad value: " + exception.toString());
return; return;
} }
*/
let res = await backend.sendAction("save-model", current); let res = await backend.sendAction("save-model", current);
if (res.modelId) { if (res.modelId) {
current.id = res.modelId; current.id = res.modelId;
models.value.push(current) models.value.push(current);
createExpanded.value = false;
editModel.value = defaultModel();
} else { } else {
ui.alert("bad response: " + JSON.stringify(res)); ui.alert("bad response: " + JSON.stringify(res));
} }
} }
function viewModel (model) {
alert(JSON.stringify(model));
}
async function deleteModel (model) { async function deleteModel (model) {
let res = await backend.sendAction("save-model", {modelId: model.id, value: null}); let res = await backend.sendAction("save-model", {modelId: model.id, value: null});
ui.alert(res.message); ui.alert(res.message);
// Remove the model from the list
models.value = models.value.filter(m => m.id !== model.id);
} }
function editKey (model, key) {
model.editing[key] = JSON.stringify(model.value[key]);
}
async function acceptKey (model, key) {
try {
let value = JSON.parse(model.editing[key]);
model.value[key] = value;
cancelKey(model, key);
let res = await backend.sendAction("save-model", {
modelId: model.id,
kind: model.kind,
name: model.name,
value: model.value,
});
} catch (exception) {
ui.alert("Bad value: " + exception.toString());
return;
}
}
const cancelKey = (model, key) => {
delete model.editing[key];
};
const deleteKey = (model, key) => {
if (confirm(`delete key ${key} with value ${JSON.stringify(model.value[key])}`)) {
delete model.value[key];
}
};
const createKey = (model, key) => {
model.value[key] = null;
editKey(model, key);
};
</script> </script>
<template> <template>
@@ -60,33 +109,177 @@
<h1>Models</h1> <h1>Models</h1>
</TitleBar> </TitleBar>
<div class="entry" v-for="model in models"> <div v-if="models.length === 0" class="empty">
{{ model.name }} No models found.
<button @click="viewModel(model)">View</button>
<button @click="deleteModel(model)">Delete</button>
</div> </div>
<div class="form-container"> <div v-else v-for="model in models" :key="model.id" class="model-entry">
<template v-if="createExpanded"> <div class="model-header" @click="toggleModel(model.id)">
<p>Kinds is only text for now. Value must be like:</p> <span class="model-name">{{ model.name }}</span>
<pre>{}</pre> <span class="model-kind">{{ model.kind }}</span>
<input type="text" v-model="editModel.kind" placeholder="kind"> <div class="model-actions">
<input type="text" v-model="editModel.name" placeholder="name"> <button @click.stop="deleteModel(model)">Delete</button>
<textarea v-model="editModel.inputValue" placeholder="{}"></textarea> </div>
<div> </div>
<div v-if="expandedModelId.has(model.id)" class="model-details">
<div class="detail-row">
<div class="detail-key">ID</div>
<div class="detail-value">{{ model.id }}</div>
</div>
<div class="detail-row">
<div class="detail-key">Kind</div>
<div class="detail-value">{{ model.kind }}</div>
</div>
<div v-for="(value, key) in model.value" :key="key" class="detail-row">
<div class="detail-key">{{ key }}</div>
<template v-if="model.editing[key]">
<textarea v-model="model.editing[key]"></textarea>
<div class="detail-actions">
<button @click="acceptKey(model, key)">Accept</button>
<button @click="cancelKey(model, key)">Cancel</button>
</div>
</template>
<template v-else>
<pre class="detail-pre">{{ JSON.stringify(value, null, 2) }}</pre>
<div class="detail-actions">
<button @click="editKey(model, key)">Edit</button>
<button @click="deleteKey(model, key)">Delete</button>
</div>
</template>
</div>
<div class="detail-row add-value-row">
<input type="text" v-model="newKeyValue" placeholder="key">
<button @click="createKey(model, newKeyValue)">Add</button>
</div>
</div>
</div>
<div class="model-entry">
<div class="model-header" @click="createExpanded = !createExpanded">
<span class="model-name">Create Model</span>
</div>
<div v-if="createExpanded" class="model-details">
<div class="detail-row">
<div class="detail-key">Kind</div>
<div class="detail-value"><input type="text" v-model="editModel.kind"></div>
</div>
<div class="detail-row">
<div class="detail-key">Name</div>
<div class="detail-value"><input type="text" v-model="editModel.name"></div>
</div>
<div class="detail-row">
<div class="detail-key">Engine</div>
<div class="detail-value"><input type="text" v-model="editModel.value.engine"></div>
</div>
<div class="detail-row">
<div class="detail-key">Model</div>
<div class="detail-value"><input type="text" v-model="editModel.value.model"></div>
</div>
<div class="detail-row">
<button @click="createModel()">Create</button> <button @click="createModel()">Create</button>
<button @click="createExpanded=false">Cancel</button> <button @click="createExpanded=false">Cancel</button>
</div> </div>
</template>
<button @click="createExpanded=true" v-else>Create Model</button>
</div> </div>
</div>
</main> </main>
</template> </template>
<style scoped> <style scoped>
.form-container { .empty {
display: flex; padding: 1em;
flex-direction: column; color: var(--color-text-mute);
}
.model-entry {
border-bottom: 1px solid var(--color-border);
}
.model-header {
padding: 0.5em;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
}
.model-header:hover {
background-color: var(--color-background-hover);
}
.model-name {
font-weight: bold;
color: var(--color-text-heading);
flex: 1;
}
.model-kind {
color: var(--color-text-mute);
font-size: 0.85em;
margin-right: 1em;
}
.model-actions {
display: flex;
gap: 0.5em;
}
.model-details {
padding: 0.5em 1em;
background-color: var(--color-background-mute);
}
.detail-row {
margin-bottom: 0.5em;
display: flex;
}
.detail-key {
font-weight: bold;
color: var(--color-text-heading);
width: 120px;
}
.detail-value {
color: var(--color-text);
flex: 1;
}
.detail-pre {
margin: 0;
padding: 0.5em;
background-color: var(--color-background-muted);
border-radius: 4px;
white-space: pre-wrap;
word-break: break-all;
font-size: 0.85em;
flex: 1;
}
.form-container {
margin-top: 1em;
padding: 1em;
border-top: 1px solid var(--color-border);
}
.form-container input, .form-container textarea {
display: block;
width: 100%;
padding: 0.5em;
margin: 0.5em 0;
border: 1px solid var(--color-border);
border-radius: 4px;
}
.form-container textarea {
min-height: 100px;
resize: vertical;
}
.form-actions {
display: flex;
gap: 0.5em;
margin-top: 0.5em;
} }
.entry {}
</style> </style>
+1
View File
@@ -5,6 +5,7 @@ source .venv/bin/activate
if [ -f .env ]; then if [ -f .env ]; then
source .env source .env
export REPLICATE_API_TOKEN export REPLICATE_API_TOKEN
export OPENROUTER_API_KEY
else else
echo "No .env file found." echo "No .env file found."
echo "Write a .env file with the variables: REPLICATE_API_TOKEN" echo "Write a .env file with the variables: REPLICATE_API_TOKEN"