diff --git a/backend/src/app.py b/backend/src/app.py index 5a4920f..0c49a12 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -318,12 +318,12 @@ class App(ChatMixin): print(f"Deleted model: {model_id}") return None else: - model = await db.Model.update_or_create( - id=model_id, + await db.Model.filter(id=model_id).update( name=name, kind=kind, value=value, ) + model = await db.Model.get(id=model_id) print(f"Saved model: {model.id} - {name}") return model diff --git a/backend/src/server.py b/backend/src/server.py index f74d10b..04669af 100644 --- a/backend/src/server.py +++ b/backend/src/server.py @@ -184,7 +184,7 @@ async def start_server(app): elif action == "get-models": 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 }) diff --git a/frontend/src/backend.js b/frontend/src/backend.js index fb6bb30..057d61b 100644 --- a/frontend/src/backend.js +++ b/frontend/src/backend.js @@ -354,8 +354,11 @@ export function invalidateCache({ chats=false, agents=false } = {}) { // --- Connection --- export function onConnect (callback) { - // TODO: return cancel function connectListeners.push(callback); + if (socket && socket.readyState === WebSocket.OPEN) { + callback(); + } + // TODO: return cancel function } function reconnect() { diff --git a/frontend/src/runs.js b/frontend/src/runs.js index ce04913..95c0c4d 100644 --- a/frontend/src/runs.js +++ b/frontend/src/runs.js @@ -87,12 +87,12 @@ async function init() { // Listen for connection establishment const res = await backend.sendAction("get-agent-runs"); runs = res.runs || []; - + // Listen for run events unsubscribeRunEvents = backend.onAction('run-event', updateRun); } -init(); +backend.onConnect(init); // Get all runs export function getRuns() { diff --git a/frontend/src/views/Models.vue b/frontend/src/views/Models.vue index 2acff63..8cad962 100644 --- a/frontend/src/views/Models.vue +++ b/frontend/src/views/Models.vue @@ -10,23 +10,35 @@ import * as ui from '../ui'; const models = ref([]); + const expandedModelId = ref(new Set()); 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 () => { 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 () { - let current = Object.assign({}, editModel.value); + let current = Object.assign({editing: {}}, editModel.value); if (!current.name) { ui.alert("Missing name"); return; } + /* try { current.value = JSON.parse(current.inputValue); delete current.inputValue; @@ -34,24 +46,61 @@ ui.alert("Bad value: " + exception.toString()); return; } + */ let res = await backend.sendAction("save-model", current); if (res.modelId) { current.id = res.modelId; - models.value.push(current) + models.value.push(current); + createExpanded.value = false; + editModel.value = defaultModel(); } else { ui.alert("bad response: " + JSON.stringify(res)); } } - function viewModel (model) { - alert(JSON.stringify(model)); - } - async function deleteModel (model) { let res = await backend.sendAction("save-model", {modelId: model.id, value: null}); 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); + }; + + .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; + } + \ No newline at end of file diff --git a/run.sh b/run.sh index 755371a..6f238fe 100644 --- a/run.sh +++ b/run.sh @@ -5,6 +5,7 @@ source .venv/bin/activate if [ -f .env ]; then source .env export REPLICATE_API_TOKEN + export OPENROUTER_API_KEY else echo "No .env file found." echo "Write a .env file with the variables: REPLICATE_API_TOKEN"