Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pages",
"version": "0.0.4",
"version": "0.0.5",
"description": "Doctorina Pages - Multiple lightweight HTML pages",
"type": "module",
"scripts": {
Expand Down
46 changes: 26 additions & 20 deletions pages/chat/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,8 @@ const i18n = {
attrDefault: 'Default',
attrTargetUrl: 'App URL to redirect to',
attrTargetUrlDefault: 'app.doctorina.com',
attrPlaceholder: 'Static placeholder text',
attrPlaceholderDefault: 'None',
attrPhrases: 'JSON array of phrases for typing animation',
attrPhrasesDefault: 'None',
attrChildren: 'Child <code>&lt;p&gt;</code> elements: 1 = static placeholder, 2+ = typing animation',
attrChildrenDefault: 'None',
attrParams: 'JSON object of extra query parameters (e.g. UTM tags)',
attrParamsDefault: 'None',
examples: 'Examples',
Expand All @@ -39,6 +37,9 @@ const i18n = {
exPlaceholderDesc: 'Simple static placeholder text:',
exParamsTitle: 'With UTM Parameters',
exParamsDesc: 'Pass custom query parameters (e.g. UTM tags) to the target URL:',
demoPhrase1: 'What symptoms do you have?',
demoPhrase2: 'Ask about medications',
demoPhrase3: 'Describe how you feel',
multiWidget: 'Multiple Widgets',
multiWidgetDesc: 'Use the class <code>doctorina-chat</code> instead of id to place multiple widgets on the same page. Each can have its own configuration.',
programmatic: 'Programmatic API',
Expand All @@ -60,10 +61,8 @@ const i18n = {
attrDefault: 'Predeterminado',
attrTargetUrl: 'URL de la app para redirigir',
attrTargetUrlDefault: 'app.doctorina.com',
attrPlaceholder: 'Texto estático del placeholder',
attrPlaceholderDefault: 'Ninguno',
attrPhrases: 'Array JSON de frases para la animación de escritura',
attrPhrasesDefault: 'Ninguno',
attrChildren: 'Elementos hijos <code>&lt;p&gt;</code>: 1 = placeholder estático, 2+ = animación de escritura',
attrChildrenDefault: 'Ninguno',
attrParams: 'Objeto JSON con parámetros extra (ej. etiquetas UTM)',
attrParamsDefault: 'Ninguno',
examples: 'Ejemplos',
Expand All @@ -75,6 +74,9 @@ const i18n = {
exPlaceholderDesc: 'Texto estático simple en el campo de entrada:',
exParamsTitle: 'Con parámetros UTM',
exParamsDesc: 'Pasa parámetros personalizados (ej. etiquetas UTM) a la URL de destino:',
demoPhrase1: '¿Qué síntomas tienes?',
demoPhrase2: 'Pregunta sobre medicamentos',
demoPhrase3: 'Describe cómo te sientes',
multiWidget: 'Múltiples widgets',
multiWidgetDesc: 'Usa la clase <code>doctorina-chat</code> en lugar de id para colocar múltiples widgets en la misma página. Cada uno puede tener su propia configuración.',
programmatic: 'API programática',
Expand All @@ -96,10 +98,8 @@ const i18n = {
attrDefault: 'По умолчанию',
attrTargetUrl: 'URL приложения для редиректа',
attrTargetUrlDefault: 'app.doctorina.com',
attrPlaceholder: 'Статический текст placeholder',
attrPlaceholderDefault: 'Нет',
attrPhrases: 'JSON-массив фраз для анимации печатания',
attrPhrasesDefault: 'Нет',
attrChildren: 'Дочерние элементы <code>&lt;p&gt;</code>: 1 = статический placeholder, 2+ = анимация печатания',
attrChildrenDefault: 'Нет',
attrParams: 'JSON-объект с дополнительными query-параметрами (напр. UTM-метки)',
attrParamsDefault: 'Нет',
examples: 'Примеры',
Expand All @@ -111,6 +111,9 @@ const i18n = {
exPlaceholderDesc: 'Простой статический текст в поле ввода:',
exParamsTitle: 'С UTM-параметрами',
exParamsDesc: 'Передача произвольных query-параметров (напр. UTM-меток) в целевой URL:',
demoPhrase1: 'Какие у вас симптомы?',
demoPhrase2: 'Спросите о лекарствах',
demoPhrase3: 'Опишите как вы себя чувствуете',
multiWidget: 'Несколько виджетов',
multiWidgetDesc: 'Используйте класс <code>doctorina-chat</code> вместо id, чтобы разместить несколько виджетов на одной странице. Каждый может иметь свою конфигурацию.',
programmatic: 'Программный API',
Expand All @@ -131,13 +134,15 @@ const SNIPPET_BASIC = `<!-- Doctorina Chat Widget -->
<div id="doctorina-chat"></div>
<script defer src="${WIDGET_HOST}/widget/doctorina-chat.js"></script>`;

const SNIPPET_PHRASES = `<div id="doctorina-chat"
data-phrases='["What symptoms do you have?", "Ask about medications", "Get a health consultation"]'>
const SNIPPET_PHRASES = `<div id="doctorina-chat">
<p>What symptoms do you have?</p>
<p>Ask about medications</p>
<p>Get a health consultation</p>
</div>
<script defer src="${WIDGET_HOST}/widget/doctorina-chat.js"></script>`;

const SNIPPET_PLACEHOLDER = `<div id="doctorina-chat"
data-placeholder="Type your health question...">
const SNIPPET_PLACEHOLDER = `<div id="doctorina-chat">
<p>Type your health question...</p>
</div>
<script defer src="${WIDGET_HOST}/widget/doctorina-chat.js"></script>`;

Expand Down Expand Up @@ -172,8 +177,10 @@ function render(lang: Lang): void {
<p>${t.demoDesc}</p>
<div class="demo-widget">
<div class="doctorina-chat"
data-target-url="https://doctorina-development.web.app"
data-phrases='["What symptoms do you have?", "Ask about medications", "Describe how you feel"]'>
data-target-url="https://doctorina-development.web.app">
<p>${t.demoPhrase1}</p>
<p>${t.demoPhrase2}</p>
<p>${t.demoPhrase3}</p>
</div>
</div>
</section>
Expand All @@ -197,8 +204,7 @@ function render(lang: Lang): void {
</thead>
<tbody>
<tr><td>data-target-url</td><td>${t.attrTargetUrl}</td><td>${t.attrTargetUrlDefault}</td></tr>
<tr><td>data-placeholder</td><td>${t.attrPlaceholder}</td><td>${t.attrPlaceholderDefault}</td></tr>
<tr><td>data-phrases</td><td>${t.attrPhrases}</td><td>${t.attrPhrasesDefault}</td></tr>
<tr><td>&lt;p&gt; children</td><td>${t.attrChildren}</td><td>${t.attrChildrenDefault}</td></tr>
<tr><td>data-params</td><td>${t.attrParams}</td><td>${t.attrParamsDefault}</td></tr>
</tbody>
</table>
Expand Down
23 changes: 9 additions & 14 deletions src/widget/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,16 @@ export function parseConfig(container: HTMLElement): WidgetConfig {
targetUrl: targetUrl.replace(/\/+$/, ''),
};

const placeholder = container.getAttribute('data-placeholder');
if (placeholder) {
config.placeholder = placeholder;
}
// Child elements = phrases for placeholder animation (translation-plugin friendly)
// 1 element → static placeholder, 2+ elements → animated cycling
const childTexts = Array.from(container.children)
.map((el) => el.textContent?.trim() ?? '')
.filter((text) => text.length > 0);

const phrasesAttr = container.getAttribute('data-phrases');
if (phrasesAttr) {
try {
const parsed = JSON.parse(phrasesAttr);
if (Array.isArray(parsed) && parsed.length > 0) {
config.phrases = parsed.filter((p): p is string => typeof p === 'string' && p.trim() !== '');
}
} catch {
console.warn('[DoctorinaChat] Invalid data-phrases JSON:', phrasesAttr);
}
if (childTexts.length === 1) {
config.placeholder = childTexts[0];
} else if (childTexts.length > 1) {
config.phrases = childTexts;
}

const paramsAttr = container.getAttribute('data-params');
Expand Down
1 change: 1 addition & 0 deletions src/widget/doctorina-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function init(): void {
container.dataset.dchatInited = '1';

const config = parseConfig(container);
container.innerHTML = '';

injectStyles();
createWidget(container, config);
Expand Down
6 changes: 6 additions & 0 deletions src/widget/placeholder-animation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ export function startPlaceholderAnimation(
): () => void {
if (phrases.length === 0) return () => {};

// Respect user motion preferences — show first phrase as static placeholder
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
textarea.placeholder = phrases[0];
return () => {};
}

let phraseIndex = 0;
let charIndex = 0;
let deleting = false;
Expand Down
5 changes: 4 additions & 1 deletion src/widget/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ const CSS = `

.dchat-textarea:focus {
outline: none;
box-shadow: none;
}

.dchat-textarea::placeholder {
Expand Down Expand Up @@ -75,6 +74,10 @@ const CSS = `
position: relative;
}

.dchat-btn:focus-visible {
box-shadow: 0 0 0 2px rgba(37, 211, 102, 0.35);
}

.dchat-btn:active:not(:disabled) {
transform: scale(0.94);
}
Expand Down
17 changes: 9 additions & 8 deletions src/widget/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,16 @@ import { startPlaceholderAnimation } from './placeholder-animation';
const SEND_ICON_SVG = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>`;

export function createWidget(container: HTMLElement, config: WidgetConfig): void {
// Build DOM
const box = document.createElement('div');
box.className = 'dchat-box';
box.setAttribute('role', 'form');
box.setAttribute('aria-label', 'Chat message');
box.setAttribute('aria-label', container.getAttribute('data-aria-label') || 'Chat message');

const textarea = document.createElement('textarea');
textarea.className = 'dchat-textarea';
textarea.rows = 1;
textarea.setAttribute('autocomplete', 'off');
textarea.setAttribute('aria-label', 'Chat message');
textarea.setAttribute('aria-label', container.getAttribute('data-aria-input') || 'Message');
textarea.maxLength = 4096;

if (config.placeholder) {
Expand All @@ -25,8 +24,7 @@ export function createWidget(container: HTMLElement, config: WidgetConfig): void
const btn = document.createElement('button');
btn.className = 'dchat-btn';
btn.type = 'button';
btn.setAttribute('aria-label', 'Send message');
btn.title = 'Send message';
btn.setAttribute('aria-label', container.getAttribute('data-aria-send') || 'Send');
btn.disabled = true;
btn.setAttribute('aria-disabled', 'true');
btn.innerHTML = SEND_ICON_SVG;
Expand Down Expand Up @@ -63,26 +61,29 @@ export function createWidget(container: HTMLElement, config: WidgetConfig): void

btn.addEventListener('click', submit);

textarea.addEventListener('keydown', (e) => {
function onKeydown(e: KeyboardEvent): void {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
submit();
}
});
}
textarea.addEventListener('keydown', onKeydown);

// Placeholder animation
let destroyAnimation: (() => void) | undefined;
if (config.phrases && config.phrases.length > 0) {
destroyAnimation = startPlaceholderAnimation(textarea, config.phrases);
}

// Expose cleanup for SPA / dynamic removal
// Cleanup for SPA / dynamic removal
container.addEventListener(
'dchat:destroy',
() => {
destroyAnimation?.();
textarea.removeEventListener('input', autoResize);
textarea.removeEventListener('input', updateButton);
textarea.removeEventListener('keydown', onKeydown);
btn.removeEventListener('click', submit);
},
{ once: true },
);
Expand Down
Loading