jostlebot's picture
Reorder tool buttons
d0a6e1f
raw
history blame
66.2 kB
<!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;
}
.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;
}
.tool-desc.tend-desc {
border-left-color: var(--tend-color);
background: rgba(16, 185, 129, 0.1);
}
.tool-desc.love-desc {
border-left-color: var(--love-color);
background: rgba(236, 72, 153, 0.1);
}
.tool-desc.wisdom-desc {
border-left-color: #f59e0b;
background: rgba(245, 158, 11, 0.1);
}
.tool-btn.wisdom {
border-color: #f59e0b;
color: #f59e0b;
}
.tool-btn.wisdom:hover, .tool-btn.wisdom.active {
background: #f59e0b;
color: white;
}
/* 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 Row */
.tool-buttons {
display: flex;
gap: 6px;
padding: 12px 20px;
background: var(--bg-card);
border-top: 1px solid var(--border);
flex-wrap: wrap;
justify-content: center;
}
.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;
}
.tool-btn:hover {
border-color: var(--accent);
color: var(--accent-light);
background: rgba(99, 102, 241, 0.1);
}
.tool-btn.active {
border-color: var(--accent);
background: var(--accent);
color: white;
}
.tool-btn.tend {
border-color: var(--tend-color);
color: var(--tend-color);
}
.tool-btn.tend:hover, .tool-btn.tend.active {
background: var(--tend-color);
color: white;
}
.tool-btn.send {
background: var(--accent);
border-color: var(--accent);
color: white;
}
.tool-btn.send:hover {
background: var(--accent-light);
}
.tool-btn.love {
border-color: var(--love-color);
color: var(--love-color);
}
.tool-btn.love:hover, .tool-btn.love.active {
background: var(--love-color);
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 { padding: 10px; }
.tool-btn { padding: 6px 10px; font-size: 0.75rem; }
}
</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="tool-desc tend-desc" onclick="showToolInfo('tend')">
<h4>TEND</h4>
<p>Transform your message with NVC clarity and warmth. Preserves your authentic voice.</p>
</div>
<div class="tool-desc" onclick="showToolInfo('nvc')">
<h4>NVC</h4>
<p>Guided I-statement builder. Step-by-step help crafting what you want to say.</p>
</div>
<div class="tool-desc" onclick="showToolInfo('feel')">
<h4>FEEL</h4>
<p>Identify feelings in the conversation. What emotions are present?</p>
</div>
<div class="tool-desc" onclick="showToolInfo('need')">
<h4>NEED</h4>
<p>Extract underlying needs. What's really being asked for?</p>
</div>
<div class="tool-desc" onclick="showToolInfo('repair')">
<h4>REPAIR</h4>
<p>After ruptures. Help craft genuine reconnection.</p>
</div>
<div class="tool-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 wisdom-desc" onclick="showToolInfo('wisdom')">
<h4>WISDOM</h4>
<p>Draw from sacred texts. Choose your tradition 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 Row -->
<div class="tool-buttons">
<button class="tool-btn send" onclick="sendMessage()">SEND</button>
<button class="tool-btn tend" onclick="useTool('tend')">TEND</button>
<button class="tool-btn" onclick="useTool('soma')">SOMA</button>
<button class="tool-btn" onclick="useTool('feel')">FEEL</button>
<button class="tool-btn" onclick="useTool('need')">NEED</button>
<button class="tool-btn" onclick="useTool('nvc')">PRACTICE NVC</button>
<button class="tool-btn" onclick="useTool('repair')">REPAIR</button>
<button class="tool-btn wisdom" onclick="useTool('wisdom')">WISDOM</button>
<button class="tool-btn love" onclick="useTool('love')">LOVE</button>
</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' },
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' },
repair: { name: 'Repair Support', icon: '🙏', api: 'repair_support' },
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;
// NVC Feelings vocabulary (from Marshall Rosenberg)
const FEELINGS_DATA = {
'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'
],
'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'
]
};
// 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;
const content = document.getElementById('tool-content');
let html = '<p style="margin-bottom: 16px; color: var(--text-secondary);">Choose a wisdom tradition to draw from:</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: 16px; color: var(--text-muted); font-size: 0.8rem;">Select a tradition, then click Submit to get wisdom relevant to your conversation.</p>';
content.innerHTML = html;
}
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 'soma':
inputText = 'Guide me through a body check-in.';
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) {
addToolMsg('user', `Drawing from: ${selectedTradition}`);
// Build conversation context for wisdom
const convContext = conversationHistory.map(m => `${m.role === 'user' ? 'User' : 'Partner'}: ${m.content}`).join('\n');
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}.
Conversation so far:
${convContext || 'No conversation yet.'}
${userInput ? 'User adds: ' + userInput : ''}
Based on the themes in this conversation (conflict, connection, fear, love, boundaries, etc.), provide ONE relevant quote from ${selectedTradition} that speaks to their 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();
}
}
// ============================================================================
// 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>