Spaces:
Running
Running
| <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> | |