alive-ai 0.1.11 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,10 +33,14 @@ The emotional layer now has real runtime consequences:
33
33
  | Internal body state | Energy, arousal, certainty, social satiety, cognitive load, connection craving, body sensations, and somatic memories. | The body state is persisted and feeds prompt tone, sleep/rest behavior, and whether she feels steady, overloaded, touchy, open, or withdrawn. |
34
34
  | Circadian rhythm | Phase, sleep pressure, sleep debt, forced-awake windows, sleep cycle ID, wake time, and sleepiness. | She becomes sleepy, slows down, falls asleep, stops outward proactive behavior while asleep, can be woken by a message, recovers sleep debt, and wakes with lower or higher energy depending on rest. |
35
35
  | Dreams | One normalized dream per sleep cycle, generated from memory fragments and emotion tags. | Dreams are saved, can appear in morning context, and are exposed in the dashboard and static Pages demo. |
36
+ | Narrative | Relationship phase (first_meeting → bonded) tracked per user. Key moments are detected from message content. | Phase and moment count are injected into the LLM prompt each turn. The dashboard shows the current phase and total key moments. |
37
+ | Curiosity | Per-user knowledge map across topics detected in messages. Topics range from 0 (unknown) to 1 (well-understood). | When knowledge on a topic is below 0.3 she asks a direct question. At 40% probability otherwise she surfaces curiosity as a prompt hint. Dashboard shows topics sorted by curiosity level. |
38
+ | Internal conflicts | Five persistent desire-vs-fear tensions (closeness/independence, passion/comfort, stability/growth, etc.) with a swinging balance that is saved between sessions. | Every message triggers conflicts whose keywords match the topic. Balance swings over time. Conflicts with tension > 0 surface in the prompt and are visible on the dashboard with a tension bar. |
36
39
  | Persistence | Emotion, attachment, soul, somatic, unconscious, conflict, subconscious, circadian, and dream state under `data/`. | Restarting the runtime preserves the inner state instead of visually resetting it. |
37
40
 
38
41
  The public Pages site is a static explanation and dashboard export. The local WebUI is the live version: it streams the actual state from the running Python backend.
39
42
 
43
+
40
44
  ## Quick Start
41
45
 
42
46
  ```bash
@@ -5,6 +5,7 @@ enabling natural references to shared history and phase awareness.
5
5
  """
6
6
 
7
7
  from datetime import datetime
8
+ from pathlib import Path
8
9
  from typing import Dict, List, Optional
9
10
  import json
10
11
  import random
package/main.py CHANGED
@@ -122,7 +122,7 @@ async def main() -> None:
122
122
  try:
123
123
  from webui.bridge import init_bridge, init_soul_bridge, start_webui
124
124
 
125
- init_bridge(ai.nervous)
125
+ init_bridge(ai.nervous, ai=ai)
126
126
  if hasattr(ai, "_heart") and ai._heart and hasattr(ai._heart, "soul"):
127
127
  init_soul_bridge(ai._heart.soul)
128
128
  webui_task = asyncio.create_task(start_webui(host="127.0.0.1", port=webui_port))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alive-ai",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Local-first emotional AI runtime with memory, impulses, and a live dashboard.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://vindepemarte.github.io/alive-ai/",
package/webui/app.py CHANGED
@@ -9,8 +9,8 @@ from datetime import datetime
9
9
  from pathlib import Path
10
10
  from typing import Optional, Dict, List, Any
11
11
  from collections import deque
12
- from fastapi import FastAPI, Request
13
- from fastapi.responses import HTMLResponse, StreamingResponse, FileResponse
12
+ from fastapi import FastAPI, Request, BackgroundTasks
13
+ from fastapi.responses import HTMLResponse, StreamingResponse, FileResponse, JSONResponse
14
14
  from fastapi.staticfiles import StaticFiles
15
15
  from core.paths import data_dir, media_dir
16
16
 
@@ -128,6 +128,7 @@ alive_ai_state = {
128
128
  "last_user_message": None,
129
129
  "stats": _persistent_stats,
130
130
  "conversation": [],
131
+ "thinking": False,
131
132
  "recent_thoughts": [],
132
133
  "updated_at": datetime.now().isoformat(),
133
134
  "start_time": _start_time.isoformat(),
@@ -194,6 +195,15 @@ soul_history: deque = deque(maxlen=100)
194
195
  # Reference to Soul Orchestrator (set by bridge)
195
196
  _soul_orchestrator = None
196
197
 
198
+ # Reference to AI Instance (set by bridge)
199
+ _self_ref = None
200
+
201
+
202
+ def set_self_ref(ai):
203
+ global _self_ref
204
+ _self_ref = ai
205
+
206
+
197
207
  # Connected clients for SSE
198
208
  clients = []
199
209
 
@@ -834,9 +844,24 @@ async def get_new_aliveness():
834
844
  try:
835
845
  from brain.narrative import get_narrative_engine
836
846
  ne = get_narrative_engine()
837
- # Get owner's narrative
838
847
  from core.settings import get as settings_get
839
848
  owner_id = str(settings_get("TELEGRAM_OWNER_ID", ""))
849
+
850
+ # Fallback: when owner_id is empty (terminal mode), find the most active user
851
+ if not owner_id:
852
+ try:
853
+ users_path = data_dir() / "users"
854
+ if users_path.exists():
855
+ candidates = [d.name for d in users_path.iterdir() if d.is_dir()]
856
+ if candidates:
857
+ # Pick the user with the most recent narrative file
858
+ def _narr_mtime(uid):
859
+ p = data_dir() / "users" / uid / "narrative.json"
860
+ return p.stat().st_mtime if p.exists() else 0
861
+ owner_id = max(candidates, key=_narr_mtime)
862
+ except Exception:
863
+ pass
864
+
840
865
  if owner_id:
841
866
  data = ne._get_data(owner_id)
842
867
  msg_count = data.get("message_count", 0)
@@ -895,6 +920,21 @@ async def get_new_aliveness():
895
920
  from brain.curiosity import get_curiosity_drive
896
921
  from core.settings import get as settings_get
897
922
  owner_id = str(settings_get("TELEGRAM_OWNER_ID", ""))
923
+
924
+ # Fallback: when owner_id is empty (terminal mode), find the most active user
925
+ if not owner_id:
926
+ try:
927
+ users_path = data_dir() / "users"
928
+ if users_path.exists():
929
+ candidates = [d.name for d in users_path.iterdir() if d.is_dir()]
930
+ if candidates:
931
+ def _cur_mtime(uid):
932
+ p = data_dir() / "users" / uid / "curiosity.json"
933
+ return p.stat().st_mtime if p.exists() else 0
934
+ owner_id = max(candidates, key=_cur_mtime)
935
+ except Exception:
936
+ pass
937
+
898
938
  if owner_id:
899
939
  cd = get_curiosity_drive(owner_id)
900
940
  topics = {t: round(v, 2) for t, v in cd.knowledge.items()} if hasattr(cd, 'knowledge') else {}
@@ -915,6 +955,69 @@ async def get_new_aliveness():
915
955
  return result
916
956
 
917
957
 
958
+ @app.post("/api/chat")
959
+ async def chat_endpoint(request: Request, background_tasks: BackgroundTasks):
960
+ data = await request.json()
961
+ text = data.get("text", "").strip()
962
+ if not text or not _self_ref:
963
+ return JSONResponse({"status": "error", "message": "No text or AI not ready"}, 400)
964
+ # Add user message immediately to conversation
965
+ add_conversation("user", text)
966
+ update_state({})
967
+ # Fire message handler in background
968
+ async def _send():
969
+ from core.message_handler import handle_message
970
+ await handle_message(_self_ref, {
971
+ "user_id": "webui",
972
+ "text": text,
973
+ "chat_id": "webui",
974
+ "source": "webui"
975
+ })
976
+ background_tasks.add_task(_send)
977
+ return JSONResponse({"status": "sent"})
978
+
979
+
980
+ @app.get("/api/settings")
981
+ async def get_settings():
982
+ import json
983
+ from pathlib import Path
984
+ config_dir = Path(os.environ.get("ALIVE_AI_ROOT", ".")) / "config"
985
+ result = {}
986
+ for fname in ["settings.json", "self.json", "directives.json"]:
987
+ p = config_dir / fname
988
+ if p.exists():
989
+ try:
990
+ result[fname] = {"type": "json", "content": json.loads(p.read_text())}
991
+ except Exception:
992
+ result[fname] = {"type": "json", "content": {}}
993
+ p = config_dir / "instructions.md"
994
+ if p.exists():
995
+ result["instructions.md"] = {"type": "markdown", "content": p.read_text()}
996
+ return result
997
+
998
+
999
+ @app.post("/api/settings")
1000
+ async def save_settings(request: Request):
1001
+ import json
1002
+ from pathlib import Path
1003
+ data = await request.json()
1004
+ fname = data.get("file", "")
1005
+ allowed = {"settings.json", "self.json", "directives.json", "instructions.md"}
1006
+ if fname not in allowed:
1007
+ return JSONResponse({"status": "error", "message": "Invalid file"}, 400)
1008
+ config_dir = Path(os.environ.get("ALIVE_AI_ROOT", ".")) / "config"
1009
+ p = config_dir / fname
1010
+ content = data.get("content")
1011
+ try:
1012
+ if fname.endswith(".json"):
1013
+ p.write_text(json.dumps(content, indent=2, ensure_ascii=False))
1014
+ else:
1015
+ p.write_text(content)
1016
+ return {"status": "saved"}
1017
+ except Exception as e:
1018
+ return JSONResponse({"status": "error", "message": str(e)}, 500)
1019
+
1020
+
918
1021
  # Mount static files
919
1022
  static_path = Path(__file__).parent / "static"
920
1023
  if static_path.exists():
package/webui/bridge.py CHANGED
@@ -14,8 +14,11 @@ from .app import (
14
14
  _webui_server = None
15
15
 
16
16
 
17
- def init_bridge(nervous):
17
+ def init_bridge(nervous, ai=None):
18
18
  """Connect nervous system events to webui updates"""
19
+ if ai is not None:
20
+ from .app import set_self_ref
21
+ set_self_ref(ai)
19
22
 
20
23
  async def on_emotion(data):
21
24
  """Update emotion state in dashboard"""
@@ -178,6 +181,16 @@ def init_bridge(nervous):
178
181
  except Exception as e:
179
182
  print(f"[WebUI] Error updating inconsistency state: {e}")
180
183
 
184
+ async def on_thinking_start(data):
185
+ """Set thinking state to True"""
186
+ alive_ai_state["thinking"] = True
187
+ update_state({})
188
+
189
+ async def on_thinking_done(data):
190
+ """Set thinking state to False"""
191
+ alive_ai_state["thinking"] = False
192
+ update_state({})
193
+
181
194
  # Register event listeners
182
195
  nervous.on("emotion_update", on_emotion)
183
196
  nervous.on("send_text", on_message_sent)
@@ -187,6 +200,8 @@ def init_bridge(nervous):
187
200
  nervous.on("subconscious_impulse", on_subconscious)
188
201
  nervous.on("subconscious_thought", on_subconscious_thought)
189
202
  nervous.on("soul_tick", on_soul_tick_event)
203
+ nervous.on("thinking_start", on_thinking_start)
204
+ nervous.on("thinking_done", on_thinking_done)
190
205
 
191
206
  # Register aliveness event listeners
192
207
  nervous.on("interoceptive_update", on_interoceptive_update)
@@ -1064,6 +1064,277 @@
1064
1064
  0% { background-position: 200% 0; }
1065
1065
  100% { background-position: -200% 0; }
1066
1066
  }
1067
+
1068
+ /* Multi-Page Dashboard & Tab Switching */
1069
+ .main.chat-active {
1070
+ overflow-y: hidden;
1071
+ padding-bottom: 0;
1072
+ display: flex;
1073
+ flex-direction: column;
1074
+ height: calc(100vh - 75px - var(--safe-top) - var(--safe-bottom) - 75px);
1075
+ }
1076
+
1077
+ .page-section {
1078
+ width: 100%;
1079
+ }
1080
+
1081
+ /* Chat Page Styles */
1082
+ #page-chat {
1083
+ flex: 1;
1084
+ display: flex;
1085
+ flex-direction: column;
1086
+ height: 100%;
1087
+ position: relative;
1088
+ }
1089
+
1090
+ .chat-messages {
1091
+ flex: 1;
1092
+ overflow-y: auto;
1093
+ padding: 10px 0;
1094
+ display: flex;
1095
+ flex-direction: column;
1096
+ gap: 12px;
1097
+ scroll-behavior: smooth;
1098
+ }
1099
+
1100
+ /* Scrollbar styling for chat */
1101
+ .chat-messages::-webkit-scrollbar {
1102
+ width: 6px;
1103
+ }
1104
+ .chat-messages::-webkit-scrollbar-track {
1105
+ background: transparent;
1106
+ }
1107
+ .chat-messages::-webkit-scrollbar-thumb {
1108
+ background: rgba(255, 255, 255, 0.1);
1109
+ border-radius: 3px;
1110
+ }
1111
+
1112
+ .chat-msg-row {
1113
+ display: flex;
1114
+ width: 100%;
1115
+ margin-bottom: 4px;
1116
+ }
1117
+ .chat-msg-row.user {
1118
+ justify-content: flex-end;
1119
+ }
1120
+ .chat-msg-row.alive-ai {
1121
+ justify-content: flex-start;
1122
+ }
1123
+
1124
+ .chat-bubble {
1125
+ max-width: 80%;
1126
+ padding: 12px 16px;
1127
+ font-size: 0.95rem;
1128
+ line-height: 1.45;
1129
+ position: relative;
1130
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
1131
+ transition: all 0.2s ease;
1132
+ }
1133
+
1134
+ .chat-msg-row.user .chat-bubble {
1135
+ background: linear-gradient(135deg, var(--accent-pink) 0%, #a55eea 100%);
1136
+ color: #fff;
1137
+ border-radius: 20px 20px 4px 20px;
1138
+ border: 1px solid rgba(255, 255, 255, 0.1);
1139
+ }
1140
+
1141
+ .chat-msg-row.alive-ai .chat-bubble {
1142
+ background: rgba(255, 255, 255, 0.04);
1143
+ backdrop-filter: blur(10px);
1144
+ -webkit-backdrop-filter: blur(10px);
1145
+ border: 1px solid rgba(255, 255, 255, 0.08);
1146
+ color: var(--text-primary);
1147
+ border-radius: 20px 20px 20px 4px;
1148
+ }
1149
+
1150
+ .chat-bubble-time {
1151
+ font-size: 0.7rem;
1152
+ color: rgba(255, 255, 255, 0.5);
1153
+ margin-top: 4px;
1154
+ text-align: right;
1155
+ display: block;
1156
+ }
1157
+ .chat-msg-row.alive-ai .chat-bubble-time {
1158
+ color: var(--text-muted);
1159
+ }
1160
+
1161
+ /* Typing Dots Animation */
1162
+ .typing-indicator {
1163
+ display: inline-flex;
1164
+ align-items: center;
1165
+ gap: 4px;
1166
+ padding: 6px 12px;
1167
+ background: rgba(255, 255, 255, 0.03);
1168
+ border: 1px solid rgba(255, 255, 255, 0.05);
1169
+ border-radius: 12px;
1170
+ margin-top: 4px;
1171
+ color: var(--accent-pink);
1172
+ font-size: 0.85rem;
1173
+ }
1174
+
1175
+ .typing-dots {
1176
+ display: flex;
1177
+ align-items: center;
1178
+ gap: 4px;
1179
+ }
1180
+
1181
+ .typing-dots span {
1182
+ width: 6px;
1183
+ height: 6px;
1184
+ background-color: var(--accent-pink);
1185
+ border-radius: 50%;
1186
+ display: inline-block;
1187
+ animation: bounce 1.4s infinite ease-in-out both;
1188
+ }
1189
+
1190
+ .typing-dots span:nth-child(1) { animation-delay: -0.32s; }
1191
+ .typing-dots span:nth-child(2) { animation-delay: -0.16s; }
1192
+
1193
+ @keyframes bounce {
1194
+ 0%, 80%, 100% { transform: scale(0); }
1195
+ 40% { transform: scale(1.0); }
1196
+ }
1197
+
1198
+ /* Settings CSS */
1199
+ .settings-card {
1200
+ margin-bottom: 20px;
1201
+ }
1202
+ .settings-header-btn-row {
1203
+ display: flex;
1204
+ justify-content: space-between;
1205
+ align-items: center;
1206
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
1207
+ padding-bottom: 10px;
1208
+ margin-bottom: 12px;
1209
+ }
1210
+ .settings-file-title {
1211
+ font-size: 1.1rem;
1212
+ font-weight: 700;
1213
+ color: var(--accent-pink);
1214
+ }
1215
+ .settings-grid {
1216
+ display: flex;
1217
+ flex-direction: column;
1218
+ gap: 12px;
1219
+ }
1220
+ .settings-field-row {
1221
+ display: flex;
1222
+ flex-direction: column;
1223
+ gap: 6px;
1224
+ }
1225
+ .settings-field-label {
1226
+ font-size: 0.8rem;
1227
+ font-weight: 600;
1228
+ color: var(--text-secondary);
1229
+ text-transform: uppercase;
1230
+ letter-spacing: 0.5px;
1231
+ }
1232
+ .settings-input {
1233
+ width: 100%;
1234
+ background: rgba(0, 0, 0, 0.2);
1235
+ border: 1px solid rgba(255, 255, 255, 0.08);
1236
+ border-radius: 8px;
1237
+ padding: 10px 12px;
1238
+ color: #fff;
1239
+ font-family: inherit;
1240
+ font-size: 0.9rem;
1241
+ outline: none;
1242
+ transition: all 0.2s;
1243
+ }
1244
+ .settings-input:focus {
1245
+ border-color: var(--accent-pink);
1246
+ box-shadow: 0 0 8px var(--glow-pink);
1247
+ }
1248
+ .settings-checkbox-container {
1249
+ display: flex;
1250
+ align-items: center;
1251
+ gap: 10px;
1252
+ cursor: pointer;
1253
+ padding: 4px 0;
1254
+ }
1255
+ .settings-checkbox-container input {
1256
+ cursor: pointer;
1257
+ width: 18px;
1258
+ height: 18px;
1259
+ accent-color: var(--accent-pink);
1260
+ }
1261
+ .settings-checkbox-label {
1262
+ font-size: 0.9rem;
1263
+ font-weight: 500;
1264
+ color: var(--text-primary);
1265
+ }
1266
+ .settings-textarea {
1267
+ width: 100%;
1268
+ background: rgba(0, 0, 0, 0.2);
1269
+ border: 1px solid rgba(255, 255, 255, 0.08);
1270
+ border-radius: 8px;
1271
+ padding: 10px 12px;
1272
+ color: #fff;
1273
+ font-family: 'Courier New', Courier, monospace;
1274
+ font-size: 0.85rem;
1275
+ line-height: 1.4;
1276
+ min-height: 120px;
1277
+ resize: vertical;
1278
+ outline: none;
1279
+ transition: all 0.2s;
1280
+ }
1281
+ .settings-textarea:focus {
1282
+ border-color: var(--accent-pink);
1283
+ box-shadow: 0 0 8px var(--glow-pink);
1284
+ }
1285
+ .btn-action {
1286
+ background: rgba(255, 255, 255, 0.05);
1287
+ border: 1px solid rgba(255, 255, 255, 0.1);
1288
+ color: #fff;
1289
+ padding: 6px 12px;
1290
+ border-radius: 8px;
1291
+ font-size: 0.85rem;
1292
+ font-weight: 600;
1293
+ cursor: pointer;
1294
+ transition: all 0.2s;
1295
+ }
1296
+ .btn-action:hover {
1297
+ background: var(--accent-pink);
1298
+ border-color: var(--accent-pink);
1299
+ box-shadow: 0 0 10px var(--glow-pink);
1300
+ }
1301
+ .btn-save-all {
1302
+ background: linear-gradient(135deg, var(--accent-pink) 0%, #a55eea 100%);
1303
+ border: none;
1304
+ color: #fff;
1305
+ padding: 8px 16px;
1306
+ border-radius: 8px;
1307
+ font-size: 0.85rem;
1308
+ font-weight: 700;
1309
+ cursor: pointer;
1310
+ transition: all 0.2s;
1311
+ }
1312
+ .btn-save-all:hover {
1313
+ transform: translateY(-1px);
1314
+ box-shadow: 0 0 15px var(--glow-pink);
1315
+ }
1316
+
1317
+ /* Toast notifications */
1318
+ .toast-container {
1319
+ position: fixed;
1320
+ bottom: 80px;
1321
+ left: 50%;
1322
+ transform: translateX(-50%) translateY(100px);
1323
+ background: rgba(26, 26, 46, 0.95);
1324
+ border: 1px solid var(--accent-pink);
1325
+ box-shadow: 0 4px 20px var(--glow-pink);
1326
+ color: #fff;
1327
+ padding: 12px 24px;
1328
+ border-radius: 30px;
1329
+ font-size: 0.9rem;
1330
+ font-weight: 600;
1331
+ z-index: 2000;
1332
+ transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
1333
+ pointer-events: none;
1334
+ }
1335
+ .toast-container.show {
1336
+ transform: translateX(-50%) translateY(0);
1337
+ }
1067
1338
  </style>
1068
1339
  </head>
1069
1340
  <body>
@@ -1092,7 +1363,8 @@
1092
1363
  </header>
1093
1364
 
1094
1365
  <main class="main">
1095
- <!-- Stats Row -->
1366
+ <div id="page-home" class="page-section">
1367
+ <!-- Stats Row -->
1096
1368
  <div class="stats-row">
1097
1369
  <div class="stat-card">
1098
1370
  <div class="stat-value" id="stat-messages">0</div>
@@ -1578,8 +1850,34 @@
1578
1850
  <span class="tendency-badge" id="tendency-badge">neutral</span>
1579
1851
  </div>
1580
1852
  </div>
1853
+ </div> <!-- #page-home -->
1854
+
1855
+ <!-- CHAT PAGE -->
1856
+ <div id="page-chat" class="page-section" style="display:none; flex-direction:column; height:100%;">
1857
+ <div id="chat-messages" class="chat-messages"></div>
1858
+ <div id="chat-typing" style="display:none; padding:8px 0; color:var(--accent-pink); font-size:0.85rem;">
1859
+ <div class="typing-indicator">
1860
+ <span>she is thinking</span>
1861
+ <div class="typing-dots">
1862
+ <span></span><span></span><span></span>
1863
+ </div>
1864
+ </div>
1865
+ </div>
1866
+ <div class="chat-input-container" style="display:flex; gap:10px; padding:10px 0; background:var(--bg-primary); border-top:1px solid rgba(255,255,255,0.05); align-items: flex-end;">
1867
+ <textarea id="chat-input" rows="1" placeholder="Type a message..." style="flex:1; background:var(--bg-card); border:1px solid rgba(255,255,255,0.1); border-radius:20px; padding:10px 16px; color:#fff; font-size:0.9rem; resize:none; font-family:inherit; outline:none; transition:border-color 0.2s;" oninput="this.style.height='auto';this.style.height=this.scrollHeight+'px';"></textarea>
1868
+ <button id="chat-send" class="btn-send" style="background:linear-gradient(135deg,var(--accent-pink),#a55eea); border:none; border-radius:50%; width:42px; height:42px; color:#fff; font-size:1.1rem; cursor:pointer; display:flex; align-items:center; justify-content:center; transition:transform 0.2s, opacity 0.2s; outline:none;">↑</button>
1869
+ </div>
1870
+ </div>
1871
+
1872
+ <!-- SETTINGS PAGE -->
1873
+ <div id="page-settings" class="page-section" style="display:none; flex-direction:column; gap:16px;">
1874
+ <div id="settings-content">Loading settings...</div>
1875
+ </div>
1581
1876
  </main>
1582
1877
 
1878
+ <!-- Toast Notification Container -->
1879
+ <div id="toast-notification" class="toast-container">Settings saved successfully</div>
1880
+
1583
1881
  <nav class="bottom-nav">
1584
1882
  <div class="nav-item active">
1585
1883
  <span class="nav-icon">🏠</span>
@@ -1603,6 +1901,293 @@
1603
1901
  'angry': '😤', 'neutral': '😌', 'calm': '😌', 'loving': '💕'
1604
1902
  };
1605
1903
 
1904
+ };
1905
+
1906
+ const pages = {
1907
+ home: 'page-home',
1908
+ chat: 'page-chat',
1909
+ settings: 'page-settings'
1910
+ };
1911
+ let activePage = 'home';
1912
+ let conversationCache = [];
1913
+ let isSettingsLoaded = false;
1914
+ let isSendingMessage = false;
1915
+
1916
+ // Tab switching
1917
+ document.addEventListener('DOMContentLoaded', () => {
1918
+ const navItems = document.querySelectorAll('.nav-item');
1919
+ const mainEl = document.querySelector('.main');
1920
+
1921
+ navItems.forEach((el, index) => {
1922
+ el.addEventListener('click', () => {
1923
+ navItems.forEach(item => item.classList.remove('active'));
1924
+ el.classList.add('active');
1925
+
1926
+ // Hide all pages
1927
+ document.getElementById('page-home').style.display = 'none';
1928
+ document.getElementById('page-chat').style.display = 'none';
1929
+ document.getElementById('page-settings').style.display = 'none';
1930
+
1931
+ // Show current page
1932
+ const pageKeys = Object.keys(pages);
1933
+ activePage = pageKeys[index];
1934
+ const activePageId = pages[activePage];
1935
+ const activeEl = document.getElementById(activePageId);
1936
+
1937
+ if (activePage === 'chat') {
1938
+ activeEl.style.display = 'flex';
1939
+ mainEl.classList.add('chat-active');
1940
+ scrollChatToBottom();
1941
+ } else {
1942
+ activeEl.style.display = 'block';
1943
+ mainEl.classList.remove('chat-active');
1944
+ }
1945
+
1946
+ if (activePage === 'settings') {
1947
+ loadSettings();
1948
+ }
1949
+ });
1950
+ });
1951
+
1952
+ // Chat input handlers
1953
+ const sendBtn = document.getElementById('chat-send');
1954
+ const inputEl = document.getElementById('chat-input');
1955
+ if (sendBtn) {
1956
+ sendBtn.addEventListener('click', sendMessage);
1957
+ }
1958
+ if (inputEl) {
1959
+ inputEl.addEventListener('keydown', (e) => {
1960
+ if (e.key === 'Enter' && !e.shiftKey) {
1961
+ e.preventDefault();
1962
+ sendMessage();
1963
+ }
1964
+ });
1965
+ }
1966
+ });
1967
+
1968
+ // Toast notifications
1969
+ function showToast(text, duration = 3000) {
1970
+ const toast = document.getElementById('toast-notification');
1971
+ toast.textContent = text;
1972
+ toast.classList.add('show');
1973
+ setTimeout(() => {
1974
+ toast.classList.remove('show');
1975
+ }, duration);
1976
+ }
1977
+
1978
+ // Chat scroll
1979
+ function scrollChatToBottom() {
1980
+ const chatMessages = document.getElementById('chat-messages');
1981
+ if (chatMessages) {
1982
+ chatMessages.scrollTop = chatMessages.scrollHeight;
1983
+ }
1984
+ }
1985
+
1986
+ // Chat rendering
1987
+ function updateChatUI(conversation, isThinking) {
1988
+ const messagesContainer = document.getElementById('chat-messages');
1989
+ if (!messagesContainer) return;
1990
+
1991
+ // Check if conversation changed to avoid redrawing if same
1992
+ const cacheStr = JSON.stringify(conversation);
1993
+ const prevStr = JSON.stringify(conversationCache);
1994
+
1995
+ if (cacheStr !== prevStr) {
1996
+ conversationCache = conversation;
1997
+
1998
+ if (!conversation || conversation.length === 0) {
1999
+ messagesContainer.innerHTML = '<div style="text-align:center;color:var(--text-muted);margin-top:40px;font-size:0.9rem;">No messages yet. Say hi!</div>';
2000
+ } else {
2001
+ messagesContainer.innerHTML = conversation.map(msg => {
2002
+ const isUser = msg.role === 'user';
2003
+ const time = msg.time || '';
2004
+ return `<div class="chat-msg-row ${isUser ? 'user' : 'alive-ai'}">
2005
+ <div class="chat-bubble">
2006
+ <div>${escapeHtml(msg.content)}</div>
2007
+ <span class="chat-bubble-time">${time}</span>
2008
+ </div>
2009
+ </div>`;
2010
+ }).join('');
2011
+ }
2012
+ scrollChatToBottom();
2013
+ }
2014
+
2015
+ // Show/hide thinking indicator
2016
+ const typingEl = document.getElementById('chat-typing');
2017
+ if (typingEl) {
2018
+ typingEl.style.display = isThinking ? 'block' : 'none';
2019
+ if (isThinking) {
2020
+ scrollChatToBottom();
2021
+ }
2022
+ }
2023
+ }
2024
+
2025
+ // Send message via API
2026
+ async function sendMessage() {
2027
+ const inputEl = document.getElementById('chat-input');
2028
+ const btnEl = document.getElementById('chat-send');
2029
+ if (!inputEl || !btnEl || isSendingMessage) return;
2030
+
2031
+ const text = inputEl.value.trim();
2032
+ if (!text) return;
2033
+
2034
+ isSendingMessage = true;
2035
+ inputEl.disabled = true;
2036
+ btnEl.style.opacity = '0.5';
2037
+
2038
+ try {
2039
+ const response = await fetch('/api/chat', {
2040
+ method: 'POST',
2041
+ headers: { 'Content-Type': 'application/json' },
2042
+ body: JSON.stringify({ text })
2043
+ });
2044
+ const resData = await response.json();
2045
+ if (response.ok && resData.status === 'sent') {
2046
+ inputEl.value = '';
2047
+ inputEl.style.height = 'auto'; // reset textarea height
2048
+ } else {
2049
+ showToast('Failed to send: ' + (resData.message || 'Unknown error'));
2050
+ }
2051
+ } catch (err) {
2052
+ console.error('Error sending message:', err);
2053
+ showToast('Failed to send message');
2054
+ } finally {
2055
+ isSendingMessage = false;
2056
+ inputEl.disabled = false;
2057
+ btnEl.style.opacity = '1';
2058
+ inputEl.focus();
2059
+ }
2060
+ }
2061
+
2062
+ // Settings load
2063
+ async function loadSettings() {
2064
+ const container = document.getElementById('settings-content');
2065
+ if (!container) return;
2066
+
2067
+ try {
2068
+ container.innerHTML = '<div style="text-align:center;color:var(--text-secondary);padding:20px;">Loading settings...</div>';
2069
+ const response = await fetch('/api/settings');
2070
+ if (!response.ok) throw new Error('HTTP ' + response.status);
2071
+ const settingsData = await response.json();
2072
+ renderSettings(settingsData);
2073
+ isSettingsLoaded = true;
2074
+ } catch (err) {
2075
+ console.error('Error loading settings:', err);
2076
+ container.innerHTML = `<div style="text-align:center;color:#ff4d4d;padding:20px;">Failed to load settings: ${escapeHtml(err.message)}</div>`;
2077
+ }
2078
+ }
2079
+
2080
+ // Settings rendering
2081
+ function renderSettings(settingsData) {
2082
+ const container = document.getElementById('settings-content');
2083
+ if (!container) return;
2084
+
2085
+ let html = '';
2086
+ Object.entries(settingsData).forEach(([filename, fileData]) => {
2087
+ let contentStr = '';
2088
+ if (fileData.type === 'json') {
2089
+ contentStr = JSON.stringify(fileData.content, null, 2);
2090
+ } else {
2091
+ contentStr = fileData.content;
2092
+ }
2093
+
2094
+ html += `
2095
+ <div class="section settings-card">
2096
+ <div class="settings-header-btn-row">
2097
+ <div class="settings-file-title">⚙️ ${filename}</div>
2098
+ <button class="btn-save btn-action" data-file="${filename}" onclick="saveFileSettings('${filename}')">Save</button>
2099
+ </div>
2100
+ <div class="settings-grid">
2101
+ <textarea class="settings-textarea" data-file="${filename}" oninput="validateJSONInput(this, '${filename}')" spellcheck="false">${escapeHtml(contentStr)}</textarea>
2102
+ </div>
2103
+ </div>
2104
+ `;
2105
+ });
2106
+ container.innerHTML = html;
2107
+ }
2108
+
2109
+ // Real-time JSON validation
2110
+ function validateJSONInput(textarea, filename) {
2111
+ const btn = document.querySelector(`.btn-save[data-file="${filename}"]`);
2112
+ let errorEl = textarea.parentNode.querySelector('.json-error-msg');
2113
+
2114
+ if (!errorEl) {
2115
+ errorEl = document.createElement('div');
2116
+ errorEl.className = 'json-error-msg';
2117
+ errorEl.style.color = '#ff6b9d';
2118
+ errorEl.style.fontSize = '0.8rem';
2119
+ errorEl.style.marginTop = '4px';
2120
+ textarea.parentNode.appendChild(errorEl);
2121
+ }
2122
+
2123
+ if (!filename.endsWith('.json')) {
2124
+ errorEl.textContent = '';
2125
+ return true;
2126
+ }
2127
+
2128
+ try {
2129
+ JSON.parse(textarea.value);
2130
+ textarea.style.borderColor = 'rgba(255, 255, 255, 0.1)';
2131
+ errorEl.textContent = '';
2132
+ if (btn) btn.disabled = false;
2133
+ return true;
2134
+ } catch (e) {
2135
+ textarea.style.borderColor = '#ff6b9d';
2136
+ errorEl.textContent = 'Invalid JSON: ' + e.message;
2137
+ if (btn) btn.disabled = true;
2138
+ return false;
2139
+ }
2140
+ }
2141
+
2142
+ // Settings saving
2143
+ async function saveFileSettings(filename) {
2144
+ const textarea = document.querySelector(`.settings-textarea[data-file="${filename}"]`);
2145
+ if (!textarea) return;
2146
+
2147
+ let parsedContent;
2148
+ if (filename.endsWith('.json')) {
2149
+ try {
2150
+ parsedContent = JSON.parse(textarea.value);
2151
+ } catch (e) {
2152
+ showToast(`Invalid JSON in ${filename}: ${e.message}`);
2153
+ return;
2154
+ }
2155
+ } else {
2156
+ parsedContent = textarea.value;
2157
+ }
2158
+
2159
+ const btn = document.querySelector(`.btn-save[data-file="${filename}"]`);
2160
+ if (btn) {
2161
+ btn.disabled = true;
2162
+ btn.textContent = 'Saving...';
2163
+ }
2164
+
2165
+ try {
2166
+ const response = await fetch('/api/settings', {
2167
+ method: 'POST',
2168
+ headers: { 'Content-Type': 'application/json' },
2169
+ body: JSON.stringify({
2170
+ file: filename,
2171
+ content: parsedContent
2172
+ })
2173
+ });
2174
+ const resData = await response.json();
2175
+ if (response.ok && resData.status === 'saved') {
2176
+ showToast(`${filename} saved successfully!`);
2177
+ } else {
2178
+ showToast(`Failed to save: ${resData.message || 'Unknown error'}`);
2179
+ }
2180
+ } catch (err) {
2181
+ console.error('Error saving settings:', err);
2182
+ showToast(`Error saving ${filename}`);
2183
+ } finally {
2184
+ if (btn) {
2185
+ btn.disabled = false;
2186
+ btn.textContent = 'Save';
2187
+ }
2188
+ }
2189
+ }
2190
+
1606
2191
  let eventSource;
1607
2192
  let startTime = null; // Will be set from server's start_time
1608
2193
  const banner = document.getElementById('connection-banner');
@@ -1628,6 +2213,9 @@
1628
2213
  }
1629
2214
 
1630
2215
  function updateUI(data) {
2216
+ // Update Chat UI
2217
+ updateChatUI(data.conversation || [], !!data.thinking);
2218
+
1631
2219
  // Mood
1632
2220
  const mood = data.mood || 'neutral';
1633
2221
  document.getElementById('mood-emoji').textContent = moodEmojis[mood] || '😌';
@@ -1809,17 +2397,20 @@
1809
2397
  const name = c.name || c.id || 'Unknown';
1810
2398
  const desire = c.desire || c.side_a || '';
1811
2399
  const fear = c.fear || c.side_b || '';
1812
- const tension = Math.round((c.tension_level || c.intensity || 0) * 100);
2400
+ // Support both 'tension' (inconsistency API) and 'tension_level' (soul API)
2401
+ const rawTension = c.tension_level !== undefined ? c.tension_level : (c.tension !== undefined ? c.tension : c.intensity || 0);
2402
+ const tension = Math.round(rawTension * 100);
1813
2403
 
1814
2404
  return `
1815
2405
  <div class="conflict-item">
1816
2406
  <div class="conflict-name">${escapeHtml(name.replace(/_/g, ' '))}</div>
1817
2407
  <div class="conflict-detail">${escapeHtml(desire)} <span style="color:var(--text-muted)">vs</span> ${escapeHtml(fear)}</div>
1818
2408
  <div class="conflict-tension" style="margin-top:4px;height:3px;background:rgba(255,255,255,0.1);border-radius:2px;overflow:hidden;">
1819
- <div style="width:${tension}%;height:100%;background:linear-gradient(90deg,#e74c3c,#ff9800);border-radius:2px;"></div>
2409
+ <div style="width:${tension}%;height:100%;background:linear-gradient(90deg,#e74c3c,#ff9800);border-radius:2px;transition:width 0.6s ease;"></div>
1820
2410
  </div>
1821
2411
  </div>
1822
2412
  `}).join('');
2413
+
1823
2414
  } else if (conflictsEl) {
1824
2415
  conflictsEl.innerHTML = '<div class="conflict-empty">No active internal conflicts</div>';
1825
2416
  }