Agent creation and editing support via UI

This commit is contained in:
Shivering Ocelot committed 2025-07-13 03:53:43 -05:00
1 parent f8edd01c37
commit 0f7384cc1d
7 files changed
+267 -15

No files matched your search

+4
View File
@@ -7,6 +7,10 @@
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
<!--link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.6.0/css/fontawesome.min.css" integrity="sha384-NvKbDTEnL+A8F/AA5Tc5kmMLSJHUO868P+lDtTpJIeQdGYaUIuLr4lVGOEA1OcMy" crossorigin="anonymous"-->
<!-- Checkout https://icons.getbootstrap.com, may be a good alternative -->
<title>Horde Chat</title>
<script type="module" src="/src/main.js"></script>
+12 -3
View File
@@ -243,7 +243,7 @@ export async function getChats() {
if (response && response.chats) {
cache.chats = response.chats.map(msg => new ChatInfo({
id: msg.id,
name: msg.name?.length > 0 ? msg.name : `[Chat ${msg.id}]`,
name: msg.name || `<chat-${msg.id}>`,
}));
} else {
throw new Error("Invalid response for get-chats: " + JSON.stringify(response));
@@ -254,7 +254,7 @@ export async function getChats() {
export async function getChat(chatId) {
const allChats = await getChats();
return allChats.find(chat => chat.id === chatId) || null;
return allChats.find(it => it.id == chatId);
}
export async function getAgents() {
@@ -263,7 +263,11 @@ export async function getAgents() {
if (response && response.agents) {
cache.agents = response.agents.map(msg => ({
id: msg.id,
name: msg.name?.length > 0 ? msg.name : `<agent-${msg.id}>`,
name: msg.name || `<agent-${msg.id}>`,
agentname: msg.agentname,
description: msg.description,
picture: msg.picture,
kind: msg.kind,
}));
} else {
throw new Error("Invalid response for get-agents: " + JSON.stringify(response));
@@ -272,6 +276,11 @@ export async function getAgents() {
return cache.agents;
}
export async function getAgent(agentId) {
const allAgents = await getAgents();
return allAgents.find(it => it.id == agentId);
}
export function invalidateCache({ chats=false, agents=false } = {}) {
if (chats) cache.chats = null;
if (agents) cache.agents = null;
+6
View File
@@ -33,7 +33,13 @@ const router = createRouter({
path: `/chat/create`,
component: () => import('../views/ChatCreate.vue')
},
{
name: 'agent-create',
path: `/agent/create`,
component: () => import('../views/AgentCreate.vue')
},
objectView('chat', () => import('../views/Chat.vue')),
objectView('agent', () => import('../views/Agent.vue')),
],
})
+150
View File
@@ -0,0 +1,150 @@
<script setup>
import {
ref, watch,
onMounted, onUnmounted,
} from "vue";
import * as backend from '../backend';
import * as ui from '../ui';
const props = defineProps({
id: {
type: String,
required: true,
},
});
const agent = ref({});
const kindInfo = ref({});
const configFields = ref([]);
// === State Management === //
async function loadAgentData (agentId) {
const _agent = await backend.getAgent(agentId);
if (!_agent) {
ui.alert("Agent not found");
return;
}
agent.value = {
id: _agent.id,
name: _agent.name,
agentname: _agent.agentname,
description: _agent.description,
picture: _agent.picture,
};
backend.sendAction("get-agent-kind-info", {kind: _agent.kind}).then(res => {
kindInfo.description = res.info.description;
});
backend.sendAction('get-agent-config-fields', {agentId}).then(res => {
configFields.value = res.fields.map(field => ({
name: field.name,
type: field.type,
value: field.value,
default: null,
enabled: field.value != null,
}));
});
}
// This won't fire when immediately, the mounted callback will take care
watch(() => props.id, newId => {
loadAgentData(newId);
})
onMounted(async () => {
loadAgentData(props.id);
});
function updateIdentifier() {
const value = prompt(`Enter a new identifier for ${agent.value.name}`);
if (value) {
backend.sendAction("update-agent", {
agentId: agent.value.id,
agentname: value,
}).then(res => {
ui.alert(res.error ?
'Error updating agent identifier' :
'Agent identifier updated'
);
if (!res.error) {
agent.value.agentname = value;
backend.invalidateCache({agents: true});
}
});
} else {
ui.alert("Cancelled identifier update");
}
}
function updateName() {
const value = prompt(`Enter a new name for ${agent.value.name}`);
if (value) {
backend.sendAction("update-agent", {
agentId: agent.value.id,
agentname: value,
}).then(res => {
ui.alert(res.error ?
'Error updating agent name' :
'Agent name updated'
);
if (!res.error) {
agent.value.name = value;
backend.invalidateCache({agents: true});
}
});
} else {
ui.alert("Cancelled identifier update");
}
}
async function saveChanges() {
const dataObject = {};
for (let field of configFields.value) {
if (field.enabled) {
dataObject[field.name] = field.value;
}
}
let res = await backend.sendAction("update-agent", {
agentId: agent.value.id,
data: dataObject,
});
ui.alert(res.error ?
'Error updating agent' :
'Agent updated'
);
if (!res.error) {
backend.invalidateCache({agents: true});
}
}
</script>
<template>
<main>
<h1>{{ agent.name || `(Agent #${id})` }}</h1>
<section>
<p>{{ description }}</p>
<p>Agent ID: {{ id }}, Identifier: {{ agent.agentname }}</p>
<div>
<button @click="updateIdentifier">Update Identifier</button>
<button @click="updateName">Update Name</button>
</div>
<form @submit.prevent="saveChanges">
<div v-for="(configField, index) in configFields" :key="index">
<label>{{ configField.name }}</label>
<input type="checkbox" v-model="configField.enabled" />
<textarea v-model="configField.value" />
</div>
<button type="submit">Save</button>
</form>
</section>
</main>
</template>
+88
View File
@@ -0,0 +1,88 @@
<script setup>
import {
ref, computed, reactive,
onMounted, onUnmounted,
} from "vue";
import * as backend from '../backend';
import * as ui from '../ui';
// List of agent class names
const kinds = ref([]);
const agent = ref({
name: "",
identifier: "",
kind: "",
});
async function submitAgent () {
const payload = {
'name': agent.value.name,
'kind': agent.value.kind,
};
if (!payload.kind) {
ui.alert("You must select a kind");
return;
}
if (agent.value.identifier) {
payload.identifier = agent.value.identifier;
}
let response = await backend.sendAction("create-agent", payload);
if (response.error) {
ui.alert(`Could not create agent\n${response.error}`);
} else {
ui.alert(`Created Agent "${payload.name}"`);
backend.invalidateCache({agents: true});
ui.goto(`/agent/${response.agentId}`);
}
}
onMounted(async () => {
kinds.value = (await backend.sendAction("get-agent-kinds")).kinds;
});
</script>
<template>
<main>
<h1>Create New Chat</h1>
<div class="main-container">
<input v-model="agent.name" type="text" placeholder="name">
<input v-model="agent.identifier" type="text" placeholder="identifier">
<select v-model="agent.kind">
<option v-for="k in kinds" :value="k">{{k}}</option>
</select>
<button @click="submitAgent()">Create Agent</button>
</div>
</main>
</template>
<style scoped>
.main-container {
display: flex;
flex-direction: column;
}
.multi-select-box {
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
}
.multi-select-box div {
border-bottom: 1px solid var(--color-border);
}
.multi-select-box div:last {
border-bottom: none;
}
.multi-select-box div:hover {
background-color: var(--color-background-mute);
}
.multi-select-box div.selected {
background-color: var(--color-background-soft);
}
</style>
+6 -5
View File
@@ -66,10 +66,10 @@
}
// This won't fire when called, the mounted callback will take care
watch(() => props.id, newChatId => {
watch(() => props.id, newId => {
if (_isMounted) {
unloadChatState();
loadChatState(newChatId);
loadChatState(newId);
}
})
@@ -154,7 +154,7 @@
// === UI Helper Functions === //
function requestRescroll ({forceBottom=false, bottomRelative=false}) {
function requestRescroll ({forceBottom=false, bottomRelative=false}={}) {
const elem = chatContainerElem.value;
const scroll = elem.scrollTop;
@@ -166,13 +166,14 @@
// Chat was fully scrolled down
if (forceBottom || scroll == max) {
elem.scrollTop = elem.scrollTopMax;
} else if (bottomRelative) {
// TODO: Keep scroll at the same distance from the bottom.
// This will be used when the user scrolls to the top and previous messages are loaded
} else {
// Do nothing, scroll remains in place
}
});
});
// Something different has to be done when previous messages are added
}
function msgClass (msg) {
+1 -7
View File
@@ -8,10 +8,7 @@
import * as ui from '../ui';
// List of agents, with their id, name, and whether they're currently selected
const agents = ref([
{id: 1, name: "John"},
{id: 2, name: "Steve"},
]);
const agents = ref([]);
const title = ref("");
async function submitChat () {
@@ -31,15 +28,12 @@
}
}
// === Initialize === //
onMounted(async () => {
agents.value = (await backend.getAgents()).map(it => ({
id: it.id,
name: it.name,
selected: false,
}));
// TODO: Get agent list
});
</script>