import { getEl, addSafeEventListener } from './util/dom-utils.js';
import { escapeHtml } from './util/format-utils.js';
import { customConfirm } from './util/confirm-dialog.js';
/**
* class Context
* Handles the display of context-related information (topics and considerations).
*/
class Context {
/**
* Initializes a new instance of the Context class.
* @param {Object} storage - Storage instance.
* @param {Object} app_callbacks - Application callbacks.
*/
constructor(storage, app_callbacks) {
this.storage = storage;
this.app_callbacks = app_callbacks;
}
/**
* Renders the context view.
* @returns {string} HTML string for the context view.
*/
render() {
let html = '<div class="div-scroll-container-inner">';
html += '<h4>Topics</h4><hr><div id="topics-display-area">';
html += this._generateTopicChipsHtml();
html += '</div>';
html += `
<div class="add-topic-section" style="width: 100%;">
<input type="text" id="new-topic-input" placeholder="Add new topic" style="width: 100%;box-sizing: border-box;margin: 4pt 0;"/>
<div style="text-align: right;">
<button id="add-topic-button" style="min-width: 6em;">Add Topic</button>
</div>
</div>
`;
html += '<h4>Considerations</h4><hr><div id="considerations-display-area">';
html += this._generateConsiderationsHtml();
html += '</div>';
html += `
<div class="add-consideration-section" style="width: 100%;">
<textarea id="new-consideration-input" class="prompt-style-input" style="margin-bottom: 0;" placeholder="Add new consideration"></textarea>
<div style="text-align: right;">
<button id="add-consideration-button" style="min-width: 6em;">Add Consideration</button>
</div>
</div>
`;
html += '</div>';
return html;
}
/**
* Generates the HTML for the topic chips or "No Topics" message.
* @returns {string} HTML string.
* @private
*/
_generateTopicChipsHtml() {
const key_topics = this.storage.KEY_CONVERSATION_TOPICS;
const conversation = this.storage.get_selected_conversation();
if (conversation && conversation[key_topics] && conversation[key_topics].length > 0) {
let chipsHtml = '<div id="topic-chips" class="div-options-chips">';
conversation[key_topics].forEach(topic => {
const escaped = escapeHtml(topic);
chipsHtml += `
<span class="span-option-chip" data-topic="${escaped}">
${escaped}
<button class="delete-topic-button" data-topic="${escaped}" style="padding: 0;height: 1.2em;line-height: 0;">×</button>
</span>`;
});
chipsHtml += '</div>';
return chipsHtml;
} else {
return '<p id="no-topics-message">No Topics</p>';
}
}
/**
* Generates the HTML for considerations list or "No Considerations" message.
* @returns {string} HTML string.
* @private
*/
_generateConsiderationsHtml() {
const key_considerations = this.storage.KEY_CONVERSATION_CONSIDERATIONS;
const conversation = this.storage.get_selected_conversation();
if (conversation && conversation[key_considerations] && conversation[key_considerations].length > 0) {
let considerationsHtml = '<div id="considerations-list">';
conversation[key_considerations].forEach(consideration => {
const escaped = escapeHtml(consideration);
considerationsHtml += `
<div class="consideration-item" data-consideration="${escaped}" style="display: flex; justify-content: space-between; align-items: center;">
<span style="flex-grow: 1;">${escaped}</span>
<button class="delete-consideration-button" data-consideration="${escaped}" style="min-width: 2em; text-align: center;">×</button>
</div>`;
});
considerationsHtml += '</div>';
return considerationsHtml;
} else {
return '<p id="no-considerations-message">No Considerations</p>';
}
}
/**
* Dynamically updates the topic chips display in the DOM.
* @private
*/
_updateTopicChipsDisplay() {
const topicsDisplayArea = getEl('topics-display-area');
if (topicsDisplayArea) {
topicsDisplayArea.innerHTML = this._generateTopicChipsHtml();
this._attachTopicChipListeners();
}
}
/**
* Dynamically updates the considerations display in the DOM.
* @private
*/
_updateConsiderationsDisplay() {
const considerationsDisplayArea = getEl('considerations-display-area');
if (considerationsDisplayArea) {
considerationsDisplayArea.innerHTML = this._generateConsiderationsHtml();
this._attachConsiderationClickListeners();
}
}
/**
* Handles adding a new topic from the input.
* @private
*/
_addTopicAction() {
const newTopicInput = getEl('new-topic-input');
if (!newTopicInput) return;
const newTopic = newTopicInput.value.trim();
if (newTopic) {
const wordCount = newTopic.split(' ').filter(word => word !== '').length;
if (wordCount > 5) {
alert('Topic can contain up to five words.');
return;
}
const topicRegex = /^[a-zA-Z0-9\s'-]+$/;
if (!topicRegex.test(newTopic)) {
alert('Topic can only contain letters, numbers, spaces, hyphens, and apostrophes.');
return;
}
this.addTopic(newTopic);
newTopicInput.value = '';
this._updateTopicChipsDisplay();
}
}
/**
* Handles adding a new consideration from the textarea.
* @private
*/
_addConsiderationAction() {
const newConsiderationInput = getEl('new-consideration-input');
if (!newConsiderationInput) return;
const newConsideration = newConsiderationInput.value.trim();
if (newConsideration) {
if (newConsideration.includes('\n')) {
alert('Considerations cannot contain new lines.');
return;
}
const wordCount = newConsideration.split(' ').filter(word => word !== '').length;
if (wordCount > 200) {
alert('Consideration can be a maximum of 200 words.');
return;
}
this.addConsideration(newConsideration);
newConsiderationInput.value = '';
this._updateConsiderationsDisplay();
}
}
/**
* Attaches event listeners for the topic chips and add topic button.
*/
attachEventListeners() {
this._attachTopicChipListeners();
this._attachConsiderationClickListeners();
const addTopicButton = getEl('add-topic-button');
addSafeEventListener(addTopicButton, 'click', () => this._addTopicAction());
const newTopicInput = getEl('new-topic-input');
addSafeEventListener(newTopicInput, 'keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this._addTopicAction();
}
});
const addConsiderationButton = getEl('add-consideration-button');
addSafeEventListener(addConsiderationButton, 'click', () => this._addConsiderationAction());
const newConsiderationInput = getEl('new-consideration-input');
addSafeEventListener(newConsiderationInput, 'keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this._addConsiderationAction();
}
});
}
/**
* Attaches click listeners to the topic chips.
* @private
*/
_attachTopicChipListeners() {
const topicChipsContainer = getEl('topic-chips');
addSafeEventListener(topicChipsContainer, 'click', async (event) => {
if (event.target && event.target.classList.contains('delete-topic-button')) {
const topicToDelete = event.target.dataset.topic;
if (topicToDelete && (await customConfirm('Delete Topic', `Do you want to delete the topic "${topicToDelete}"?`))) {
this.deleteTopic(topicToDelete);
}
}
});
}
/**
* Attaches click listeners to consideration items.
* @private
*/
_attachConsiderationClickListeners() {
const considerationsContainer = getEl('considerations-list');
addSafeEventListener(considerationsContainer, 'click', async (event) => {
if (event.target && event.target.classList.contains('delete-consideration-button')) {
const considerationToDelete = event.target.dataset.consideration;
if (considerationToDelete && (await customConfirm('Delete Consideration', `Do you want to delete the consideration "${considerationToDelete}"?`))) {
this.deleteConsideration(considerationToDelete);
}
}
});
}
/**
* Adds a new topic to the conversation.
* @param {string} topic - The topic to add.
*/
addTopic(topic) {
const config = this.storage.get_app_config();
const guid = config[this.storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
if (guid) {
this.storage.addTopicToConversation(guid, topic);
this._updateTopicChipsDisplay();
}
}
/**
* Adds a new consideration to the conversation.
* @param {string} consideration - The consideration to add.
*/
addConsideration(consideration) {
const config = this.storage.get_app_config();
const guid = config[this.storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
if (guid) {
this.storage.addConsiderationToConversation(guid, consideration);
this._updateConsiderationsDisplay();
}
}
/**
* Deletes a topic from the conversation.
* @param {string} topic - The topic to delete.
*/
deleteTopic(topic) {
const config = this.storage.get_app_config();
const guid = config[this.storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
if (guid) {
this.storage.deleteTopicFromConversation(guid, topic);
this._updateTopicChipsDisplay();
}
}
/**
* Deletes a consideration from the conversation.
* @param {string} consideration - The consideration to delete.
*/
deleteConsideration(consideration) {
const config = this.storage.get_app_config();
const guid = config[this.storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
if (guid) {
this.storage.deleteConsiderationFromConversation(guid, consideration);
this._updateConsiderationsDisplay();
}
}
}
export default Context;