alive-ai 0.1.14 → 0.1.15
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 -2
- package/core/message_handler.py +3 -1
- package/heart/circadian.py +27 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/webui/app.py +36 -15
- package/webui/persistence.py +112 -18
- package/webui/static/index.html +21 -11
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ The emotional layer now has real runtime consequences:
|
|
|
31
31
|
| Complex emotions | Guilt, pride, jealousy, embarrassment, and anticipation. | They do not just label the dashboard. They push fear, sadness, anger, dominance, trust, arousal, joy, and future-facing behavior in different directions. |
|
|
32
32
|
| Hormones | Oxytocin, dopamine, serotonin, cortisol, melatonin, plus residual metabolites. | Hormones modulate perception, soul valence/arousal, emotional deltas, somatic body state, interoception, impulse probability, and prompt guidance. Stress makes her more vigilant; bonding increases trust; dopamine increases pursuit; serotonin stabilizes; melatonin slows her down. |
|
|
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
|
-
| 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. |
|
|
34
|
+
| Circadian rhythm | Phase, sleep pressure, sleep debt in hours, 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. After 2am, high sleep pressure shortens user wake-up windows so she can drift back to sleep instead of staying pinned awake. |
|
|
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
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
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. |
|
|
@@ -277,7 +277,9 @@ The real WebUI streams local runtime state over Server-Sent Events and shows:
|
|
|
277
277
|
- attachment, circadian rhythm, sleepiness, body memory, dreams, curiosity, and conflicts,
|
|
278
278
|
- runtime health through local endpoints.
|
|
279
279
|
|
|
280
|
-
The WebUI hydrates from durable runtime stores instead of only the current browser session. Chat rows are journaled per active user under `data/users/<user>/webui_chat.jsonl
|
|
280
|
+
The WebUI hydrates from durable runtime stores instead of only the current browser session. It resolves the active dashboard user from explicit WebUI input, live Telegram activity, configured owner ID, runtime state, and finally the most active user folder on disk. Chat rows are journaled per active user under `data/users/<user>/webui_chat.jsonl` and merged with episodic Telegram conversation history, including legacy flat `data/conversations` history after upgrades. `/state` and the SSE stream now use the same composed snapshot: visible chat, runtime state, soul state, aliveness state, current thoughts, memory counters, and the active dashboard user.
|
|
281
|
+
|
|
282
|
+
Sleep debt is stored and shown as hours on a 0-8h pressure scale. The UI no longer reports it as a misleading capped percentage, so a persisted `5.6h` debt displays as `5.6h` with the matching pressure bar.
|
|
281
283
|
|
|
282
284
|
Settings edits validate JSON before saving and write atomically, so a bad edit cannot corrupt the existing config file. The Settings tab also protects unsaved edits while switching tabs.
|
|
283
285
|
|
package/core/message_handler.py
CHANGED
|
@@ -268,7 +268,9 @@ def _get_or_create_user_memory(self, user_id: str):
|
|
|
268
268
|
from brain.memory import Memory
|
|
269
269
|
from core.user_manager import UserManager
|
|
270
270
|
|
|
271
|
-
|
|
271
|
+
user_manager = UserManager()
|
|
272
|
+
user_manager.migrate_legacy_data(user_id)
|
|
273
|
+
instance_data_path = user_manager.get_user_paths(user_id)["base"]
|
|
272
274
|
|
|
273
275
|
memory = Memory(
|
|
274
276
|
nervous=self.nervous,
|
package/heart/circadian.py
CHANGED
|
@@ -140,9 +140,21 @@ class CircadianEngine:
|
|
|
140
140
|
return round(self._clamp(sleepiness), 2)
|
|
141
141
|
|
|
142
142
|
def _should_auto_sleep(self, now: datetime) -> bool:
|
|
143
|
-
|
|
143
|
+
sleepiness = self.get_sleepiness()
|
|
144
|
+
if not (2 <= now.hour < 6 and sleepiness >= 0.85):
|
|
144
145
|
return False
|
|
145
|
-
|
|
146
|
+
if not self._is_forced_awake(now):
|
|
147
|
+
return True
|
|
148
|
+
# After 2am, sleep pressure can override a user wake-up. Messages can
|
|
149
|
+
# briefly rouse her, but they should not pin her awake for another hour.
|
|
150
|
+
if sleepiness >= 0.95:
|
|
151
|
+
return True
|
|
152
|
+
try:
|
|
153
|
+
until = datetime.fromisoformat(self.forced_awake_until) if self.forced_awake_until else now
|
|
154
|
+
forced_started = until - timedelta(minutes=self._forced_awake_duration_minutes(now))
|
|
155
|
+
return (now - forced_started).total_seconds() >= 10 * 60
|
|
156
|
+
except Exception:
|
|
157
|
+
return sleepiness >= 0.9
|
|
146
158
|
|
|
147
159
|
def _should_auto_wake(self, now: datetime) -> bool:
|
|
148
160
|
slept = self._hours_asleep(now)
|
|
@@ -231,9 +243,21 @@ class CircadianEngine:
|
|
|
231
243
|
self._save()
|
|
232
244
|
return True
|
|
233
245
|
|
|
234
|
-
def
|
|
246
|
+
def _forced_awake_duration_minutes(self, now: datetime = None) -> int:
|
|
247
|
+
now = now or self._now()
|
|
248
|
+
sleepiness = self.get_sleepiness()
|
|
249
|
+
if 2 <= now.hour < 6 and sleepiness >= 0.9:
|
|
250
|
+
return 10
|
|
251
|
+
if 0 <= now.hour < 6 and sleepiness >= 0.85:
|
|
252
|
+
return 20
|
|
253
|
+
if now.hour >= 23 and sleepiness >= 0.75:
|
|
254
|
+
return 30
|
|
255
|
+
return 45
|
|
256
|
+
|
|
257
|
+
def stay_up_for_user(self, duration_minutes: int = None):
|
|
235
258
|
"""User is keeping her awake past bedtime."""
|
|
236
259
|
now = self._now()
|
|
260
|
+
duration_minutes = duration_minutes or self._forced_awake_duration_minutes(now)
|
|
237
261
|
self.forced_awake = True
|
|
238
262
|
self.forced_awake_until = (now + timedelta(minutes=duration_minutes)).isoformat()
|
|
239
263
|
self.last_transition_reason = "staying_up_for_user"
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/webui/app.py
CHANGED
|
@@ -15,6 +15,7 @@ from fastapi.staticfiles import StaticFiles
|
|
|
15
15
|
from core.paths import data_dir, media_dir
|
|
16
16
|
from .persistence import (
|
|
17
17
|
append_chat_message,
|
|
18
|
+
count_visible_messages,
|
|
18
19
|
load_chat_messages,
|
|
19
20
|
new_message_id,
|
|
20
21
|
resolve_active_user_id,
|
|
@@ -27,26 +28,45 @@ app = FastAPI(title="Alive-AI Dashboard")
|
|
|
27
28
|
_start_time = datetime.now()
|
|
28
29
|
|
|
29
30
|
|
|
30
|
-
def load_persistent_stats() -> dict:
|
|
31
|
+
def load_persistent_stats(active_user: str = None) -> dict:
|
|
31
32
|
"""Load stats from actual data sources on startup"""
|
|
32
33
|
stats = {"messages": 0, "memories": 0, "evaluations": 0}
|
|
33
34
|
|
|
34
35
|
# Try different base paths
|
|
35
36
|
base_paths = [data_dir()]
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
if active_user:
|
|
39
|
+
try:
|
|
40
|
+
stats["messages"] = count_visible_messages(active_user)
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
# Count actual per-user conversation rows and WebUI journal rows.
|
|
38
45
|
for base_path in base_paths:
|
|
39
46
|
try:
|
|
40
|
-
# Look for summaries in users/*/summaries/
|
|
41
47
|
users_path = base_path / "users"
|
|
42
48
|
if users_path.exists():
|
|
43
49
|
count = 0
|
|
44
50
|
for user_dir in users_path.iterdir():
|
|
45
|
-
|
|
46
|
-
if
|
|
47
|
-
|
|
51
|
+
conv_path = user_dir / "conversations"
|
|
52
|
+
if conv_path.exists():
|
|
53
|
+
for conv_file in conv_path.glob("*.jsonl"):
|
|
54
|
+
with conv_file.open() as fh:
|
|
55
|
+
for line in fh:
|
|
56
|
+
try:
|
|
57
|
+
row = json.loads(line)
|
|
58
|
+
if row.get("user"):
|
|
59
|
+
count += 1
|
|
60
|
+
if row.get("ai"):
|
|
61
|
+
count += 1
|
|
62
|
+
except Exception:
|
|
63
|
+
continue
|
|
64
|
+
journal_path = user_dir / "webui_chat.jsonl"
|
|
65
|
+
if journal_path.exists():
|
|
66
|
+
with journal_path.open() as fh:
|
|
67
|
+
count += sum(1 for _ in fh)
|
|
48
68
|
if count > 0:
|
|
49
|
-
stats["messages"] = count
|
|
69
|
+
stats["messages"] = max(stats["messages"], count)
|
|
50
70
|
break
|
|
51
71
|
except Exception:
|
|
52
72
|
pass
|
|
@@ -313,6 +333,10 @@ def build_snapshot(user_id: str = None) -> dict:
|
|
|
313
333
|
snapshot["soul"] = soul_state
|
|
314
334
|
snapshot["aliveness"] = aliveness_state
|
|
315
335
|
snapshot["conversation"] = load_chat_messages(active_user)
|
|
336
|
+
snapshot["stats"] = {
|
|
337
|
+
**snapshot.get("stats", {}),
|
|
338
|
+
**load_persistent_stats(active_user),
|
|
339
|
+
}
|
|
316
340
|
thoughts = _subconscious_thoughts()
|
|
317
341
|
snapshot["recent_thoughts"] = thoughts
|
|
318
342
|
snapshot["current_thought"] = thoughts[-1]["thought"] if thoughts else alive_ai_state.get("current_thought")
|
|
@@ -491,7 +515,7 @@ async def health():
|
|
|
491
515
|
@app.get("/api/stats")
|
|
492
516
|
async def get_persistent_stats():
|
|
493
517
|
"""Get stats refreshed from actual data sources"""
|
|
494
|
-
stats = load_persistent_stats()
|
|
518
|
+
stats = load_persistent_stats(_active_user_id())
|
|
495
519
|
|
|
496
520
|
# Update global state with fresh stats
|
|
497
521
|
alive_ai_state["stats"] = stats
|
|
@@ -936,8 +960,7 @@ async def get_new_aliveness():
|
|
|
936
960
|
try:
|
|
937
961
|
from brain.narrative import get_narrative_engine
|
|
938
962
|
ne = get_narrative_engine()
|
|
939
|
-
|
|
940
|
-
owner_id = str(settings_get("TELEGRAM_OWNER_ID", ""))
|
|
963
|
+
owner_id = _active_user_id()
|
|
941
964
|
|
|
942
965
|
# Fallback: when owner_id is empty (terminal mode), find the most active user
|
|
943
966
|
if not owner_id:
|
|
@@ -972,7 +995,7 @@ async def get_new_aliveness():
|
|
|
972
995
|
|
|
973
996
|
result["narrative"] = {
|
|
974
997
|
"phase": data.get("phase", "first_meeting"),
|
|
975
|
-
"message_count": msg_count,
|
|
998
|
+
"message_count": max(msg_count, count_visible_messages(owner_id)),
|
|
976
999
|
"moments": len(data.get("key_moments", []))
|
|
977
1000
|
}
|
|
978
1001
|
else:
|
|
@@ -991,8 +1014,7 @@ async def get_new_aliveness():
|
|
|
991
1014
|
# Linguistic
|
|
992
1015
|
try:
|
|
993
1016
|
from brain.linguistic import get_linguistic_profile
|
|
994
|
-
|
|
995
|
-
owner_id = str(settings_get("TELEGRAM_OWNER_ID", ""))
|
|
1017
|
+
owner_id = _active_user_id()
|
|
996
1018
|
if owner_id:
|
|
997
1019
|
lp = get_linguistic_profile(owner_id)
|
|
998
1020
|
patterns = lp.get_absorbed_patterns() if hasattr(lp, 'get_absorbed_patterns') else {}
|
|
@@ -1010,8 +1032,7 @@ async def get_new_aliveness():
|
|
|
1010
1032
|
# Curiosity
|
|
1011
1033
|
try:
|
|
1012
1034
|
from brain.curiosity import get_curiosity_drive
|
|
1013
|
-
|
|
1014
|
-
owner_id = str(settings_get("TELEGRAM_OWNER_ID", ""))
|
|
1035
|
+
owner_id = _active_user_id()
|
|
1015
1036
|
|
|
1016
1037
|
# Fallback: when owner_id is empty (terminal mode), find the most active user
|
|
1017
1038
|
if not owner_id:
|
package/webui/persistence.py
CHANGED
|
@@ -10,7 +10,7 @@ import re
|
|
|
10
10
|
import uuid
|
|
11
11
|
from datetime import datetime
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import Any, Dict, List, Optional
|
|
13
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
14
14
|
|
|
15
15
|
from core.paths import data_dir
|
|
16
16
|
|
|
@@ -26,23 +26,93 @@ def normalize_user_id(user_id: Any) -> str:
|
|
|
26
26
|
return safe or "webui"
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def _configured_owner_id() -> str:
|
|
30
|
+
owner = os.environ.get("TELEGRAM_OWNER_ID", "")
|
|
31
|
+
if owner:
|
|
32
|
+
return owner
|
|
33
|
+
try:
|
|
34
|
+
from core.settings import get as settings_get
|
|
35
|
+
return str(settings_get("TELEGRAM_OWNER_ID", "") or "")
|
|
36
|
+
except Exception:
|
|
37
|
+
return ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _tracked_active_user_id() -> str:
|
|
41
|
+
try:
|
|
42
|
+
from core.user_tracker import get_user_tracker
|
|
43
|
+
active = get_user_tracker().get_active_users(within_minutes=24 * 60)
|
|
44
|
+
if active:
|
|
45
|
+
active = sorted(active, key=lambda u: u.last_interaction, reverse=True)
|
|
46
|
+
return active[0].user_id
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
return ""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _path_activity_score(path: Path) -> Tuple[float, int]:
|
|
53
|
+
latest = path.stat().st_mtime if path.exists() else 0.0
|
|
54
|
+
count = 0
|
|
55
|
+
for pattern in ("conversations/*.jsonl", "webui_chat.jsonl", "narrative.json",
|
|
56
|
+
"facts.json", "emotional_memories.json"):
|
|
57
|
+
for item in path.glob(pattern):
|
|
58
|
+
try:
|
|
59
|
+
latest = max(latest, item.stat().st_mtime)
|
|
60
|
+
if item.is_file():
|
|
61
|
+
count += 1
|
|
62
|
+
except Exception:
|
|
63
|
+
continue
|
|
64
|
+
return latest, count
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _most_active_disk_user_id() -> str:
|
|
68
|
+
users = data_dir() / "users"
|
|
69
|
+
if not users.exists():
|
|
70
|
+
return ""
|
|
71
|
+
candidates = []
|
|
72
|
+
for child in users.iterdir():
|
|
73
|
+
if not child.is_dir() or child.name in {"default", "webui"}:
|
|
74
|
+
continue
|
|
75
|
+
latest, count = _path_activity_score(child)
|
|
76
|
+
if count:
|
|
77
|
+
candidates.append((latest, child.name))
|
|
78
|
+
if not candidates:
|
|
79
|
+
return ""
|
|
80
|
+
return max(candidates)[1]
|
|
81
|
+
|
|
82
|
+
|
|
29
83
|
def resolve_active_user_id(explicit: Any = None, self_ref: Any = None,
|
|
30
84
|
dashboard_state: Optional[Dict[str, Any]] = None) -> str:
|
|
31
85
|
if explicit:
|
|
32
86
|
return normalize_user_id(explicit)
|
|
33
87
|
|
|
34
88
|
dashboard_state = dashboard_state or {}
|
|
35
|
-
|
|
89
|
+
active = dashboard_state.get("active_user")
|
|
90
|
+
if active and normalize_user_id(active) not in {"default", "webui"}:
|
|
36
91
|
return normalize_user_id(dashboard_state["active_user"])
|
|
37
92
|
|
|
38
|
-
|
|
39
|
-
if
|
|
40
|
-
return normalize_user_id(
|
|
93
|
+
tracked = _tracked_active_user_id()
|
|
94
|
+
if tracked:
|
|
95
|
+
return normalize_user_id(tracked)
|
|
41
96
|
|
|
42
|
-
owner =
|
|
97
|
+
owner = _configured_owner_id()
|
|
43
98
|
if owner:
|
|
44
99
|
return normalize_user_id(owner)
|
|
45
100
|
|
|
101
|
+
runtime_state = getattr(self_ref, "state", None)
|
|
102
|
+
runtime_user = getattr(runtime_state, "user_id", None) if runtime_state else None
|
|
103
|
+
if runtime_user and normalize_user_id(runtime_user) not in {"default", "webui"}:
|
|
104
|
+
return normalize_user_id(runtime_state.user_id)
|
|
105
|
+
|
|
106
|
+
disk_user = _most_active_disk_user_id()
|
|
107
|
+
if disk_user:
|
|
108
|
+
return normalize_user_id(disk_user)
|
|
109
|
+
|
|
110
|
+
if active:
|
|
111
|
+
return normalize_user_id(active)
|
|
112
|
+
|
|
113
|
+
if runtime_user:
|
|
114
|
+
return normalize_user_id(runtime_user)
|
|
115
|
+
|
|
46
116
|
return "webui"
|
|
47
117
|
|
|
48
118
|
|
|
@@ -134,19 +204,26 @@ def _load_journal(user_id: str) -> List[Dict[str, Any]]:
|
|
|
134
204
|
def _load_episodic_fallback(user_id: str, limit_turns: int) -> List[Dict[str, Any]]:
|
|
135
205
|
base = user_base(user_id) / "conversations"
|
|
136
206
|
legacy = data_dir() / "conversations"
|
|
137
|
-
|
|
138
|
-
if
|
|
207
|
+
conv_dirs = [base]
|
|
208
|
+
if legacy != base:
|
|
209
|
+
conv_dirs.append(legacy)
|
|
210
|
+
bot_prefixed = [p for p in (data_dir() / "users").glob(f"*_{normalize_user_id(user_id)}")
|
|
211
|
+
if (p / "conversations").exists()]
|
|
212
|
+
conv_dirs.extend(p / "conversations" for p in bot_prefixed)
|
|
213
|
+
|
|
214
|
+
existing_dirs = [p for p in conv_dirs if p.exists() and list(p.glob("*.jsonl"))]
|
|
215
|
+
if not existing_dirs:
|
|
139
216
|
return []
|
|
140
217
|
|
|
141
218
|
turns: List[Dict[str, Any]] = []
|
|
142
|
-
for
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
219
|
+
for conv_dir in existing_dirs:
|
|
220
|
+
for file in sorted(conv_dir.glob("*.jsonl"), reverse=True):
|
|
221
|
+
file_rows = _read_jsonl(file)
|
|
222
|
+
turns.extend(reversed(file_rows))
|
|
223
|
+
turns = sorted(turns, key=lambda row: row.get("timestamp", ""), reverse=True)[:limit_turns]
|
|
147
224
|
|
|
148
225
|
messages: List[Dict[str, Any]] = []
|
|
149
|
-
for row in reversed(turns
|
|
226
|
+
for row in reversed(turns):
|
|
150
227
|
ts = row.get("timestamp", "")
|
|
151
228
|
if row.get("user"):
|
|
152
229
|
messages.append(_format_entry({
|
|
@@ -168,7 +245,24 @@ def _load_episodic_fallback(user_id: str, limit_turns: int) -> List[Dict[str, An
|
|
|
168
245
|
|
|
169
246
|
|
|
170
247
|
def load_chat_messages(user_id: str, limit: int = 60) -> List[Dict[str, Any]]:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
248
|
+
if limit and limit > 0:
|
|
249
|
+
episodic_limit = max(1, limit // 2)
|
|
250
|
+
else:
|
|
251
|
+
episodic_limit = 1_000_000
|
|
252
|
+
messages = _load_episodic_fallback(user_id, episodic_limit)
|
|
253
|
+
messages.extend(_load_journal(user_id))
|
|
254
|
+
|
|
255
|
+
deduped: Dict[str, Dict[str, Any]] = {}
|
|
256
|
+
for msg in messages:
|
|
257
|
+
key = msg.get("message_id") or f"{msg.get('role')}:{msg.get('timestamp')}:{msg.get('content')}"
|
|
258
|
+
deduped[key] = msg
|
|
259
|
+
|
|
260
|
+
ordered = sorted(
|
|
261
|
+
deduped.values(),
|
|
262
|
+
key=lambda m: m.get("timestamp") or ""
|
|
263
|
+
)
|
|
264
|
+
return ordered[-limit:] if limit and limit > 0 else ordered
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def count_visible_messages(user_id: str) -> int:
|
|
268
|
+
return len(load_chat_messages(user_id, limit=0))
|
package/webui/static/index.html
CHANGED
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
html, body {
|
|
41
41
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
42
42
|
background: var(--bg-primary);
|
|
43
|
-
|
|
44
|
-
min-height:
|
|
43
|
+
height: 100%;
|
|
44
|
+
min-height: 100%;
|
|
45
45
|
color: var(--text-primary);
|
|
46
|
-
overflow
|
|
46
|
+
overflow: hidden;
|
|
47
47
|
-webkit-font-smoothing: antialiased;
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -70,12 +70,14 @@
|
|
|
70
70
|
|
|
71
71
|
/* Mobile App Shell */
|
|
72
72
|
.app {
|
|
73
|
-
|
|
73
|
+
height: 100vh;
|
|
74
|
+
height: 100dvh;
|
|
74
75
|
min-height: -webkit-fill-available;
|
|
75
76
|
display: flex;
|
|
76
77
|
flex-direction: column;
|
|
77
78
|
padding-top: var(--safe-top);
|
|
78
79
|
padding-bottom: var(--safe-bottom);
|
|
80
|
+
overflow: hidden;
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
/* Header */
|
|
@@ -170,6 +172,7 @@
|
|
|
170
172
|
/* Main Content */
|
|
171
173
|
.main {
|
|
172
174
|
flex: 1;
|
|
175
|
+
min-height: 0;
|
|
173
176
|
overflow-y: auto;
|
|
174
177
|
padding: 0 16px 100px;
|
|
175
178
|
max-width: 600px;
|
|
@@ -683,6 +686,8 @@
|
|
|
683
686
|
border-radius: 24px;
|
|
684
687
|
margin-top: 20px;
|
|
685
688
|
margin-bottom: 20px;
|
|
689
|
+
height: calc(100dvh - 40px);
|
|
690
|
+
min-height: 0;
|
|
686
691
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
|
687
692
|
overflow: hidden;
|
|
688
693
|
background: var(--bg-secondary);
|
|
@@ -1078,7 +1083,7 @@
|
|
|
1078
1083
|
padding-bottom: 0;
|
|
1079
1084
|
display: flex;
|
|
1080
1085
|
flex-direction: column;
|
|
1081
|
-
height:
|
|
1086
|
+
height: auto;
|
|
1082
1087
|
min-height: 0;
|
|
1083
1088
|
}
|
|
1084
1089
|
|
|
@@ -1330,6 +1335,8 @@
|
|
|
1330
1335
|
bottom: 80px;
|
|
1331
1336
|
left: 50%;
|
|
1332
1337
|
transform: translateX(-50%) translateY(100px);
|
|
1338
|
+
opacity: 0;
|
|
1339
|
+
visibility: hidden;
|
|
1333
1340
|
background: rgba(26, 26, 46, 0.95);
|
|
1334
1341
|
border: 1px solid var(--accent-pink);
|
|
1335
1342
|
box-shadow: 0 4px 20px var(--glow-pink);
|
|
@@ -1339,11 +1346,13 @@
|
|
|
1339
1346
|
font-size: 0.9rem;
|
|
1340
1347
|
font-weight: 600;
|
|
1341
1348
|
z-index: 2000;
|
|
1342
|
-
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
1349
|
+
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.2s ease, visibility 0.2s ease;
|
|
1343
1350
|
pointer-events: none;
|
|
1344
1351
|
}
|
|
1345
1352
|
.toast-container.show {
|
|
1346
1353
|
transform: translateX(-50%) translateY(0);
|
|
1354
|
+
opacity: 1;
|
|
1355
|
+
visibility: visible;
|
|
1347
1356
|
}
|
|
1348
1357
|
</style>
|
|
1349
1358
|
</head>
|
|
@@ -1742,7 +1751,7 @@
|
|
|
1742
1751
|
<div class="circadian-debt">
|
|
1743
1752
|
<div class="circadian-debt-label">
|
|
1744
1753
|
<span>Sleep Debt</span>
|
|
1745
|
-
<span id="circadian-debt-val">0
|
|
1754
|
+
<span id="circadian-debt-val">0.0h</span>
|
|
1746
1755
|
</div>
|
|
1747
1756
|
<div class="circadian-debt-bar">
|
|
1748
1757
|
<div class="circadian-debt-fill" id="circadian-debt-bar" style="width:0%"></div>
|
|
@@ -1873,7 +1882,7 @@
|
|
|
1873
1882
|
</div>
|
|
1874
1883
|
</div>
|
|
1875
1884
|
</div>
|
|
1876
|
-
<div class="chat-input-container" style="display:flex; gap:10px; padding:10px 0; background:var(--bg-
|
|
1885
|
+
<div class="chat-input-container" style="display:flex; gap:10px; padding:10px 0; background:var(--bg-secondary); border-top:1px solid rgba(255,255,255,0.05); align-items: flex-end;">
|
|
1877
1886
|
<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; max-height:120px; overflow-y:auto;"></textarea>
|
|
1878
1887
|
<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>
|
|
1879
1888
|
</div>
|
|
@@ -2577,9 +2586,10 @@
|
|
|
2577
2586
|
badge.className = 'circadian-sleep-badge ' + (sleeping ? 'sleeping' : 'awake');
|
|
2578
2587
|
document.getElementById('circadian-sleep-text').textContent = sleeping ? 'Sleeping' : 'Awake';
|
|
2579
2588
|
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
document.getElementById('circadian-debt-
|
|
2589
|
+
const debtHours = clampNumber(c.sleep_debt, 0, 8);
|
|
2590
|
+
const debtFill = toPct(debtHours, 0, 8);
|
|
2591
|
+
document.getElementById('circadian-debt-val').textContent = debtHours.toFixed(1) + 'h';
|
|
2592
|
+
document.getElementById('circadian-debt-bar').style.width = debtFill + '%';
|
|
2583
2593
|
|
|
2584
2594
|
const mods = c.modifiers || {};
|
|
2585
2595
|
['energy', 'inhibition', 'warmth', 'verbosity'].forEach(m => {
|