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 +4 -0
- package/brain/narrative.py +1 -0
- package/main.py +1 -1
- package/package.json +1 -1
- package/webui/app.py +106 -3
- package/webui/bridge.py +16 -1
- package/webui/static/index.html +594 -3
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
|
package/brain/narrative.py
CHANGED
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
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)
|
package/webui/static/index.html
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|