articulation therapy web based should include me as slp is creating the app
mkdir -p /home/daytona/workspace/articulation-therapy && ls /home/daytona/workspace/
~/articulation-therapy/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>ArticulatePro โ SLP Articulation Therapy</title>
<style>
:root {
--primary: #4f7cac;
--primary-dark: #36587c;
--accent: #f4a261;
--accent-dark: #e07a3a;
--success: #2a9d8f;
--danger: #e63946;
--bg: #f0f4f8;
--card: #ffffff;
--text: #1d2d44;
--muted: #6b7a99;
--border: #d0dbe8;
--radius: 12px;
--shadow: 0 2px 12px rgba(0,0,0,0.10);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Segoe UI', Arial, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; }
/* โโ NAV โโ */
nav {
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary) 100%);
color: #fff; display: flex; align-items: center; justify-content: space-between;
padding: 0 28px; height: 62px; box-shadow: 0 2px 8px rgba(0,0,0,0.18);
}
nav .brand { font-size: 1.35rem; font-weight: 700; letter-spacing: .5px; }
nav .brand span { color: var(--accent); }
nav .slp-badge { font-size: .82rem; background: rgba(255,255,255,.18); padding: 4px 12px; border-radius: 20px; }
nav .nav-links { display: flex; gap: 8px; }
nav button.nav-btn {
background: rgba(255,255,255,.15); border: none; color: #fff;
padding: 6px 16px; border-radius: 20px; cursor: pointer; font-size: .9rem; transition: background .2s;
}
nav button.nav-btn:hover, nav button.nav-btn.active { background: var(--accent); color: #fff; }
nav button#logoutBtn { background: rgba(230,57,70,.7); }
nav button#logoutBtn:hover { background: var(--danger); }
/* โโ PAGES โโ */
.page { display: none; padding: 30px 28px; max-width: 1100px; margin: 0 auto; }
.page.active { display: block; }
/* โโ LOGIN โโ */
#loginPage {
display: flex; align-items: center; justify-content: center;
min-height: calc(100vh - 62px); padding: 20px;
}
.login-card {
background: var(--card); border-radius: var(--radius); box-shadow: var(--shadow);
padding: 48px 44px; width: 100%; max-width: 420px; text-align: center;
}
.login-card .logo { font-size: 2.6rem; margin-bottom: 8px; }
.login-card h2 { font-size: 1.5rem; color: var(--primary-dark); margin-bottom: 4px; }
.login-card .subtitle { color: var(--muted); font-size: .9rem; margin-bottom: 28px; }
.form-group { margin-bottom: 18px; text-align: left; }
.form-group label { font-size: .85rem; font-weight: 600; color: var(--muted); display: block; margin-bottom: 6px; }
.form-group input, .form-group select, .form-group textarea {
width: 100%; padding: 11px 14px; border: 1.5px solid var(--border);
border-radius: 8px; font-size: .97rem; color: var(--text); background: #f9fbfd;
transition: border .2s;
}
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
outline: none; border-color: var(--primary); background: #fff;
}
.btn {
display: inline-flex; align-items: center; gap: 7px;
padding: 10px 22px; border: none; border-radius: 8px; cursor: pointer;
font-size: .95rem; font-weight: 600; transition: all .2s;
}
.btn-primary { background: var(--primary); color: #fff; }
.btn-primary:hover { background: var(--primary-dark); }
.btn-accent { background: var(--accent); color: #fff; }
.btn-accent:hover { background: var(--accent-dark); }
.btn-success { background: var(--success); color: #fff; }
.btn-success:hover { background: #228478; }
.btn-danger { background: var(--danger); color: #fff; }
.btn-danger:hover { background: #c0303c; }
.btn-outline { background: #fff; color: var(--primary); border: 1.5px solid var(--primary); }
.btn-outline:hover { background: var(--primary); color: #fff; }
.btn-sm { padding: 6px 14px; font-size: .85rem; }
.btn-full { width: 100%; justify-content: center; }
.error-msg { color: var(--danger); font-size: .88rem; margin-top: 8px; display: none; }
/* โโ SECTION HEADER โโ */
.section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px; }
.section-header h2 { font-size: 1.4rem; color: var(--primary-dark); }
/* โโ CARDS / GRID โโ */
.cards-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 18px; }
.client-card {
background: var(--card); border-radius: var(--radius); padding: 20px 22px;
box-shadow: var(--shadow); border-left: 5px solid var(--primary); cursor: pointer;
transition: transform .15s, box-shadow .15s;
}
.client-card:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0,0,0,0.13); }
.client-card h3 { font-size: 1.1rem; margin-bottom: 4px; }
.client-card .meta { color: var(--muted); font-size: .85rem; margin-bottom: 10px; }
.client-card .sounds { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
.tag {
background: #e8f0fc; color: var(--primary); padding: 3px 10px;
border-radius: 20px; font-size: .78rem; font-weight: 600;
}
.tag.accent { background: #fdebd0; color: var(--accent-dark); }
/* โโ MODAL โโ */
.modal-overlay {
display: none; position: fixed; inset: 0; background: rgba(0,0,0,.45);
z-index: 200; align-items: center; justify-content: center;
}
.modal-overlay.open { display: flex; }
.modal {
background: var(--card); border-radius: var(--radius); padding: 32px 36px;
max-width: 500px; width: 95%; box-shadow: 0 8px 40px rgba(0,0,0,.22); position: relative;
}
.modal h3 { font-size: 1.2rem; margin-bottom: 20px; color: var(--primary-dark); }
.modal .close-btn {
position: absolute; top: 14px; right: 18px; background: none; border: none;
font-size: 1.4rem; cursor: pointer; color: var(--muted);
}
/* โโ SESSION โโ */
.session-layout { display: grid; grid-template-columns: 220px 1fr; gap: 22px; }
.sidebar { display: flex; flex-direction: column; gap: 14px; }
.sidebar-card {
background: var(--card); border-radius: var(--radius); padding: 16px 18px;
box-shadow: var(--shadow);
}
.sidebar-card h4 { font-size: .88rem; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; margin-bottom: 12px; }
.sound-btn {
display: block; width: 100%; text-align: center; padding: 8px;
background: #f0f4f8; border: 1.5px solid var(--border); border-radius: 8px;
cursor: pointer; font-size: 1.05rem; font-weight: 700; margin-bottom: 7px;
transition: all .15s; color: var(--text);
}
.sound-btn:hover, .sound-btn.active { background: var(--primary); color: #fff; border-color: var(--primary); }
.level-btn {
display: block; width: 100%; text-align: left; padding: 8px 12px;
background: #f0f4f8; border: 1.5px solid var(--border); border-radius: 8px;
cursor: pointer; font-size: .92rem; margin-bottom: 7px; transition: all .15s; color: var(--text);
}
.level-btn:hover, .level-btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
/* โโ MAIN SESSION AREA โโ */
.session-main { display: flex; flex-direction: column; gap: 18px; }
.target-card {
background: var(--card); border-radius: var(--radius); padding: 28px 32px;
box-shadow: var(--shadow); text-align: center;
}
.target-card .target-word {
font-size: 3.2rem; font-weight: 800; color: var(--primary-dark); letter-spacing: 2px;
margin-bottom: 6px; line-height: 1;
}
.target-card .target-cue {
font-size: 1.05rem; color: var(--muted); margin-bottom: 18px; font-style: italic;
}
.target-card .cue-image {
font-size: 5rem; margin-bottom: 18px; display: block;
filter: drop-shadow(0 2px 6px rgba(0,0,0,.12));
}
.response-btns { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; margin-top: 12px; }
.response-btns .btn { min-width: 130px; font-size: 1rem; padding: 12px 24px; }
.nav-arrows { display: flex; gap: 12px; justify-content: center; margin-top: 10px; }
/* โโ RECORDING โโ */
.record-card {
background: var(--card); border-radius: var(--radius); padding: 20px 24px;
box-shadow: var(--shadow);
}
.record-card h4 { font-size: .95rem; font-weight: 700; color: var(--muted); margin-bottom: 14px; }
.record-controls { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.rec-indicator {
width: 14px; height: 14px; border-radius: 50%;
background: #ccc; display: inline-block; margin-right: 4px; transition: background .3s;
}
.rec-indicator.recording { background: var(--danger); animation: pulse 1s infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
audio { height: 38px; }
/* โโ PROGRESS โโ */
.progress-section { display: flex; flex-direction: column; gap: 22px; }
.progress-card {
background: var(--card); border-radius: var(--radius); padding: 22px 26px;
box-shadow: var(--shadow);
}
.progress-card h3 { font-size: 1.05rem; margin-bottom: 16px; color: var(--primary-dark); }
.stat-row { display: flex; gap: 18px; flex-wrap: wrap; margin-bottom: 16px; }
.stat-box {
flex: 1; min-width: 110px; background: #f0f4f8; border-radius: 10px;
padding: 16px 18px; text-align: center;
}
.stat-box .num { font-size: 2rem; font-weight: 800; color: var(--primary); }
.stat-box .lbl { font-size: .8rem; color: var(--muted); margin-top: 2px; }
.progress-bar-wrap { margin-bottom: 12px; }
.progress-bar-wrap .bar-label { display: flex; justify-content: space-between; font-size: .88rem; margin-bottom: 5px; }
.progress-bar { height: 14px; background: #dde6f0; border-radius: 8px; overflow: hidden; }
.progress-bar .fill { height: 100%; background: linear-gradient(90deg, var(--primary), var(--success)); border-radius: 8px; transition: width .5s; }
.session-log { max-height: 280px; overflow-y: auto; }
.session-log table { width: 100%; border-collapse: collapse; font-size: .88rem; }
.session-log th { background: #f0f4f8; padding: 8px 12px; text-align: left; color: var(--muted); font-weight: 700; }
.session-log td { padding: 8px 12px; border-bottom: 1px solid var(--border); }
.correct { color: var(--success); font-weight: 700; }
.incorrect { color: var(--danger); font-weight: 700; }
/* โโ NOTES โโ */
.notes-section { display: flex; flex-direction: column; gap: 18px; }
.notes-toolbar { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }
.note-entry {
background: var(--card); border-radius: var(--radius); padding: 18px 22px;
box-shadow: var(--shadow); border-left: 4px solid var(--accent);
}
.note-entry .note-header { display: flex; justify-content: space-between; font-size: .82rem; color: var(--muted); margin-bottom: 8px; }
.note-entry p { font-size: .95rem; line-height: 1.55; white-space: pre-wrap; }
.notes-list { display: flex; flex-direction: column; gap: 14px; }
/* โโ RESPONSIVE โโ */
@media(max-width:700px) {
.session-layout { grid-template-columns: 1fr; }
.target-card .target-word { font-size: 2.2rem; }
nav { padding: 0 14px; }
.page { padding: 18px 14px; }
.login-card { padding: 32px 20px; }
}
/* โโ EMPTY STATE โโ */
.empty-state { text-align: center; padding: 60px 20px; color: var(--muted); }
.empty-state .icon { font-size: 3.5rem; margin-bottom: 14px; }
.empty-state p { font-size: 1.05rem; }
/* โโ CHART โโ */
canvas { max-width: 100%; }
.select-inline { padding: 7px 12px; border: 1.5px solid var(--border); border-radius: 8px; font-size: .9rem; background: #fff; color: var(--text); cursor: pointer; }
</style>
</head>
<body>
<!-- NAV -->
<nav id="mainNav" style="display:none">
<div class="brand">Articulate<span>Pro</span></div>
<div class="slp-badge" id="slpBadge">SLP Portal</div>
<div class="nav-links">
<button class="nav-btn active" onclick="showPage('clients')">๐ฅ Clients</button>
<button class="nav-btn" onclick="showPage('session')">๐ Session</button>
<button class="nav-btn" onclick="showPage('progress')">๐ Progress</button>
<button class="nav-btn" onclick="showPage('notes')">๐ Notes</button>
<button class="nav-btn" id="logoutBtn" onclick="logout()">Sign Out</button>
</div>
</nav>
<!-- LOGIN PAGE -->
<div id="loginPage" class="page active">
<div class="login-card">
<div class="logo">๐ฃ๏ธ</div>
<h2>ArticulatePro</h2>
<p class="subtitle">Speech-Language Pathology Articulation Therapy Platform</p>
<div class="form-group">
<label>SLP Username</label>
<input type="text" id="loginUser" placeholder="e.g. jsmith_slp"/>
</div>
<div class="form-group">
<label>Password</label>
<input type="password" id="loginPass" placeholder="โขโขโขโขโขโขโขโข"/>
</div>
<button class="btn btn-primary btn-full" onclick="doLogin()">Sign In โ</button>
<p class="error-msg" id="loginError">Invalid credentials. Default: admin / slp1234</p>
<p style="margin-top:18px;font-size:.8rem;color:var(--muted)">Demo credentials: <strong>admin</strong> / <strong>slp1234</strong></p>
</div>
</div>
<!-- CLIENTS PAGE -->
<div id="clientsPage" class="page">
<div class="section-header">
<h2>๐ฅ Client Roster</h2>
<button class="btn btn-primary" onclick="openAddClient()">+ Add Client</button>
</div>
<div class="cards-grid" id="clientsGrid"></div>
<div class="empty-state" id="clientsEmpty" style="display:none">
<div class="icon">๐ถ</div>
<p>No clients yet. Add your first client to get started.</p>
</div>
</div>
<!-- SESSION PAGE -->
<div id="sessionPage" class="page">
<div class="section-header">
<h2>๐ Therapy Session</h2>
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
<select class="select-inline" id="sessionClientSelect" onchange="loadSessionClient()">
<option value="">โ Select Client โ</option>
</select>
<button class="btn btn-accent btn-sm" onclick="endSession()" id="endSessionBtn" style="display:none">โ
End Session</button>
</div>
</div>
<div id="sessionNoClient" style="text-align:center;padding:50px;color:var(--muted)">
<p style="font-size:1.1rem">Select a client above to begin a therapy session.</p>
</div>
<div id="sessionLayout" class="session-layout" style="display:none">
<!-- SIDEBAR -->
<div class="sidebar">
<div class="sidebar-card">
<h4>๐ Target Sound</h4>
<div id="soundBtns"></div>
</div>
<div class="sidebar-card">
<h4>๐ถ Hierarchy Level</h4>
<button class="level-btn active" onclick="setLevel('isolation','Isolation')" id="lvl-isolation">Isolation</button>
<button class="level-btn" onclick="setLevel('syllable','Syllable')" id="lvl-syllable">Syllable</button>
<button class="level-btn" onclick="setLevel('word','Word')" id="lvl-word">Word</button>
<button class="level-btn" onclick="setLevel('phrase','Phrase')" id="lvl-phrase">Phrase</button>
<button class="level-btn" onclick="setLevel('sentence','Sentence')" id="lvl-sentence">Sentence</button>
<button class="level-btn" onclick="setLevel('conversation','Conversation')" id="lvl-conversation">Conversation</button>
</div>
<div class="sidebar-card">
<h4>๐ Position</h4>
<button class="sound-btn" onclick="setPosition('initial')" id="pos-initial">Initial</button>
<button class="sound-btn" onclick="setPosition('medial')" id="pos-medial">Medial</button>
<button class="sound-btn" onclick="setPosition('final')" id="pos-final">Final</button>
</div>
</div>
<!-- MAIN -->
<div class="session-main">
<div class="target-card">
<span class="cue-image" id="cueImage">๐ฃ๏ธ</span>
<div class="target-word" id="targetWord">โ</div>
<div class="target-cue" id="targetCue">Select a sound and level to begin</div>
<div class="nav-arrows">
<button class="btn btn-outline btn-sm" onclick="prevItem()">โ Prev</button>
<span id="itemCounter" style="padding:6px 10px;color:var(--muted);font-size:.9rem">0 / 0</span>
<button class="btn btn-outline btn-sm" onclick="nextItem()">Next โ</button>
</div>
<div class="response-btns">
<button class="btn btn-success" onclick="markResponse(true)">โ Correct</button>
<button class="btn btn-danger" onclick="markResponse(false)">โ Incorrect</button>
</div>
</div>
<!-- RECORDING -->
<div class="record-card">
<h4>๐ค Audio Recording</h4>
<div class="record-controls">
<span class="rec-indicator" id="recDot"></span>
<button class="btn btn-primary btn-sm" id="recStartBtn" onclick="startRecording()">โบ Record</button>
<button class="btn btn-danger btn-sm" id="recStopBtn" onclick="stopRecording()" disabled>โน Stop</button>
<audio id="audioPlayback" controls style="display:none"></audio>
</div>
<p style="font-size:.8rem;color:var(--muted);margin-top:8px">Record client's production for documentation and feedback.</p>
</div>
<!-- LIVE TALLY -->
<div class="record-card">
<h4>๐ Session Tally</h4>
<div class="stat-row" id="liveTally">
<div class="stat-box"><div class="num" id="tallyCorrect">0</div><div class="lbl">Correct</div></div>
<div class="stat-box"><div class="num" id="tallyIncorrect">0</div><div class="lbl">Incorrect</div></div>
<div class="stat-box"><div class="num" id="tallyPct">โ</div><div class="lbl">Accuracy</div></div>
</div>
<div class="progress-bar"><div class="fill" id="tallyBar" style="width:0%"></div></div>
</div>
</div>
</div>
</div>
<!-- PROGRESS PAGE -->
<div id="progressPage" class="page">
<div class="section-header">
<h2>๐ Progress Tracker</h2>
<select class="select-inline" id="progressClientSelect" onchange="loadProgressClient()">
<option value="">โ Select Client โ</option>
</select>
</div>
<div id="progressContent">
<div class="empty-state">
<div class="icon">๐</div>
<p>Select a client to view their progress data.</p>
</div>
</div>
</div>
<!-- NOTES PAGE -->
<div id="notesPage" class="page">
<div class="section-header">
<h2>๐ Session Notes</h2>
<select class="select-inline" id="notesClientSelect" onchange="loadNotesClient()">
<option value="">โ Select Client โ</option>
</select>
</div>
<div id="notesContent">
<div class="empty-state">
<div class="icon">๐</div>
<p>Select a client to view or add session notes.</p>
</div>
</div>
</div>
<!-- ADD/EDIT CLIENT MODAL -->
<div class="modal-overlay" id="clientModal">
<div class="modal">
<button class="close-btn" onclick="closeModal('clientModal')">โ</button>
<h3 id="clientModalTitle">Add New Client</h3>
<div class="form-group">
<label>Full Name *</label>
<input type="text" id="clientName" placeholder="e.g. Jamie Thompson"/>
</div>
<div class="form-group">
<label>Age</label>
<input type="number" id="clientAge" min="2" max="99" placeholder="e.g. 7"/>
</div>
<div class="form-group">
<label>Diagnosis / Notes</label>
<input type="text" id="clientDiag" placeholder="e.g. Phonological disorder, /r/ distortion"/>
</div>
<div class="form-group">
<label>Target Sounds (select all that apply)</label>
<div id="soundCheckboxes" style="display:flex;flex-wrap:wrap;gap:8px;margin-top:6px"></div>
</div>
<div style="display:flex;gap:10px;margin-top:20px">
<button class="btn btn-primary btn-full" onclick="saveClient()">Save Client</button>
</div>
<input type="hidden" id="clientEditId"/>
</div>
</div>
<!-- VIEW CLIENT MODAL -->
<div class="modal-overlay" id="viewClientModal">
<div class="modal">
<button class="close-btn" onclick="closeModal('viewClientModal')">โ</button>
<h3 id="viewClientName">Client</h3>
<div id="viewClientInfo"></div>
<div style="display:flex;gap:10px;margin-top:20px;flex-wrap:wrap">
<button class="btn btn-primary btn-sm" onclick="startSessionForClient()">๐ Start Session</button>
<button class="btn btn-outline btn-sm" onclick="editClientFromView()">โ๏ธ Edit</button>
<button class="btn btn-danger btn-sm" onclick="deleteClient()">๐ Delete</button>
</div>
</div>
</div>
<script>
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// DATA & STATE
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const ALL_SOUNDS = ['/r/', '/s/', '/z/', '/l/', '/sh/', '/ch/', '/th/', '/f/', '/v/', '/k/', '/g/', '/p/', '/b/', '/t/', '/d/', '/m/', '/n/', '/ng/', '/j/', '/w/', '/h/'];
const TARGETS = {
'/r/': {
isolation: { initial:['/r/ sound'], medial:['/r/ sound'], final:['/r/ sound'] },
syllable: { initial:['ra','re','ri','ro','ru'], medial:['ara','ire','ore'], final:['ar','er','ir','or','ur'] },
word: { initial:['rabbit','rainbow','red','run','rock','rope','ring'], medial:['carrot','mirror','parrot','pirate','ferry'], final:['car','door','bear','star','chair','floor'] },
phrase: { initial:['red rose','running rabbit','race car'], medial:['hairy monster','very tired','caring nurse'], final:['big bear','blue car','far star'] },
sentence: { initial:['The rabbit ran really fast.','Red roses are really pretty.','Ricky reads every morning.'], medial:['The parrot has colorful feathers.','Her mirror is very shiny.','Barry carried the heavy barrel.'], final:['The bear lives far away.','Her car is near the door.','The star shines in clear air.'] },
conversation: { initial:['Tell me about something red you like.'], medial:['Describe your favorite story.'], final:['Where is your favorite place?'] }
},
'/s/': {
isolation: { initial:['/s/ sound'], medial:['/s/ sound'], final:['/s/ sound'] },
syllable: { initial:['sa','see','so','sue'], medial:['asa','ese','iso'], final:['as','us','is'] },
word: { initial:['sun','soup','soap','sock','seal','seven'], medial:['lesson','eraser','outside','blossom'], final:['bus','grass','ice','race','goose','dice'] },
phrase: { initial:['sunny sky','silly snake','soft song'], medial:['outside lesson','eraser set'], final:['race bus','ice palace'] },
sentence: { initial:['Sara sees a sunny sky.','The snake is very silly.','Seven seals sit by the sea.'], medial:['The lesson is outside.','Lucy uses an eraser.','The blossom smells so sweet.'], final:['The bus stops at the grass.','Ice melts on the race track.','The goose honks twice.'] },
conversation: { initial:['What is your favorite season?'], medial:['Tell me about something awesome outside.'], final:['What toys do you like to race?'] }
},
'/l/': {
isolation: { initial:['/l/ sound'], medial:['/l/ sound'], final:['/l/ sound'] },
syllable: { initial:['la','lee','lo','lu'], medial:['ala','elo','uli'], final:['al','el','il','ul'] },
word: { initial:['lion','lamp','leaf','lemon','log','lake'], medial:['balloon','yellow','pillow','silly','follow'], final:['ball','bell','hill','school','owl','apple'] },
phrase: { initial:['lemon lake','long leaf','little lamb'], medial:['yellow balloon','silly pillow'], final:['tall hill','small bell','shiny apple'] },
sentence: { initial:['The lion lives by the lake.','Lucy loves lemon lollipops.','Look at the lovely lamp.'], medial:['The yellow balloon flew away.','Billy has a silly pillow.','The valley is full of flowers.'], final:['The ball rolled down the hill.','The school bell rang at noon.','An owl sat on a tall wall.'] },
conversation: { initial:['What do you love doing after school?'], medial:['Tell me about something yellow.'], final:['What do you like to play ball with?'] }
},
'/sh/': {
isolation: { initial:['/sh/ sound'], medial:['/sh/ sound'], final:['/sh/ sound'] },
syllable: { initial:['sha','she','sho','shu'], medial:['asha','eshe'], final:['ash','ish','ush'] },
word: { initial:['shoe','ship','sheep','shark','shirt','shower'], medial:['mushroom','fishing','sunshine','cushion'], final:['fish','dish','wish','brush','rush','trash'] },
phrase: { initial:['shiny shoe','shy sheep','short shirt'], medial:['fishing boat','sunshine day'], final:['gold fish','magic wish','big brush'] },
sentence: { initial:['She shines her shoes every morning.','The shy sheep ran away.','Shane wore a short shirt.'], medial:['The mushroom is in the sunshine.','They went fishing by the shore.','She had a cushion on her chair.'], final:['The fish swam in the dish.','She made a wish on a star.','Use the brush to clean the trash.'] },
conversation: { initial:['What do you wish for?'], medial:['Tell me about a place in the sunshine.'], final:['What is your favorite dish?'] }
},
'/th/': {
isolation: { initial:['/th/ sound'], medial:['/th/ sound'], final:['/th/ sound'] },
syllable: { initial:['tha','thee','tho'], medial:['etha','ithi'], final:['ath','ith','uth'] },
word: { initial:['thumb','three','thick','think','thin','throw'], medial:['birthday','healthy','toothbrush','nothing','together'], final:['bath','tooth','mouth','earth','path','cloth'] },
phrase: { initial:['three thumbs','thick thread','thin thing'], medial:['birthday cake','healthy teeth'], final:['bubble bath','clean tooth','muddy path'] },
sentence: { initial:['Three thick threads broke.','Think before you throw.','The thin boy threw the ball.'], medial:['Her birthday is on Thursday.','Healthy teeth need toothbrushes.','Nothing happened at the gathering.'], final:['He took a bath on the path.','Her tooth fell out at the mouth.','The cloth covered the earth below.'] },
conversation: { initial:['What do you think about this picture?'], medial:['Tell me about a healthy habit.'], final:['What does brushing your teeth feel like?'] }
},
'/k/': {
isolation: { initial:['/k/ sound'], medial:['/k/ sound'], final:['/k/ sound'] },
syllable: { initial:['ka','kee','ko','ku'], medial:['aka','eko'], final:['ak','ok','ik'] },
word: { initial:['cat','kite','cup','car','cake','key'], medial:['cookie','bucket','acorn','chicken','jacket'], final:['book','duck','rock','truck','milk','snack'] },
phrase: { initial:['cool cat','kite in the sky','cute kitten'], medial:['chicken cookie','acorn bucket'], final:['blue duck','hard rock','cold milk'] },
sentence: { initial:['The cat carries a cup.','Kids like to fly kites.','Callie baked a carrot cake.'], medial:['The chicken ate a cookie.','The jacket has a bucket pocket.','The acorn fell in the backyard.'], final:['The duck sat on a rock.','The truck carried cold milk.','Get a snack from the book bag.'] },
conversation: { initial:['What is your cat or pet like?'], medial:['Tell me what you like to cook.'], final:['What snack do you like best?'] }
},
'/g/': {
isolation: { initial:['/g/ sound'], medial:['/g/ sound'], final:['/g/ sound'] },
syllable: { initial:['ga','gee','go','goo'], medial:['aga','ego'], final:['ag','og','ug'] },
word: { initial:['goat','game','gate','girl','green','gift'], medial:['tiger','eagle','wagon','sugar','igloo'], final:['bag','dog','frog','log','egg','pig'] },
phrase: { initial:['green goat','great gift','golden gate'], medial:['sugar wagon','tiger cage'], final:['big dog','spotted frog','brown log'] },
sentence: { initial:['The goat got a great gift.','Go get the green gate.','Greta gave gifts to girls.'], medial:['The wagon has a tiger on it.','She added sugar to her drink.','The eagle flew to the igloo.'], final:['The dog jumped on the log.','The frog sat on a big egg.','Put the bag by the pig pen.'] },
conversation: { initial:['Tell me about your favorite game.'], medial:['What sugary foods do you like?'], final:['What is your favorite animal?'] }
}
};
// Add generic targets for sounds not in the table
ALL_SOUNDS.forEach(s => {
if (!TARGETS[s]) {
TARGETS[s] = {
isolation: { initial:[s+' sound'], medial:[s+' sound'], final:[s+' sound'] },
syllable: { initial:['Practice syllables with '+s], medial:['Practice syllables with '+s], final:['Practice syllables with '+s] },
word: { initial:['word 1','word 2','word 3'], medial:['word 4','word 5','word 6'], final:['word 7','word 8','word 9'] },
phrase: { initial:['phrase 1','phrase 2'], medial:['phrase 3','phrase 4'], final:['phrase 5','phrase 6'] },
sentence: { initial:['Practice '+s+' in a sentence.'], medial:['Use '+s+' in the middle.'], final:['End with '+s+' sound.'] },
conversation: { initial:['Have a conversation using '+s+'.'], medial:['Have a conversation using '+s+'.'], final:['Have a conversation using '+s+'.'] }
};
}
});
const CUE_IMAGES = {
'/r/':'๐ฆ', '/s/':'๐', '/z/':'โก', '/l/':'๐ฆ', '/sh/':'๐คซ', '/ch/':'๐',
'/th/':'๐
', '/f/':'๐ฆ', '/v/':'๐ป', '/k/':'๐ฑ', '/g/':'๐', '/p/':'๐ฆ',
'/b/':'๐', '/t/':'๐ฆ', '/d/':'๐ฅ', '/m/':'๐', '/n/':'๐ต', '/ng/':'๐ถ',
'/j/':'๐คธ', '/w/':'๐', '/h/':'๐ '
};
const CUE_HINTS = {
'/r/':'Tongue tip up, back of tongue raised. Voice on.',
'/s/':'Teeth together, air over tongue tip. No voice.',
'/z/':'Like /s/ but voice on. Feel the buzz!',
'/l/':'Tongue tip touches behind top teeth. Voice on.',
'/sh/':'Lips round, teeth close, tongue back. No voice.',
'/ch/':'Start /t/ then release into /sh/.',
'/th/':'Tongue tip lightly between teeth. (Voiced or voiceless)',
'/f/':'Top teeth on bottom lip. Blow air. No voice.',
'/v/':'Like /f/ but voice on. Feel the vibration.',
'/k/':'Back of tongue touches soft palate. No voice at first.',
'/g/':'Like /k/ but voice on.',
'/p/':'Lips together, pop open with air. No voice.',
'/b/':'Like /p/ but voice on.',
'/t/':'Tongue tip taps behind top teeth. No voice pop.',
'/d/':'Like /t/ but voice on.',
'/m/':'Lips together, hum through nose.',
'/n/':'Tongue tip up, hum through nose.',
'/ng/':'Back of tongue up, hum through nose.',
'/j/':'Like /ch/ but voice on.',
'/w/':'Round lips, then open into vowel.',
'/h/':'Open mouth, breathe out.'
};
let state = {
loggedIn: false,
slpName: '',
clients: [],
sessions: [],
notes: [],
currentClientId: null,
session: {
clientId: null,
sound: null,
level: 'word',
levelLabel: 'Word',
position: 'initial',
items: [],
itemIndex: 0,
correct: 0,
incorrect: 0,
log: []
}
};
function loadState() {
const saved = localStorage.getItem('articulatepro_state');
if (saved) {
const s = JSON.parse(saved);
state.slpName = s.slpName || '';
state.clients = s.clients || [];
state.sessions = s.sessions || [];
state.notes = s.notes || [];
}
}
function saveState() {
localStorage.setItem('articulatepro_state', JSON.stringify({
slpName: state.slpName,
clients: state.clients,
sessions: state.sessions,
notes: state.notes
}));
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// AUTH
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function doLogin() {
const u = document.getElementById('loginUser').value.trim();
const p = document.getElementById('loginPass').value.trim();
const err = document.getElementById('loginError');
// Check saved credentials first
const saved = JSON.parse(localStorage.getItem('articulatepro_creds') || '{"user":"admin","pass":"slp1234","name":"Your Name, M.S., CCC-SLP"}');
if (u === saved.user && p === saved.pass) {
state.loggedIn = true;
state.slpName = saved.name || 'SLP Provider';
document.getElementById('slpBadge').textContent = state.slpName;
err.style.display = 'none';
document.getElementById('loginPage').classList.remove('active');
document.getElementById('mainNav').style.display = 'flex';
showPage('clients');
} else {
err.style.display = 'block';
}
}
document.addEventListener('keydown', e => {
if (e.key === 'Enter' && !state.loggedIn) doLogin();
});
function logout() {
state.loggedIn = false;
document.getElementById('mainNav').style.display = 'none';
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.getElementById('loginPage').classList.add('active');
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// NAVIGATION
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function showPage(name) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
document.getElementById(name + 'Page').classList.add('active');
const btns = document.querySelectorAll('.nav-btn');
const map = {clients:0, session:1, progress:2, notes:3};
if (map[name] !== undefined) btns[map[name]].classList.add('active');
if (name === 'clients') renderClients();
if (name === 'session') populateClientSelect('sessionClientSelect');
if (name === 'progress') { populateClientSelect('progressClientSelect'); }
if (name === 'notes') { populateClientSelect('notesClientSelect'); }
}
function populateClientSelect(selId) {
const sel = document.getElementById(selId);
const prev = sel.value;
sel.innerHTML = '<option value="">โ Select Client โ</option>';
state.clients.forEach(c => {
const o = document.createElement('option');
o.value = c.id; o.textContent = c.name + (c.age ? ' (age '+c.age+')' : '');
sel.appendChild(o);
});
if (prev) sel.value = prev;
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// CLIENTS
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function renderClients() {
const grid = document.getElementById('clientsGrid');
const empty = document.getElementById('clientsEmpty');
grid.innerHTML = '';
if (!state.clients.length) { empty.style.display = 'block'; return; }
empty.style.display = 'none';
state.clients.forEach(c => {
const div = document.createElement('div');
div.className = 'client-card';
div.innerHTML = `
<h3>${escHtml(c.name)}</h3>
<div class="meta">${c.age ? 'Age: '+c.age : ''} ${c.diag ? 'ยท '+escHtml(c.diag) : ''}</div>
<div class="sounds">${(c.sounds||[]).map(s=>'<span class="tag">'+s+'</span>').join('')}</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn btn-primary btn-sm" onclick="startSessionForClientId('${c.id}')">๐ Session</button>
<button class="btn btn-outline btn-sm" onclick="viewClient('${c.id}')">View</button>
</div>`;
grid.appendChild(div);
});
}
function openAddClient() {
document.getElementById('clientModalTitle').textContent = 'Add New Client';
document.getElementById('clientName').value = '';
document.getElementById('clientAge').value = '';
document.getElementById('clientDiag').value = '';
document.getElementById('clientEditId').value = '';
buildSoundCheckboxes([]);
openModal('clientModal');
}
function buildSoundCheckboxes(selected) {
const wrap = document.getElementById('soundCheckboxes');
wrap.innerHTML = '';
ALL_SOUNDS.forEach(s => {
const label = document.createElement('label');
label.style.cssText = 'display:flex;align-items:center;gap:4px;font-size:.88rem;cursor:pointer;background:#f0f4f8;padding:5px 10px;border-radius:8px;';
const cb = document.createElement('input');
cb.type = 'checkbox'; cb.value = s;
if (selected.includes(s)) cb.checked = true;
label.appendChild(cb);
label.appendChild(document.createTextNode(s));
wrap.appendChild(label);
});
}
function saveClient() {
const name = document.getElementById('clientName').value.trim();
if (!name) { alert('Client name is required.'); return; }
const sounds = Array.from(document.querySelectorAll('#soundCheckboxes input:checked')).map(i=>i.value);
const editId = document.getElementById('clientEditId').value;
if (editId) {
const c = state.clients.find(x=>x.id===editId);
if (c) { c.name=name; c.age=document.getElementById('clientAge').value; c.diag=document.getElementById('clientDiag').value; c.sounds=sounds; }
} else {
state.clients.push({ id: 'c'+Date.now(), name, age:document.getElementById('clientAge').value, diag:document.getElementById('clientDiag').value, sounds });
}
saveState(); closeModal('clientModal'); renderClients();
}
let viewingClientId = null;
function viewClient(id) {
const c = state.clients.find(x=>x.id===id);
if (!c) return;
viewingClientId = id;
document.getElementById('viewClientName').textContent = c.name;
document.getElementById('viewClientInfo').innerHTML = `
<p><strong>Age:</strong> ${c.age||'โ'}</p>
<p style="margin-top:6px"><strong>Diagnosis/Notes:</strong> ${escHtml(c.diag)||'โ'}</p>
<p style="margin-top:6px"><strong>Target Sounds:</strong></p>
<div class="sounds" style="margin-top:6px">${(c.sounds||[]).map(s=>'<span class="tag">'+s+'</span>').join('') || 'โ'}</div>`;
openModal('viewClientModal');
}
function editClientFromView() {
closeModal('viewClientModal');
const c = state.clients.find(x=>x.id===viewingClientId);
if (!c) return;
document.getElementById('clientModalTitle').textContent = 'Edit Client';
document.getElementById('clientName').value = c.name;
document.getElementById('clientAge').value = c.age||'';
document.getElementById('clientDiag').value = c.diag||'';
document.getElementById('clientEditId').value = c.id;
buildSoundCheckboxes(c.sounds||[]);
openModal('clientModal');
}
function deleteClient() {
if (!confirm('Delete this client and all their data?')) return;
state.clients = state.clients.filter(x=>x.id!==viewingClientId);
state.sessions = state.sessions.filter(x=>x.clientId!==viewingClientId);
state.notes = state.notes.filter(x=>x.clientId!==viewingClientId);
saveState(); closeModal('viewClientModal'); renderClients();
}
function startSessionForClient() {
closeModal('viewClientModal');
startSessionForClientId(viewingClientId);
}
function startSessionForClientId(id) {
showPage('session');
document.getElementById('sessionClientSelect').value = id;
loadSessionClient();
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// SESSION
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function loadSessionClient() {
const id = document.getElementById('sessionClientSelect').value;
const noClient = document.getElementById('sessionNoClient');
const layout = document.getElementById('sessionLayout');
if (!id) { noClient.style.display='block'; layout.style.display='none'; document.getElementById('endSessionBtn').style.display='none'; return; }
const c = state.clients.find(x=>x.id===id);
state.session = { clientId:id, sound:null, level:'word', levelLabel:'Word', position:'initial', items:[], itemIndex:0, correct:0, incorrect:0, log:[] };
noClient.style.display='none'; layout.style.display='grid';
document.getElementById('endSessionBtn').style.display='inline-flex';
updateTally();
// Build sound buttons
const wrap = document.getElementById('soundBtns');
wrap.innerHTML = '';
const sounds = (c && c.sounds && c.sounds.length) ? c.sounds : ALL_SOUNDS.slice(0,8);
sounds.forEach(s => {
const btn = document.createElement('button');
btn.className = 'sound-btn'; btn.textContent = s;
btn.onclick = () => selectSound(s);
btn.id = 'sb-'+s.replace(/\//g,'');
wrap.appendChild(btn);
});
// Default position active
document.querySelectorAll('.sound-btn[id^="pos-"]').forEach(b => b.classList.remove('active'));
document.getElementById('pos-initial').classList.add('active');
// Default level active
document.querySelectorAll('.level-btn').forEach(b => b.classList.remove('active'));
document.getElementById('lvl-word').classList.add('active');
state.session.level = 'word'; state.session.levelLabel = 'Word';
renderTargetPlaceholder();
}
function selectSound(sound) {
state.session.sound = sound;
document.querySelectorAll('#soundBtns .sound-btn').forEach(b => b.classList.remove('active'));
const btn = document.getElementById('sb-'+sound.replace(/\//g,''));
if (btn) btn.classList.add('active');
loadItems();
}
function setLevel(level, label) {
state.session.level = level; state.session.levelLabel = label;
document.querySelectorAll('.level-btn').forEach(b => b.classList.remove('active'));
document.getElementById('lvl-'+level).classList.add('active');
if (state.session.sound) loadItems(); else renderTargetPlaceholder();
}
function setPosition(pos) {
state.session.position = pos;
document.querySelectorAll('[id^="pos-"]').forEach(b => b.classList.remove('active'));
document.getElementById('pos-'+pos).classList.add('active');
if (state.session.sound) loadItems(); else renderTargetPlaceholder();
}
function loadItems() {
const {sound, level, position} = state.session;
if (!sound) { renderTargetPlaceholder(); return; }
const sData = TARGETS[sound];
if (!sData || !sData[level]) return;
const items = sData[level][position] || sData[level]['initial'] || [];
state.session.items = items; state.session.itemIndex = 0;
renderTarget();
}
function renderTargetPlaceholder() {
document.getElementById('targetWord').textContent = 'โ';
document.getElementById('targetCue').textContent = 'Select a sound to begin';
document.getElementById('cueImage').textContent = '๐ฃ๏ธ';
document.getElementById('itemCounter').textContent = '0 / 0';
}
function renderTarget() {
const {items, itemIndex, sound} = state.session;
if (!items.length) { renderTargetPlaceholder(); return; }
const item = items[itemIndex] || items[0];
document.getElementById('targetWord').textContent = item;
document.getElementById('targetCue').textContent = CUE_HINTS[sound] || '';
document.getElementById('cueImage').textContent = CUE_IMAGES[sound] || '๐ฃ๏ธ';
document.getElementById('itemCounter').textContent = (itemIndex+1)+' / '+items.length;
}
function nextItem() {
if (!state.session.items.length) return;
state.session.itemIndex = (state.session.itemIndex + 1) % state.session.items.length;
renderTarget();
}
function prevItem() {
if (!state.session.items.length) return;
state.session.itemIndex = (state.session.itemIndex - 1 + state.session.items.length) % state.session.items.length;
renderTarget();
}
function markResponse(correct) {
const {items, itemIndex, sound, level, levelLabel, position} = state.session;
if (!items.length || !sound) return;
const item = items[itemIndex];
if (correct) state.session.correct++; else state.session.incorrect++;
state.session.log.push({ item, sound, level, levelLabel, position, correct, time: new Date().toLocaleTimeString() });
updateTally();
nextItem();
// flash feedback
const card = document.querySelector('.target-card');
card.style.background = correct ? '#eafaf6' : '#fdecea';
setTimeout(() => { card.style.background = ''; }, 400);
}
function updateTally() {
const {correct, incorrect} = state.session;
const total = correct + incorrect;
document.getElementById('tallyCorrect').textContent = correct;
document.getElementById('tallyIncorrect').textContent = incorrect;
const pct = total ? Math.round(correct/total*100) : 0;
document.getElementById('tallyPct').textContent = total ? pct+'%' : 'โ';
document.getElementById('tallyBar').style.width = total ? pct+'%' : '0%';
}
function endSession() {
const {clientId, correct, incorrect, sound, level, levelLabel, log} = state.session;
if (!clientId) return;
const total = correct + incorrect;
if (!total && !confirm('No responses recorded. Save session anyway?')) return;
const session = {
id: 's'+Date.now(), clientId, date: new Date().toLocaleDateString(), time: new Date().toLocaleTimeString(),
sound, level, levelLabel, correct, incorrect, total, pct: total ? Math.round(correct/total*100) : 0, log
};
state.sessions.push(session);
// Auto-create a note
const note = {
id: 'n'+Date.now(), clientId,
date: new Date().toLocaleDateString(), time: new Date().toLocaleTimeString(),
text: `Session: ${sound||'โ'} at ${levelLabel} level.\nCorrect: ${correct}/${total} (${session.pct}%).\nResponses logged: ${log.length}.`
};
state.notes.push(note);
saveState();
alert(`Session saved! Accuracy: ${session.pct}% (${correct}/${total})`);
document.getElementById('endSessionBtn').style.display = 'none';
document.getElementById('sessionClientSelect').value = '';
document.getElementById('sessionLayout').style.display = 'none';
document.getElementById('sessionNoClient').style.display = 'block';
state.session = { clientId:null, sound:null, level:'word', levelLabel:'Word', position:'initial', items:[], itemIndex:0, correct:0, incorrect:0, log:[] };
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// RECORDING
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let mediaRecorder = null, audioChunks = [];
function startRecording() {
navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];
mediaRecorder.ondataavailable = e => { if(e.data.size>0) audioChunks.push(e.data); };
mediaRecorder.onstop = () => {
const blob = new Blob(audioChunks, { type: 'audio/webm' });
const url = URL.createObjectURL(blob);
const audio = document.getElementById('audioPlayback');
audio.src = url; audio.style.display = 'block';
stream.getTracks().forEach(t=>t.stop());
};
mediaRecorder.start();
document.getElementById('recDot').classList.add('recording');
document.getElementById('recStartBtn').disabled = true;
document.getElementById('recStopBtn').disabled = false;
}).catch(() => alert('Microphone access denied. Please allow microphone access in your browser.'));
}
function stopRecording() {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop();
document.getElementById('recDot').classList.remove('recording');
document.getElementById('recStartBtn').disabled = false;
document.getElementById('recStopBtn').disabled = true;
}
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// PROGRESS
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function loadProgressClient() {
const id = document.getElementById('progressClientSelect').value;
const content = document.getElementById('progressContent');
if (!id) {
content.innerHTML = '<div class="empty-state"><div class="icon">๐</div><p>Select a client to view their progress data.</p></div>';
return;
}
const c = state.clients.find(x=>x.id===id);
const sessions = state.sessions.filter(x=>x.clientId===id);
if (!sessions.length) {
content.innerHTML = '<div class="empty-state"><div class="icon">๐</div><p>No sessions recorded for this client yet.</p></div>';
return;
}
// Summary stats
const totalSessions = sessions.length;
const totalTrials = sessions.reduce((a,s)=>a+s.total,0);
const totalCorrect = sessions.reduce((a,s)=>a+s.correct,0);
const overallPct = totalTrials ? Math.round(totalCorrect/totalTrials*100) : 0;
// Sound breakdown
const soundMap = {};
sessions.forEach(s => {
if (!s.sound) return;
if (!soundMap[s.sound]) soundMap[s.sound] = {correct:0, total:0};
soundMap[s.sound].correct += s.correct;
soundMap[s.sound].total += s.total;
});
let html = `<div class="progress-section">
<div class="progress-card">
<h3>Overall Stats โ ${escHtml(c.name)}</h3>
<div class="stat-row">
<div class="stat-box"><div class="num">${totalSessions}</div><div class="lbl">Sessions</div></div>
<div class="stat-box"><div class="num">${totalTrials}</div><div class="lbl">Total Trials</div></div>
<div class="stat-box"><div class="num">${totalCorrect}</div><div class="lbl">Correct</div></div>
<div class="stat-box"><div class="num" style="color:${overallPct>=80?'var(--success)':overallPct>=60?'var(--accent-dark)':'var(--danger)'}">${overallPct}%</div><div class="lbl">Accuracy</div></div>
</div>
</div>`;
if (Object.keys(soundMap).length) {
html += `<div class="progress-card"><h3>Accuracy by Sound</h3>`;
Object.entries(soundMap).forEach(([sound, d]) => {
const pct = d.total ? Math.round(d.correct/d.total*100) : 0;
const color = pct>=80?'var(--success)':pct>=60?'var(--accent-dark)':'var(--danger)';
html += `<div class="progress-bar-wrap">
<div class="bar-label"><span>${sound} โ ${d.correct}/${d.total} trials</span><span style="color:${color};font-weight:700">${pct}%</span></div>
<div class="progress-bar"><div class="fill" style="width:${pct}%;background:${color}"></div></div>
</div>`;
});
html += `</div>`;
}
// Session log table
html += `<div class="progress-card"><h3>Session History</h3>
<div class="session-log"><table>
<tr><th>Date</th><th>Sound</th><th>Level</th><th>Correct</th><th>Total</th><th>Accuracy</th></tr>
${sessions.slice().reverse().map(s=>`<tr>
<td>${s.date} ${s.time||''}</td>
<td>${s.sound||'โ'}</td>
<td>${s.levelLabel||s.level||'โ'}</td>
<td class="correct">${s.correct}</td>
<td>${s.total}</td>
<td style="font-weight:700;color:${s.pct>=80?'var(--success)':s.pct>=60?'var(--accent-dark)':'var(--danger)'}">${s.pct}%</td>
</tr>`).join('')}
</table></div></div></div>`;
content.innerHTML = html;
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// NOTES
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function loadNotesClient() {
const id = document.getElementById('notesClientSelect').value;
const content = document.getElementById('notesContent');
if (!id) { content.innerHTML = '<div class="empty-state"><div class="icon">๐</div><p>Select a client to view or add session notes.</p></div>'; return; }
const c = state.clients.find(x=>x.id===id);
const notes = state.notes.filter(x=>x.clientId===id);
let html = `<div class="notes-section">
<div style="background:var(--card);border-radius:var(--radius);padding:20px 24px;box-shadow:var(--shadow)">
<h4 style="margin-bottom:14px;color:var(--primary-dark)">Add Note for ${escHtml(c.name)}</h4>
<div class="form-group"><textarea id="newNoteText" rows="4" placeholder="Write session notes, observations, goals, home practice recommendations..."></textarea></div>
<button class="btn btn-accent" onclick="addNote('${id}')">๐พ Save Note</button>
</div>`;
if (notes.length) {
html += `<div class="notes-list">`;
notes.slice().reverse().forEach(n => {
html += `<div class="note-entry">
<div class="note-header">
<span>๐
${n.date} at ${n.time||''}</span>
<button class="btn btn-danger btn-sm" onclick="deleteNote('${n.id}')">โ Delete</button>
</div>
<p>${escHtml(n.text)}</p>
</div>`;
});
html += `</div>`;
} else {
html += `<div class="empty-state" style="padding:30px"><div class="icon">๐ญ</div><p>No notes yet for this client.</p></div>`;
}
html += `</div>`;
content.innerHTML = html;
}
function addNote(clientId) {
const text = document.getElementById('newNoteText').value.trim();
if (!text) { alert('Please write a note before saving.'); return; }
state.notes.push({ id:'n'+Date.now(), clientId, date: new Date().toLocaleDateString(), time: new Date().toLocaleTimeString(), text });
saveState();
loadNotesClient();
}
function deleteNote(noteId) {
if (!confirm('Delete this note?')) return;
state.notes = state.notes.filter(n=>n.id!==noteId);
saveState();
const id = document.getElementById('notesClientSelect').value;
if (id) loadNotesClient();
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// MODAL HELPERS
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function openModal(id) { document.getElementById(id).classList.add('open'); }
function closeModal(id) { document.getElementById(id).classList.remove('open'); }
document.querySelectorAll('.modal-overlay').forEach(m => {
m.addEventListener('click', e => { if (e.target === m) m.classList.remove('open'); });
});
function escHtml(str) {
if (!str) return '';
return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// INIT
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
loadState();
</script>
</body>
</html>
wc -l /home/daytona/workspace/articulation-therapy/index.html && echo "File size: $(du -sh /home/daytona/workspace/articulation-therapy/index.html | cut -f1)"
node -e " const fs = require('fs'); const html = fs.readFileSync('/home/daytona/workspace/articulation-therapy/index.html','utf8'); const checks = [ ['SLP Login page', html.includes('doLogin()')], ['Client Manager', html.includes('renderClients()')], ['Phoneme targeting', html.includes('selectSound(')], ['Word/phrase/sentence hierarchy', html.includes('setLevel(')], ['Progress tracking', html.includes('loadProgressClient()')], ['Session notes', html.includes('addNote(')], ['Visual cue cards', html.includes('cueImage')], ['Audio recording', html.includes('startRecording()')], ['Correct/Incorrect marking', html.includes('markResponse(')], ['SLP badge/name display', html.includes('slpBadge')], ['LocalStorage persistence', html.includes('localStorage')], ['Client add/edit/delete', html.includes('deleteClient()')], ['Position initial/medial/final', html.includes('setPosition(')], ['Session log table', html.includes('Session History')], ['Notes per client', html.includes('loadNotesClient()')], ]; checks.forEach(([label, pass]) => console.log((pass?'โ ':'โ')+' '+label)); console.log('\nAll checks:', checks.every(([,v])=>v) ? 'ALL PASSED' : 'SOME FAILED'); "
index
HTML File ยท HTML
admin | Password: slp1234| Feature | Details |
|---|---|
| SLP Login | Password-protected portal, your name/credentials shown in the nav |
| Client Roster | Add clients with age, diagnosis, and target sounds; edit/delete anytime |
| Therapy Session | Pick client โ select target sound โ set hierarchy level โ set word position |
| Hierarchy Levels | Isolation โ Syllable โ Word โ Phrase โ Sentence โ Conversation |
| Word Positions | Initial / Medial / Final |
| Sounds Covered | 21 phonemes with pre-loaded word lists: /r/, /s/, /l/, /sh/, /th/, /k/, /g/ + 14 more |
| Visual Cue Cards | Emoji cue image + articulation placement hint for each sound |
| Audio Recording | Browser mic recording with playback โ no uploads needed |
| Correct/Incorrect | One-tap tally with live accuracy % and progress bar |
| Progress Tracker | Per-client stats, accuracy by sound, session history table |
| Session Notes | Timestamped text notes per client, auto-note after each session |
localStorage.setItem('articulatepro_creds', JSON.stringify({user:'yourlogin', pass:'yourpassword', name:'Dr. Jane Smith, CCC-SLP'}));
non speech oro motor excersis for speech