alive-ai 0.1.11 → 0.1.13
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 +6 -0
- package/brain/narrative.py +1 -0
- package/cli/check_webui_static.js +29 -0
- package/main.py +1 -1
- package/package.json +2 -2
- package/pyproject.toml +1 -1
- package/webui/app.py +113 -3
- package/webui/bridge.py +16 -1
- package/webui/static/icon.svg +7 -0
- package/webui/static/index.html +594 -3
- package/webui/static/manifest.json +18 -0
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
|
|
@@ -273,6 +277,8 @@ The real WebUI streams local runtime state over Server-Sent Events and shows:
|
|
|
273
277
|
- attachment, circadian rhythm, sleepiness, body memory, dreams, curiosity, and conflicts,
|
|
274
278
|
- runtime health through local endpoints.
|
|
275
279
|
|
|
280
|
+
The WebUI script is intentionally shipped as a single static file because the npm package has to run locally without a frontend build step. `npm run smoke` compiles the Python modules and checks the CLI; release validation also parses the embedded dashboard script so tab navigation, chat, settings, and thought rendering cannot be broken by a syntax error.
|
|
281
|
+
|
|
276
282
|
GitHub Pages cannot run the Python/FastAPI backend, so the public page includes a static export of the actual WebUI with mocked state:
|
|
277
283
|
|
|
278
284
|
```text
|
package/brain/narrative.py
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
const root = path.resolve(__dirname, "..");
|
|
6
|
+
const htmlPath = path.join(root, "webui", "static", "index.html");
|
|
7
|
+
const manifestPath = path.join(root, "webui", "static", "manifest.json");
|
|
8
|
+
|
|
9
|
+
const html = fs.readFileSync(htmlPath, "utf8");
|
|
10
|
+
const scriptRegex = /<script\b[^>]*>([\s\S]*?)<\/script>/gi;
|
|
11
|
+
let count = 0;
|
|
12
|
+
for (const match of html.matchAll(scriptRegex)) {
|
|
13
|
+
count += 1;
|
|
14
|
+
try {
|
|
15
|
+
new Function(match[1]);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error(`Invalid inline script #${count} in ${path.relative(root, htmlPath)}:`);
|
|
18
|
+
console.error(error.message);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (count === 0) {
|
|
24
|
+
console.error(`No inline scripts found in ${path.relative(root, htmlPath)}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
29
|
+
console.log(`WebUI static check passed (${count} inline script parsed).`);
|
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.
|
|
3
|
+
"version": "0.1.13",
|
|
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/",
|
|
@@ -57,6 +57,6 @@
|
|
|
57
57
|
"node": ">=18"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
|
-
"smoke": "node cli/index.js --help && python3 -m compileall -q alive_ai brain core heart input output skills webui"
|
|
60
|
+
"smoke": "node cli/index.js --help && node cli/check_webui_static.js && python3 -m compileall -q alive_ai brain core heart input output skills webui"
|
|
61
61
|
}
|
|
62
62
|
}
|
package/pyproject.toml
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
|
|
|
@@ -338,6 +348,13 @@ async def dashboard():
|
|
|
338
348
|
return HTMLResponse(content=html_path.read_text())
|
|
339
349
|
|
|
340
350
|
|
|
351
|
+
@app.get("/favicon.ico")
|
|
352
|
+
async def favicon():
|
|
353
|
+
"""Serve the dashboard icon for browsers that still request /favicon.ico."""
|
|
354
|
+
icon_path = Path(__file__).parent / "static" / "icon.svg"
|
|
355
|
+
return FileResponse(icon_path, media_type="image/svg+xml")
|
|
356
|
+
|
|
357
|
+
|
|
341
358
|
@app.get("/events")
|
|
342
359
|
async def sse_events(request: Request):
|
|
343
360
|
"""SSE endpoint for real-time updates"""
|
|
@@ -834,9 +851,24 @@ async def get_new_aliveness():
|
|
|
834
851
|
try:
|
|
835
852
|
from brain.narrative import get_narrative_engine
|
|
836
853
|
ne = get_narrative_engine()
|
|
837
|
-
# Get owner's narrative
|
|
838
854
|
from core.settings import get as settings_get
|
|
839
855
|
owner_id = str(settings_get("TELEGRAM_OWNER_ID", ""))
|
|
856
|
+
|
|
857
|
+
# Fallback: when owner_id is empty (terminal mode), find the most active user
|
|
858
|
+
if not owner_id:
|
|
859
|
+
try:
|
|
860
|
+
users_path = data_dir() / "users"
|
|
861
|
+
if users_path.exists():
|
|
862
|
+
candidates = [d.name for d in users_path.iterdir() if d.is_dir()]
|
|
863
|
+
if candidates:
|
|
864
|
+
# Pick the user with the most recent narrative file
|
|
865
|
+
def _narr_mtime(uid):
|
|
866
|
+
p = data_dir() / "users" / uid / "narrative.json"
|
|
867
|
+
return p.stat().st_mtime if p.exists() else 0
|
|
868
|
+
owner_id = max(candidates, key=_narr_mtime)
|
|
869
|
+
except Exception:
|
|
870
|
+
pass
|
|
871
|
+
|
|
840
872
|
if owner_id:
|
|
841
873
|
data = ne._get_data(owner_id)
|
|
842
874
|
msg_count = data.get("message_count", 0)
|
|
@@ -895,6 +927,21 @@ async def get_new_aliveness():
|
|
|
895
927
|
from brain.curiosity import get_curiosity_drive
|
|
896
928
|
from core.settings import get as settings_get
|
|
897
929
|
owner_id = str(settings_get("TELEGRAM_OWNER_ID", ""))
|
|
930
|
+
|
|
931
|
+
# Fallback: when owner_id is empty (terminal mode), find the most active user
|
|
932
|
+
if not owner_id:
|
|
933
|
+
try:
|
|
934
|
+
users_path = data_dir() / "users"
|
|
935
|
+
if users_path.exists():
|
|
936
|
+
candidates = [d.name for d in users_path.iterdir() if d.is_dir()]
|
|
937
|
+
if candidates:
|
|
938
|
+
def _cur_mtime(uid):
|
|
939
|
+
p = data_dir() / "users" / uid / "curiosity.json"
|
|
940
|
+
return p.stat().st_mtime if p.exists() else 0
|
|
941
|
+
owner_id = max(candidates, key=_cur_mtime)
|
|
942
|
+
except Exception:
|
|
943
|
+
pass
|
|
944
|
+
|
|
898
945
|
if owner_id:
|
|
899
946
|
cd = get_curiosity_drive(owner_id)
|
|
900
947
|
topics = {t: round(v, 2) for t, v in cd.knowledge.items()} if hasattr(cd, 'knowledge') else {}
|
|
@@ -915,6 +962,69 @@ async def get_new_aliveness():
|
|
|
915
962
|
return result
|
|
916
963
|
|
|
917
964
|
|
|
965
|
+
@app.post("/api/chat")
|
|
966
|
+
async def chat_endpoint(request: Request, background_tasks: BackgroundTasks):
|
|
967
|
+
data = await request.json()
|
|
968
|
+
text = data.get("text", "").strip()
|
|
969
|
+
if not text or not _self_ref:
|
|
970
|
+
return JSONResponse({"status": "error", "message": "No text or AI not ready"}, 400)
|
|
971
|
+
# Add user message immediately to conversation
|
|
972
|
+
add_conversation("user", text)
|
|
973
|
+
update_state({})
|
|
974
|
+
# Fire message handler in background
|
|
975
|
+
async def _send():
|
|
976
|
+
from core.message_handler import handle_message
|
|
977
|
+
await handle_message(_self_ref, {
|
|
978
|
+
"user_id": "webui",
|
|
979
|
+
"text": text,
|
|
980
|
+
"chat_id": "webui",
|
|
981
|
+
"source": "webui"
|
|
982
|
+
})
|
|
983
|
+
background_tasks.add_task(_send)
|
|
984
|
+
return JSONResponse({"status": "sent"})
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
@app.get("/api/settings")
|
|
988
|
+
async def get_settings():
|
|
989
|
+
import json
|
|
990
|
+
from pathlib import Path
|
|
991
|
+
config_dir = Path(os.environ.get("ALIVE_AI_ROOT", ".")) / "config"
|
|
992
|
+
result = {}
|
|
993
|
+
for fname in ["settings.json", "self.json", "directives.json"]:
|
|
994
|
+
p = config_dir / fname
|
|
995
|
+
if p.exists():
|
|
996
|
+
try:
|
|
997
|
+
result[fname] = {"type": "json", "content": json.loads(p.read_text())}
|
|
998
|
+
except Exception:
|
|
999
|
+
result[fname] = {"type": "json", "content": {}}
|
|
1000
|
+
p = config_dir / "instructions.md"
|
|
1001
|
+
if p.exists():
|
|
1002
|
+
result["instructions.md"] = {"type": "markdown", "content": p.read_text()}
|
|
1003
|
+
return result
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
@app.post("/api/settings")
|
|
1007
|
+
async def save_settings(request: Request):
|
|
1008
|
+
import json
|
|
1009
|
+
from pathlib import Path
|
|
1010
|
+
data = await request.json()
|
|
1011
|
+
fname = data.get("file", "")
|
|
1012
|
+
allowed = {"settings.json", "self.json", "directives.json", "instructions.md"}
|
|
1013
|
+
if fname not in allowed:
|
|
1014
|
+
return JSONResponse({"status": "error", "message": "Invalid file"}, 400)
|
|
1015
|
+
config_dir = Path(os.environ.get("ALIVE_AI_ROOT", ".")) / "config"
|
|
1016
|
+
p = config_dir / fname
|
|
1017
|
+
content = data.get("content")
|
|
1018
|
+
try:
|
|
1019
|
+
if fname.endswith(".json"):
|
|
1020
|
+
p.write_text(json.dumps(content, indent=2, ensure_ascii=False))
|
|
1021
|
+
else:
|
|
1022
|
+
p.write_text(content)
|
|
1023
|
+
return {"status": "saved"}
|
|
1024
|
+
except Exception as e:
|
|
1025
|
+
return JSONResponse({"status": "error", "message": str(e)}, 500)
|
|
1026
|
+
|
|
1027
|
+
|
|
918
1028
|
# Mount static files
|
|
919
1029
|
static_path = Path(__file__).parent / "static"
|
|
920
1030
|
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)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
|
2
|
+
<rect width="128" height="128" rx="28" fill="#10121f"/>
|
|
3
|
+
<circle cx="64" cy="64" r="42" fill="none" stroke="#00d4ff" stroke-width="8"/>
|
|
4
|
+
<path d="M39 72c9 14 41 14 50 0" fill="none" stroke="#ff4d9d" stroke-width="8" stroke-linecap="round"/>
|
|
5
|
+
<circle cx="49" cy="53" r="7" fill="#f7f7fb"/>
|
|
6
|
+
<circle cx="79" cy="53" r="7" fill="#f7f7fb"/>
|
|
7
|
+
</svg>
|
package/webui/static/index.html
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
6
6
|
<meta name="theme-color" content="#0f0f1a">
|
|
7
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
7
8
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
8
9
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
9
10
|
<title>Alive-AI</title>
|
|
11
|
+
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
|
|
10
12
|
<link rel="manifest" href="/static/manifest.json">
|
|
11
13
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
12
14
|
<style>
|
|
@@ -1064,6 +1066,277 @@
|
|
|
1064
1066
|
0% { background-position: 200% 0; }
|
|
1065
1067
|
100% { background-position: -200% 0; }
|
|
1066
1068
|
}
|
|
1069
|
+
|
|
1070
|
+
/* Multi-Page Dashboard & Tab Switching */
|
|
1071
|
+
.main.chat-active {
|
|
1072
|
+
overflow-y: hidden;
|
|
1073
|
+
padding-bottom: 0;
|
|
1074
|
+
display: flex;
|
|
1075
|
+
flex-direction: column;
|
|
1076
|
+
height: calc(100vh - 75px - var(--safe-top) - var(--safe-bottom) - 75px);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
.page-section {
|
|
1080
|
+
width: 100%;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/* Chat Page Styles */
|
|
1084
|
+
#page-chat {
|
|
1085
|
+
flex: 1;
|
|
1086
|
+
display: flex;
|
|
1087
|
+
flex-direction: column;
|
|
1088
|
+
height: 100%;
|
|
1089
|
+
position: relative;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
.chat-messages {
|
|
1093
|
+
flex: 1;
|
|
1094
|
+
overflow-y: auto;
|
|
1095
|
+
padding: 10px 0;
|
|
1096
|
+
display: flex;
|
|
1097
|
+
flex-direction: column;
|
|
1098
|
+
gap: 12px;
|
|
1099
|
+
scroll-behavior: smooth;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/* Scrollbar styling for chat */
|
|
1103
|
+
.chat-messages::-webkit-scrollbar {
|
|
1104
|
+
width: 6px;
|
|
1105
|
+
}
|
|
1106
|
+
.chat-messages::-webkit-scrollbar-track {
|
|
1107
|
+
background: transparent;
|
|
1108
|
+
}
|
|
1109
|
+
.chat-messages::-webkit-scrollbar-thumb {
|
|
1110
|
+
background: rgba(255, 255, 255, 0.1);
|
|
1111
|
+
border-radius: 3px;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
.chat-msg-row {
|
|
1115
|
+
display: flex;
|
|
1116
|
+
width: 100%;
|
|
1117
|
+
margin-bottom: 4px;
|
|
1118
|
+
}
|
|
1119
|
+
.chat-msg-row.user {
|
|
1120
|
+
justify-content: flex-end;
|
|
1121
|
+
}
|
|
1122
|
+
.chat-msg-row.alive-ai {
|
|
1123
|
+
justify-content: flex-start;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
.chat-bubble {
|
|
1127
|
+
max-width: 80%;
|
|
1128
|
+
padding: 12px 16px;
|
|
1129
|
+
font-size: 0.95rem;
|
|
1130
|
+
line-height: 1.45;
|
|
1131
|
+
position: relative;
|
|
1132
|
+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
|
1133
|
+
transition: all 0.2s ease;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
.chat-msg-row.user .chat-bubble {
|
|
1137
|
+
background: linear-gradient(135deg, var(--accent-pink) 0%, #a55eea 100%);
|
|
1138
|
+
color: #fff;
|
|
1139
|
+
border-radius: 20px 20px 4px 20px;
|
|
1140
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
.chat-msg-row.alive-ai .chat-bubble {
|
|
1144
|
+
background: rgba(255, 255, 255, 0.04);
|
|
1145
|
+
backdrop-filter: blur(10px);
|
|
1146
|
+
-webkit-backdrop-filter: blur(10px);
|
|
1147
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
1148
|
+
color: var(--text-primary);
|
|
1149
|
+
border-radius: 20px 20px 20px 4px;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
.chat-bubble-time {
|
|
1153
|
+
font-size: 0.7rem;
|
|
1154
|
+
color: rgba(255, 255, 255, 0.5);
|
|
1155
|
+
margin-top: 4px;
|
|
1156
|
+
text-align: right;
|
|
1157
|
+
display: block;
|
|
1158
|
+
}
|
|
1159
|
+
.chat-msg-row.alive-ai .chat-bubble-time {
|
|
1160
|
+
color: var(--text-muted);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/* Typing Dots Animation */
|
|
1164
|
+
.typing-indicator {
|
|
1165
|
+
display: inline-flex;
|
|
1166
|
+
align-items: center;
|
|
1167
|
+
gap: 4px;
|
|
1168
|
+
padding: 6px 12px;
|
|
1169
|
+
background: rgba(255, 255, 255, 0.03);
|
|
1170
|
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
1171
|
+
border-radius: 12px;
|
|
1172
|
+
margin-top: 4px;
|
|
1173
|
+
color: var(--accent-pink);
|
|
1174
|
+
font-size: 0.85rem;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
.typing-dots {
|
|
1178
|
+
display: flex;
|
|
1179
|
+
align-items: center;
|
|
1180
|
+
gap: 4px;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
.typing-dots span {
|
|
1184
|
+
width: 6px;
|
|
1185
|
+
height: 6px;
|
|
1186
|
+
background-color: var(--accent-pink);
|
|
1187
|
+
border-radius: 50%;
|
|
1188
|
+
display: inline-block;
|
|
1189
|
+
animation: bounce 1.4s infinite ease-in-out both;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
.typing-dots span:nth-child(1) { animation-delay: -0.32s; }
|
|
1193
|
+
.typing-dots span:nth-child(2) { animation-delay: -0.16s; }
|
|
1194
|
+
|
|
1195
|
+
@keyframes bounce {
|
|
1196
|
+
0%, 80%, 100% { transform: scale(0); }
|
|
1197
|
+
40% { transform: scale(1.0); }
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/* Settings CSS */
|
|
1201
|
+
.settings-card {
|
|
1202
|
+
margin-bottom: 20px;
|
|
1203
|
+
}
|
|
1204
|
+
.settings-header-btn-row {
|
|
1205
|
+
display: flex;
|
|
1206
|
+
justify-content: space-between;
|
|
1207
|
+
align-items: center;
|
|
1208
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
1209
|
+
padding-bottom: 10px;
|
|
1210
|
+
margin-bottom: 12px;
|
|
1211
|
+
}
|
|
1212
|
+
.settings-file-title {
|
|
1213
|
+
font-size: 1.1rem;
|
|
1214
|
+
font-weight: 700;
|
|
1215
|
+
color: var(--accent-pink);
|
|
1216
|
+
}
|
|
1217
|
+
.settings-grid {
|
|
1218
|
+
display: flex;
|
|
1219
|
+
flex-direction: column;
|
|
1220
|
+
gap: 12px;
|
|
1221
|
+
}
|
|
1222
|
+
.settings-field-row {
|
|
1223
|
+
display: flex;
|
|
1224
|
+
flex-direction: column;
|
|
1225
|
+
gap: 6px;
|
|
1226
|
+
}
|
|
1227
|
+
.settings-field-label {
|
|
1228
|
+
font-size: 0.8rem;
|
|
1229
|
+
font-weight: 600;
|
|
1230
|
+
color: var(--text-secondary);
|
|
1231
|
+
text-transform: uppercase;
|
|
1232
|
+
letter-spacing: 0.5px;
|
|
1233
|
+
}
|
|
1234
|
+
.settings-input {
|
|
1235
|
+
width: 100%;
|
|
1236
|
+
background: rgba(0, 0, 0, 0.2);
|
|
1237
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
1238
|
+
border-radius: 8px;
|
|
1239
|
+
padding: 10px 12px;
|
|
1240
|
+
color: #fff;
|
|
1241
|
+
font-family: inherit;
|
|
1242
|
+
font-size: 0.9rem;
|
|
1243
|
+
outline: none;
|
|
1244
|
+
transition: all 0.2s;
|
|
1245
|
+
}
|
|
1246
|
+
.settings-input:focus {
|
|
1247
|
+
border-color: var(--accent-pink);
|
|
1248
|
+
box-shadow: 0 0 8px var(--glow-pink);
|
|
1249
|
+
}
|
|
1250
|
+
.settings-checkbox-container {
|
|
1251
|
+
display: flex;
|
|
1252
|
+
align-items: center;
|
|
1253
|
+
gap: 10px;
|
|
1254
|
+
cursor: pointer;
|
|
1255
|
+
padding: 4px 0;
|
|
1256
|
+
}
|
|
1257
|
+
.settings-checkbox-container input {
|
|
1258
|
+
cursor: pointer;
|
|
1259
|
+
width: 18px;
|
|
1260
|
+
height: 18px;
|
|
1261
|
+
accent-color: var(--accent-pink);
|
|
1262
|
+
}
|
|
1263
|
+
.settings-checkbox-label {
|
|
1264
|
+
font-size: 0.9rem;
|
|
1265
|
+
font-weight: 500;
|
|
1266
|
+
color: var(--text-primary);
|
|
1267
|
+
}
|
|
1268
|
+
.settings-textarea {
|
|
1269
|
+
width: 100%;
|
|
1270
|
+
background: rgba(0, 0, 0, 0.2);
|
|
1271
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
1272
|
+
border-radius: 8px;
|
|
1273
|
+
padding: 10px 12px;
|
|
1274
|
+
color: #fff;
|
|
1275
|
+
font-family: 'Courier New', Courier, monospace;
|
|
1276
|
+
font-size: 0.85rem;
|
|
1277
|
+
line-height: 1.4;
|
|
1278
|
+
min-height: 120px;
|
|
1279
|
+
resize: vertical;
|
|
1280
|
+
outline: none;
|
|
1281
|
+
transition: all 0.2s;
|
|
1282
|
+
}
|
|
1283
|
+
.settings-textarea:focus {
|
|
1284
|
+
border-color: var(--accent-pink);
|
|
1285
|
+
box-shadow: 0 0 8px var(--glow-pink);
|
|
1286
|
+
}
|
|
1287
|
+
.btn-action {
|
|
1288
|
+
background: rgba(255, 255, 255, 0.05);
|
|
1289
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
1290
|
+
color: #fff;
|
|
1291
|
+
padding: 6px 12px;
|
|
1292
|
+
border-radius: 8px;
|
|
1293
|
+
font-size: 0.85rem;
|
|
1294
|
+
font-weight: 600;
|
|
1295
|
+
cursor: pointer;
|
|
1296
|
+
transition: all 0.2s;
|
|
1297
|
+
}
|
|
1298
|
+
.btn-action:hover {
|
|
1299
|
+
background: var(--accent-pink);
|
|
1300
|
+
border-color: var(--accent-pink);
|
|
1301
|
+
box-shadow: 0 0 10px var(--glow-pink);
|
|
1302
|
+
}
|
|
1303
|
+
.btn-save-all {
|
|
1304
|
+
background: linear-gradient(135deg, var(--accent-pink) 0%, #a55eea 100%);
|
|
1305
|
+
border: none;
|
|
1306
|
+
color: #fff;
|
|
1307
|
+
padding: 8px 16px;
|
|
1308
|
+
border-radius: 8px;
|
|
1309
|
+
font-size: 0.85rem;
|
|
1310
|
+
font-weight: 700;
|
|
1311
|
+
cursor: pointer;
|
|
1312
|
+
transition: all 0.2s;
|
|
1313
|
+
}
|
|
1314
|
+
.btn-save-all:hover {
|
|
1315
|
+
transform: translateY(-1px);
|
|
1316
|
+
box-shadow: 0 0 15px var(--glow-pink);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/* Toast notifications */
|
|
1320
|
+
.toast-container {
|
|
1321
|
+
position: fixed;
|
|
1322
|
+
bottom: 80px;
|
|
1323
|
+
left: 50%;
|
|
1324
|
+
transform: translateX(-50%) translateY(100px);
|
|
1325
|
+
background: rgba(26, 26, 46, 0.95);
|
|
1326
|
+
border: 1px solid var(--accent-pink);
|
|
1327
|
+
box-shadow: 0 4px 20px var(--glow-pink);
|
|
1328
|
+
color: #fff;
|
|
1329
|
+
padding: 12px 24px;
|
|
1330
|
+
border-radius: 30px;
|
|
1331
|
+
font-size: 0.9rem;
|
|
1332
|
+
font-weight: 600;
|
|
1333
|
+
z-index: 2000;
|
|
1334
|
+
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
1335
|
+
pointer-events: none;
|
|
1336
|
+
}
|
|
1337
|
+
.toast-container.show {
|
|
1338
|
+
transform: translateX(-50%) translateY(0);
|
|
1339
|
+
}
|
|
1067
1340
|
</style>
|
|
1068
1341
|
</head>
|
|
1069
1342
|
<body>
|
|
@@ -1092,7 +1365,8 @@
|
|
|
1092
1365
|
</header>
|
|
1093
1366
|
|
|
1094
1367
|
<main class="main">
|
|
1095
|
-
|
|
1368
|
+
<div id="page-home" class="page-section">
|
|
1369
|
+
<!-- Stats Row -->
|
|
1096
1370
|
<div class="stats-row">
|
|
1097
1371
|
<div class="stat-card">
|
|
1098
1372
|
<div class="stat-value" id="stat-messages">0</div>
|
|
@@ -1578,8 +1852,34 @@
|
|
|
1578
1852
|
<span class="tendency-badge" id="tendency-badge">neutral</span>
|
|
1579
1853
|
</div>
|
|
1580
1854
|
</div>
|
|
1855
|
+
</div> <!-- #page-home -->
|
|
1856
|
+
|
|
1857
|
+
<!-- CHAT PAGE -->
|
|
1858
|
+
<div id="page-chat" class="page-section" style="display:none; flex-direction:column; height:100%;">
|
|
1859
|
+
<div id="chat-messages" class="chat-messages"></div>
|
|
1860
|
+
<div id="chat-typing" style="display:none; padding:8px 0; color:var(--accent-pink); font-size:0.85rem;">
|
|
1861
|
+
<div class="typing-indicator">
|
|
1862
|
+
<span>she is thinking</span>
|
|
1863
|
+
<div class="typing-dots">
|
|
1864
|
+
<span></span><span></span><span></span>
|
|
1865
|
+
</div>
|
|
1866
|
+
</div>
|
|
1867
|
+
</div>
|
|
1868
|
+
<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;">
|
|
1869
|
+
<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>
|
|
1870
|
+
<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>
|
|
1871
|
+
</div>
|
|
1872
|
+
</div>
|
|
1873
|
+
|
|
1874
|
+
<!-- SETTINGS PAGE -->
|
|
1875
|
+
<div id="page-settings" class="page-section" style="display:none; flex-direction:column; gap:16px;">
|
|
1876
|
+
<div id="settings-content">Loading settings...</div>
|
|
1877
|
+
</div>
|
|
1581
1878
|
</main>
|
|
1582
1879
|
|
|
1880
|
+
<!-- Toast Notification Container -->
|
|
1881
|
+
<div id="toast-notification" class="toast-container">Settings saved successfully</div>
|
|
1882
|
+
|
|
1583
1883
|
<nav class="bottom-nav">
|
|
1584
1884
|
<div class="nav-item active">
|
|
1585
1885
|
<span class="nav-icon">🏠</span>
|
|
@@ -1603,6 +1903,291 @@
|
|
|
1603
1903
|
'angry': '😤', 'neutral': '😌', 'calm': '😌', 'loving': '💕'
|
|
1604
1904
|
};
|
|
1605
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
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Alive-AI WebUI",
|
|
3
|
+
"short_name": "Alive-AI",
|
|
4
|
+
"description": "Local dashboard for Alive-AI runtime state, chat, thoughts, emotions, and settings.",
|
|
5
|
+
"start_url": "/",
|
|
6
|
+
"scope": "/",
|
|
7
|
+
"display": "standalone",
|
|
8
|
+
"background_color": "#10121f",
|
|
9
|
+
"theme_color": "#00d4ff",
|
|
10
|
+
"icons": [
|
|
11
|
+
{
|
|
12
|
+
"src": "/static/icon.svg",
|
|
13
|
+
"sizes": "any",
|
|
14
|
+
"type": "image/svg+xml",
|
|
15
|
+
"purpose": "any maskable"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|