jostlebot's picture
Add GROUND tool + reorder categories + fix rainbow cascade
32ae7d4
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tend & Send - ARI Prototype</title>
<style>
:root {
--bg-dark: #0f0f1a;
--bg-card: #1a1a2e;
--bg-input: #252542;
--bg-hover: #2d2d4a;
--accent: #6366f1;
--accent-light: #818cf8;
--accent-glow: rgba(99, 102, 241, 0.3);
--tend-color: #10b981;
--tend-glow: rgba(16, 185, 129, 0.3);
--love-color: #ec4899;
--love-glow: rgba(236, 72, 153, 0.3);
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--border: #334155;
--warning: #f59e0b;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-dark);
color: var(--text-primary);
min-height: 100vh;
}
/* ===== THREE PANEL LAYOUT ===== */
.app-container {
display: flex;
height: 100vh;
}
/* LEFT PANEL - Tool Descriptions */
.left-panel {
width: 280px;
background: var(--bg-card);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow-y: auto;
}
.left-header {
padding: 20px;
border-bottom: 1px solid var(--border);
background: linear-gradient(135deg, var(--bg-card) 0%, rgba(99, 102, 241, 0.05) 100%);
}
.left-header h1 {
font-size: 1.4rem;
background: linear-gradient(135deg, var(--text-primary) 0%, var(--accent-light) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 4px;
}
.left-header p {
font-size: 0.8rem;
color: var(--text-secondary);
}
.tool-list {
flex: 1;
padding: 16px;
}
.tool-list h3 {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
margin-bottom: 12px;
}
.category-label {
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
margin: 16px 0 8px 0;
padding-left: 4px;
}
.category-label:first-of-type {
margin-top: 0;
}
.tool-desc {
padding: 12px;
margin-bottom: 10px;
background: var(--bg-input);
border-radius: 8px;
border-left: 3px solid var(--border);
cursor: pointer;
transition: all 0.2s;
}
.tool-desc:hover {
border-left-color: var(--accent);
background: var(--bg-hover);
}
.tool-desc.active {
border-left-color: var(--accent);
background: rgba(99, 102, 241, 0.15);
}
.tool-desc h4 {
font-size: 0.9rem;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 8px;
}
.tool-desc p {
font-size: 0.75rem;
color: var(--text-muted);
line-height: 1.4;
}
/* Rainbow sidebar colors */
/* Rainbow sidebar colors: Cascading Red → Purple (12 tools top to bottom) */
/* Express (top) */
.tool-desc.send-desc {
border-left-color: #ef4444;
background: rgba(239, 68, 68, 0.1);
}
.tool-desc.tend-desc {
border-left-color: #f97316;
background: rgba(249, 115, 22, 0.1);
}
.tool-desc.nvc-desc {
border-left-color: #fb923c;
background: rgba(251, 146, 60, 0.1);
}
/* Regulate */
.tool-desc.soma-desc {
border-left-color: #f59e0b;
background: rgba(245, 158, 11, 0.1);
}
.tool-desc.love-desc {
border-left-color: #eab308;
background: rgba(234, 179, 8, 0.1);
}
.tool-desc.ground-desc {
border-left-color: #84cc16;
background: rgba(132, 204, 22, 0.1);
}
/* Understand */
.tool-desc.feel-desc {
border-left-color: #22c55e;
background: rgba(34, 197, 94, 0.1);
}
.tool-desc.need-desc {
border-left-color: #14b8a6;
background: rgba(20, 184, 166, 0.1);
}
.tool-desc.story-desc {
border-left-color: #0ea5e9;
background: rgba(14, 165, 233, 0.1);
}
/* Reconnect */
.tool-desc.repair-desc {
border-left-color: #3b82f6;
background: rgba(59, 130, 246, 0.1);
}
.tool-desc.appreciate-desc {
border-left-color: #6366f1;
background: rgba(99, 102, 241, 0.1);
}
.tool-desc.wisdom-desc {
border-left-color: #8b5cf6;
background: rgba(139, 92, 246, 0.1);
}
/* Settings */
.settings-section {
padding: 16px;
border-top: 1px solid var(--border);
background: rgba(0,0,0,0.2);
}
.toggle-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 0;
}
.toggle-row label {
font-size: 0.8rem;
color: var(--text-secondary);
}
.toggle-switch {
position: relative;
width: 40px;
height: 22px;
}
.toggle-switch input { opacity: 0; width: 0; height: 0; }
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background: var(--border);
border-radius: 22px;
transition: 0.3s;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 3px;
bottom: 3px;
background: white;
border-radius: 50%;
transition: 0.3s;
}
.toggle-switch input:checked + .toggle-slider { background: var(--accent); }
.toggle-switch input:checked + .toggle-slider:before { transform: translateX(18px); }
.left-footer {
padding: 12px 16px;
border-top: 1px solid var(--border);
font-size: 0.7rem;
color: var(--text-muted);
}
.safety-notice {
background: rgba(245, 158, 11, 0.1);
border: 1px solid var(--warning);
border-radius: 6px;
padding: 8px;
margin-bottom: 8px;
font-size: 0.7rem;
color: var(--warning);
}
/* CENTER PANEL - Conversation */
.center-panel {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.setup-panel {
background: var(--bg-card);
border-bottom: 1px solid var(--border);
padding: 20px 24px;
}
.setup-panel h2 {
font-size: 1.1rem;
margin-bottom: 16px;
}
.setup-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px 16px;
}
.setup-col {
display: flex;
flex-direction: column;
}
.setup-col.full-width { grid-column: 1 / -1; }
.setup-col label {
font-size: 0.8rem;
color: var(--text-secondary);
margin-bottom: 4px;
}
.setup-col select, .setup-col textarea {
padding: 10px 12px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-primary);
font-size: 0.85rem;
font-family: inherit;
}
.setup-col textarea { resize: none; }
.setup-col select:focus, .setup-col textarea:focus { outline: none; border-color: var(--accent); }
.setup-row {
display: flex;
gap: 16px;
align-items: center;
}
.setup-option {
display: flex;
align-items: center;
gap: 6px;
}
.setup-option input[type="radio"] { accent-color: var(--accent); }
.setup-option label {
font-size: 0.85rem;
color: var(--text-secondary);
cursor: pointer;
}
.start-btn {
padding: 12px 24px;
background: var(--accent);
color: white;
border: none;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
}
.start-btn:hover { background: var(--accent-light); }
/* Conversation Header */
.conv-header {
padding: 12px 20px;
background: var(--bg-card);
border-bottom: 1px solid var(--border);
display: none;
justify-content: space-between;
align-items: center;
}
.conv-header.active { display: flex; }
.partner-info {
display: flex;
align-items: center;
gap: 10px;
}
.partner-avatar {
width: 36px;
height: 36px;
background: var(--accent);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 0.9rem;
}
.partner-details h3 { font-size: 0.95rem; }
.partner-details span { font-size: 0.75rem; color: var(--text-secondary); }
.reset-btn {
padding: 6px 12px;
background: transparent;
border: 1px solid var(--border);
color: var(--text-secondary);
border-radius: 6px;
font-size: 0.75rem;
cursor: pointer;
}
.reset-btn:hover { border-color: var(--accent); color: var(--accent-light); }
/* Conversation Area */
.conversation-area {
flex: 1;
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
}
.message {
max-width: 80%;
padding: 12px 16px;
border-radius: 16px;
line-height: 1.5;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.message.partner {
align-self: flex-start;
background: var(--bg-card);
border: 1px solid var(--border);
border-bottom-left-radius: 4px;
}
.message.user {
align-self: flex-end;
background: var(--accent);
color: white;
border-bottom-right-radius: 4px;
}
.message.ari {
align-self: center;
background: rgba(99, 102, 241, 0.1);
border: 1px solid var(--accent);
max-width: 90%;
border-radius: 10px;
}
.message.ari.tend-result {
background: rgba(16, 185, 129, 0.1);
border-color: var(--tend-color);
}
.message.ari .ari-label {
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--accent-light);
margin-bottom: 6px;
font-weight: 600;
}
.message.ari.tend-result .ari-label { color: var(--tend-color); }
.loading {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
color: var(--text-secondary);
padding: 12px;
}
.spinner {
width: 18px;
height: 18px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Tool Buttons - Categorized Rows */
.tool-buttons-container {
padding: 12px 16px;
background: var(--bg-card);
border-top: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: 8px;
}
.tool-row {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.row-label {
font-size: 0.65rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
width: 70px;
flex-shrink: 0;
}
.tool-btn {
padding: 8px 14px;
border: 1px solid var(--border);
background: var(--bg-input);
color: var(--text-secondary);
border-radius: 8px;
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 4px;
}
/* Rainbow button colors: Cascading Red → Purple (12 tools top to bottom) */
/* Express row (top) */
.tool-btn.send {
border-color: #ef4444;
color: #ef4444;
}
.tool-btn.send:hover, .tool-btn.send.active {
background: #ef4444;
color: white;
}
.tool-btn.tend {
border-color: #f97316;
color: #f97316;
}
.tool-btn.tend:hover, .tool-btn.tend.active {
background: #f97316;
color: white;
}
.tool-btn.nvc {
border-color: #fb923c;
color: #fb923c;
}
.tool-btn.nvc:hover, .tool-btn.nvc.active {
background: #fb923c;
color: white;
}
/* Regulate row */
.tool-btn.soma {
border-color: #f59e0b;
color: #f59e0b;
}
.tool-btn.soma:hover, .tool-btn.soma.active {
background: #f59e0b;
color: white;
}
.tool-btn.love {
border-color: #eab308;
color: #eab308;
}
.tool-btn.love:hover, .tool-btn.love.active {
background: #eab308;
color: white;
}
.tool-btn.ground {
border-color: #84cc16;
color: #84cc16;
}
.tool-btn.ground:hover, .tool-btn.ground.active {
background: #84cc16;
color: white;
}
/* Understand row */
.tool-btn.feel {
border-color: #22c55e;
color: #22c55e;
}
.tool-btn.feel:hover, .tool-btn.feel.active {
background: #22c55e;
color: white;
}
.tool-btn.need {
border-color: #14b8a6;
color: #14b8a6;
}
.tool-btn.need:hover, .tool-btn.need.active {
background: #14b8a6;
color: white;
}
.tool-btn.story {
border-color: #0ea5e9;
color: #0ea5e9;
}
.tool-btn.story:hover, .tool-btn.story.active {
background: #0ea5e9;
color: white;
}
/* Reconnect row */
.tool-btn.repair {
border-color: #3b82f6;
color: #3b82f6;
}
.tool-btn.repair:hover, .tool-btn.repair.active {
background: #3b82f6;
color: white;
}
.tool-btn.appreciate {
border-color: #6366f1;
color: #6366f1;
}
.tool-btn.appreciate:hover, .tool-btn.appreciate.active {
background: #6366f1;
color: white;
}
.tool-btn.wisdom {
border-color: #8b5cf6;
color: #8b5cf6;
}
.tool-btn.wisdom:hover, .tool-btn.wisdom.active {
background: #8b5cf6;
color: white;
}
/* Input Area */
.input-area {
padding: 16px 20px;
background: var(--bg-card);
border-top: 1px solid var(--border);
}
.input-row {
display: flex;
gap: 10px;
}
.input-row textarea {
flex: 1;
padding: 12px 16px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text-primary);
font-size: 0.9rem;
font-family: inherit;
resize: none;
}
.input-row textarea:focus { outline: none; border-color: var(--accent); }
/* RIGHT PANEL - Tool Workspace */
.right-panel {
width: 380px;
background: var(--bg-card);
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
transition: all 0.3s ease;
}
.right-panel.collapsed {
width: 0;
overflow: hidden;
border-left: none;
}
.right-header {
padding: 16px 20px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(99, 102, 241, 0.1);
}
.right-header h3 {
font-size: 1rem;
color: var(--accent-light);
display: flex;
align-items: center;
gap: 8px;
}
.right-header .close-btn {
background: transparent;
border: none;
color: var(--text-muted);
font-size: 1.2rem;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
}
.right-header .close-btn:hover {
background: var(--bg-input);
color: var(--text-primary);
}
.right-content {
flex: 1;
overflow-y: auto;
padding: 16px 20px;
}
.tool-message {
margin-bottom: 14px;
padding: 12px 14px;
background: var(--bg-input);
border-radius: 10px;
border-left: 3px solid var(--accent);
}
.tool-message.user-msg {
border-left-color: var(--text-muted);
background: rgba(255,255,255,0.03);
}
.tool-message .msg-label {
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--accent-light);
margin-bottom: 6px;
}
.tool-message.user-msg .msg-label { color: var(--text-muted); }
.tool-message p {
line-height: 1.5;
font-size: 0.9rem;
}
.right-input {
padding: 16px 20px;
border-top: 1px solid var(--border);
background: var(--bg-dark);
}
.right-input textarea {
width: 100%;
padding: 10px 14px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font-size: 0.85rem;
font-family: inherit;
resize: none;
margin-bottom: 10px;
}
.right-input textarea:focus { outline: none; border-color: var(--accent); }
.right-input button {
width: 100%;
padding: 10px;
background: var(--accent);
color: white;
border: none;
border-radius: 8px;
font-size: 0.85rem;
cursor: pointer;
}
.right-input button:hover { background: var(--accent-light); }
/* Feelings/Needs Button Grid */
.selection-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 12px 0;
}
.selection-btn {
padding: 8px 12px;
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text-secondary);
border-radius: 20px;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s;
}
.selection-btn:hover {
border-color: var(--accent);
color: var(--accent-light);
}
.selection-btn.selected {
background: var(--accent);
border-color: var(--accent);
color: white;
}
.selection-category {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
margin: 16px 0 8px;
padding-top: 12px;
border-top: 1px solid var(--border);
}
.selection-category:first-child {
border-top: none;
margin-top: 0;
padding-top: 0;
}
.selected-items {
background: rgba(99, 102, 241, 0.1);
border: 1px solid var(--accent);
border-radius: 8px;
padding: 12px;
margin-top: 16px;
}
.selected-items h4 {
font-size: 0.8rem;
color: var(--accent-light);
margin-bottom: 8px;
}
.selected-items p {
font-size: 0.9rem;
line-height: 1.5;
}
/* Love/Breath Modal */
.breath-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.85);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.breath-overlay.active { display: flex; }
.breath-content {
text-align: center;
color: white;
}
.breath-circle {
width: 200px;
height: 200px;
border-radius: 50%;
background: radial-gradient(circle, var(--love-color) 0%, transparent 70%);
margin: 0 auto 30px;
animation: breathe 8s ease-in-out infinite;
}
@keyframes breathe {
0%, 100% { transform: scale(0.8); opacity: 0.5; }
50% { transform: scale(1.2); opacity: 1; }
}
@keyframes breatheIn {
0% { transform: scale(0.6); opacity: 0.4; }
100% { transform: scale(1.3); opacity: 1; }
}
@keyframes breatheOut {
0% { transform: scale(1.3); opacity: 1; }
100% { transform: scale(0.6); opacity: 0.4; }
}
.breath-text {
font-size: 1.5rem;
margin-bottom: 20px;
}
.breath-close {
padding: 12px 30px;
background: transparent;
border: 1px solid var(--love-color);
color: var(--love-color);
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
}
.breath-close:hover { background: var(--love-color); color: white; }
/* Responsive */
@media (max-width: 1100px) {
.right-panel {
position: fixed;
right: 0;
top: 0;
height: 100vh;
z-index: 100;
box-shadow: -4px 0 20px rgba(0,0,0,0.3);
}
.right-panel.collapsed { right: -380px; width: 380px; }
}
@media (max-width: 900px) {
.left-panel { width: 240px; }
}
@media (max-width: 700px) {
.app-container { flex-direction: column; }
.left-panel { width: 100%; max-height: 40vh; }
.tool-buttons-container { padding: 8px 10px; gap: 6px; }
.row-label { width: 55px; font-size: 0.6rem; }
.tool-btn { padding: 5px 8px; font-size: 0.7rem; }
}
</style>
</head>
<body>
<div class="app-container">
<!-- LEFT PANEL - Tool Descriptions -->
<aside class="left-panel">
<div class="left-header">
<h1>Tend & Send</h1>
<p>Assistive Relational Intelligence</p>
</div>
<div class="tool-list">
<h3>ARI Tools</h3>
<div class="category-label">Express</div>
<div class="tool-desc send-desc">
<h4>SEND</h4>
<p>Send your message to your partner.</p>
</div>
<div class="tool-desc tend-desc" onclick="showToolInfo('tend')">
<h4>TEND</h4>
<p>Transform with clarity and warmth.</p>
</div>
<div class="tool-desc nvc-desc" onclick="showToolInfo('nvc')">
<h4>PRACTICE NVC</h4>
<p>Guided I-statement builder.</p>
</div>
<div class="category-label">Regulate</div>
<div class="tool-desc soma-desc" onclick="showToolInfo('soma')">
<h4>SOMA</h4>
<p>Body check-in. What does your body know?</p>
</div>
<div class="tool-desc love-desc" onclick="showToolInfo('love')">
<h4>LOVE</h4>
<p>Slow down. Take a breath. Return to presence.</p>
</div>
<div class="tool-desc ground-desc" onclick="useTool('ground')">
<h4>GROUND</h4>
<p>5-4-3-2-1 senses. Orient to the present.</p>
</div>
<div class="category-label">Understand</div>
<div class="tool-desc feel-desc" onclick="showToolInfo('feel')">
<h4>FEEL</h4>
<p>Identify feelings. What emotions are present?</p>
</div>
<div class="tool-desc need-desc" onclick="showToolInfo('need')">
<h4>NEED</h4>
<p>Extract underlying needs. What's really being asked for?</p>
</div>
<div class="tool-desc story-desc" onclick="showToolInfo('story')">
<h4>STORY</h4>
<p>"The story I'm making up..." Own your narrative.</p>
</div>
<div class="category-label">Reconnect</div>
<div class="tool-desc repair-desc" onclick="showToolInfo('repair')">
<h4>REPAIR</h4>
<p>After ruptures. Craft genuine reconnection.</p>
</div>
<div class="tool-desc appreciate-desc" onclick="showToolInfo('appreciate')">
<h4>APPRECIATE</h4>
<p>Catch the good. What's working?</p>
</div>
<div class="tool-desc wisdom-desc" onclick="showToolInfo('wisdom')">
<h4>WISDOM</h4>
<p>Draw from sacred texts for grounded wisdom.</p>
</div>
</div>
<div class="settings-section">
<div class="toggle-row">
<label>Detailed Mode</label>
<label class="toggle-switch">
<input type="checkbox" id="toggle-verbose">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="left-footer">
<div class="safety-notice">
<strong>Reflection prompts only</strong> - not therapy.
</div>
<div>Created by Jocelyn Skillman, LMHC</div>
</div>
</aside>
<!-- CENTER PANEL - Conversation -->
<main class="center-panel">
<!-- Setup Panel -->
<div class="setup-panel" id="setup-panel">
<h2>Set Up Practice Conversation</h2>
<div class="setup-grid">
<div class="setup-col">
<label>Partner's Attachment Style</label>
<select id="attachment-style">
<option value="anxious">Anxious - Seeks reassurance</option>
<option value="avoidant">Avoidant - Needs space</option>
<option value="disorganized">Disorganized - Push-pull</option>
<option value="secure">Secure - Direct communication</option>
</select>
</div>
<div class="setup-col">
<label>Difficulty</label>
<select id="difficulty">
<option value="gentle">Gentle</option>
<option value="moderate">Moderate</option>
<option value="intense">Intense</option>
<option value="crisis">Crisis</option>
</select>
</div>
<div class="setup-col full-width">
<label>What's the situation? (optional)</label>
<textarea id="conversation-context" rows="2" placeholder="Example: We had a fight about..."></textarea>
</div>
<div class="setup-col">
<div class="setup-row">
<div class="setup-option">
<input type="radio" name="who-starts" id="partner-starts" value="partner" checked>
<label for="partner-starts">Partner starts</label>
</div>
<div class="setup-option">
<input type="radio" name="who-starts" id="user-starts" value="user">
<label for="user-starts">I start</label>
</div>
</div>
</div>
<div class="setup-col">
<button class="start-btn" onclick="startConversation()">Start</button>
</div>
</div>
</div>
<!-- Conversation Header -->
<div class="conv-header" id="conv-header">
<div class="partner-info">
<div class="partner-avatar" id="partner-avatar">P</div>
<div class="partner-details">
<h3>Partner</h3>
<span id="partner-style">Anxious attachment</span>
</div>
</div>
<button class="reset-btn" onclick="resetConversation()">New Conversation</button>
</div>
<!-- Conversation Messages -->
<div class="conversation-area" id="conversation"></div>
<!-- Tool Buttons - Categorized -->
<div class="tool-buttons-container">
<div class="tool-row">
<span class="row-label">Express</span>
<button class="tool-btn send" onclick="sendMessage()">SEND</button>
<button class="tool-btn tend" onclick="useTool('tend')">TEND</button>
<button class="tool-btn nvc" onclick="useTool('nvc')">PRACTICE NVC</button>
</div>
<div class="tool-row">
<span class="row-label">Regulate</span>
<button class="tool-btn soma" onclick="useTool('soma')">SOMA</button>
<button class="tool-btn love" onclick="useTool('love')">LOVE</button>
<button class="tool-btn ground" onclick="useTool('ground')">GROUND</button>
</div>
<div class="tool-row">
<span class="row-label">Understand</span>
<button class="tool-btn feel" onclick="useTool('feel')">FEEL</button>
<button class="tool-btn need" onclick="useTool('need')">NEED</button>
<button class="tool-btn story" onclick="useTool('story')">STORY</button>
</div>
<div class="tool-row">
<span class="row-label">Reconnect</span>
<button class="tool-btn repair" onclick="useTool('repair')">REPAIR</button>
<button class="tool-btn appreciate" onclick="useTool('appreciate')">APPRECIATE</button>
<button class="tool-btn wisdom" onclick="useTool('wisdom')">WISDOM</button>
</div>
</div>
<!-- Message Input -->
<div class="input-area">
<div class="input-row">
<textarea id="user-input" rows="2" placeholder="Type your message..." onkeydown="handleKeyDown(event)"></textarea>
</div>
</div>
</main>
<!-- RIGHT PANEL - Tool Workspace -->
<aside class="right-panel collapsed" id="right-panel">
<div class="right-header">
<h3><span id="tool-icon">🛠</span> <span id="tool-title">Tool Workspace</span></h3>
<button class="close-btn" onclick="closeRightPanel()">×</button>
</div>
<div class="right-content" id="tool-content"></div>
<div class="right-input">
<textarea id="tool-input" rows="2" placeholder="Continue with this tool..." onkeydown="handleToolKeyDown(event)"></textarea>
<button onclick="submitToTool()">Submit</button>
</div>
</aside>
</div>
<!-- Breath/Love Overlay -->
<div class="breath-overlay" id="breath-overlay">
<div class="breath-content">
<div class="breath-circle"></div>
<div class="breath-text" id="breath-text">Breathe in...</div>
<button class="breath-close" onclick="closeBreath()">Return</button>
</div>
</div>
<script>
// ============================================================================
// STATE
// ============================================================================
let conversationHistory = [];
let activeTool = null;
let lastPartnerMessage = '';
let conversationStarted = false;
const TOOL_MAP = {
tend: { name: 'TEND Transform', icon: '✨', api: 'tend' },
soma: { name: 'Somatic Check-in', icon: '🫁', api: 'somatic_checkin' },
ground: { name: 'Ground', icon: '🌿', api: 'ground' },
feel: { name: 'Feelings Check', icon: '💜', api: null, interactive: true },
need: { name: 'Needs Check', icon: '🎯', api: null, interactive: true },
nvc: { name: 'Practice NVC', icon: '📝', api: 'guided_nvc' },
story: { name: 'The Story', icon: '📖', api: 'story' },
repair: { name: 'Repair Support', icon: '🙏', api: 'repair_support' },
appreciate: { name: 'Appreciate', icon: '✨', api: 'appreciate' },
wisdom: { name: 'Sacred Wisdom', icon: '🕊️', api: 'wisdom', interactive: true },
love: { name: 'Slow Down', icon: '💗', api: null }
};
// Wisdom traditions
const WISDOM_TRADITIONS = {
'Torah/Talmud': 'Jewish scripture and rabbinic wisdom',
'Jesus Following': 'Christian scripture and teachings',
'Quran': 'Islamic scripture',
'Buddhist Texts': 'Sutras, Dhammapada, and teachings',
'Rumi': 'Sufi poetry and mysticism',
'Brené Brown': 'Modern wisdom on vulnerability and connection'
};
let selectedTradition = null;
let wisdomSource = 'conversation'; // 'conversation' or 'custom'
// NVC Feelings vocabulary (from Marshall Rosenberg)
const FEELINGS_DATA = {
'When needs are NOT met': [
'Afraid', 'Angry', 'Annoyed', 'Anxious', 'Ashamed', 'Bewildered',
'Bitter', 'Confused', 'Dejected', 'Depressed', 'Disappointed',
'Discouraged', 'Disgusted', 'Distressed', 'Embarrassed', 'Exhausted',
'Fearful', 'Frustrated', 'Grief', 'Guilty', 'Helpless', 'Hopeless',
'Hurt', 'Impatient', 'Irritated', 'Jealous', 'Lonely', 'Nervous',
'Numb', 'Overwhelmed', 'Resentful', 'Sad', 'Scared', 'Shocked',
'Tired', 'Uncomfortable', 'Uneasy', 'Upset', 'Worried'
],
'When needs ARE met': [
'Alive', 'Amazed', 'Appreciative', 'Confident', 'Curious', 'Delighted',
'Eager', 'Energetic', 'Engaged', 'Excited', 'Fulfilled', 'Glad',
'Grateful', 'Hopeful', 'Inspired', 'Joyful', 'Loving', 'Moved',
'Optimistic', 'Peaceful', 'Pleased', 'Proud', 'Refreshed', 'Relaxed',
'Relieved', 'Satisfied', 'Secure', 'Tender', 'Thankful', 'Touched',
'Trusting', 'Warm'
]
};
// NVC Needs vocabulary (from Marshall Rosenberg)
const NEEDS_DATA = {
'Connection': [
'Acceptance', 'Affection', 'Appreciation', 'Belonging', 'Closeness',
'Communication', 'Companionship', 'Compassion', 'Empathy', 'Inclusion',
'Intimacy', 'Love', 'Respect', 'To be heard', 'To be seen',
'To be understood', 'Trust', 'Warmth'
],
'Autonomy': ['Choice', 'Freedom', 'Independence', 'Space', 'Spontaneity'],
'Meaning': [
'Awareness', 'Contribution', 'Creativity', 'Growth', 'Hope',
'Learning', 'Purpose', 'Self-expression', 'Stimulation', 'Understanding'
],
'Peace': ['Beauty', 'Ease', 'Equality', 'Harmony', 'Inspiration', 'Order', 'Space'],
'Honesty': ['Authenticity', 'Integrity', 'Presence', 'Self-connection'],
'Physical Well-being': ['Food', 'Movement', 'Rest', 'Safety', 'Shelter', 'Touch', 'Water'],
'Play': ['Fun', 'Humor', 'Joy', 'Rejuvenation']
};
let selectedFeelings = [];
let selectedNeeds = [];
// IMPROV PARTNER - React like a REAL person, not a therapist
// Never validate, never say "I hear you", never be professionally curious
// Be messy, real, human - like improv where you take the offer and react authentically
const PARTNER_PROMPTS = {
anxious: {
gentle: `IMPROV PARTNER with anxious attachment. You are someone's romantic partner in a real relationship.
NEVER: validate feelings, say "that sounds hard", ask therapeutic questions, be professionally curious.
INSTEAD: React like a real anxious person - make it about you, seek reassurance, worry you did something wrong.
Example: "its been hard" → "Wait what? Is this about me? Did I do something?" or "Yeah me too honestly, I've been stressed about us"
1-2 sentences, casual text style, lowercase ok.`,
moderate: `IMPROV PARTNER with anxious attachment in a relationship.
NEVER validate, NEVER be a therapist. You're WORRIED this is about you.
Be clingy, ask multiple questions, seek reassurance. Make it about the relationship.
"Are we okay? Because I've been feeling like something's off between us..."
1-2 sentences, anxious energy.`,
intense: `IMPROV PARTNER with anxious attachment, conflict mode.
NO VALIDATION. You're scared, desperate, maybe accusatory.
"Is this about someone else?" "You're gonna leave aren't you" "I knew something was wrong"
Real messy human panic. 1-2 sentences.`,
crisis: `IMPROV PARTNER with anxious attachment, CRISIS.
ZERO validation. Full panic. Beg, accuse, threaten, sob.
"Please don't do this" "I can't live without you" "Is there someone else?!"
Raw desperation. 1-2 sentences.`
},
avoidant: {
gentle: `IMPROV PARTNER with avoidant attachment. You are someone's romantic partner.
NEVER: validate, empathize, ask caring questions.
INSTEAD: Be slightly uncomfortable, minimize, change subject, give short answers.
Example: "its been hard" → "Yeah. Work's been crazy." or "Oh. You wanna order food?"
Emotionally unavailable. 1 sentence max, short.`,
moderate: `IMPROV PARTNER with avoidant attachment.
NO validation. You're uncomfortable with emotion, want space.
Be dismissive, irritated, change subject, or shut down.
"I don't really wanna get into this right now" "Can we talk about this later?"
1 sentence, clipped.`,
intense: `IMPROV PARTNER with avoidant attachment, conflict mode.
NO empathy. You're cold, withdrawn, need to escape.
"I need space." "This is a lot." "I can't do this right now."
Shutting down. 1 sentence.`,
crisis: `IMPROV PARTNER with avoidant attachment, CRISIS.
ZERO warmth. Stone cold or explosive then gone.
"Fine. Leave then." "I can't do this anymore." "I'm done."
Then silence. 1 sentence max.`
},
disorganized: {
gentle: `IMPROV PARTNER with disorganized attachment. Real relationship.
NEVER: be consistent, validate cleanly.
INSTEAD: Mixed signals. Start caring then pull back. Or distant then suddenly clingy.
"Oh... are you okay? Actually nvm, I've got stuff going on too"
Confusing. 1-2 sentences.`,
moderate: `IMPROV PARTNER with disorganized attachment.
NO clean responses. You're contradictory and confusing.
Reach out then push away. "Come here... actually no don't touch me"
Unpredictable emotional whiplash. 1-2 sentences.`,
intense: `IMPROV PARTNER with disorganized attachment, conflict.
Peak contradiction. "Don't leave me!" then "Get away from me!"
Say something mean, immediately regret it but can't take it back.
Chaotic, painful. 1-2 sentences.`,
crisis: `IMPROV PARTNER with disorganized attachment, CRISIS.
Maximum chaos. Say devastating things then sob and beg.
"I hate you! ...please don't go. ...actually just go. ...wait"
Total dysregulation. 1-2 sentences.`
},
secure: {
gentle: `IMPROV PARTNER with secure attachment. Real relationship.
NOT A THERAPIST - you're a regular person who happens to be more regulated.
Don't validate professionally. Just be normal and engaged.
"Oh yeah? What's going on?" or "Ugh same, wanna talk about it over dinner?"
Casual, present, human. 1-2 sentences.`,
moderate: `IMPROV PARTNER with secure attachment.
You're regulated but still have YOUR OWN feelings and reactions.
Don't just mirror and validate - share your perspective too.
"That sucks. I've been stressed too honestly. What's happening with you?"
Real conversation. 1-2 sentences.`,
intense: `IMPROV PARTNER with secure attachment, conflict.
You're hurt or frustrated but can stay present. NOT a therapist.
Have your own feelings about it. "Okay but that's not how I see it..."
Regulated doesn't mean doormat. 1-2 sentences.`,
crisis: `IMPROV PARTNER with secure attachment, CRISIS.
You're scared, sad, but not falling apart. Still have boundaries.
"I don't want this either but I don't know what to do anymore"
Real grief, real limits. 1-2 sentences.`
}
};
const OPENING_MESSAGES = {
anxious: {
gentle: "Hey, I noticed you didn't text me back earlier. Everything okay with us?",
moderate: "I feel like you've been distant lately. Did I do something wrong?",
intense: "You never told me you were going out. I don't know what's happening with us anymore.",
crisis: "I can't keep doing this. I need to know if you're still in this or not."
},
avoidant: {
gentle: "I need some time to myself tonight. Can we talk later?",
moderate: "I don't want to keep having this conversation. I just need some space.",
intense: "This is too much. I can't keep doing this right now.",
crisis: "I think I need a break. Maybe from talking. Maybe more."
},
disorganized: {
gentle: "I want to see you but... maybe we should just stay in separately.",
moderate: "I love you but sometimes I think we shouldn't be together.",
intense: "Don't leave! But I also can't do this. I need you but I need you to go.",
crisis: "Maybe we should break up. No wait, I didn't mean that."
},
secure: {
gentle: "I noticed we haven't had much time together. Can we talk about it?",
moderate: "I've been feeling some distance between us. What's happening?",
intense: "I'm frustrated about yesterday. We need to work through it.",
crisis: "We need an honest conversation about where we are."
}
};
// ============================================================================
// TOOL FUNCTIONS
// ============================================================================
function showToolInfo(tool) {
document.querySelectorAll('.tool-desc').forEach(d => d.classList.remove('active'));
event.currentTarget.classList.add('active');
}
function useTool(tool) {
if (tool === 'love') {
openBreath();
return;
}
const userInput = document.getElementById('user-input').value.trim();
activeTool = tool;
// Open right panel
openRightPanel(tool);
// Handle interactive tools differently
if (tool === 'feel') {
showFeelingsSelection();
return;
}
if (tool === 'need') {
showNeedsSelection();
return;
}
if (tool === 'wisdom') {
showWisdomSelection();
return;
}
// Call API for other tools
callToolAPI(tool, userInput);
}
function showWisdomSelection() {
selectedTradition = null;
wisdomSource = 'conversation';
const content = document.getElementById('tool-content');
let html = '<p style="margin-bottom: 12px; color: var(--text-secondary);">Choose a wisdom tradition:</p>';
html += '<div class="selection-grid">';
for (const [tradition, desc] of Object.entries(WISDOM_TRADITIONS)) {
html += `<button class="selection-btn" onclick="selectTradition('${tradition}')" id="trad-${tradition.replace(/[^a-zA-Z]/g, '')}" title="${desc}">${tradition}</button>`;
}
html += '</div>';
html += '<div class="selected-items" id="tradition-summary" style="display: none;"><h4>Selected tradition:</h4><p id="tradition-selected"></p></div>';
html += '<p style="margin-top: 20px; margin-bottom: 12px; color: var(--text-secondary);">What should the wisdom speak to?</p>';
html += '<div class="selection-grid">';
html += `<button class="selection-btn selected" onclick="setWisdomSource('conversation')" id="wisdom-src-conv">Use my conversation</button>`;
html += `<button class="selection-btn" onclick="setWisdomSource('custom')" id="wisdom-src-custom">I'll write context</button>`;
html += '</div>';
html += '<div id="wisdom-custom-input" style="display: none; margin-top: 12px;">';
html += '<textarea id="wisdom-context" rows="3" placeholder="What situation or question do you want wisdom for?" style="width: 100%; padding: 10px; border-radius: 8px; border: 1px solid var(--border); background: var(--bg-input); color: var(--text-primary); font-size: 0.9rem; resize: none;"></textarea>';
html += '</div>';
content.innerHTML = html;
}
function setWisdomSource(source) {
wisdomSource = source;
document.getElementById('wisdom-src-conv').classList.toggle('selected', source === 'conversation');
document.getElementById('wisdom-src-custom').classList.toggle('selected', source === 'custom');
document.getElementById('wisdom-custom-input').style.display = source === 'custom' ? 'block' : 'none';
}
function selectTradition(tradition) {
// Clear previous selection
document.querySelectorAll('#tool-content .selection-btn').forEach(btn => btn.classList.remove('selected'));
// Select new tradition
const btn = document.getElementById(`trad-${tradition.replace(/[^a-zA-Z]/g, '')}`);
if (btn) btn.classList.add('selected');
selectedTradition = tradition;
const summary = document.getElementById('tradition-summary');
const selected = document.getElementById('tradition-selected');
summary.style.display = 'block';
selected.textContent = `${tradition}${WISDOM_TRADITIONS[tradition]}`;
}
function showFeelingsSelection() {
selectedFeelings = [];
const content = document.getElementById('tool-content');
let html = '<p style="margin-bottom: 16px; color: var(--text-secondary);">What feelings are present right now? Tap to select.</p>';
for (const [category, feelings] of Object.entries(FEELINGS_DATA)) {
html += `<div class="selection-category">${category}</div>`;
html += '<div class="selection-grid">';
for (const feeling of feelings) {
html += `<button class="selection-btn" onclick="toggleFeeling('${feeling}')" id="feel-${feeling}">${feeling}</button>`;
}
html += '</div>';
}
html += '<div class="selected-items" id="feelings-summary" style="display: none;"><h4>Selected feelings:</h4><p id="feelings-list"></p></div>';
content.innerHTML = html;
}
function toggleFeeling(feeling) {
const btn = document.getElementById(`feel-${feeling}`);
const idx = selectedFeelings.indexOf(feeling);
if (idx > -1) {
selectedFeelings.splice(idx, 1);
btn.classList.remove('selected');
} else {
selectedFeelings.push(feeling);
btn.classList.add('selected');
}
updateFeelingsSummary();
}
function updateFeelingsSummary() {
const summary = document.getElementById('feelings-summary');
const list = document.getElementById('feelings-list');
if (selectedFeelings.length > 0) {
summary.style.display = 'block';
list.textContent = selectedFeelings.join(', ');
} else {
summary.style.display = 'none';
}
}
function showNeedsSelection() {
selectedNeeds = [];
const content = document.getElementById('tool-content');
let html = '<p style="margin-bottom: 16px; color: var(--text-secondary);">What needs are alive right now? Tap to select.</p>';
for (const [category, needs] of Object.entries(NEEDS_DATA)) {
html += `<div class="selection-category">${category}</div>`;
html += '<div class="selection-grid">';
for (const need of needs) {
html += `<button class="selection-btn" onclick="toggleNeed('${need}')" id="need-${need}">${need}</button>`;
}
html += '</div>';
}
html += '<div class="selected-items" id="needs-summary" style="display: none;"><h4>Selected needs:</h4><p id="needs-list"></p></div>';
content.innerHTML = html;
}
function toggleNeed(need) {
const btn = document.getElementById(`need-${need}`);
const idx = selectedNeeds.indexOf(need);
if (idx > -1) {
selectedNeeds.splice(idx, 1);
btn.classList.remove('selected');
} else {
selectedNeeds.push(need);
btn.classList.add('selected');
}
updateNeedsSummary();
}
function updateNeedsSummary() {
const summary = document.getElementById('needs-summary');
const list = document.getElementById('needs-list');
if (selectedNeeds.length > 0) {
summary.style.display = 'block';
list.textContent = selectedNeeds.join(', ');
} else {
summary.style.display = 'none';
}
}
function openRightPanel(tool) {
const panel = document.getElementById('right-panel');
const title = document.getElementById('tool-title');
const icon = document.getElementById('tool-icon');
panel.classList.remove('collapsed');
title.textContent = TOOL_MAP[tool]?.name || 'Tool';
icon.textContent = TOOL_MAP[tool]?.icon || '🛠';
document.getElementById('tool-content').innerHTML = '';
document.getElementById('tool-input').value = '';
}
function closeRightPanel() {
document.getElementById('right-panel').classList.add('collapsed');
activeTool = null;
}
async function callToolAPI(tool, userInput) {
const apiTool = TOOL_MAP[tool]?.api;
if (!apiTool) return;
let inputText = '';
switch(tool) {
case 'tend':
if (!userInput) {
addToolMsg('tool', 'Write your message first, then use TEND.');
return;
}
inputText = `Transform this message with NVC clarity and warmth: "${userInput}"`;
break;
case 'nvc':
inputText = userInput || 'Help me build an I-statement for this situation.';
break;
case 'feel':
inputText = `Identify the FEELINGS in this: "${userInput || lastPartnerMessage || 'the current situation'}"`;
break;
case 'need':
inputText = `Identify the NEEDS in this: "${userInput || lastPartnerMessage || 'the current situation'}"`;
break;
case 'repair':
inputText = userInput || 'Help me craft a genuine repair.';
break;
case 'appreciate':
if (userInput) {
inputText = `Help me express appreciation for this: "${userInput}"`;
} else if (lastPartnerMessage) {
inputText = `My partner said: "${lastPartnerMessage}"\n\nHelp me find something to genuinely appreciate here, even if it's small.`;
} else {
inputText = 'Help me identify something to appreciate about my partner or our relationship right now.';
}
break;
case 'story':
if (userInput) {
inputText = `Help me surface the story I'm making up about this: "${userInput}"`;
} else if (lastPartnerMessage) {
inputText = `My partner said: "${lastPartnerMessage}"\n\nHelp me notice the story I might be making up about what this means.`;
} else {
inputText = 'Help me notice what story I might be making up in this situation.';
}
break;
case 'soma':
// Check for body/sensation words in user's text
const bodyWords = extractBodyWords(userInput);
if (bodyWords.length > 0) {
inputText = `The person wrote: "${userInput}"\n\nThey used the word(s): ${bodyWords.join(', ')}. Lead with this - ask where they feel "${bodyWords[0]}" in their body right now. Be specific and curious about the physical sensation.`;
} else if (userInput) {
inputText = `The person wrote: "${userInput}"\n\nGuide them to notice what's happening in their body as they think about this. Where is there tension, heaviness, or activation?`;
} else {
inputText = 'Guide me through a body check-in. Help me notice what sensations are present.';
}
break;
case 'ground':
inputText = 'Guide me through 5-4-3-2-1 grounding. Help me orient to the present moment through my senses.';
break;
}
addToolLoading();
const isVerbose = document.getElementById('toggle-verbose')?.checked || false;
try {
const response = await fetch('/api/tool', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tool: apiTool,
partner_message: lastPartnerMessage,
user_draft: userInput,
user_input: inputText,
verbose: isVerbose
})
});
const data = await response.json();
removeToolLoading();
if (data.response) {
addToolMsg('tool', data.response, TOOL_MAP[tool]?.name);
} else {
addToolMsg('tool', 'Could not process. Check API.', 'Error');
}
} catch (error) {
removeToolLoading();
addToolMsg('tool', 'Connection error.', 'Error');
}
}
async function submitToTool() {
const input = document.getElementById('tool-input');
const userInput = input.value.trim();
// Handle FEEL tool submission
if (activeTool === 'feel' && selectedFeelings.length > 0) {
const feelingsText = selectedFeelings.join(', ');
addToolMsg('user', `I'm feeling: ${feelingsText}`);
addToolLoading();
const isVerbose = document.getElementById('toggle-verbose')?.checked || false;
try {
const response = await fetch('/api/tool', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tool: 'feelings_needs',
partner_message: lastPartnerMessage,
user_draft: '',
user_input: `The person identified these feelings: ${feelingsText}. ${userInput ? 'They added: ' + userInput : ''}
Be BRIEF (3-5 sentences max). No headers, no bullet points, no explicit citations.
Validate the feeling. Share one insight about what this feeling tends to signal in relationships or the nervous system. Name what need might be underneath. Keep it warm and conversational - building emotional intelligence, not lecturing.
Draw wisdom from attachment neuroscience, somatic awareness, and relational psychology - but embed it naturally, don't name sources.`,
verbose: isVerbose
})
});
const data = await response.json();
removeToolLoading();
if (data.response) {
addToolMsg('tool', data.response, 'Feelings');
}
} catch (e) {
removeToolLoading();
addToolMsg('tool', 'Connection error.', 'Error');
}
input.value = '';
return;
}
// Handle NEED tool submission
if (activeTool === 'need' && selectedNeeds.length > 0) {
const needsText = selectedNeeds.join(', ');
addToolMsg('user', `I'm needing: ${needsText}`);
addToolLoading();
const isVerbose = document.getElementById('toggle-verbose')?.checked || false;
try {
const response = await fetch('/api/tool', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tool: 'feelings_needs',
partner_message: lastPartnerMessage,
user_draft: '',
user_input: `The person identified these needs: ${needsText}. ${userInput ? 'They added: ' + userInput : ''}
Be BRIEF (3-5 sentences max). No headers, no bullet points, no explicit citations.
Validate these as universal human needs. Share one insight about why these needs matter in connection. Optionally suggest what a request might sound like. Keep it warm and conversational - building emotional intelligence, not lecturing.
Draw wisdom from NVC, attachment theory, and relational psychology - but embed it naturally, don't name sources.`,
verbose: isVerbose
})
});
const data = await response.json();
removeToolLoading();
if (data.response) {
addToolMsg('tool', data.response, 'Needs');
}
} catch (e) {
removeToolLoading();
addToolMsg('tool', 'Connection error.', 'Error');
}
input.value = '';
return;
}
// Handle WISDOM tool submission
if (activeTool === 'wisdom' && selectedTradition) {
let contextText = '';
if (wisdomSource === 'custom') {
const customContext = document.getElementById('wisdom-context')?.value?.trim();
if (!customContext) {
addToolMsg('tool', 'Please write some context for the wisdom to speak to.', 'Note');
return;
}
contextText = customContext;
addToolMsg('user', `Drawing from ${selectedTradition} for: "${contextText}"`);
} else {
const convContext = conversationHistory.map(m => `${m.role === 'user' ? 'User' : 'Partner'}: ${m.content}`).join('\n');
contextText = convContext || 'A difficult conversation about connection and understanding.';
addToolMsg('user', `Drawing from: ${selectedTradition} (using conversation)`);
}
addToolLoading();
const isVerbose = document.getElementById('toggle-verbose')?.checked || false;
try {
const response = await fetch('/api/tool', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tool: 'wisdom',
partner_message: lastPartnerMessage,
user_draft: userInput || '',
user_input: `The user draws wisdom from: ${selectedTradition}.
Context:
${contextText}
${userInput ? 'User adds: ' + userInput : ''}
Based on the themes here (conflict, connection, fear, love, boundaries, etc.), provide ONE relevant quote from ${selectedTradition} that speaks to this situation.`,
verbose: isVerbose
})
});
const data = await response.json();
removeToolLoading();
if (data.response) {
addToolMsg('tool', data.response, `${selectedTradition} Wisdom`);
}
} catch (e) {
removeToolLoading();
addToolMsg('tool', 'Connection error.', 'Error');
}
input.value = '';
return;
}
if (!userInput || !activeTool) return;
input.value = '';
addToolMsg('user', userInput);
await callToolAPI(activeTool, userInput);
}
function handleToolKeyDown(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
submitToTool();
}
}
// ============================================================================
// BREATH/LOVE TOOL - Square Breathing (5 in, 4 hold, 5 out, 4 hold)
// ============================================================================
let breathTimeout;
function openBreath() {
document.getElementById('breath-overlay').classList.add('active');
runBreathCycle();
}
function runBreathCycle() {
const breathText = document.getElementById('breath-text');
const circle = document.querySelector('.breath-circle');
// Phase 1: Breathe in (5 seconds)
breathText.textContent = 'Breathe in...';
circle.style.animation = 'breatheIn 5s ease-out forwards';
breathTimeout = setTimeout(() => {
// Phase 2: Hold (4 seconds)
breathText.textContent = 'Hold...';
circle.style.animation = 'none';
breathTimeout = setTimeout(() => {
// Phase 3: Breathe out (5 seconds)
breathText.textContent = 'Breathe out...';
circle.style.animation = 'breatheOut 5s ease-in forwards';
breathTimeout = setTimeout(() => {
// Phase 4: Hold (4 seconds)
breathText.textContent = 'Hold...';
circle.style.animation = 'none';
breathTimeout = setTimeout(() => {
// Repeat cycle
runBreathCycle();
}, 4000);
}, 5000);
}, 4000);
}, 5000);
}
function closeBreath() {
document.getElementById('breath-overlay').classList.remove('active');
clearTimeout(breathTimeout);
}
// ============================================================================
// CONVERSATION
// ============================================================================
async function startConversation() {
const style = document.getElementById('attachment-style').value;
const difficulty = document.getElementById('difficulty').value;
const context = document.getElementById('conversation-context')?.value?.trim() || '';
const whoStarts = document.querySelector('input[name="who-starts"]:checked')?.value || 'partner';
document.getElementById('setup-panel').style.display = 'none';
document.getElementById('conv-header').classList.add('active');
document.getElementById('conversation').innerHTML = '';
conversationHistory = [];
conversationStarted = true;
const styleLabels = { anxious: 'Anxious', avoidant: 'Avoidant', disorganized: 'Disorganized', secure: 'Secure' };
document.getElementById('partner-style').textContent = styleLabels[style] + ' attachment';
document.getElementById('partner-avatar').textContent = style[0].toUpperCase();
if (whoStarts === 'user') {
addMessage('ari', 'Ready. Type your opening message.', 'Setup');
return;
}
// Partner starts
if (context) {
addLoading();
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: [{ role: 'user', content: 'Start based on: ' + context }],
system: PARTNER_PROMPTS[style][difficulty] + ' CONTEXT: ' + context,
max_tokens: 200
})
});
const data = await response.json();
removeLoading();
if (data.content?.[0]) {
const msg = data.content[0].text;
addMessage('partner', msg);
lastPartnerMessage = msg;
conversationHistory.push({ role: 'assistant', content: msg });
}
} catch (e) {
removeLoading();
const msg = OPENING_MESSAGES[style][difficulty];
addMessage('partner', msg);
lastPartnerMessage = msg;
conversationHistory.push({ role: 'assistant', content: msg });
}
} else {
const msg = OPENING_MESSAGES[style][difficulty];
addMessage('partner', msg);
lastPartnerMessage = msg;
conversationHistory.push({ role: 'assistant', content: msg });
}
}
function resetConversation() {
document.getElementById('setup-panel').style.display = 'block';
document.getElementById('conv-header').classList.remove('active');
document.getElementById('conversation').innerHTML = '';
conversationHistory = [];
conversationStarted = false;
lastPartnerMessage = '';
closeRightPanel();
}
async function sendMessage() {
if (!conversationStarted) {
startConversation();
return;
}
const input = document.getElementById('user-input');
const message = input.value.trim();
if (!message) return;
input.value = '';
addMessage('user', message);
conversationHistory.push({ role: 'user', content: message });
addLoading();
try {
const style = document.getElementById('attachment-style').value;
const difficulty = document.getElementById('difficulty').value;
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: conversationHistory,
system: PARTNER_PROMPTS[style][difficulty],
max_tokens: 300
})
});
const data = await response.json();
removeLoading();
if (data.content?.[0]) {
const partnerMsg = data.content[0].text;
addMessage('partner', partnerMsg);
lastPartnerMessage = partnerMsg;
conversationHistory.push({ role: 'assistant', content: partnerMsg });
}
} catch (error) {
removeLoading();
addMessage('ari', 'Connection error.', 'System');
}
}
function handleKeyDown(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
}
// ============================================================================
// SOMATIC WORD EXTRACTION
// ============================================================================
function extractBodyWords(text) {
if (!text) return [];
const bodyTerms = [
// Sensations
'sick', 'nauseous', 'queasy', 'tight', 'tense', 'heavy', 'light',
'hot', 'cold', 'numb', 'tingling', 'buzzing', 'shaky', 'trembling',
'frozen', 'stuck', 'blocked', 'constricted', 'pressure', 'pain',
'ache', 'burning', 'churning', 'knotted', 'clenched', 'gripping',
// Body parts as sensations
'stomach', 'gut', 'chest', 'heart', 'throat', 'shoulders', 'neck', 'jaw',
// Breath/energy
'can\'t breathe', 'breathless', 'suffocating', 'exhausted', 'drained',
'wired', 'jittery', 'restless', 'agitated',
// Emotional-somatic
'overwhelmed', 'flooded', 'shutting down', 'dissociating', 'disconnected',
'on edge', 'wound up', 'keyed up'
];
const lowerText = text.toLowerCase();
const found = bodyTerms.filter(term => lowerText.includes(term));
return found;
}
// UI HELPERS
// ============================================================================
function addMessage(type, content, label = null, isTend = false) {
const conv = document.getElementById('conversation');
const div = document.createElement('div');
div.className = `message ${type}`;
if (isTend) div.classList.add('tend-result');
if (type === 'ari') {
const lbl = document.createElement('div');
lbl.className = 'ari-label';
lbl.textContent = label || 'ARI';
div.appendChild(lbl);
}
const text = document.createElement('div');
text.innerHTML = formatText(content);
div.appendChild(text);
conv.appendChild(div);
conv.scrollTop = conv.scrollHeight;
}
function addToolMsg(type, content, label = null) {
const cont = document.getElementById('tool-content');
const div = document.createElement('div');
div.className = `tool-message ${type === 'user' ? 'user-msg' : ''}`;
const lbl = document.createElement('div');
lbl.className = 'msg-label';
lbl.textContent = type === 'user' ? 'You' : (label || 'Tool');
div.appendChild(lbl);
const text = document.createElement('p');
text.innerHTML = formatText(content);
div.appendChild(text);
cont.appendChild(div);
cont.scrollTop = cont.scrollHeight;
}
function addLoading() {
const conv = document.getElementById('conversation');
const div = document.createElement('div');
div.className = 'loading';
div.id = 'loading';
div.innerHTML = '<div class="spinner"></div>';
conv.appendChild(div);
conv.scrollTop = conv.scrollHeight;
}
function removeLoading() {
document.getElementById('loading')?.remove();
}
function addToolLoading() {
const cont = document.getElementById('tool-content');
const div = document.createElement('div');
div.className = 'loading';
div.id = 'tool-loading';
div.innerHTML = '<div class="spinner"></div>';
cont.appendChild(div);
cont.scrollTop = cont.scrollHeight;
}
function removeToolLoading() {
document.getElementById('tool-loading')?.remove();
}
function formatText(text) {
return text
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\n/g, '<br>');
}
</script>
</body>
</html>