add chats
This commit is contained in:
parent
8f43531b47
commit
94328e0b93
12 changed files with 372 additions and 14 deletions
42
static/chat/treehouse/dev/chats/test.json
Normal file
42
static/chat/treehouse/dev/chats/test.json
Normal file
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"nodes": {
|
||||
"init": {
|
||||
"kind": "say",
|
||||
"content": "hello! nice to meet you.",
|
||||
"then": "question1"
|
||||
},
|
||||
"question1": {
|
||||
"kind": "ask",
|
||||
"questions": [
|
||||
{ "content": "Who are you?", "then": "whoAreYou" },
|
||||
{ "content": "What is this?", "then": "whatIsThis" }
|
||||
]
|
||||
},
|
||||
"whoAreYou": {
|
||||
"kind": "say",
|
||||
"content": "I'm liquidex. y'know, the guy running this place.",
|
||||
"then": "question1"
|
||||
},
|
||||
"whatIsThis": {
|
||||
"kind": "say",
|
||||
"content": "this is a test of the treehouse dialogue system. basically, I made myself a little framework for writing conversations between characters, and this is a test page for it.",
|
||||
"then": "whatIsThisQuestion"
|
||||
},
|
||||
"whatIsThisQuestion": {
|
||||
"kind": "ask",
|
||||
"questions": [
|
||||
{ "content": "Right.", "then": "question1" },
|
||||
{
|
||||
"content": "\"Dialogues\", seriously? What, are you British?",
|
||||
"then": "british"
|
||||
}
|
||||
]
|
||||
},
|
||||
"british": {
|
||||
"kind": "say",
|
||||
"content": "no, but I think British is pretty damn funny.",
|
||||
"then": "question1"
|
||||
},
|
||||
"end": { "kind": "end" }
|
||||
}
|
||||
}
|
83
static/css/components/chat.css
Normal file
83
static/css/components/chat.css
Normal file
|
@ -0,0 +1,83 @@
|
|||
:root {
|
||||
--icon-choose: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYgMTJMMTAgOEw2IDQiIHN0cm9rZT0iIzU1NDIzZSIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjwvc3ZnPgo=');
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--icon-choose: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYgMTJMMTAgOEw2IDQiIHN0cm9rZT0iI2Q3Y2RiZiIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjwvc3ZnPgo=');
|
||||
}
|
||||
}
|
||||
|
||||
th-chat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
th-chat-said {
|
||||
display: block;
|
||||
|
||||
border: 1px solid var(--border-1);
|
||||
padding: 12px 16px;
|
||||
margin: 8px 0;
|
||||
border-radius: 8px;
|
||||
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
th-chat-asked {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
&>button {
|
||||
/* Reset <button> */
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--text-color);
|
||||
padding: 0.5em 0;
|
||||
margin-right: 2rem;
|
||||
|
||||
--recursive-wght: 500;
|
||||
text-decoration: underline;
|
||||
text-align: right;
|
||||
opacity: 80%;
|
||||
|
||||
cursor: pointer;
|
||||
transition: opacity var(--transition-duration);
|
||||
|
||||
&.asked {
|
||||
display: none;
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
--recursive-wght: 600;
|
||||
cursor: default;
|
||||
opacity: 100%;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 0.5em;
|
||||
background-image: var(--icon-choose);
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
opacity: 0%;
|
||||
vertical-align: middle;
|
||||
translate: -1em 0;
|
||||
transition: opacity var(--transition-duration), translate var(--transition-duration);
|
||||
}
|
||||
|
||||
&:hover::before, &[disabled]::before {
|
||||
opacity: 50%;
|
||||
translate: 0 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -101,7 +101,8 @@ body::selection {
|
|||
body,
|
||||
pre,
|
||||
code,
|
||||
kbd {
|
||||
kbd,
|
||||
button {
|
||||
font-family: 'RecVar', sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
|
|
165
static/js/components/chat.js
Normal file
165
static/js/components/chat.js
Normal file
|
@ -0,0 +1,165 @@
|
|||
import { addSpell, spell } from "treehouse/spells.js";
|
||||
import { Branch } from "treehouse/tree.js";
|
||||
|
||||
const persistenceKey = "treehouse.chats";
|
||||
let persistentState = JSON.parse(localStorage.getItem(persistenceKey)) || {};
|
||||
|
||||
persistentState.log ??= {};
|
||||
savePersistentState();
|
||||
|
||||
function savePersistentState() {
|
||||
localStorage.setItem(persistenceKey, JSON.stringify(persistentState));
|
||||
}
|
||||
|
||||
class Chat extends HTMLElement {
|
||||
constructor(branch) {
|
||||
super();
|
||||
this.branch = branch;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.id = spell(this.branch, Branch).namedID;
|
||||
this.model = JSON.parse(spell(this.branch, Branch).branchContent.textContent);
|
||||
|
||||
this.state = new ChatState(this, this.model);
|
||||
this.state.onInteract = () => {
|
||||
persistentState.log[this.id] = this.state.log;
|
||||
savePersistentState();
|
||||
};
|
||||
this.state.exec("init");
|
||||
|
||||
let log = persistentState.log[this.id];
|
||||
if (log != null) {
|
||||
this.state.replay(log);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("th-chat", Chat);
|
||||
|
||||
class Said extends HTMLElement {
|
||||
constructor({ content }) {
|
||||
super();
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.innerHTML = this.content;
|
||||
this.dispatchEvent(new Event(".animationsComplete"));
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("th-chat-said", Said);
|
||||
|
||||
class Asked extends HTMLElement {
|
||||
constructor({ content, alreadyAsked }) {
|
||||
super();
|
||||
this.content = content;
|
||||
this.alreadyAsked = alreadyAsked;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.button = document.createElement("button");
|
||||
this.button.innerHTML = this.content;
|
||||
this.button.addEventListener("click", _ => {
|
||||
this.dispatchEvent(new Event(".click"));
|
||||
});
|
||||
if (this.alreadyAsked) {
|
||||
this.button.classList.add("asked");
|
||||
}
|
||||
this.appendChild(this.button);
|
||||
}
|
||||
|
||||
interactionFinished() {
|
||||
this.button.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("th-chat-asked", Asked);
|
||||
|
||||
class ChatState {
|
||||
constructor(container, model) {
|
||||
this.container = container;
|
||||
this.model = model;
|
||||
this.log = [];
|
||||
this.results = {};
|
||||
this.wereAsked = new Set();
|
||||
this.onInteract = _ => {};
|
||||
}
|
||||
|
||||
replay(log) {
|
||||
for (let entry of log) {
|
||||
this.interact(entry);
|
||||
}
|
||||
}
|
||||
|
||||
exec(name) {
|
||||
let node = this.model.nodes[name];
|
||||
let results = this.results[name];
|
||||
this.results[name] = this[node.kind](name, node, results);
|
||||
}
|
||||
|
||||
say(_, node) {
|
||||
let said = new Said({ content: node.content });
|
||||
said.addEventListener(".animationsComplete", _ => this.exec(node.then));
|
||||
this.container.appendChild(said);
|
||||
}
|
||||
|
||||
ask(name, node) {
|
||||
let questions = [];
|
||||
for (let i_ = 0; i_ < node.questions.length; ++i_) {
|
||||
let i = i_;
|
||||
let key = `${name}[${i}]`;
|
||||
|
||||
let question = node.questions[i];
|
||||
let asked = new Asked({ content: question.content, alreadyAsked: this.wereAsked.has(key) });
|
||||
asked.addEventListener(".click", _ => {
|
||||
this.interact({
|
||||
kind: "ask.choose",
|
||||
name,
|
||||
option: i,
|
||||
key,
|
||||
});
|
||||
});
|
||||
this.container.appendChild(asked);
|
||||
questions[i] = asked;
|
||||
}
|
||||
return questions;
|
||||
}
|
||||
|
||||
end() {}
|
||||
|
||||
interact(interaction) {
|
||||
let node = this.model.nodes[interaction.name];
|
||||
|
||||
this.log.push(interaction);
|
||||
this.onInteract();
|
||||
|
||||
switch (interaction.kind) {
|
||||
case "ask.choose": {
|
||||
if (this.wereAsked.has(interaction.key)) {
|
||||
this.log.pop();
|
||||
}
|
||||
this.wereAsked.add(interaction.key);
|
||||
|
||||
let questions = this.results[interaction.name];
|
||||
let question = node.questions[interaction.option];
|
||||
let asked = questions[interaction.option];
|
||||
asked.interactionFinished();
|
||||
this.exec(question.then);
|
||||
for (let q of questions) {
|
||||
if (q != asked) {
|
||||
q.parentNode.removeChild(q);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addSpell("chat", class {
|
||||
constructor(branch) {
|
||||
branch.replaceWith(new Chat(branch));
|
||||
}
|
||||
});
|
|
@ -70,10 +70,12 @@ let mutationObserver = new MutationObserver(records => {
|
|||
// NOTE: Added nodes may contain children which also need to be processed.
|
||||
// Collect those that have [data-cast] on them and apply spells to them.
|
||||
for (let addedNode of record.addedNodes) {
|
||||
if (addedNode.getAttribute("data-cast") != null) {
|
||||
mutatedNodes.add(addedNode);
|
||||
if (!(addedNode instanceof Text)) {
|
||||
if (addedNode.getAttribute("data-cast") != null) {
|
||||
mutatedNodes.add(addedNode);
|
||||
}
|
||||
addedNode.querySelectorAll("[data-cast]").forEach(element => mutatedNodes.add(element));
|
||||
}
|
||||
addedNode.querySelectorAll("[data-cast]").forEach(element => mutatedNodes.add(element));
|
||||
}
|
||||
applySpells(mutatedNodes);
|
||||
}
|
||||
|
|
3
static/svg/dark/choose.svg
Normal file
3
static/svg/dark/choose.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 12L10 8L6 4" stroke="#d7cdbf" stroke-width="2"/>
|
||||
</svg>
|
After Width: | Height: | Size: 164 B |
3
static/svg/light/choose.svg
Normal file
3
static/svg/light/choose.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 12L10 8L6 4" stroke="#55423e" stroke-width="2"/>
|
||||
</svg>
|
After Width: | Height: | Size: 164 B |
Loading…
Add table
Add a link
Reference in a new issue