initial commit
23
talemate_frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
24
talemate_frontend/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# talemate_frontend
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
5
talemate_frontend/babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
19
talemate_frontend/jsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
}
|
||||
}
|
||||
19878
talemate_frontend/package-lock.json
generated
Normal file
49
talemate_frontend/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "talemate_frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "5.9.55",
|
||||
"core-js": "^3.8.3",
|
||||
"roboto-fontface": "*",
|
||||
"vue": "^3.2.13",
|
||||
"vuetify": "^3.3.11",
|
||||
"webfontloader": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.16",
|
||||
"@babel/eslint-parser": "^7.12.16",
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||
"@vue/cli-service": "~5.0.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-vue": "^8.0.3",
|
||||
"vue-cli-plugin-vuetify": "~2.5.8",
|
||||
"webpack-plugin-vuetify": "^2.0.0-alpha.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "@babel/eslint-parser"
|
||||
},
|
||||
"rules": {}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead",
|
||||
"not ie 11"
|
||||
]
|
||||
}
|
||||
BIN
talemate_frontend/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
17
talemate_frontend/public/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title>Talemate</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
16
talemate_frontend/src/App.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<TalemateApp />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TalemateApp from './components/TalemateApp.vue'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
TalemateApp
|
||||
}
|
||||
}
|
||||
</script>
|
||||
BIN
talemate_frontend/src/assets/logo-11-final.png
Normal file
|
After Width: | Height: | Size: 1012 KiB |
BIN
talemate_frontend/src/assets/logo-13-adjusted.png
Normal file
|
After Width: | Height: | Size: 442 KiB |
BIN
talemate_frontend/src/assets/logo-13-muted.png
Normal file
|
After Width: | Height: | Size: 318 KiB |
BIN
talemate_frontend/src/assets/logo-13.1-adjusted.png
Normal file
|
After Width: | Height: | Size: 496 KiB |
BIN
talemate_frontend/src/assets/logo-13.1-backdrop.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
talemate_frontend/src/assets/logo-13.1-muted.png
Normal file
|
After Width: | Height: | Size: 350 KiB |
BIN
talemate_frontend/src/assets/logo-13.1-transparent.png
Normal file
|
After Width: | Height: | Size: 570 KiB |
BIN
talemate_frontend/src/assets/logo-13.1.png
Normal file
|
After Width: | Height: | Size: 693 KiB |
BIN
talemate_frontend/src/assets/logo.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
6
talemate_frontend/src/assets/logo.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="488" height="424" viewBox="0 0 488 424" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M249.126 95.017L151.843 263.694L243.959 423.473L365.966 211.973L487.918 0.473206H303.629L249.126 95.017Z" fill="#1697F6"/>
|
||||
<path d="M122.007 211.973L128.396 223.096L219.402 65.2635L256.793 0.473206H243.959H0L122.007 211.973Z" fill="#AEDDFF"/>
|
||||
<path d="M303.629 0.473206C349.743 152.355 243.959 423.473 243.959 423.473L151.843 263.694L303.629 0.473206Z" fill="#1867C0"/>
|
||||
<path d="M256.793 0.473206C62.5042 0.473206 128.397 223.096 128.397 223.096L256.793 0.473206Z" fill="#7BC6FF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 598 B |
145
talemate_frontend/src/components/AIAgent.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div v-if="isConnected()">
|
||||
<v-list v-for="(agent, index) in state.agents" :key="index">
|
||||
<v-list-item @click="editAgent(index)">
|
||||
<v-list-item-title>
|
||||
<v-progress-circular v-if="agent.status === 'busy'" indeterminate color="primary"
|
||||
size="14"></v-progress-circular>
|
||||
<v-icon v-else-if="agent.status === 'uninitialized'" color="orange" size="14">mdi-checkbox-blank-circle</v-icon>
|
||||
<v-icon v-else-if="agent.status === 'disabled'" color="grey-darken-2" size="14">mdi-checkbox-blank-circle</v-icon>
|
||||
<v-icon v-else color="green" size="14">mdi-checkbox-blank-circle</v-icon>
|
||||
<span class="ml-1" v-if="agent.label"> {{ agent.label }}</span>
|
||||
<span class="ml-1" v-else> {{ agent.name }}</span>
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ agent.client }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<AgentModal :dialog="dialog" :formTitle="formTitle" @save="saveAgent" @update:dialog="updateDialog"></AgentModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AgentModal from './AgentModal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AgentModal
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
state: {
|
||||
agents: [],
|
||||
dialog: false,
|
||||
currentAgent: {
|
||||
type: '',
|
||||
client: '',
|
||||
status: 'idle',
|
||||
label: '',
|
||||
name: '',
|
||||
data: {},
|
||||
},
|
||||
formTitle: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
inject: [
|
||||
'getWebsocket',
|
||||
'registerMessageHandler',
|
||||
'isConnected',
|
||||
'getClients',
|
||||
],
|
||||
provide() {
|
||||
return {
|
||||
state: this.state
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
configurationRequired() {
|
||||
let clients = this.getClients();
|
||||
|
||||
for(let i = 0; i < this.state.agents.length; i++) {
|
||||
let agent = this.state.agents[i];
|
||||
|
||||
if(agent.status === 'warning' || agent.status === 'error') {
|
||||
console.log("agents: configuration required (1)", agent.status)
|
||||
return true;
|
||||
}
|
||||
|
||||
// loop through all clients until we find the client assigned
|
||||
// to the agent, then check the client status to see if it's ok
|
||||
for(let j = 0; j < clients.length; j++) {
|
||||
let client = clients[j];
|
||||
if(client.name === agent.client) {
|
||||
if(client.status === 'warning' || client.status === 'error' || client.status === 'disabled') {
|
||||
console.log("agents: configuration required (2)", client.status)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
getActive() {
|
||||
return this.state.agents.find(a => a.status === 'busy');
|
||||
},
|
||||
openModal() {
|
||||
this.state.formTitle = 'Add AI Agent';
|
||||
this.state.dialog = true;
|
||||
console.log("got here")
|
||||
},
|
||||
saveAgent(agent) {
|
||||
const index = this.state.agents.findIndex(c => c.name === agent.name);
|
||||
if (index === -1) {
|
||||
this.state.agents.push(agent);
|
||||
} else {
|
||||
this.state.agents[index] = agent;
|
||||
}
|
||||
this.state.dialog = false;
|
||||
this.$emit('agents-updated', this.state.agents);
|
||||
},
|
||||
editAgent(index) {
|
||||
this.state.currentAgent = { ...this.state.agents[index] };
|
||||
this.state.formTitle = 'Edit AI Agent';
|
||||
this.state.dialog = true;
|
||||
},
|
||||
deleteAgent(index) {
|
||||
if (window.confirm('Are you sure you want to delete this agent?')) {
|
||||
this.state.agents.splice(index, 1);
|
||||
this.$emit('agents-updated', this.state.agents);
|
||||
}
|
||||
},
|
||||
updateDialog(newVal) {
|
||||
this.state.dialog = newVal;
|
||||
},
|
||||
handleMessage(data) {
|
||||
// Handle agent_status message type
|
||||
if (data.type === 'agent_status') {
|
||||
// Find the client with the given name
|
||||
const agent = this.state.agents.find(agent => agent.name === data.name);
|
||||
if (agent) {
|
||||
// Update the model name of the client
|
||||
agent.client = data.client;
|
||||
agent.data = data.data;
|
||||
agent.status = data.status;
|
||||
agent.label = data.message;
|
||||
} else {
|
||||
// Add the agent to the list of agents
|
||||
this.state.agents.push({
|
||||
name: data.name,
|
||||
client: data.client,
|
||||
status: data.status,
|
||||
data: data.data,
|
||||
label: data.message,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.registerMessageHandler(this.handleMessage);
|
||||
},
|
||||
}
|
||||
</script>
|
||||
201
talemate_frontend/src/components/AIClient.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<div v-if="isConnected()">
|
||||
<v-list v-for="(client, index) in state.clients" :key="index">
|
||||
<v-list-item>
|
||||
|
||||
<v-divider v-if="index !== 0" class="mb-3"></v-divider>
|
||||
<v-list-item-title>
|
||||
<v-progress-circular v-if="client.status === 'busy'" indeterminate color="primary"
|
||||
size="14"></v-progress-circular>
|
||||
|
||||
<v-icon v-else-if="client.status == 'warning'" color="orange" size="14">mdi-checkbox-blank-circle</v-icon>
|
||||
<v-icon v-else-if="client.status == 'error'" color="red" size="14">mdi-checkbox-blank-circle</v-icon>
|
||||
<v-icon v-else-if="client.status == 'disabled'" color="grey-darken-2" size="14">mdi-checkbox-blank-circle</v-icon>
|
||||
<v-icon v-else color="green" size="14">mdi-checkbox-blank-circle</v-icon>
|
||||
{{ client.name }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-caption">
|
||||
{{ client.model_name }}
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-subtitle class="text-caption">
|
||||
{{ client.type }}
|
||||
<v-chip label size="x-small" variant="outlined" class="ml-1">ctx {{ client.max_token_length }}</v-chip>
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-content density="compact">
|
||||
<v-slider
|
||||
hide-details
|
||||
v-model="client.max_token_length"
|
||||
:min="1024"
|
||||
:max="16384"
|
||||
:step="512"
|
||||
@update:modelValue="saveClient(client)"
|
||||
@click.stop
|
||||
density="compact"
|
||||
></v-slider>
|
||||
</v-list-item-content>
|
||||
<v-list-item-subtitle class="text-center">
|
||||
|
||||
<v-tooltip text="Edit client">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn size="x-small" class="mr-1" v-bind="props" variant="tonal" density="comfortable" rounded="sm" @click.stop="editClient(index)" icon="mdi-cogs"></v-btn>
|
||||
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip text="Assign to all agents">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn size="x-small" class="mr-1" v-bind="props" variant="tonal" density="comfortable" rounded="sm" @click.stop="assignClientToAllAgents(index)" icon="mdi-transit-connection-variant"></v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip text="Delete client">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn size="x-small" class="mr-1" v-bind="props" variant="tonal" density="comfortable" rounded="sm" @click.stop="deleteClient(index)" icon="mdi-close-thick"></v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<ClientModal :dialog="dialog" :formTitle="formTitle" @save="saveClient" @update:dialog="updateDialog"></ClientModal>
|
||||
<v-alert type="warning" variant="tonal" v-if="state.clients.length === 0">You have no LLM clients configured. Add one.</v-alert>
|
||||
<v-btn @click="openModal" prepend-icon="mdi-plus-box">Add client</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClientModal from './ClientModal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ClientModal,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
clientStatusCheck: null,
|
||||
state: {
|
||||
clients: [],
|
||||
dialog: false,
|
||||
currentClient: {
|
||||
name: '',
|
||||
type: '',
|
||||
apiUrl: '',
|
||||
model_name: '',
|
||||
max_token_length: 2048,
|
||||
}, // Add a new field to store the model name
|
||||
formTitle: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
inject: [
|
||||
'getWebsocket',
|
||||
'registerMessageHandler',
|
||||
'isConnected',
|
||||
'chekcingStatus',
|
||||
'getAgents',
|
||||
],
|
||||
provide() {
|
||||
return {
|
||||
state: this.state
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
configurationRequired() {
|
||||
if(this.state.clients.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// cycle through clients and check if any are status 'error' or 'warning'
|
||||
for (let i = 0; i < this.state.clients.length; i++) {
|
||||
if (this.state.clients[i].status === 'error' || this.state.clients[i].status === 'warning') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
getActive() {
|
||||
return this.state.clients.find(a => a.status === 'busy');
|
||||
},
|
||||
openModal() {
|
||||
this.state.currentClient = {
|
||||
name: 'TextGenWebUI',
|
||||
type: 'textgenwebui',
|
||||
apiUrl: 'http://localhost:5000/api',
|
||||
model_name: '',
|
||||
max_token_length: 4096,
|
||||
};
|
||||
this.state.formTitle = 'Add Client';
|
||||
this.state.dialog = true;
|
||||
},
|
||||
saveClient(client) {
|
||||
const index = this.state.clients.findIndex(c => c.name === client.name);
|
||||
if (index === -1) {
|
||||
this.state.clients.push(client);
|
||||
} else {
|
||||
this.state.clients[index] = client;
|
||||
}
|
||||
console.log("Saving client", client)
|
||||
this.state.dialog = false; // Close the dialog after saving the client
|
||||
this.$emit('clients-updated', this.state.clients);
|
||||
},
|
||||
editClient(index) {
|
||||
this.state.currentClient = { ...this.state.clients[index] };
|
||||
this.state.formTitle = 'Edit AI Client';
|
||||
this.state.dialog = true;
|
||||
},
|
||||
deleteClient(index) {
|
||||
if (window.confirm('Are you sure you want to delete this client?')) {
|
||||
this.state.clients.splice(index, 1);
|
||||
this.$emit('clients-updated', this.state.clients);
|
||||
}
|
||||
},
|
||||
assignClientToAllAgents(index) {
|
||||
let agents = this.getAgents();
|
||||
let client = this.state.clients[index];
|
||||
|
||||
for (let i = 0; i < agents.length; i++) {
|
||||
agents[i].client = client.name;
|
||||
this.$emit('client-assigned', agents);
|
||||
}
|
||||
},
|
||||
updateDialog(newVal) {
|
||||
this.state.dialog = newVal;
|
||||
},
|
||||
handleMessage(data) {
|
||||
|
||||
// Handle client_status message type
|
||||
if (data.type === 'client_status') {
|
||||
// Find the client with the given name
|
||||
const client = this.state.clients.find(client => client.name === data.name);
|
||||
|
||||
if (client) {
|
||||
// Update the model name of the client
|
||||
client.model_name = data.model_name;
|
||||
client.type = data.message;
|
||||
client.status = data.status;
|
||||
client.max_token_length = data.max_token_length;
|
||||
client.apiUrl = data.apiUrl;
|
||||
} else {
|
||||
console.log("Adding new client", data);
|
||||
this.state.clients.push({
|
||||
name: data.name,
|
||||
model_name: data.model_name,
|
||||
type: data.message,
|
||||
status: data.status,
|
||||
max_token_length: data.max_token_length,
|
||||
apiUrl: data.apiUrl,
|
||||
});
|
||||
// sort the clients by name
|
||||
this.state.clients.sort((a, b) => (a.name > b.name) ? 1 : -1);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.registerMessageHandler(this.handleMessage);
|
||||
},
|
||||
}
|
||||
</script>
|
||||
68
talemate_frontend/src/components/AgentModal.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<v-dialog v-model="localDialog" persistent max-width="600px">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<span class="headline">{{ formTitle }}</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<v-text-field v-model="agent.name" readonly label="Agent"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-select v-model="agent.client" :items="agent.data.client" label="Client"></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="blue darken-1" text @click="close">Close</v-btn>
|
||||
<v-btn color="blue darken-1" text @click="save">Save</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
dialog: Boolean,
|
||||
formTitle: String
|
||||
},
|
||||
inject: ['state'],
|
||||
data() {
|
||||
return {
|
||||
localDialog: this.state.dialog,
|
||||
agent: { ...this.state.currentAgent }
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
'state.dialog': {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
this.localDialog = newVal;
|
||||
}
|
||||
},
|
||||
'state.currentAgent': {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
this.agent = { ...newVal };
|
||||
}
|
||||
},
|
||||
localDialog(newVal) {
|
||||
this.$emit('update:dialog', newVal);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit('update:dialog', false);
|
||||
},
|
||||
save() {
|
||||
this.$emit('save', this.agent);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
142
talemate_frontend/src/components/AppConfig.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<v-dialog v-model="dialog" scrollable max-width="50%">
|
||||
<v-card v-if="app_config !== null">
|
||||
|
||||
<v-tabs color="primary" v-model="tab">
|
||||
<v-tab value="game">
|
||||
<v-icon start>mdi-gamepad-square</v-icon>
|
||||
Game
|
||||
</v-tab>
|
||||
<v-tab value="creator">
|
||||
<v-icon start>mdi-palette-outline</v-icon>
|
||||
Creator
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<v-window v-model="tab">
|
||||
<v-window-item value="game">
|
||||
<v-card flat>
|
||||
<v-card-title>
|
||||
Default player character
|
||||
<v-tooltip location="top" max-width="500" text="This will be default player character that will be added to a game if the game does not come with a defined player character. Essentially this is relevant for when you load character-cards that aren't in the talemate scene format.">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon size="x-small" v-bind="props" v-on="on">mdi-help</v-icon>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<v-text-field v-model="app_config.game.default_player_character.name" label="Name"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-text-field v-model="app_config.game.default_player_character.gender" label="Gender"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-textarea v-model="app_config.game.default_player_character.description" auto-grow label="Description"></v-textarea>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-color-picker v-model="app_config.game.default_player_character.color" hide-inputs label="Color" elevation="0"></v-color-picker>
|
||||
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-window-item>
|
||||
<v-window-item value="creator">
|
||||
<v-card flat>
|
||||
<v-card-title>
|
||||
Content context
|
||||
<v-tooltip location="top" max-width="500" text="Available choices when generating characters or scenarios within talemate.">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon size="x-small" v-bind="props" v-on="on">mdi-help</v-icon>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</v-card-title>
|
||||
<v-card-text style="max-height:600px; overflow-y:scroll;">
|
||||
<v-list density="compact">
|
||||
<v-list-item v-for="(value, index) in app_config.creator.content_context" :key="index">
|
||||
<v-list-item-title><v-icon color="red">mdi-delete</v-icon>{{ value }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<v-text-field v-model="content_context_input" label="Add content context" @keyup.enter="app_config.creator.content_context.push(content_context_input); app_config.creator.content_context_input = ''"></v-text-field>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
<v-card-actions>
|
||||
<v-btn color="primary" text @click="saveConfig">Save</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<v-card v-else>
|
||||
<v-card-title>
|
||||
<span class="headline">Configuration</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-progress-circular indeterminate color="primary" size="20"></v-progress-circular>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'AppConfig',
|
||||
data() {
|
||||
return {
|
||||
tab: 'game',
|
||||
dialog: false,
|
||||
app_config: null,
|
||||
content_context_input: '',
|
||||
}
|
||||
},
|
||||
inject: ['getWebsocket', 'registerMessageHandler', 'setWaitingForInput', 'requestSceneAssets', 'requestAppConfig'],
|
||||
|
||||
methods: {
|
||||
show() {
|
||||
this.requestAppConfig();
|
||||
this.dialog = true;
|
||||
|
||||
},
|
||||
exit() {
|
||||
this.dialog = false
|
||||
},
|
||||
|
||||
handleMessage(message) {
|
||||
if (message.type == "app_config") {
|
||||
this.app_config = message.data;
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type == 'config') {
|
||||
if(message.action == 'save_complete') {
|
||||
this.exit();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
sendRequest(data) {
|
||||
data.type = 'config';
|
||||
this.getWebsocket().send(JSON.stringify(data));
|
||||
},
|
||||
|
||||
saveConfig() {
|
||||
this.sendRequest({
|
||||
action: 'save',
|
||||
config: this.app_config,
|
||||
})
|
||||
},
|
||||
|
||||
},
|
||||
created() {
|
||||
this.registerMessageHandler(this.handleMessage);
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
473
talemate_frontend/src/components/CharacterCreator.vue
Normal file
@@ -0,0 +1,473 @@
|
||||
<template>
|
||||
<v-dialog v-model="dialog" scrollable max-width="50%">
|
||||
<v-window>
|
||||
<v-stepper editable v-model="step" :items="steps" :hide-actions="!actionsAvailable()">
|
||||
|
||||
<template v-slot:[`item.1`]>
|
||||
<v-card density="compact">
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="4">
|
||||
<v-select :items="templates" label="Template" v-model="selected_template"></v-select>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<v-switch label="Is Player Character" color="green" v-model="is_player_character"></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<v-slider label="Use Spice" min="0" max="0.5" step="0.05" v-model="use_spice"></v-slider>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-combobox :items="content_context
|
||||
" label="Content Context" v-model="scenario_context"></v-combobox>
|
||||
<v-textarea label="Character prompt" v-model="character_prompt"></v-textarea>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<template v-slot:[`item.2`]>
|
||||
<v-card>
|
||||
<v-card-text style="max-height:600px; overflow-y:scroll;">
|
||||
|
||||
<v-alert class="mb-1" type="info" v-if="!detail_questions.length" variant="tonal">
|
||||
There will be attributes generated based on the template you selected. You can add additional custom attrbutes as well.
|
||||
</v-alert>
|
||||
|
||||
<!-- custom attributes -->
|
||||
|
||||
<v-row>
|
||||
<v-col cols="4">
|
||||
<v-text-field label="Attribute name" v-model="new_attribute_name"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-text-field @keydown.prevent.enter="addAttribute()" label="Instructions to the AI when generating content for this attribute. (enter to add)" v-model="new_attribute_instruction"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-chip v-for="(instruction, name) in custom_attributes" :key="name" class="ma-1" @click="(delete custom_attributes[name])">
|
||||
{{ name }}: {{ instruction }}
|
||||
</v-chip>
|
||||
|
||||
<v-divider class="mb-2"></v-divider>
|
||||
|
||||
<span class="mt-2 mb-2 text-subtitle-2" prepend-icon="mdi-memory">Generated Attributes</span>
|
||||
|
||||
<!-- generated attributes -->
|
||||
|
||||
<v-list-item v-for="(value, name) in base_attributes" :key="name">
|
||||
<v-list-item-title class="text-capitalize">
|
||||
{{ name }}
|
||||
<v-btn size="small" color="primary" variant="text" density="compact" rounded="sm" prepend-icon="mdi-refresh" @click.stop="regenerateAttribute(name)" :disabled="generating">
|
||||
|
||||
</v-btn>
|
||||
</v-list-item-title>
|
||||
<v-row v-if="name == 'name'">
|
||||
<v-col cols="10">
|
||||
<v-text-field v-model="base_attributes[name]"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="2">
|
||||
<v-btn variant="tonal" color="primary" @click="renameCharacter()" :disabled="generating">Rename</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-textarea v-else rows="1" auto-grow v-model="base_attributes[name]"></v-textarea>
|
||||
</v-list-item>
|
||||
|
||||
|
||||
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-progress-circular class="ml-1 mr-3" size="24" v-if="generating" indeterminate color="primary"></v-progress-circular>
|
||||
<v-btn color="primary" @click="submitStep(2)" :disabled="generating" prepend-icon="mdi-memory">Generate</v-btn>
|
||||
<v-btn color="primary" @click="resetStep(2)" :disabled="generating" prepend-icon="mdi-restart">Reset</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<template v-slot:[`item.3`]>
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<v-textarea rows="10" auto-grow v-model="description"></v-textarea>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-progress-circular class="ml-1 mr-3" size="24" v-if="generating" indeterminate
|
||||
color="primary"></v-progress-circular>
|
||||
<v-btn color="primary" @click="submitStep(3)" :disabled="generating" prepend-icon="mdi-memory">Generate</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<template v-slot:[`item.4`]>
|
||||
<v-card>
|
||||
<v-card-text style="max-height:600px; overflow-y:scroll;">
|
||||
<v-alert type="info" v-if="!detail_questions.length" variant="tonal">
|
||||
There will be questions asked based on the template you selected. You can add custom questions as well.
|
||||
</v-alert>
|
||||
|
||||
<v-list>
|
||||
<v-list-item v-for="(question, index) in detail_questions" :key="index">
|
||||
<v-list-item-title class="text-capitalize">
|
||||
<v-icon color="red" @click="detail_questions.splice(index, 1)">mdi-delete</v-icon>
|
||||
{{ question }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-text-field label="Custom question" v-model="new_question" @keydown.prevent.enter="addQuestion()"></v-text-field>
|
||||
</v-list>
|
||||
|
||||
<v-list>
|
||||
<v-list-item v-for="(value, question) in details" :key="question">
|
||||
<v-list-item-title class="text-capitalize">{{ question }}</v-list-item-title>
|
||||
<v-textarea rows="1" auto-grow v-model="details[question]"></v-textarea>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-progress-circular class="ml-1 mr-3" size="24" v-if="generating" indeterminate
|
||||
color="primary"></v-progress-circular>
|
||||
<v-btn color="primary" @click="submitStep(4)" :disabled="generating" prepend-icon="mdi-memory">Generate</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<template v-slot:[`item.5`]>
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<v-textarea rows="1" :label="character+' speaks like ...'" v-model="dialogue_guide"></v-textarea>
|
||||
|
||||
<v-list>
|
||||
<v-list-item v-for="(example, index) in dialogue_examples" :key="index">
|
||||
<v-list-item-title class="text-capitalize">
|
||||
<v-icon color="red" @click="dialogue_examples.splice(index, 1)">mdi-delete</v-icon>
|
||||
{{ example }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-text-field label="Add dialogue example" v-model="new_dialogue_example" @keydown.prevent.enter="addDialogueExample()"></v-text-field>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-progress-circular class="ml-1 mr-3" size="24" v-if="generating" indeterminate color="primary"></v-progress-circular>
|
||||
<v-btn color="primary" @click="submitStep(5)" :disabled="generating" prepend-icon="mdi-memory">Generate</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<template v-slot:[`item.6`]>
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<v-alert type="info" variant="tonal">
|
||||
Your character has been generated. You can now add it to the world.
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-progress-circular class="ml-1 mr-3" size="24" v-if="generating" indeterminate color="primary"></v-progress-circular>
|
||||
<v-btn color="primary" @click="submitStep(6)" :disabled="generating">Add to world</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
</v-stepper>
|
||||
</v-window>
|
||||
</v-dialog>
|
||||
|
||||
<v-snackbar v-model="notification" color="red" :timeout="3000">
|
||||
{{ notification_text }}
|
||||
</v-snackbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { VStepper } from 'vuetify/labs/VStepper'
|
||||
export default {
|
||||
components: {
|
||||
VStepper,
|
||||
},
|
||||
name: 'CharacterCreator',
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
step: 1,
|
||||
steps: [
|
||||
'Template',
|
||||
'Attributes',
|
||||
'Description',
|
||||
'Details',
|
||||
'Dialogue',
|
||||
'Add to World',
|
||||
],
|
||||
content_context: [
|
||||
"a fun and engaging slice of life story aimed at an adult audience.",
|
||||
],
|
||||
scenario_context: "a fun and engaging slice of life story aimed at an adult audience.",
|
||||
templates: ["human"],
|
||||
selected_template: "human",
|
||||
base_attributes: {},
|
||||
details: {},
|
||||
detail_questions: [],
|
||||
new_question: "",
|
||||
dialogue_examples: [],
|
||||
new_dialogue_example: "",
|
||||
dialogue_guide: "",
|
||||
notification: false,
|
||||
notification_text: '',
|
||||
is_player_character: false,
|
||||
use_spice: 0.1,
|
||||
character_prompt: 'A 19-year-old boy who just did something embarrassing in front of his crush.',
|
||||
character: null,
|
||||
description: "",
|
||||
generating: false,
|
||||
scene:null,
|
||||
|
||||
custom_attributes: {},
|
||||
new_attribute_name: "",
|
||||
new_attribute_instruction: "",
|
||||
}
|
||||
},
|
||||
inject: ['getWebsocket', 'registerMessageHandler', 'setWaitingForInput', 'requestSceneAssets'],
|
||||
methods: {
|
||||
|
||||
actionsAvailable() {
|
||||
if(this.generating)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
show() {
|
||||
this.requestTemplates();
|
||||
this.dialog = true;
|
||||
},
|
||||
|
||||
showForCharacter(name) {
|
||||
|
||||
// retrieve character from this.scene.charactes (list) based on name
|
||||
|
||||
let character_object = null;
|
||||
for(let character of this.scene.characters) {
|
||||
if(character.name == name) {
|
||||
character_object = character;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!character_object)
|
||||
return;
|
||||
|
||||
this.base_attributes = character_object.base_attributes;
|
||||
this.base_attributes.name = name;
|
||||
this.description = character_object.description;
|
||||
this.details = character_object.details;
|
||||
this.dialogue_examples = character_object.example_dialogue;
|
||||
this.character = name;
|
||||
this.is_player_character = character_object.is_player;
|
||||
this.scenario_context = character_object.base_attributes.scenario_context || "";
|
||||
this.character_prompt = character_object.base_attributes._prompt || "";
|
||||
this.selected_template = character_object.base_attributes._template || "";
|
||||
|
||||
this.show();
|
||||
|
||||
},
|
||||
|
||||
reset() {
|
||||
|
||||
this.step = 1;
|
||||
this.base_attributes = {};
|
||||
this.details = {};
|
||||
this.description = "";
|
||||
this.detail_questions = [];
|
||||
this.custom_attributes = {};
|
||||
this.dialogue_examples = [];
|
||||
this.character = null;
|
||||
this.generating = false;
|
||||
},
|
||||
|
||||
addQuestion() {
|
||||
if(!this.new_question.length)
|
||||
return;
|
||||
|
||||
this.detail_questions.push(this.new_question);
|
||||
this.new_question = "";
|
||||
},
|
||||
|
||||
addDialogueExample() {
|
||||
if(!this.new_dialogue_example.length)
|
||||
return;
|
||||
|
||||
this.dialogue_examples.push(this.character+": "+this.new_dialogue_example);
|
||||
this.new_dialogue_example = "";
|
||||
},
|
||||
|
||||
addAttribute() {
|
||||
if(!this.new_attribute_name.length)
|
||||
return;
|
||||
|
||||
this.custom_attributes[this.new_attribute_name] = this.new_attribute_instruction;
|
||||
this.new_attribute_name = "";
|
||||
this.new_attribute_instruction = "";
|
||||
},
|
||||
|
||||
regenerateAttribute(name) {
|
||||
this.base_attributes[name] = "";
|
||||
this.submitStep(2);
|
||||
},
|
||||
|
||||
renameCharacter() {
|
||||
|
||||
// cycle through base_attributes and find the name and replace it in each
|
||||
|
||||
let current_name = this.character;
|
||||
let new_name = this.base_attributes.name;
|
||||
|
||||
for(let key in this.base_attributes) {
|
||||
let value = this.base_attributes[key];
|
||||
|
||||
if(typeof value != "string")
|
||||
continue;
|
||||
|
||||
value = value.replace(current_name, new_name)
|
||||
this.base_attributes[key] = value;
|
||||
}
|
||||
|
||||
this.character = new_name;
|
||||
},
|
||||
|
||||
exit(reset) {
|
||||
this.dialog = false;
|
||||
if(reset)
|
||||
this.reset();
|
||||
},
|
||||
|
||||
requestTemplates() {
|
||||
this.sendRequest({
|
||||
action: 'request_templates',
|
||||
});
|
||||
},
|
||||
|
||||
resetStep(step) {
|
||||
|
||||
if(!confirm("Are you sure you want to reset this step?"))
|
||||
return;
|
||||
|
||||
if(step == 2)
|
||||
this.base_attributes = {};
|
||||
if(step == 3)
|
||||
this.description = "";
|
||||
if(step == 4)
|
||||
this.details = {};
|
||||
if(step == 5)
|
||||
this.dialogue_examples = [];
|
||||
if(step == 6)
|
||||
this.character = null;
|
||||
},
|
||||
|
||||
submitStep(step) {
|
||||
|
||||
if(!this.selected_template.length && step < 6) {
|
||||
this.notification = true;
|
||||
this.notification_text = "Please select at least one template";
|
||||
return;
|
||||
}
|
||||
|
||||
if(!this.character_prompt.length && step < 6) {
|
||||
this.notification = true;
|
||||
this.notification_text = "Please enter a character prompt";
|
||||
return;
|
||||
}
|
||||
|
||||
//if(step == 2)
|
||||
// this.base_attributes = {};
|
||||
|
||||
if(step == 3)
|
||||
this.description = "";
|
||||
|
||||
if(step == 4)
|
||||
this.details = {};
|
||||
|
||||
this.sendRequest({
|
||||
action: 'submit',
|
||||
base_attributes: this.base_attributes,
|
||||
character_prompt: this.character_prompt,
|
||||
description: this.description,
|
||||
details: this.details,
|
||||
is_player_character: this.is_player_character,
|
||||
dialogue_guide: this.dialogue_guide,
|
||||
dialogue_examples: this.dialogue_examples,
|
||||
questions: this.detail_questions,
|
||||
scenario_context: this.scenario_context,
|
||||
step: step,
|
||||
template: this.selected_template,
|
||||
use_spice: this.use_spice,
|
||||
custom_attributes: this.custom_attributes,
|
||||
});
|
||||
},
|
||||
|
||||
sendRequest(data) {
|
||||
data.type = 'character_creator';
|
||||
this.getWebsocket().send(JSON.stringify(data));
|
||||
},
|
||||
|
||||
handleSendTemplates(data) {
|
||||
this.templates = data.templates;
|
||||
this.content_context = data.content_context;
|
||||
},
|
||||
|
||||
handleSetGeneratingStep(data) {
|
||||
this.step = data.step;
|
||||
this.generating = true;
|
||||
},
|
||||
|
||||
handleSetGeneratingStepDone(data) {
|
||||
this.step = data.step;
|
||||
this.generating = false;
|
||||
|
||||
if (data.step === 6) {
|
||||
this.exit();
|
||||
}
|
||||
},
|
||||
|
||||
handleBaseAttribute(data) {
|
||||
this.base_attributes[data.name] = data.value;
|
||||
if(data.name == "name") {
|
||||
this.character = data.value;
|
||||
}
|
||||
},
|
||||
|
||||
handleDetail(data) {
|
||||
this.details[data.question] = data.answer;
|
||||
},
|
||||
|
||||
handleExampleDialogue(data) {
|
||||
this.dialogue_examples.push(data.example);
|
||||
},
|
||||
|
||||
handleMessage(data) {
|
||||
|
||||
if (data.type === 'scene_status') {
|
||||
this.scene = data.data;
|
||||
}
|
||||
|
||||
if (data.type === 'character_creator') {
|
||||
if(data.action === 'send_templates') {
|
||||
this.handleSendTemplates(data);
|
||||
} else if(data.action === 'set_generating_step') {
|
||||
this.handleSetGeneratingStep(data);
|
||||
} else if(data.action === 'set_generating_step_done') {
|
||||
this.handleSetGeneratingStepDone(data);
|
||||
} else if(data.action === 'base_attribute') {
|
||||
this.handleBaseAttribute(data);
|
||||
} else if(data.action === 'detail') {
|
||||
this.handleDetail(data);
|
||||
} else if(data.action === 'example_dialogue') {
|
||||
this.handleExampleDialogue(data);
|
||||
} else if(data.action === 'exit') {
|
||||
this.exit(data.reset);
|
||||
} else if(data.action === 'description') {
|
||||
this.description = data.description;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.registerMessageHandler(this.handleMessage);
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
122
talemate_frontend/src/components/CharacterImporter.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<v-dialog v-model="dialog" scrollable max-width="50%">
|
||||
<v-window>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<span class="headline">Import Character</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-autocomplete v-model="sceneInput" :items="scenes"
|
||||
label="Search scenes" outlined @update:search="updateSearchInput" @blur="fetchCharacters"
|
||||
item-title="label" item-value="path" :loading="sceneSearchLoading">
|
||||
</v-autocomplete>
|
||||
<v-select v-model="selectedCharacter" :items="characters" label="Character" outlined></v-select>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="secondary" text @click="dialog = false" :disabled="importing">Close</v-btn>
|
||||
<v-btn color="primary" text @click="importCharacter" :disabled="importing || !selectedCharacter">Import</v-btn>
|
||||
<v-progress-circular v-if="importing" indeterminate color="primary" size="20"></v-progress-circular>
|
||||
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-window>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
components: {
|
||||
},
|
||||
name: 'CharacterImporter',
|
||||
data() {
|
||||
return {
|
||||
sceneSearchInput: null,
|
||||
sceneSearchLoading: false,
|
||||
sceneInput: "",
|
||||
scenes: [],
|
||||
characters: [],
|
||||
dialog: false,
|
||||
selectedScene: null,
|
||||
selectedCharacter: null,
|
||||
importing: false,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
sceneInput(val) {
|
||||
this.selectedScene = val;
|
||||
}
|
||||
},
|
||||
inject: ['getWebsocket', 'registerMessageHandler', 'setWaitingForInput', 'requestSceneAssets'],
|
||||
methods: {
|
||||
show() {
|
||||
this.dialog = true;
|
||||
},
|
||||
exit() {
|
||||
this.dialog = false
|
||||
},
|
||||
updateSearchInput(val) {
|
||||
this.sceneSearchInput = val;
|
||||
clearTimeout(this.searchTimeout); // Clear the previous timeout
|
||||
this.searchTimeout = setTimeout(this.fetchScenes, 300); // Start a new timeout
|
||||
},
|
||||
sendRequest(data) {
|
||||
data.type = 'character_importer';
|
||||
this.getWebsocket().send(JSON.stringify(data));
|
||||
},
|
||||
|
||||
importCharacter() {
|
||||
this.importing = true;
|
||||
this.sendRequest({
|
||||
action: 'import',
|
||||
scene_path: this.selectedScene,
|
||||
character_name: this.selectedCharacter,
|
||||
})
|
||||
},
|
||||
|
||||
handleMessage(data) {
|
||||
|
||||
if (data.type === 'scenes_list') {
|
||||
this.scenes = data.data;
|
||||
this.sceneSearchLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.type === 'character_importer') {
|
||||
|
||||
if(data.action === 'list_characters') {
|
||||
this.characters = data.characters;
|
||||
return;
|
||||
} else if(data.action === 'import_character_done') {
|
||||
this.importing = false;
|
||||
this.dialog = false;
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
},
|
||||
fetchScenes() {
|
||||
if (!this.sceneSearchInput)
|
||||
return
|
||||
this.sceneSearchLoading = true;
|
||||
console.log("Fetching scenes", this.sceneSearchInput)
|
||||
this.getWebsocket().send(JSON.stringify({ type: 'request_scenes_list', query: this.sceneSearchInput }));
|
||||
},
|
||||
fetchCharacters() {
|
||||
if (!this.selectedScene)
|
||||
return
|
||||
this.sendRequest({
|
||||
action: 'list_characters',
|
||||
scene_path: this.selectedScene,
|
||||
})
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.registerMessageHandler(this.handleMessage);
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
116
talemate_frontend/src/components/CharacterMessage.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<v-alert variant="text" closable type="info" icon="mdi-chat-outline" elevation="0" density="compact" @click:close="deleteMessage()">
|
||||
<v-alert-title :style="{ color: color }" class="text-subtitle-1">
|
||||
{{ character }}
|
||||
<v-chip size="x-small" color="indigo-lighten-4" v-if="editing">
|
||||
<v-icon class="mr-1">mdi-pencil</v-icon>
|
||||
Editing - Press `enter` to submit. Click anywhere to cancel.</v-chip>
|
||||
<v-chip size="x-small" color="grey-lighten-1" v-else-if="!editing && hovered" variant="outlined">
|
||||
<v-icon class="mr-1">mdi-pencil</v-icon>
|
||||
Double-click to edit.</v-chip>
|
||||
</v-alert-title>
|
||||
<div class="character-message">
|
||||
<div class="character-avatar">
|
||||
<!-- Placeholder for character avatar -->
|
||||
</div>
|
||||
<v-textarea ref="textarea" v-if="editing" v-model="editing_text" @keydown.enter.prevent="submitEdit()" @blur="cancelEdit()" @keydown.escape.prevent="cancelEdit()">
|
||||
</v-textarea>
|
||||
<div v-else class="character-text" @dblclick="startEdit()" @mouseover="hovered=true" @mouseout="hovered=false">
|
||||
<span v-for="(part, index) in parts" :key="index" :class="{ highlight: part.isNarrative }">
|
||||
<span>{{ part.text }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</v-alert>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['character', 'text', 'color', 'message_id'],
|
||||
inject: ['requestDeleteMessage', 'getWebsocket'],
|
||||
computed: {
|
||||
parts() {
|
||||
const parts = [];
|
||||
let start = 0;
|
||||
let match;
|
||||
const regex = /\*(.*?)\*/g;
|
||||
while ((match = regex.exec(this.text)) !== null) {
|
||||
if (match.index > start) {
|
||||
parts.push({ text: this.text.slice(start, match.index), isNarrative: false });
|
||||
}
|
||||
parts.push({ text: match[1], isNarrative: true });
|
||||
start = match.index + match[0].length;
|
||||
}
|
||||
if (start < this.text.length) {
|
||||
parts.push({ text: this.text.slice(start), isNarrative: false });
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editing: false,
|
||||
editing_text: "",
|
||||
hovered: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancelEdit() {
|
||||
console.log('cancelEdit', this.message_id);
|
||||
this.editing = false;
|
||||
},
|
||||
startEdit() {
|
||||
this.editing_text = this.text;
|
||||
this.editing = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.textarea.focus();
|
||||
});
|
||||
},
|
||||
submitEdit() {
|
||||
console.log('submitEdit', this.message_id, this.editing_text);
|
||||
this.getWebsocket().send(JSON.stringify({ type: 'edit_message', id: this.message_id, text: this.character+": "+this.editing_text }));
|
||||
this.editing = false;
|
||||
},
|
||||
deleteMessage() {
|
||||
console.log('deleteMessage', this.message_id);
|
||||
this.requestDeleteMessage(this.message_id);
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.highlight {
|
||||
color: #9FA8DA;
|
||||
font-style: italic;
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.highlight:before {
|
||||
--content: "*";
|
||||
}
|
||||
|
||||
.highlight:after {
|
||||
--content: "*";
|
||||
}
|
||||
|
||||
.character-message {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
text-shadow: 2px 2px 4px #000000;
|
||||
}
|
||||
|
||||
.character-name {
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.character-text {
|
||||
color: #E0E0E0;
|
||||
}
|
||||
.character-avatar {
|
||||
height: 50px;
|
||||
margin-top: 10px;
|
||||
}</style>
|
||||
172
talemate_frontend/src/components/CharacterSheet.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<v-dialog v-model="dialog" scrollable max-width="50%">
|
||||
<v-card>
|
||||
<v-tabs v-model="tab">
|
||||
<v-tab value="overview">Overview</v-tab>
|
||||
<v-tab value="details">Details</v-tab>
|
||||
</v-tabs>
|
||||
<v-window v-model="tab">
|
||||
<v-window-item value="overview">
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="4">
|
||||
<div v-if="cover_image">
|
||||
<v-tooltip text="Drag and drop an image here to change the cover image for this character" max-width="200" location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-img ref="coverImage" v-if="cover_image" v-bind="props" v-on:drop="onDrop" v-on:dragover.prevent :src="'data:'+media_type+';base64, '+base64"></v-img>
|
||||
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
|
||||
<div v-else v-on:dragover.prevent v-on:drop="onDrop">
|
||||
<v-img src="@/assets/logo-13.1-backdrop.png" cover></v-img>
|
||||
<v-alert type="info" variant="tonal">
|
||||
Drag and drop an image here to set the cover image for this character
|
||||
</v-alert>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<span class="text-h5">{{ name }}</span>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<p>
|
||||
{{ description }}
|
||||
</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-window-item>
|
||||
<v-window-item value="details">
|
||||
<v-card-text style="max-height:600px; overflow-y:scroll;">
|
||||
<v-list-item v-for="(value, key) in base_attributes" :key="key">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ key }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ value }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-card-text>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'CharacterSheet',
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
cover_image: null,
|
||||
image_base64: null,
|
||||
media_type: null,
|
||||
base_attributes: {},
|
||||
name: null,
|
||||
description: null,
|
||||
color: null,
|
||||
characters: [],
|
||||
tab: "overview",
|
||||
}
|
||||
},
|
||||
inject: ['getWebsocket', 'registerMessageHandler', 'setWaitingForInput', 'requestSceneAssets'],
|
||||
methods: {
|
||||
characterExists(name) {
|
||||
for (let character of this.characters) {
|
||||
// if character name is contained in name (case insensitive) and vice versa
|
||||
|
||||
if (name.toLowerCase().indexOf(character.name.toLowerCase()) !== -1 || character.name.toLowerCase().indexOf(name.toLowerCase()) !== -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
openForCharacter(character) {
|
||||
this.name = character.name;
|
||||
this.description = character.description;
|
||||
this.color = character.color;
|
||||
this.base_attributes = character.base_attributes;
|
||||
this.cover_image = character.cover_image;
|
||||
this.dialog = true;
|
||||
if (this.cover_image) {
|
||||
this.requestSceneAssets([this.cover_image]);
|
||||
}
|
||||
},
|
||||
openForCharacterName(name) {
|
||||
console.log("openForCharacterName", name, this.characters)
|
||||
for (let character of this.characters) {
|
||||
if (name.toLowerCase().indexOf(character.name.toLowerCase()) !== -1 || character.name.toLowerCase().indexOf(name.toLowerCase()) !== -1) {
|
||||
this.openForCharacter(character);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
handleMessage(data) {
|
||||
if (data.type === "scene_status" && data.status == "started") {
|
||||
this.characters = data.data.characters;
|
||||
// update the character sheet if it is open
|
||||
if (this.dialog) {
|
||||
this.openForCharacterName(this.name);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(data.type === 'scene_asset') {
|
||||
if(data.asset_id == this.cover_image) {
|
||||
this.base64 = data.asset;
|
||||
this.media_type = data.media_type;
|
||||
} else {
|
||||
this.base64 = null;
|
||||
this.cover_image = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(data.type === "scene_asset_character_cover_image") {
|
||||
console.log("scene_asset_character_cover_image", data)
|
||||
if(data.character == this.name) {
|
||||
console.log("setting cover image", data.asset_id)
|
||||
this.cover_image = null;
|
||||
this.$nextTick(() => {
|
||||
this.base64 = data.asset;
|
||||
this.media_type = data.media_type;
|
||||
this.cover_image = data.asset_id;
|
||||
});
|
||||
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
},
|
||||
onDrop(e) {
|
||||
e.preventDefault();
|
||||
let files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
let reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
let result = e.target.result;
|
||||
this.uploadCharacterImage(result);
|
||||
};
|
||||
reader.readAsDataURL(files[0]);
|
||||
}
|
||||
},
|
||||
uploadCharacterImage(image_file_base64) {
|
||||
this.getWebsocket().send(JSON.stringify({
|
||||
type: 'upload_scene_asset',
|
||||
scene_cover_image: false,
|
||||
character_cover_image: this.name,
|
||||
content: image_file_base64,
|
||||
}));
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.registerMessageHandler(this.handleMessage);
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
```
|
||||
80
talemate_frontend/src/components/ClientModal.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<v-dialog v-model="localDialog" persistent max-width="600px">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<span class="headline">{{ formTitle }}</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<v-select v-model="client.type" :items="['openai', 'textgenwebui']" label="Client Type"></v-select>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-text-field v-model="client.name" label="Client Name"></v-text-field>
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-text-field v-model="client.apiUrl" v-if="client.type === 'textgenwebui'" label="API URL"></v-text-field>
|
||||
<v-select v-model="client.model" v-if="client.type === 'openai'" :items="['gpt-4', 'gpt-3.5-turbo', 'gpt-3.5-turbo-16k']" label="Model"></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<v-text-field v-model="client.max_token_length" v-if="client.type === 'textgenwebui'" type="number" label="Context Length"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="blue darken-1" text @click="close">Close</v-btn>
|
||||
<v-btn color="blue darken-1" text @click="save">Save</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
dialog: Boolean,
|
||||
formTitle: String
|
||||
},
|
||||
inject: ['state'],
|
||||
data() {
|
||||
return {
|
||||
localDialog: this.state.dialog,
|
||||
client: { ...this.state.currentClient } // Define client data property
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
'state.dialog': {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
this.localDialog = newVal;
|
||||
}
|
||||
},
|
||||
'state.currentClient': {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
this.client = { ...newVal }; // Update client data property when currentClient changes
|
||||
}
|
||||
},
|
||||
localDialog(newVal) {
|
||||
this.$emit('update:dialog', newVal);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit('update:dialog', false);
|
||||
},
|
||||
save() {
|
||||
this.$emit('save', this.client); // Emit save event with client object
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
62
talemate_frontend/src/components/CoverImage.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div v-if="expanded">
|
||||
<v-img @click="toggle()" v-if="asset_id !== null" :src="'data:'+media_type+';base64, '+base64"></v-img>
|
||||
</div>
|
||||
<v-list-subheader v-else @click="toggle()"><v-icon>mdi-image-frame</v-icon> Cover image
|
||||
<v-icon v-if="expanded" icon="mdi-chevron-down"></v-icon>
|
||||
<v-icon v-else icon="mdi-chevron-up"></v-icon>
|
||||
</v-list-subheader>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'CoverImage',
|
||||
data() {
|
||||
return {
|
||||
asset_id: null,
|
||||
base64: null,
|
||||
media_type: null,
|
||||
expanded: true,
|
||||
}
|
||||
},
|
||||
inject: ['getWebsocket', 'registerMessageHandler', 'setWaitingForInput', 'requestSceneAssets'],
|
||||
methods: {
|
||||
|
||||
toggle() {
|
||||
this.expanded = !this.expanded;
|
||||
},
|
||||
|
||||
handleMessage(data) {
|
||||
|
||||
if(data.type === "scene_status" && data.status == "started") {
|
||||
let assets = data.data.assets;
|
||||
if(assets.cover_image !== null) {
|
||||
if(assets.cover_image != this.asset_id) {
|
||||
this.asset_id = assets.cover_image;
|
||||
this.requestSceneAssets([assets.cover_image]);
|
||||
}
|
||||
} else {
|
||||
this.asset_id = null;
|
||||
this.base64 = null;
|
||||
this.media_type = null;
|
||||
}
|
||||
}
|
||||
|
||||
if(data.type === 'scene_asset') {
|
||||
if(data.asset_id == this.asset_id) {
|
||||
this.base64 = data.asset;
|
||||
this.media_type = data.media_type;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.registerMessageHandler(this.handleMessage);
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
72
talemate_frontend/src/components/CreativeEditor.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<CreativeMenu ref="menu"/>
|
||||
<CharacterCreator ref="characterCreator"/>
|
||||
<CharacterImporter ref="characterImporter"/>
|
||||
<SceneCreator ref="sceneCreator"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import CreativeMenu from './CreativeMenu.vue';
|
||||
import CharacterCreator from './CharacterCreator.vue';
|
||||
import CharacterImporter from './CharacterImporter.vue';
|
||||
import SceneCreator from './SceneCreator.vue';
|
||||
|
||||
export default {
|
||||
name: 'CreativeEditor',
|
||||
components: {
|
||||
CreativeMenu,
|
||||
CharacterCreator,
|
||||
CharacterImporter,
|
||||
SceneCreator,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
|
||||
provide() {
|
||||
return {
|
||||
openCharacterCreator: this.openCharacterCreator,
|
||||
openCharacterCreatorForCharacter: this.openCharacterCreatorForCharacter,
|
||||
openCharacterImporter: this.openCharacterImporter,
|
||||
openSceneCreator: this.openSceneCreator,
|
||||
}
|
||||
},
|
||||
|
||||
inject: [
|
||||
'getWebsocket',
|
||||
'registerMessageHandler',
|
||||
'setWaitingForInput',
|
||||
],
|
||||
|
||||
methods: {
|
||||
handleMessage(data) {
|
||||
if(data.type === 'world_state') {
|
||||
console.log("world_state");
|
||||
}
|
||||
},
|
||||
openCharacterCreator() {
|
||||
this.$refs.characterCreator.reset();
|
||||
this.$refs.characterCreator.show();
|
||||
},
|
||||
openCharacterCreatorForCharacter(character) {
|
||||
this.$refs.characterCreator.showForCharacter(character);
|
||||
},
|
||||
openCharacterImporter() {
|
||||
this.$refs.characterImporter.show();
|
||||
},
|
||||
openSceneCreator() {
|
||||
this.$refs.sceneCreator.show();
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.registerMessageHandler(this.handleMessage);
|
||||
},
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
127
talemate_frontend/src/components/CreativeMenu.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div v-if="scene.environment === 'creative'">
|
||||
<v-list-subheader class="text-uppercase">
|
||||
<v-icon class="mr-1">mdi-account</v-icon>Characters
|
||||
</v-list-subheader>
|
||||
|
||||
<div ref="charactersContainer">
|
||||
<v-list>
|
||||
<v-list-item density="compact" v-for="(character,index) in scene.characters" :key="index">
|
||||
<v-list-item-title>
|
||||
{{ character.name }}
|
||||
</v-list-item-title>
|
||||
<div class="text-center mt-1 mb-1">
|
||||
|
||||
<v-tooltip text="Remove">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn size="x-small" class="mr-1" v-bind="props" variant="tonal" density="comfortable" rounded="sm" color="red" icon="mdi-account-cancel" @click.stop="removeCharacterFromScene(character.name)"></v-btn>
|
||||
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip text="Edit character">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn size="x-small" class="mr-1" v-bind="props" variant="tonal" density="comfortable" rounded="sm" icon="mdi-account-edit" @click.stop="openCharacterCreatorForCharacter(character.name)"></v-btn>
|
||||
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip v-if="false" text="Character sheet">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn size="x-small" class="mr-1" v-bind="props" variant="tonal" density="comfortable" rounded="sm" icon="mdi-account-details"></v-btn>
|
||||
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
<v-divider></v-divider>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-tooltip text="Add character">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn @click="openCharacterCreator" v-bind="props" density="comfortable" class="mt-1 mr-1" size="small" color="primary" icon="mdi-account-plus" rounded="sm"></v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip text="Import character from other scene">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn @click="openCharacterImporter" v-bind="props" density="comfortable" class="mt-1" size="small" color="primary" icon="mdi-account-arrow-right" rounded="sm"></v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
|
||||
|
||||
<v-list-subheader class="text-uppercase">
|
||||
<v-icon class="mr-1">mdi-image</v-icon>Scenario
|
||||
</v-list-subheader>
|
||||
|
||||
<v-list ref="sceneContainer">
|
||||
<v-list-item>
|
||||
<v-list-item-title>
|
||||
{{ scene.name }}
|
||||
</v-list-item-title>
|
||||
<v-btn block color="primary" @click="openSceneCreator" prepend-icon="mdi-pencil">Edit Scenario</v-btn>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CreativeMenu',
|
||||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
scene: {
|
||||
characters: {},
|
||||
environment: null,
|
||||
description: null,
|
||||
name: null,
|
||||
intro: null,
|
||||
}
|
||||
}
|
||||
},
|
||||
inject: [
|
||||
'getWebsocket',
|
||||
'registerMessageHandler',
|
||||
'isConnected',
|
||||
'openCharacterCreator',
|
||||
'openCharacterCreatorForCharacter',
|
||||
'openCharacterImporter',
|
||||
'openSceneCreator',
|
||||
],
|
||||
methods: {
|
||||
toggle() {
|
||||
this.expanded = !this.expanded;
|
||||
},
|
||||
|
||||
removeCharacterFromScene(character) {
|
||||
|
||||
let confirm = window.confirm(`Are you sure you want to remove ${character} from the scene?`);
|
||||
|
||||
if(!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.getWebsocket().send(JSON.stringify({
|
||||
type: 'interact',
|
||||
text: `!remove_character:${character}`,
|
||||
}));
|
||||
},
|
||||
|
||||
handleMessage(data) {
|
||||
if(data.type === 'scene_status' && data.status === 'started') {
|
||||
this.scene = data.data;
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.registerMessageHandler(this.handleMessage);
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped></style>
|
||||
65
talemate_frontend/src/components/DirectorMessage.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="director-container" v-if="show && minimized" >
|
||||
<v-chip closable @click:close="deleteMessage()" color="deep-purple-lighten-3">
|
||||
<v-icon class="mr-2">mdi-bullhorn-outline</v-icon>
|
||||
<span @click="toggle()">{{ character }}</span>
|
||||
</v-chip>
|
||||
</div>
|
||||
<v-alert v-else-if="show" class="director-message" variant="text" :closable="message_id !== null" type="info" icon="mdi-bullhorn-outline"
|
||||
elevation="0" density="compact" @click:close="deleteMessage()" >
|
||||
<div class="director-text" @click="toggle()">{{ text }}</div>
|
||||
</v-alert>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
show: true,
|
||||
minimized: true
|
||||
}
|
||||
},
|
||||
props: ['text', 'message_id', 'character'],
|
||||
inject: ['requestDeleteMessage'],
|
||||
methods: {
|
||||
toggle() {
|
||||
this.minimized = !this.minimized;
|
||||
},
|
||||
deleteMessage() {
|
||||
console.log('deleteMessage', this.message_id);
|
||||
this.requestDeleteMessage(this.message_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.highlight {
|
||||
color: #9FA8DA;
|
||||
font-style: italic;
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.highlight:before {
|
||||
--content: "*";
|
||||
}
|
||||
|
||||
.highlight:after {
|
||||
--content: "*";
|
||||
}
|
||||
|
||||
.director-text {
|
||||
color: #9FA8DA;
|
||||
}
|
||||
|
||||
.director-message {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
color: #9FA8DA;
|
||||
}
|
||||
|
||||
.director-container {
|
||||
|
||||
}
|
||||
</style>
|
||||
63
talemate_frontend/src/components/GameOptions.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
|
||||
<v-list-subheader class="text-uppercase">
|
||||
<v-icon class="mr-1">mdi-cogs</v-icon>Options
|
||||
<v-btn-toggle v-model="selected_options" density="compact">
|
||||
<v-btn value="automated_actions"><v-icon>mdi-brightness-auto</v-icon></v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-list-subheader>
|
||||
|
||||
<v-card v-if="selected_options==='automated_actions'">
|
||||
<v-card-title class="text-subtitle-2"><v-icon>mdi-brightness-auto</v-icon> Automatic actions</v-card-title>
|
||||
<v-card-text>
|
||||
<div v-for="(value, action) in scene_config.automated_actions" :key="action">
|
||||
<v-switch v-model="scene_config.automated_actions[action]" :label="cleanLabel(action)" class="ml-2 text-caption" density="compact" @update:model-value="saveSceneConfig()"></v-switch>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'GameOptions',
|
||||
data() {
|
||||
return {
|
||||
selected_options: null,
|
||||
scene_config: {
|
||||
automated_actions: {
|
||||
world_state: true,
|
||||
director: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
inject: ['getWebsocket', 'registerMessageHandler', 'setWaitingForInput'],
|
||||
|
||||
methods: {
|
||||
cleanLabel(label) {
|
||||
return label.replace(/_/g, ' ');
|
||||
},
|
||||
saveSceneConfig() {
|
||||
this.getWebsocket().send(JSON.stringify({
|
||||
type: 'scene_config',
|
||||
scene_config: this.scene_config,
|
||||
}));
|
||||
},
|
||||
handleMessage(data) {
|
||||
if(data.type === 'scene_status') {
|
||||
this.scene_config = data.data.scene_config;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.registerMessageHandler(this.handleMessage);
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
149
talemate_frontend/src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row class="text-center">
|
||||
<v-col cols="12">
|
||||
<v-img
|
||||
:src="require('../assets/logo.svg')"
|
||||
class="my-3"
|
||||
contain
|
||||
height="200"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col class="mb-4">
|
||||
<h1 class="display-2 font-weight-bold mb-3">
|
||||
Welcome to the Vuetify 3 Beta
|
||||
</h1>
|
||||
|
||||
|
||||
<p class="subheading font-weight-regular">
|
||||
For help and collaboration with other Vuetify developers,
|
||||
<br>please join our online
|
||||
<a
|
||||
href="https://community.vuetifyjs.com"
|
||||
target="_blank"
|
||||
>Discord Community</a>
|
||||
</p>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
class="mb-5"
|
||||
cols="12"
|
||||
>
|
||||
<h2 class="headline font-weight-bold mb-5">
|
||||
What's next?
|
||||
</h2>
|
||||
|
||||
<v-row justify="center">
|
||||
<a
|
||||
v-for="(next, i) in whatsNext"
|
||||
:key="i"
|
||||
:href="next.href"
|
||||
class="subheading mx-3"
|
||||
target="_blank"
|
||||
>
|
||||
{{ next.text }}
|
||||
</a>
|
||||
</v-row>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
class="mb-5"
|
||||
cols="12"
|
||||
>
|
||||
<h2 class="headline font-weight-bold mb-5">
|
||||
Important Links
|
||||
</h2>
|
||||
|
||||
<v-row justify="center">
|
||||
<a
|
||||
v-for="(link, i) in importantLinks"
|
||||
:key="i"
|
||||
:href="link.href"
|
||||
class="subheading mx-3"
|
||||
target="_blank"
|
||||
>
|
||||
{{ link.text }}
|
||||
</a>
|
||||
</v-row>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
class="mb-5"
|
||||
cols="12"
|
||||
>
|
||||
<h2 class="headline font-weight-bold mb-5">
|
||||
Ecosystem
|
||||
</h2>
|
||||
|
||||
<v-row justify="center">
|
||||
<a
|
||||
v-for="(eco, i) in ecosystem"
|
||||
:key="i"
|
||||
:href="eco.href"
|
||||
class="subheading mx-3"
|
||||
target="_blank"
|
||||
>
|
||||
{{ eco.text }}
|
||||
</a>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'HelloWorld',
|
||||
|
||||
data: () => ({
|
||||
ecosystem: [
|
||||
{
|
||||
text: 'vuetify-loader',
|
||||
href: 'https://github.com/vuetifyjs/vuetify-loader/tree/next',
|
||||
},
|
||||
{
|
||||
text: 'github',
|
||||
href: 'https://github.com/vuetifyjs/vuetify/tree/next',
|
||||
},
|
||||
{
|
||||
text: 'awesome-vuetify',
|
||||
href: 'https://github.com/vuetifyjs/awesome-vuetify',
|
||||
},
|
||||
],
|
||||
importantLinks: [
|
||||
{
|
||||
text: 'Chat',
|
||||
href: 'https://community.vuetifyjs.com',
|
||||
},
|
||||
{
|
||||
text: 'Made with Vuetify',
|
||||
href: 'https://madewithvuejs.com/vuetify',
|
||||
},
|
||||
{
|
||||
text: 'Twitter',
|
||||
href: 'https://twitter.com/vuetifyjs',
|
||||
},
|
||||
{
|
||||
text: 'Articles',
|
||||
href: 'https://medium.com/vuetify',
|
||||
},
|
||||
],
|
||||
whatsNext: [
|
||||
{
|
||||
text: 'Explore components',
|
||||
href: 'https://vuetifyjs.com',
|
||||
},
|
||||
{
|
||||
text: 'Roadmap',
|
||||
href: 'https://vuetifyjs.com/introduction/roadmap/',
|
||||
},
|
||||
{
|
||||
text: 'Frequently Asked Questions',
|
||||
href: 'https://vuetifyjs.com/getting-started/frequently-asked-questions',
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
</script>
|
||||
135
talemate_frontend/src/components/LoadScene.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<v-list-subheader @click="toggle()" class="text-uppercase"><v-icon>mdi-script-text-outline</v-icon> Load
|
||||
<v-progress-circular v-if="loading" indeterminate color="primary" size="20"></v-progress-circular>
|
||||
<v-icon v-if="expanded" icon="mdi-chevron-down"></v-icon>
|
||||
<v-icon v-else icon="mdi-chevron-up"></v-icon>
|
||||
</v-list-subheader>
|
||||
<v-list-item-group v-if="!loading && isConnected() && expanded && !configurationRequired()">
|
||||
<v-list-item>
|
||||
<v-list-item-content class="mb-3">
|
||||
<!-- Toggle buttons for switching between file upload and path input -->
|
||||
<v-btn-toggle density="compact" class="mb-3" v-model="inputMethod" mandatory>
|
||||
<v-btn value="file">
|
||||
<v-icon>mdi-file-upload</v-icon>
|
||||
</v-btn>
|
||||
<v-btn value="path">
|
||||
<v-icon>mdi-file-document-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn value="creative">
|
||||
<v-icon>mdi-palette-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
<!-- File input for file upload -->
|
||||
<div v-if="inputMethod === 'file' && !loading">
|
||||
<v-file-input prepend-icon="" style="height:200px" density="compact" v-model="sceneFile" @change="loadScene" label="Upload a character card"
|
||||
outlined accept="image/*" variant="solo-filled"></v-file-input>
|
||||
</div>
|
||||
<!-- Text field for path input -->
|
||||
<v-autocomplete v-else-if="inputMethod === 'path' && !loading" v-model="sceneInput" :items="scenes"
|
||||
label="Search scenes" outlined @update:search="updateSearchInput" item-title="label" item-value="path" :loading="sceneSearchLoading">
|
||||
</v-autocomplete>
|
||||
<!-- Upload/Load button -->
|
||||
<v-btn v-if="!loading && inputMethod === 'path'" @click="loadScene" color="primary" block class="mb-3">
|
||||
<v-icon left>mdi-folder</v-icon>
|
||||
Load
|
||||
</v-btn>
|
||||
<v-btn v-else-if="!loading && inputMethod === 'creative'" @click="loadCreative" color="primary" block class="mb-3">
|
||||
<v-icon left>mdi-palette-outline</v-icon>
|
||||
Creative Mode
|
||||
</v-btn>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
<div v-else-if="configurationRequired()">
|
||||
<v-alert type="warning" variant="tonal">You need to configure a Talemate client before you can load scenes.</v-alert>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LoadScene',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
inputMethod: 'path',
|
||||
sceneFile: [],
|
||||
sceneInput: '',
|
||||
scenes: [],
|
||||
sceneSearchInput: null,
|
||||
sceneSearchLoading: false,
|
||||
expanded: true,
|
||||
}
|
||||
},
|
||||
inject: ['getWebsocket', 'registerMessageHandler', 'isConnected', 'configurationRequired'],
|
||||
methods: {
|
||||
toggle() {
|
||||
this.expanded = !this.expanded;
|
||||
},
|
||||
updateSearchInput(val) {
|
||||
this.sceneSearchInput = val;
|
||||
clearTimeout(this.searchTimeout); // Clear the previous timeout
|
||||
this.searchTimeout = setTimeout(this.fetchScenes, 300); // Start a new timeout
|
||||
},
|
||||
fetchScenes() {
|
||||
if (!this.sceneSearchInput)
|
||||
return
|
||||
this.sceneSearchLoading = true;
|
||||
console.log("Fetching scenes", this.sceneSearchInput)
|
||||
this.getWebsocket().send(JSON.stringify({ type: 'request_scenes_list', query: this.sceneSearchInput }));
|
||||
},
|
||||
loadCreative() {
|
||||
this.loading = true;
|
||||
this.getWebsocket().send(JSON.stringify({ type: 'load_scene', file_path: "environment:creative" }));
|
||||
},
|
||||
loadScene() {
|
||||
this.loading = true;
|
||||
if (this.inputMethod === 'file' && this.sceneFile.length > 0) { // Check if the input method is "file" and there is at least one file
|
||||
// Convert the uploaded file to base64
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(this.sceneFile[0]); // Access the first file in the array
|
||||
reader.onload = () => {
|
||||
//const base64File = reader.result.split(',')[1];
|
||||
this.getWebsocket().send(JSON.stringify({
|
||||
type: 'load_scene',
|
||||
scene_data: reader.result,
|
||||
filename: this.sceneFile[0].name,
|
||||
}));
|
||||
this.sceneFile = [];
|
||||
};
|
||||
} else if (this.inputMethod === 'path' && this.sceneInput) { // Check if the input method is "path" and the scene input is not empty
|
||||
this.getWebsocket().send(JSON.stringify({ type: 'load_scene', file_path: this.sceneInput }));
|
||||
this.sceneInput = '';
|
||||
}
|
||||
},
|
||||
handleMessage(data) {
|
||||
|
||||
// Scene loaded
|
||||
if (data.type === "system") {
|
||||
if (data.id === 'scene.loaded') {
|
||||
this.loading = false;
|
||||
this.expanded = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle scenes_list message type
|
||||
if (data.type === 'scenes_list') {
|
||||
this.scenes = data.data;
|
||||
this.sceneSearchLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.registerMessageHandler(this.handleMessage);
|
||||
},
|
||||
mounted() {
|
||||
console.log("Websocket", this.getWebsocket()); // Check if websocket is available
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* styles for LoadScene component */
|
||||
</style>
|
||||
96
talemate_frontend/src/components/NarratorMessage.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<v-alert variant="text" :closable="message_id !== null" type="info" icon="mdi-script-text-outline" elevation="0" density="compact" @click:close="deleteMessage()">
|
||||
|
||||
<div class="narrator-message">
|
||||
<v-textarea ref="textarea" v-if="editing" v-model="editing_text" @keydown.enter.prevent="submitEdit()" @blur="cancelEdit()" @keydown.escape.prevent="cancelEdit()">
|
||||
</v-textarea>
|
||||
<div v-else class="narrator-text" @dblclick="startEdit()">
|
||||
<span v-for="(part, index) in parts" :key="index" :class="{ highlight: part.isNarrative }">
|
||||
{{ part.text }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</v-alert>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['text', 'message_id'],
|
||||
inject: ['requestDeleteMessage', 'getWebsocket'],
|
||||
computed: {
|
||||
parts() {
|
||||
const parts = [];
|
||||
let start = 0;
|
||||
let match;
|
||||
// Using [\s\S] instead of . to match across multiple lines
|
||||
const regex = /\*([\s\S]*?)\*/g;
|
||||
while ((match = regex.exec(this.text)) !== null) {
|
||||
if (match.index > start) {
|
||||
parts.push({ text: this.text.slice(start, match.index), isNarrative: false });
|
||||
}
|
||||
parts.push({ text: match[1], isNarrative: true });
|
||||
start = match.index + match[0].length;
|
||||
}
|
||||
if (start < this.text.length) {
|
||||
parts.push({ text: this.text.slice(start), isNarrative: false });
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editing: false,
|
||||
editing_text: "",
|
||||
hovered: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancelEdit() {
|
||||
console.log('cancelEdit', this.message_id);
|
||||
this.editing = false;
|
||||
},
|
||||
startEdit() {
|
||||
this.editing_text = this.text;
|
||||
this.editing = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.textarea.focus();
|
||||
});
|
||||
},
|
||||
submitEdit() {
|
||||
console.log('submitEdit', this.message_id, this.editing_text);
|
||||
this.getWebsocket().send(JSON.stringify({ type: 'edit_message', id: this.message_id, text: this.editing_text }));
|
||||
this.editing = false;
|
||||
},
|
||||
deleteMessage() {
|
||||
console.log('deleteMessage', this.message_id);
|
||||
this.requestDeleteMessage(this.message_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.highlight {
|
||||
color: #9FA8DA;
|
||||
font-style: italic;
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.highlight:before {
|
||||
--content: "*";
|
||||
}
|
||||
|
||||
.highlight:after {
|
||||
--content: "*";
|
||||
}
|
||||
|
||||
.narrator-text {
|
||||
color: #E0E0E0;
|
||||
}
|
||||
|
||||
.narrator-message {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
color: #26A69A;
|
||||
}</style>
|
||||
122
talemate_frontend/src/components/SceneCreator.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<v-dialog v-model="dialog" scrollable max-width="50%" @update:model-value="submit('edit')">
|
||||
<v-window>
|
||||
<v-card density="compact">
|
||||
<v-card-text>
|
||||
<v-select :disabled="generating" v-model="sceneContentContext" :items="contentContexts" label="Content Context"></v-select>
|
||||
<v-textarea :disabled="generating" v-model="generationPrompt" label="AI Prompt"></v-textarea>
|
||||
</v-card-text>
|
||||
<v-card-text style="max-height:500px; overflow-y:scroll;">
|
||||
|
||||
<v-text-field :disabled="generating" v-model="sceneName" label="Scene Name" append-inner-icon="mdi-refresh" @click:append-inner="submit('generate_name')"></v-text-field>
|
||||
|
||||
<v-textarea :disabled="generating" v-model="sceneDescription" auto-grow label="Scene Description" append-inner-icon="mdi-refresh" @click:append-inner="submit('generate_description')"></v-textarea>
|
||||
|
||||
<v-textarea :disabled="generating" v-model="sceneIntro" auto-grow label="Scene Intro" append-inner-icon="mdi-refresh" @click:append-inner="submit('generate_intro')"></v-textarea>
|
||||
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-progress-circular class="ml-1 mr-3" size="24" v-if="generating" indeterminate color="primary"></v-progress-circular>
|
||||
<v-btn color="primary" @click="submit('generate')" :disabled="generating" prepend-icon="mdi-memory">Generate</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-window>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
components: {
|
||||
},
|
||||
name: 'SceneCreator',
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
sceneName: null,
|
||||
sceneDescription: null,
|
||||
sceneIntro: null,
|
||||
sceneCoverImage: null,
|
||||
sceneContentContext: null,
|
||||
contentContexts: [],
|
||||
generationPrompt: null,
|
||||
generating: false,
|
||||
}
|
||||
},
|
||||
inject: ['getWebsocket', 'registerMessageHandler', 'setWaitingForInput', 'requestSceneAssets', 'appConfig'],
|
||||
methods: {
|
||||
show() {
|
||||
this.dialog = true;
|
||||
this.contentContexts = this.appConfig().creator.content_context;
|
||||
this.sendRequest({
|
||||
action: 'load',
|
||||
});
|
||||
},
|
||||
exit() {
|
||||
this.dialog = false
|
||||
},
|
||||
reset() {
|
||||
this.sceneName = null;
|
||||
this.sceneDescription = null;
|
||||
this.sceneIntro = null;
|
||||
this.sceneCoverImage = null;
|
||||
this.sceneContentContext = null;
|
||||
this.generationPrompt = null;
|
||||
},
|
||||
|
||||
submit(step_action) {
|
||||
|
||||
if(step_action === 'generate_description')
|
||||
this.sceneDescription = null;
|
||||
else if(step_action === 'generate_intro')
|
||||
this.sceneIntro = null;
|
||||
else if(step_action === 'generate_name')
|
||||
this.sceneName = null;
|
||||
|
||||
this.sendRequest({
|
||||
action: step_action,
|
||||
name: this.sceneName,
|
||||
description: this.sceneDescription,
|
||||
intro: this.sceneIntro,
|
||||
content_context: this.sceneContentContext,
|
||||
prompt: this.generationPrompt,
|
||||
});
|
||||
},
|
||||
|
||||
sendRequest(data) {
|
||||
data.type = 'scene_creator';
|
||||
this.getWebsocket().send(JSON.stringify(data));
|
||||
},
|
||||
|
||||
handleSetGenerating() {
|
||||
this.generating = true;
|
||||
},
|
||||
|
||||
handleSetGeneratingDone() {
|
||||
this.generating = false;
|
||||
},
|
||||
|
||||
handleMessage(data) {
|
||||
|
||||
if(data.type === 'scene_creator') {
|
||||
if(data.action === 'set_generating') {
|
||||
this.handleSetGenerating();
|
||||
} else if(data.action === 'set_generating_done') {
|
||||
this.handleSetGeneratingDone();
|
||||
} else if(data.action === 'scene_update') {
|
||||
this.sceneDescription = data.description;
|
||||
this.sceneIntro = data.intro;
|
||||
this.sceneName = data.name;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
},
|
||||
created() {
|
||||
this.registerMessageHandler(this.handleMessage);
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
66
talemate_frontend/src/components/SceneHistory.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<v-dialog v-model="dialog" scrollable max-width="50%">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
History
|
||||
</v-card-title>
|
||||
<v-card-text style="max-height:600px; overflow-y:scroll;">
|
||||
<v-list-item v-for="(text, index) in history" :key="index" class="text-body-2">
|
||||
{{ text }}
|
||||
<v-divider class="mt-1"></v-divider>
|
||||
</v-list-item>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn prepend-icon="mdi-refresh" @click="regenerateHistory()">
|
||||
Regenerate History
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'SceneHistory',
|
||||
data() {
|
||||
return {
|
||||
history: [],
|
||||
dialog: false,
|
||||
regenerating: false,
|
||||
}
|
||||
},
|
||||
inject: ['getWebsocket', 'registerMessageHandler', 'setWaitingForInput'],
|
||||
methods: {
|
||||
|
||||
open() {
|
||||
this.dialog = true;
|
||||
this.requestSceneHistory();
|
||||
},
|
||||
|
||||
handleMessage(data) {
|
||||
|
||||
if (data.type === 'scene_history') {
|
||||
this.history = data.history;
|
||||
}
|
||||
},
|
||||
|
||||
requestSceneHistory() {
|
||||
this.getWebsocket().send(JSON.stringify({
|
||||
type: "request_scene_history",
|
||||
}));
|
||||
},
|
||||
|
||||
regenerateHistory() {
|
||||
this.history = [];
|
||||
this.getWebsocket().send(JSON.stringify({ type: 'interact', text: "!rebuild_archive" }));
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.registerMessageHandler(this.handleMessage);
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
242
talemate_frontend/src/components/SceneMessages.vue
Normal file
@@ -0,0 +1,242 @@
|
||||
<template>
|
||||
<div class="message-container" ref="messageContainer" style="flex-grow: 1; overflow-y: auto;">
|
||||
<div v-for="(message, index) in messages" :key="index">
|
||||
<div v-if="message.type === 'character' || message.type === 'processing_input'"
|
||||
:class="`message ${message.type}`" :id="`message-${message.id}`" :style="{ borderColor: message.color }">
|
||||
<div class="character-message">
|
||||
<CharacterMessage :character="message.character" :text="message.text" :color="message.color" :message_id="message.id" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="message.type === 'request_input' && message.choices">
|
||||
<v-alert variant="tonal" type="info" class="system-message mb-3">
|
||||
{{ message.text }}
|
||||
</v-alert>
|
||||
<div>
|
||||
<v-radio-group inline class="radio-group" v-if="!message.multiSelect" v-model="message.selectedChoices" :disabled="message.sent">
|
||||
<div v-for="(choice, index) in message.choices" :key="index">
|
||||
<v-radio :key="index" :label="choice" :value="choice"></v-radio>
|
||||
</div>
|
||||
</v-radio-group>
|
||||
<div v-else class="choice-buttons">
|
||||
<div v-for="(choice, index) in message.choices" :key="index">
|
||||
<v-checkbox :label="choice" v-model="message.selectedChoices" :value="choice" :disabled="message.sent"></v-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<v-btn v-if="!message.sent" @click="sendAllChoices(message)" color="secondary" :disabled="message.sent">Continue</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<v-alert v-else-if="message.type === 'system'" variant="tonal" closable :type="(message.status == 'error'?'error':'info')" class="system-message mb-3"
|
||||
:text="message.text">
|
||||
</v-alert>
|
||||
<div v-else-if="message.type === 'narrator'" :class="`message ${message.type}`">
|
||||
<div class="narrator-message" :id="`message-${message.id}`">
|
||||
<NarratorMessage :text="message.text" :message_id="message.id" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="message.type === 'director'" :class="`message ${message.type}`">
|
||||
<div class="director-message" :id="`message-${message.id}`">
|
||||
<DirectorMessage :text="message.text" :message_id="message.id" :character="message.character" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else :class="`message ${message.type}`">
|
||||
{{ message.text }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CharacterMessage from './CharacterMessage.vue';
|
||||
import NarratorMessage from './NarratorMessage.vue';
|
||||
import DirectorMessage from './DirectorMessage.vue';
|
||||
|
||||
export default {
|
||||
name: 'SceneMessages',
|
||||
components: {
|
||||
CharacterMessage,
|
||||
NarratorMessage,
|
||||
DirectorMessage,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
messages: [],
|
||||
}
|
||||
},
|
||||
inject: ['getWebsocket', 'registerMessageHandler', 'setWaitingForInput'],
|
||||
provide() {
|
||||
return {
|
||||
requestDeleteMessage: this.requestDeleteMessage,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
requestDeleteMessage(message_id) {
|
||||
this.getWebsocket().send(JSON.stringify({ type: 'delete_message', id: message_id }));
|
||||
},
|
||||
|
||||
handleChoiceInput(data) {
|
||||
// Create a new message with buttons for the choices
|
||||
const message = {
|
||||
id: data.id,
|
||||
type: data.type,
|
||||
text: data.message,
|
||||
choices: data.data.choices,
|
||||
selectedChoices: data.data.default || (data.data.multi_select ? [] : null),
|
||||
multiSelect: data.data.multi_select,
|
||||
color: data.color,
|
||||
sent: false,
|
||||
};
|
||||
this.messages.push(message);
|
||||
},
|
||||
|
||||
sendChoice(message, choice) {
|
||||
const index = message.selectedChoices.indexOf(choice);
|
||||
if (index === -1) {
|
||||
// If the checkbox is checked, add the choice to the selectedChoices array
|
||||
message.selectedChoices.push(choice);
|
||||
} else {
|
||||
// If the checkbox is unchecked, remove the choice from the selectedChoices array
|
||||
message.selectedChoices.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
sendAllChoices(message) {
|
||||
|
||||
let text;
|
||||
|
||||
if(message.multiSelect) {
|
||||
text = message.selectedChoices.join(', ');
|
||||
} else {
|
||||
text = message.selectedChoices;
|
||||
}
|
||||
|
||||
// Send all selected choices to the server
|
||||
this.getWebsocket().send(JSON.stringify({ type: 'interact', text: text }));
|
||||
// Clear the selectedChoices array
|
||||
message.sent = true;
|
||||
this.setWaitingForInput(false);
|
||||
},
|
||||
|
||||
handleMessage(data) {
|
||||
|
||||
var i;
|
||||
|
||||
if (data.type == "clear_screen") {
|
||||
this.messages = [];
|
||||
}
|
||||
|
||||
if (data.type == "remove_message") {
|
||||
|
||||
// find message where type == "character" and id == data.id
|
||||
// remove that message from the array
|
||||
|
||||
for (i = 0; i < this.messages.length; i++) {
|
||||
if (this.messages[i].id == data.id) {
|
||||
this.messages.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (data.type == "message_edited") {
|
||||
|
||||
// find the message by id and update the text#
|
||||
|
||||
for (i = 0; i < this.messages.length; i++) {
|
||||
if (this.messages[i].id == data.id) {
|
||||
console.log("message_edited!", i , data.id, data.message, this.messages[i].type, data)
|
||||
if (this.messages[i].type == "character") {
|
||||
this.messages[i].text = data.message.split(':')[1].trim();
|
||||
} else {
|
||||
this.messages[i].text = data.message;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (data.message) {
|
||||
if (data.type === 'character') {
|
||||
const [character, text] = data.message.split(':');
|
||||
this.messages.push({ id: data.id, type: data.type, character: character.trim(), text: text.trim(), color: data.color }); // Add color property to the message
|
||||
} else if (data.type != 'request_input' && data.type != 'client_status' && data.type != 'agent_status') {
|
||||
this.messages.push({ id: data.id, type: data.type, text: data.message, color: data.color, character: data.character, status:data.status }); // Add color property to the message
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.registerMessageHandler(this.handleMessage);
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.message-container {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 10px;
|
||||
white-space: pre-wrap;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.message.system {
|
||||
color: #FFA726;
|
||||
}
|
||||
|
||||
.message.narrator {
|
||||
color: #26A69A;
|
||||
}
|
||||
|
||||
.message.character {
|
||||
color: #E0E0E0;
|
||||
}
|
||||
|
||||
.character-message {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.character-name {
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.character-avatar {
|
||||
height: 50px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.hotbuttons-section {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.hotbuttons-section-1,
|
||||
.hotbuttons-section-2,
|
||||
.hotbuttons-section-3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.choice-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.message.request_input {}
|
||||
</style>
|
||||
219
talemate_frontend/src/components/SceneTools.vue
Normal file
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<!-- Hotbuttons Section -->
|
||||
<div class="hotbuttons-section">
|
||||
|
||||
|
||||
<!-- Section 0: Loading indicator and rerun tool -->
|
||||
|
||||
<v-card class="hotbuttons-section-1">
|
||||
<v-card-actions>
|
||||
<v-progress-circular class="ml-1 mr-3" size="24" v-if="!isWaitingForInput()" indeterminate
|
||||
color="primary"></v-progress-circular>
|
||||
<v-icon class="ml-1 mr-3" v-else-if="isWaitingForInput()">mdi-keyboard</v-icon>
|
||||
<v-icon class="ml-1 mr-3" v-else>mdi-circle-outline</v-icon>
|
||||
|
||||
<v-divider vertical></v-divider>
|
||||
|
||||
|
||||
<v-tooltip v-if="isEnvironment('scene')" :disabled="isInputDisabled()" location="top"
|
||||
text="Redo most recent AI message">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn class="hotkey" v-bind="props" v-on="on" :disabled="isInputDisabled()"
|
||||
@click="sendHotButtonMessage('!rerun')" color="primary" icon>
|
||||
<v-icon>mdi-refresh</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip v-if="isEnvironment('scene')" :disabled="isInputDisabled()" location="top"
|
||||
text="Redo most recent AI message (Nuke Option - use this to attempt to break out of repetition)">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn class="hotkey" v-bind="props" v-on="on" :disabled="isInputDisabled()"
|
||||
@click="sendHotButtonMessage('!rerun:0.5')" color="primary" icon>
|
||||
<v-icon>mdi-nuke</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
|
||||
<v-tooltip v-if="commandActive" location="top"
|
||||
text="Abort / end action.">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn class="hotkey mr-3" v-bind="props" v-on="on" :disabled="!isWaitingForInput()"
|
||||
@click="sendHotButtonMessage('!abort')" color="primary" icon>
|
||||
<v-icon>mdi-cancel</v-icon>
|
||||
|
||||
</v-btn>
|
||||
<v-label v-text="this.commandName" class="mr-3 ml-3"></v-label>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
||||
|
||||
<!-- Section 1: Game Interaction -->
|
||||
<v-card class="hotbuttons-section-1" v-if="isEnvironment('scene')">
|
||||
<v-card-actions>
|
||||
<v-tooltip :disabled="isInputDisabled()" location="top" text="Narrate: Progress Story">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn class="hotkey mx-3" v-bind="props" v-on="on" :disabled="isInputDisabled()"
|
||||
@click="sendHotButtonMessage('!narrate_progress')" color="primary" icon>
|
||||
<v-icon>mdi-script-text-play</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip :disabled="isInputDisabled()" location="top" text="Narrate: Scene">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn class="hotkey mx-3" v-bind="props" v-on="on" :disabled="isInputDisabled()"
|
||||
@click="sendHotButtonMessage('!narrate')" color="primary" icon>
|
||||
<v-icon>mdi-script-text</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip :disabled="isInputDisabled()" location="top" text="Narrate: Character">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn class="hotkey mx-3" v-bind="props" v-on="on" :disabled="isInputDisabled()"
|
||||
@click="sendHotButtonMessage('!narrate_c')" color="primary" icon>
|
||||
<v-icon>mdi-account-voice</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip :disabled="isInputDisabled()" location="top" text="Narrate: Query">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn class="hotkey mx-3" v-bind="props" v-on="on" :disabled="isInputDisabled()"
|
||||
@click="sendHotButtonMessage('!narrate_q')" color="primary" icon>
|
||||
<v-icon>mdi-crystal-ball</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-divider vertical></v-divider>
|
||||
<v-tooltip :disabled="isInputDisabled()" location="top" text="Direct a character">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn class="hotkey mx-3" v-bind="props" v-on="on" :disabled="isInputDisabled()"
|
||||
@click="sendHotButtonMessage('!director')" color="primary" icon>
|
||||
<v-icon>mdi-bullhorn</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
||||
<!-- Section 2: Tools -->
|
||||
<v-card class="hotbuttons-section-2">
|
||||
<v-card-actions>
|
||||
|
||||
<v-tooltip :disabled="isInputDisabled()" location="top" text="Save">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn class="hotkey mx-3" v-bind="props" v-on="on" :disabled="isInputDisabled()"
|
||||
@click="sendHotButtonMessage('!save')" color="primary" icon>
|
||||
<v-icon>mdi-content-save</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip :disabled="isInputDisabled()" location="top" text="Save As">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn class="hotkey mx-3" v-bind="props" v-on="on" :disabled="isInputDisabled()"
|
||||
@click="sendHotButtonMessage('!save_as')" color="primary" icon>
|
||||
<v-icon>mdi-content-save-all</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip v-if="isEnvironment('scene')" :disabled="isInputDisabled()" location="top" text="Switch to creative mode">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn class="hotkey mx-3" v-bind="props" v-on="on" :disabled="isInputDisabled()"
|
||||
@click="sendHotButtonMessage('!setenv_creative')" color="primary" icon>
|
||||
<v-icon>mdi-palette-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip v-else-if="isEnvironment('creative')" :disabled="isInputDisabled()" location="top" text="Switch to game mode">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn class="hotkey mx-3" v-bind="props" v-on="on" :disabled="isInputDisabled()"
|
||||
@click="sendHotButtonMessage('!setenv_scene')" color="primary" icon>
|
||||
<v-icon>mdi-gamepad-square</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
|
||||
name: 'SceneTools',
|
||||
data() {
|
||||
return {
|
||||
commandActive: false,
|
||||
commandName: null,
|
||||
}
|
||||
},
|
||||
inject: [
|
||||
'getWebsocket',
|
||||
'registerMessageHandler',
|
||||
'isInputDisabled',
|
||||
'setInputDisabled',
|
||||
'isWaitingForInput',
|
||||
'scene',
|
||||
'creativeEditor',
|
||||
],
|
||||
methods: {
|
||||
|
||||
|
||||
isEnvironment(typ) {
|
||||
return this.scene().environment == typ;
|
||||
},
|
||||
|
||||
sendHotButtonMessage(message) {
|
||||
if (message == "!abort" || !this.isInputDisabled()) {
|
||||
this.getWebsocket().send(JSON.stringify({ type: 'interact', text: message }));
|
||||
this.setInputDisabled(true);
|
||||
}
|
||||
},
|
||||
|
||||
handleMessage(data) {
|
||||
|
||||
if (data.type == "command_status") {
|
||||
if(data.status == "started") {
|
||||
this.commandActive = true;
|
||||
this.commandName = data.name;
|
||||
} else {
|
||||
this.commandActive = false;
|
||||
this.commandName = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.log("Websocket", this.getWebsocket()); // Check if websocket is available
|
||||
},
|
||||
created() {
|
||||
this.registerMessageHandler(this.handleMessage);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hotbuttons-section {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.hotbuttons-section-1,
|
||||
.hotbuttons-section-2,
|
||||
.hotbuttons-section-3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 20px;
|
||||
}
|
||||
</style>
|
||||
455
talemate_frontend/src/components/TalemateApp.vue
Normal file
@@ -0,0 +1,455 @@
|
||||
<template>
|
||||
<v-app>
|
||||
|
||||
|
||||
<!-- scene navigation drawer -->
|
||||
<v-navigation-drawer v-model="sceneDrawer" app>
|
||||
<v-list>
|
||||
<v-alert v-if="!connected" type="error" variant="tonal">
|
||||
Not connected to Talemate backend
|
||||
<p class="text-body-2" color="white">
|
||||
Make sure the backend process is running.
|
||||
</p>
|
||||
</v-alert>
|
||||
<LoadScene ref="loadScene" />
|
||||
<v-divider></v-divider>
|
||||
<div :style="(sceneActive && scene.environment === 'scene' ? 'display:block' : 'display:none')">
|
||||
<GameOptions v-if="sceneActive" ref="gameOptions" />
|
||||
<v-divider></v-divider>
|
||||
<CoverImage v-if="sceneActive" ref="coverImage" />
|
||||
<WorldState v-if="sceneActive" ref="worldState" />
|
||||
</div>
|
||||
|
||||
<CreativeEditor v-if="sceneActive" ref="creativeEditor" />
|
||||
</v-list>
|
||||
|
||||
</v-navigation-drawer>
|
||||
|
||||
<!-- settings navigation drawer -->
|
||||
<v-navigation-drawer v-model="drawer" app location="right">
|
||||
<v-alert v-if="!connected" type="error" variant="tonal">
|
||||
Not connected to Talemate backend
|
||||
<p class="text-body-2" color="white">
|
||||
Make sure the backend process is running.
|
||||
</p>
|
||||
</v-alert>
|
||||
|
||||
<v-list>
|
||||
<v-list-subheader class="text-uppercase"><v-icon>mdi-network-outline</v-icon>
|
||||
Clients</v-list-subheader>
|
||||
<v-list-item-group>
|
||||
<v-list-item>
|
||||
<AIClient ref="aiClient" @save="saveClients" @clients-updated="saveClients" @client-assigned="saveAgents"></AIClient>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
<v-divider></v-divider>
|
||||
<v-list-subheader class="text-uppercase"><v-icon>mdi-transit-connection-variant</v-icon> Agents</v-list-subheader>
|
||||
<v-list-item-group>
|
||||
<v-list-item>
|
||||
<AIAgent ref="aiAgent" @save="saveAgents" @agents-updated="saveAgents"></AIAgent>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
<!-- More sections can be added here -->
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<!-- system bar -->
|
||||
<v-system-bar>
|
||||
<v-icon icon="mdi-network-outline"></v-icon>
|
||||
<v-progress-circular v-if="activeAgentName() !== null" indeterminate color="primary" size="11"
|
||||
class="mr-1 ml-1"></v-progress-circular>
|
||||
<span class="mr-1">{{ activeAgentName() }}</span>
|
||||
<v-icon icon="mdi-transit-connection-variant"></v-icon>
|
||||
<v-progress-circular v-if="activeClientName() !== null" indeterminate color="primary" size="11"
|
||||
class="mr-1 ml-1"></v-progress-circular>
|
||||
<span class="mr-1">{{ activeClientName() }}</span>
|
||||
<v-divider vertical></v-divider>
|
||||
<span v-if="connecting" class="ml-1"><v-icon class="mr-1">mdi-progress-helper</v-icon>connecting</span>
|
||||
<span v-else-if="connected" class="ml-1"><v-icon class="mr-1" color="green" size="14">mdi-checkbox-blank-circle</v-icon>connected</span>
|
||||
<span v-else class="ml-1"><v-icon class="mr-1">mdi-progress-close</v-icon>disconnected</span>
|
||||
<v-spacer></v-spacer>
|
||||
<span v-if="version !== null">v{{ version }}</span>
|
||||
<span v-if="configurationRequired()">
|
||||
<v-icon icon="mdi-application-cog"></v-icon>
|
||||
<span class="ml-1">Configuration required</span>
|
||||
</span>
|
||||
|
||||
</v-system-bar>
|
||||
|
||||
<!-- app bar -->
|
||||
<v-app-bar app>
|
||||
<v-app-bar-nav-icon @click="toggleNavigation('game')"><v-icon>mdi-script</v-icon></v-app-bar-nav-icon>
|
||||
<v-toolbar-title v-if="scene.name !== undefined">
|
||||
{{ scene.name || 'Untitled Scenario' }}
|
||||
<v-chip size="x-small" v-if="scene.environment === 'creative'" class="ml-1"><v-icon text="Creative" size="14"
|
||||
class="mr-1">mdi-palette-outline</v-icon>Creative Mode</v-chip>
|
||||
<v-chip size="x-small" v-else-if="scene.environment === 'scene'" class="ml-1"><v-icon text="Play" size="14"
|
||||
class="mr-1">mdi-gamepad-square</v-icon>Game Mode</v-chip>
|
||||
|
||||
<v-btn v-if="scene.environment === 'scene'" class="ml-1" @click="openSceneHistory()"><v-icon size="14"
|
||||
class="mr-1">mdi-playlist-star</v-icon>History</v-btn>
|
||||
|
||||
</v-toolbar-title>
|
||||
<v-toolbar-title v-else>
|
||||
Talemate
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-app-bar-nav-icon @click="openAppConfig()"><v-icon>mdi-cog</v-icon></v-app-bar-nav-icon>
|
||||
<v-app-bar-nav-icon @click="toggleNavigation('settings')" v-if="configurationRequired()"
|
||||
color="red"><v-icon>mdi-application-cog</v-icon></v-app-bar-nav-icon>
|
||||
<v-app-bar-nav-icon @click="toggleNavigation('settings')"
|
||||
v-else><v-icon>mdi-application-cog</v-icon></v-app-bar-nav-icon>
|
||||
|
||||
</v-app-bar>
|
||||
<v-main style="height: 100%; display: flex; flex-direction: column;">
|
||||
<v-container :class="(sceneActive ? '' : 'backdrop')" style="display: flex; flex-direction: column; height: 100%;">
|
||||
|
||||
<SceneMessages ref="sceneMessages" v-if="sceneActive" />
|
||||
|
||||
<div style="flex-shrink: 0;" v-if="sceneActive">
|
||||
|
||||
<SceneTools />
|
||||
<CharacterSheet ref="characterSheet" />
|
||||
<SceneHistory ref="sceneHistory" />
|
||||
|
||||
<v-text-field v-model="messageInput" :label="inputHint" outlined ref="messageInput" @keyup.enter="sendMessage"
|
||||
:disabled="inputDisabled">
|
||||
<template v-slot:append>
|
||||
<v-btn @click="sendMessage" color="primary" icon>
|
||||
<v-icon v-if="messageInput">mdi-send</v-icon>
|
||||
<v-icon v-else>mdi-skip-next</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</div>
|
||||
</v-container>
|
||||
</v-main>
|
||||
|
||||
<AppConfig ref="appConfig" />
|
||||
<v-snackbar v-model="errorNotification" color="red" :timeout="3000">
|
||||
{{ errorMessage }}
|
||||
</v-snackbar>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AIClient from './AIClient.vue';
|
||||
import AIAgent from './AIAgent.vue';
|
||||
import LoadScene from './LoadScene.vue';
|
||||
import SceneTools from './SceneTools.vue';
|
||||
import SceneMessages from './SceneMessages.vue';
|
||||
import WorldState from './WorldState.vue';
|
||||
import GameOptions from './GameOptions.vue';
|
||||
import CoverImage from './CoverImage.vue';
|
||||
import CharacterSheet from './CharacterSheet.vue';
|
||||
import SceneHistory from './SceneHistory.vue';
|
||||
import CreativeEditor from './CreativeEditor.vue';
|
||||
import AppConfig from './AppConfig.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AIClient,
|
||||
AIAgent,
|
||||
LoadScene,
|
||||
SceneTools,
|
||||
SceneMessages,
|
||||
WorldState,
|
||||
GameOptions,
|
||||
CoverImage,
|
||||
CharacterSheet,
|
||||
SceneHistory,
|
||||
CreativeEditor,
|
||||
AppConfig,
|
||||
},
|
||||
name: 'TalemateApp',
|
||||
data() {
|
||||
return {
|
||||
version: null,
|
||||
loading: false,
|
||||
sceneActive: false,
|
||||
drawer: false,
|
||||
sceneDrawer: true,
|
||||
websocket: null,
|
||||
inputDisabled: false,
|
||||
waitingForInput: false,
|
||||
connectTimeout: null,
|
||||
connected: false,
|
||||
connecting: false,
|
||||
reconnect: true,
|
||||
errorMessage: null,
|
||||
errorNotification: false,
|
||||
inputHint: 'Enter your text...',
|
||||
messageInput: '',
|
||||
reconnectInterval: 3000,
|
||||
messageHandlers: [],
|
||||
scene: {},
|
||||
appConfig: {},
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.log("mounted!")
|
||||
this.connect();
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Close the WebSocket connection when the component is destroyed
|
||||
if (this.websocket) {
|
||||
this.reconnect = false;
|
||||
this.websocket.close();
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
getWebsocket: () => this.websocket,
|
||||
registerMessageHandler: this.registerMessageHandler,
|
||||
isInputDisabled: () => this.inputDisabled,
|
||||
setInputDisabled: (disabled) => this.inputDisabled = disabled,
|
||||
isWaitingForInput: () => this.waitingForInput,
|
||||
setWaitingForInput: (waiting) => this.waitingForInput = waiting,
|
||||
isConnected: () => this.connected,
|
||||
scene: () => this.scene,
|
||||
getClients: () => this.getClients(),
|
||||
getAgents: () => this.getAgents(),
|
||||
requestSceneAssets: (asset_ids) => this.requestSceneAssets(asset_ids),
|
||||
openCharacterSheet: (characterName) => this.openCharacterSheet(characterName),
|
||||
characterSheet: () => this.$refs.characterSheet,
|
||||
creativeEditor: () => this.$refs.creativeEditor,
|
||||
requestAppConfig: () => this.requestAppConfig(),
|
||||
appConfig: () => this.appConfig,
|
||||
configurationRequired: () => this.configurationRequired(),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
||||
connect() {
|
||||
|
||||
if (this.connected || this.connecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.connecting = true;
|
||||
|
||||
this.websocket = new WebSocket('ws://localhost:5050/ws');
|
||||
console.log("Websocket connecting ...")
|
||||
this.websocket.onmessage = this.handleMessage;
|
||||
this.websocket.onopen = () => {
|
||||
console.log('WebSocket connection established');
|
||||
this.connected = true;
|
||||
this.connecting = false;
|
||||
this.requestAppConfig();
|
||||
};
|
||||
this.websocket.onclose = (event) => {
|
||||
console.log('WebSocket connection closed', event);
|
||||
this.connected = false;
|
||||
this.connecting = false;
|
||||
this.sceneActive = false;
|
||||
this.scene = {};
|
||||
this.loading = false;
|
||||
if(this.reconnect)
|
||||
this.connect();
|
||||
};
|
||||
this.websocket.onerror = (error) => {
|
||||
console.log('WebSocket error', error);
|
||||
// Close the WebSocket connection when an error occurs
|
||||
this.websocket.close();
|
||||
this.setNavigation('settings');
|
||||
};
|
||||
},
|
||||
|
||||
registerMessageHandler(handler) {
|
||||
this.messageHandlers.push(handler);
|
||||
},
|
||||
|
||||
handleMessage(event) {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
//console.log(data);
|
||||
|
||||
this.messageHandlers.forEach(handler => handler(data));
|
||||
|
||||
// Scene loaded
|
||||
if (data.type === "system") {
|
||||
if (data.id === 'scene.loaded') {
|
||||
this.loading = false;
|
||||
this.sceneActive = true;
|
||||
}
|
||||
if(data.status == 'error') {
|
||||
this.errorNotification = true;
|
||||
this.errorMessage = data.message;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.type == "scene_status") {
|
||||
this.scene = {
|
||||
name: data.name,
|
||||
environment: data.data.environment,
|
||||
}
|
||||
this.sceneActive = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.type == "client_status" || data.type == "agent_status") {
|
||||
if (this.configurationRequired()) {
|
||||
this.setNavigation('settings');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.type === 'app_config') {
|
||||
this.appConfig = data.data;
|
||||
this.version = data.version;
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.type === 'request_input') {
|
||||
|
||||
this.waitingForInput = true;
|
||||
|
||||
if (data.data && data.data["input_type"] == "select") {
|
||||
// If the input_type is 'choice', send the data to SceneMessages
|
||||
this.$refs.sceneMessages.handleChoiceInput(data);
|
||||
} else {
|
||||
// Enable the input field when a request_input message comes in
|
||||
this.inputDisabled = false;
|
||||
if (data.message) {
|
||||
// Update the input field hint when a request_input message with a value comes in
|
||||
this.inputHint = data.message;
|
||||
} else if (data.character) {
|
||||
// Reset the input field hint when a request_input message without a value comes in
|
||||
this.inputHint = `${data.character}:`;
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.messageInput)
|
||||
// Highlight the user text input element when a request_input message comes in
|
||||
this.$refs.messageInput.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
if (data.type === 'processing_input') {
|
||||
// Disable the input field when a processing_input message comes in
|
||||
this.inputDisabled = true;
|
||||
this.waitingForInput = false;
|
||||
}
|
||||
if (data.type === "character" || data.type === "system") {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.messageInput && this.$refs.messageInput.$el)
|
||||
this.$refs.messageInput.$el.scrollIntoView(false);
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
sendMessage() {
|
||||
if (!this.inputDisabled) {
|
||||
this.websocket.send(JSON.stringify({ type: 'interact', text: this.messageInput }));
|
||||
this.messageInput = '';
|
||||
this.inputDisabled = true;
|
||||
this.waitingForInput = false;
|
||||
}
|
||||
},
|
||||
requestAppConfig() {
|
||||
this.websocket.send(JSON.stringify({ type: 'request_app_config' }));
|
||||
},
|
||||
saveClients(clients) {
|
||||
this.websocket.send(JSON.stringify({ type: 'configure_clients', clients: clients }));
|
||||
},
|
||||
saveAgents(agents) {
|
||||
console.log({ type: 'configure_agents', agents: agents })
|
||||
this.websocket.send(JSON.stringify({ type: 'configure_agents', agents: agents }));
|
||||
},
|
||||
requestSceneAssets(asset_ids) {
|
||||
this.websocket.send(JSON.stringify({ type: 'request_scene_assets', asset_ids: asset_ids }));
|
||||
},
|
||||
setNavigation(navigation) {
|
||||
if (navigation == "game")
|
||||
this.sceneDrawer = true;
|
||||
else if (navigation == "settings")
|
||||
this.drawer = true;
|
||||
},
|
||||
toggleNavigation(navigation) {
|
||||
if (navigation == "game")
|
||||
this.sceneDrawer = !this.sceneDrawer;
|
||||
else if (navigation == "settings")
|
||||
this.drawer = !this.drawer;
|
||||
},
|
||||
getClients() {
|
||||
if (!this.$refs.aiClient) {
|
||||
return [];
|
||||
}
|
||||
return this.$refs.aiClient.state.clients;
|
||||
},
|
||||
getAgents() {
|
||||
if (!this.$refs.aiAgent) {
|
||||
return [];
|
||||
}
|
||||
return this.$refs.aiAgent.state.agents;
|
||||
},
|
||||
activeClientName() {
|
||||
if (!this.$refs.aiClient) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let client = this.$refs.aiClient.getActive();
|
||||
if (client) {
|
||||
return client.name;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
activeAgentName() {
|
||||
if (!this.$refs.aiAgent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let agent = this.$refs.aiAgent.getActive();
|
||||
|
||||
if (agent) {
|
||||
return agent.name;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
configurationRequired() {
|
||||
if (!this.$refs.aiClient || this.connecting || (!this.connecting && !this.connected)) {
|
||||
return false;
|
||||
}
|
||||
return this.$refs.aiAgent.configurationRequired();
|
||||
},
|
||||
openCharacterSheet(characterName) {
|
||||
this.$refs.characterSheet.openForCharacterName(characterName);
|
||||
},
|
||||
openSceneHistory() {
|
||||
this.$refs.sceneHistory.open();
|
||||
},
|
||||
openAppConfig() {
|
||||
this.$refs.appConfig.show();
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.message.request_input {
|
||||
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
background-image: url('/src/assets/logo-13.1-backdrop.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 512px 512px;
|
||||
}
|
||||
|
||||
.backdrop-active {
|
||||
background-image: url('/src/assets/logo-13.1-backdrop.png');
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
background-size: 512px 512px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
background-image: url('/src/assets/logo-13.1-transparent.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: fit;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
</style>
|
||||
130
talemate_frontend/src/components/WorldState.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<v-list-subheader class="text-uppercase">
|
||||
<v-icon class="mr-1">mdi-earth</v-icon>World
|
||||
<v-progress-circular class="ml-1 mr-3" size="14" v-if="requesting" indeterminate color="primary"></v-progress-circular>
|
||||
<v-btn v-else size="x-small" class="mr-1" v-bind="props" variant="tonal" density="comfortable" rounded="sm" @click.stop="refresh()" icon="mdi-refresh"></v-btn>
|
||||
|
||||
</v-list-subheader>
|
||||
|
||||
<div ref="charactersContainer">
|
||||
|
||||
<v-expansion-panels density="compact" v-for="(character,name) in characters" :key="name">
|
||||
<v-expansion-panel rounded="0" density="compact">
|
||||
<v-expansion-panel-title class="text-subtitle-2" diable-icon-rotate>
|
||||
{{ name }}
|
||||
<v-chip label size="x-small" variant="outlined" class="ml-1">{{ character.emotion }}</v-chip>
|
||||
<template v-slot:actions>
|
||||
<v-icon icon="mdi-account"></v-icon>
|
||||
</template>
|
||||
</v-expansion-panel-title>
|
||||
|
||||
<v-expansion-panel-text class="text-body-2">
|
||||
{{ character.snapshot }}
|
||||
<div class="text-center mt-1">
|
||||
<v-tooltip text="Look at">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn size="x-small" class="mr-1" v-bind="props" variant="tonal" density="comfortable" rounded="sm" @click.stop="lookAtCharacter(name)" icon="mdi-eye"></v-btn>
|
||||
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip v-if="characterSheet().characterExists(name)" text="Character details">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn size="x-small" class="mr-1" v-bind="props" variant="tonal" density="comfortable" rounded="sm" @click.stop="openCharacterSheet(name)" icon="mdi-account-details"></v-btn>
|
||||
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
<v-divider class="mt-1"></v-divider>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
|
||||
</div>
|
||||
<div ref="objectsContainer">
|
||||
|
||||
<v-expansion-panels density="compact" v-for="(obj,name) in items" :key="name">
|
||||
<v-expansion-panel rounded="0" density="compact">
|
||||
<v-expansion-panel-title class="text-subtitle-2" diable-icon-rotate>
|
||||
{{ name}}
|
||||
<template v-slot:actions>
|
||||
<v-icon icon="mdi-cube"></v-icon>
|
||||
</template>
|
||||
</v-expansion-panel-title>
|
||||
|
||||
<v-expansion-panel-text class="text-body-2">
|
||||
{{ obj.snapshot }}
|
||||
<div class="text-center mt-1">
|
||||
<v-tooltip text="Look at">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn size="x-small" class="mr-1" v-bind="props" variant="tonal" density="comfortable" rounded="sm" @click.stop="lookAtItem(name)" icon="mdi-eye"></v-btn>
|
||||
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
<v-divider class="mt-1"></v-divider>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'WorldState',
|
||||
data() {
|
||||
return {
|
||||
characters: {},
|
||||
items: {},
|
||||
location: null,
|
||||
requesting: false,
|
||||
}
|
||||
},
|
||||
|
||||
inject: [
|
||||
'getWebsocket',
|
||||
'registerMessageHandler',
|
||||
'setWaitingForInput',
|
||||
'openCharacterSheet',
|
||||
'characterSheet',
|
||||
],
|
||||
|
||||
methods: {
|
||||
lookAtCharacter(name) {
|
||||
this.getWebsocket().send(JSON.stringify({
|
||||
type: 'interact',
|
||||
text: `!narrate_c:${name}`,
|
||||
}));
|
||||
},
|
||||
lookAtItem(name) {
|
||||
this.getWebsocket().send(JSON.stringify({
|
||||
type: 'interact',
|
||||
text: `!narrate_q:describe the apperance of ${name}.:true`,
|
||||
}));
|
||||
},
|
||||
refresh() {
|
||||
this.getWebsocket().send(JSON.stringify({
|
||||
type: 'interact',
|
||||
text: '!ws',
|
||||
}));
|
||||
},
|
||||
handleMessage(data) {
|
||||
if(data.type === 'world_state') {
|
||||
this.characters = data.data.characters;
|
||||
this.items = data.data.items;
|
||||
this.location = data.data.location;
|
||||
this.requesting = (data.status==="requested")
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.registerMessageHandler(this.handleMessage);
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
10
talemate_frontend/src/main.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import vuetify from './plugins/vuetify'
|
||||
import { loadFonts } from './plugins/webfontloader'
|
||||
|
||||
loadFonts()
|
||||
|
||||
createApp(App)
|
||||
.use(vuetify)
|
||||
.mount('#app')
|
||||
12
talemate_frontend/src/plugins/vuetify.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// Styles
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
import 'vuetify/styles'
|
||||
|
||||
// Vuetify
|
||||
import { createVuetify } from 'vuetify'
|
||||
|
||||
export default createVuetify({
|
||||
theme : {
|
||||
defaultTheme: 'dark'
|
||||
}
|
||||
})
|
||||
15
talemate_frontend/src/plugins/webfontloader.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* plugins/webfontloader.js
|
||||
*
|
||||
* webfontloader documentation: https://github.com/typekit/webfontloader
|
||||
*/
|
||||
|
||||
export async function loadFonts () {
|
||||
const webFontLoader = await import(/* webpackChunkName: "webfontloader" */'webfontloader')
|
||||
|
||||
webFontLoader.load({
|
||||
google: {
|
||||
families: ['Roboto:100,300,400,500,700,900&display=swap'],
|
||||
},
|
||||
})
|
||||
}
|
||||
22
talemate_frontend/vue.config.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const { defineConfig } = require('@vue/cli-service')
|
||||
module.exports = defineConfig({
|
||||
transpileDependencies: true,
|
||||
|
||||
pluginOptions: {
|
||||
vuetify: {
|
||||
// https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vuetify-loader
|
||||
}
|
||||
},
|
||||
|
||||
devServer: {
|
||||
client: {
|
||||
overlay: {
|
||||
warnings: false,
|
||||
errors: false,
|
||||
},
|
||||
|
||||
// or
|
||||
overlay: false,
|
||||
}
|
||||
}
|
||||
})
|
||||