initial commit

This commit is contained in:
FinalWombat
2023-05-05 00:50:02 +03:00
commit 6d93b041c5
232 changed files with 39974 additions and 0 deletions

23
talemate_frontend/.gitignore vendored Normal file
View 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?

View 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/).

View File

@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

View 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

File diff suppressed because it is too large Load Diff

View 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"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1012 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View 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

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>
```

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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')

View 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'
}
})

View 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'],
},
})
}

View 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,
}
}
})