2 directories, 29 files

tinai

Home / testing / ai / tinai
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;">&times;</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;">&times;</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;
🌐
context.js ×
Type: Web, text/plain
9.37 Kilobytes
Last Modified 2026-09-04 14:23:37
⬇ Download File