Upload folder using huggingface_hub
Browse files- web/agent_wrapper.py +17 -8
- web/routes/api.py +9 -0
- web/routes/websocket.py +21 -3
- web/static/css/style.css +85 -0
- web/static/js/chat.js +105 -1
- web/templates/index.html +19 -0
web/agent_wrapper.py
CHANGED
|
@@ -37,12 +37,13 @@ class AgentSession:
|
|
| 37 |
Manages a single agent session with streaming support.
|
| 38 |
"""
|
| 39 |
|
| 40 |
-
def __init__(self):
|
| 41 |
self._agent = None
|
| 42 |
self._repl_tool: Optional[PythonREPLTool] = None
|
| 43 |
self._messages: List[Dict] = []
|
| 44 |
self._initialized = False
|
| 45 |
-
|
|
|
|
| 46 |
# Global singleton keeps the dataset cache (shared across sessions)
|
| 47 |
self._memory = get_memory()
|
| 48 |
# Per-session conversation memory — never touches other sessions
|
|
@@ -57,10 +58,17 @@ class AgentSession:
|
|
| 57 |
"""Initialize the agent and tools."""
|
| 58 |
logger.info("Initializing agent session...")
|
| 59 |
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
logger.warning("ARRAYLAKE_API_KEY not found")
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
-
if not
|
| 64 |
logger.error("OPENAI_API_KEY not found")
|
| 65 |
return
|
| 66 |
|
|
@@ -82,11 +90,12 @@ class AgentSession:
|
|
| 82 |
# Replace the default REPL with our configured one
|
| 83 |
tools = [t for t in tools if t.name != "python_repl"] + [self._repl_tool]
|
| 84 |
|
| 85 |
-
# Initialize LLM
|
| 86 |
logger.info("Connecting to LLM...")
|
| 87 |
llm = ChatOpenAI(
|
| 88 |
model=CONFIG.model_name,
|
| 89 |
-
temperature=CONFIG.temperature
|
|
|
|
| 90 |
)
|
| 91 |
|
| 92 |
# Use session-local memory for datasets (NOT global!)
|
|
@@ -254,12 +263,12 @@ class AgentSession:
|
|
| 254 |
_sessions: Dict[str, AgentSession] = {}
|
| 255 |
|
| 256 |
|
| 257 |
-
def create_session(connection_id: str) -> AgentSession:
|
| 258 |
"""Create a new session for a connection."""
|
| 259 |
if connection_id in _sessions:
|
| 260 |
# Close existing session first
|
| 261 |
_sessions[connection_id].close()
|
| 262 |
-
session = AgentSession()
|
| 263 |
_sessions[connection_id] = session
|
| 264 |
logger.info(f"Created session for connection: {connection_id}")
|
| 265 |
return session
|
|
|
|
| 37 |
Manages a single agent session with streaming support.
|
| 38 |
"""
|
| 39 |
|
| 40 |
+
def __init__(self, api_keys: Optional[Dict[str, str]] = None):
|
| 41 |
self._agent = None
|
| 42 |
self._repl_tool: Optional[PythonREPLTool] = None
|
| 43 |
self._messages: List[Dict] = []
|
| 44 |
self._initialized = False
|
| 45 |
+
self._api_keys = api_keys or {}
|
| 46 |
+
|
| 47 |
# Global singleton keeps the dataset cache (shared across sessions)
|
| 48 |
self._memory = get_memory()
|
| 49 |
# Per-session conversation memory — never touches other sessions
|
|
|
|
| 58 |
"""Initialize the agent and tools."""
|
| 59 |
logger.info("Initializing agent session...")
|
| 60 |
|
| 61 |
+
# Resolve API keys: user-provided take priority over env vars
|
| 62 |
+
openai_key = self._api_keys.get("openai_api_key") or os.environ.get("OPENAI_API_KEY")
|
| 63 |
+
arraylake_key = self._api_keys.get("arraylake_api_key") or os.environ.get("ARRAYLAKE_API_KEY")
|
| 64 |
+
|
| 65 |
+
if not arraylake_key:
|
| 66 |
logger.warning("ARRAYLAKE_API_KEY not found")
|
| 67 |
+
else:
|
| 68 |
+
# Set in env so retrieval tools can pick it up
|
| 69 |
+
os.environ["ARRAYLAKE_API_KEY"] = arraylake_key
|
| 70 |
|
| 71 |
+
if not openai_key:
|
| 72 |
logger.error("OPENAI_API_KEY not found")
|
| 73 |
return
|
| 74 |
|
|
|
|
| 90 |
# Replace the default REPL with our configured one
|
| 91 |
tools = [t for t in tools if t.name != "python_repl"] + [self._repl_tool]
|
| 92 |
|
| 93 |
+
# Initialize LLM with resolved key
|
| 94 |
logger.info("Connecting to LLM...")
|
| 95 |
llm = ChatOpenAI(
|
| 96 |
model=CONFIG.model_name,
|
| 97 |
+
temperature=CONFIG.temperature,
|
| 98 |
+
api_key=openai_key,
|
| 99 |
)
|
| 100 |
|
| 101 |
# Use session-local memory for datasets (NOT global!)
|
|
|
|
| 263 |
_sessions: Dict[str, AgentSession] = {}
|
| 264 |
|
| 265 |
|
| 266 |
+
def create_session(connection_id: str, api_keys: Optional[Dict[str, str]] = None) -> AgentSession:
|
| 267 |
"""Create a new session for a connection."""
|
| 268 |
if connection_id in _sessions:
|
| 269 |
# Close existing session first
|
| 270 |
_sessions[connection_id].close()
|
| 271 |
+
session = AgentSession(api_keys=api_keys)
|
| 272 |
_sessions[connection_id] = session
|
| 273 |
logger.info(f"Created session for connection: {connection_id}")
|
| 274 |
return session
|
web/routes/api.py
CHANGED
|
@@ -51,6 +51,15 @@ class ConfigResponse(BaseModel):
|
|
| 51 |
model: str
|
| 52 |
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
@router.get("/health", response_model=HealthResponse)
|
| 55 |
async def health_check():
|
| 56 |
"""Check if the server and agent are healthy."""
|
|
|
|
| 51 |
model: str
|
| 52 |
|
| 53 |
|
| 54 |
+
@router.get("/keys-status")
|
| 55 |
+
async def keys_status():
|
| 56 |
+
"""Check which API keys are configured via environment variables."""
|
| 57 |
+
return {
|
| 58 |
+
"openai": bool(os.environ.get("OPENAI_API_KEY")),
|
| 59 |
+
"arraylake": bool(os.environ.get("ARRAYLAKE_API_KEY")),
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
|
| 63 |
@router.get("/health", response_model=HealthResponse)
|
| 64 |
async def health_check():
|
| 65 |
"""Check if the server and agent are healthy."""
|
web/routes/websocket.py
CHANGED
|
@@ -51,14 +51,32 @@ async def websocket_chat(websocket: WebSocket):
|
|
| 51 |
logger.info(f"New connection: {connection_id}")
|
| 52 |
|
| 53 |
try:
|
| 54 |
-
#
|
| 55 |
from web.agent_wrapper import create_session, get_session, close_session
|
| 56 |
-
session =
|
| 57 |
-
|
| 58 |
while True:
|
| 59 |
data = await websocket.receive_json()
|
| 60 |
message = data.get("message", "").strip()
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
if not message:
|
| 63 |
continue
|
| 64 |
|
|
|
|
| 51 |
logger.info(f"New connection: {connection_id}")
|
| 52 |
|
| 53 |
try:
|
| 54 |
+
# Session created lazily after we receive API keys
|
| 55 |
from web.agent_wrapper import create_session, get_session, close_session
|
| 56 |
+
session = None
|
| 57 |
+
|
| 58 |
while True:
|
| 59 |
data = await websocket.receive_json()
|
| 60 |
message = data.get("message", "").strip()
|
| 61 |
|
| 62 |
+
# Handle API key configuration from client
|
| 63 |
+
if data.get("type") == "configure_keys":
|
| 64 |
+
api_keys = {
|
| 65 |
+
"openai_api_key": data.get("openai_api_key", ""),
|
| 66 |
+
"arraylake_api_key": data.get("arraylake_api_key", ""),
|
| 67 |
+
}
|
| 68 |
+
session = create_session(connection_id, api_keys=api_keys)
|
| 69 |
+
ready = session.is_ready()
|
| 70 |
+
await manager.send_json(websocket, {
|
| 71 |
+
"type": "keys_configured",
|
| 72 |
+
"ready": ready,
|
| 73 |
+
})
|
| 74 |
+
continue
|
| 75 |
+
|
| 76 |
+
# Create default session if not yet created (keys from env)
|
| 77 |
+
if session is None:
|
| 78 |
+
session = create_session(connection_id)
|
| 79 |
+
|
| 80 |
if not message:
|
| 81 |
continue
|
| 82 |
|
web/static/css/style.css
CHANGED
|
@@ -766,4 +766,89 @@ dialog .close-modal:hover {
|
|
| 766 |
|
| 767 |
[data-theme="light"] ::selection {
|
| 768 |
background: rgba(26, 115, 232, 0.2);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 769 |
}
|
|
|
|
| 766 |
|
| 767 |
[data-theme="light"] ::selection {
|
| 768 |
background: rgba(26, 115, 232, 0.2);
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
/* ===== API KEYS PANEL ===== */
|
| 772 |
+
.api-keys-panel {
|
| 773 |
+
margin: 0 auto 16px;
|
| 774 |
+
max-width: 480px;
|
| 775 |
+
background: var(--bg-tertiary);
|
| 776 |
+
border: 1px solid var(--glass-border);
|
| 777 |
+
border-radius: 12px;
|
| 778 |
+
overflow: hidden;
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
+
.api-keys-header {
|
| 782 |
+
padding: 12px 16px;
|
| 783 |
+
font-weight: 600;
|
| 784 |
+
color: var(--text-primary);
|
| 785 |
+
background: var(--glass-bg);
|
| 786 |
+
border-bottom: 1px solid var(--glass-border);
|
| 787 |
+
}
|
| 788 |
+
|
| 789 |
+
.api-keys-body {
|
| 790 |
+
padding: 16px;
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
.api-keys-note {
|
| 794 |
+
font-size: 13px;
|
| 795 |
+
color: var(--text-secondary);
|
| 796 |
+
margin-bottom: 12px;
|
| 797 |
+
line-height: 1.4;
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
.api-key-field {
|
| 801 |
+
margin-bottom: 12px;
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
+
.api-key-field label {
|
| 805 |
+
display: block;
|
| 806 |
+
font-size: 13px;
|
| 807 |
+
font-weight: 500;
|
| 808 |
+
color: var(--text-secondary);
|
| 809 |
+
margin-bottom: 4px;
|
| 810 |
+
}
|
| 811 |
+
|
| 812 |
+
.api-key-field .required {
|
| 813 |
+
color: #ef4444;
|
| 814 |
+
}
|
| 815 |
+
|
| 816 |
+
.api-key-field input {
|
| 817 |
+
width: 100%;
|
| 818 |
+
padding: 8px 12px;
|
| 819 |
+
background: var(--bg-primary);
|
| 820 |
+
border: 1px solid var(--glass-border);
|
| 821 |
+
border-radius: 6px;
|
| 822 |
+
color: var(--text-primary);
|
| 823 |
+
font-family: monospace;
|
| 824 |
+
font-size: 13px;
|
| 825 |
+
box-sizing: border-box;
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
.api-key-field input:focus {
|
| 829 |
+
outline: none;
|
| 830 |
+
border-color: var(--accent-primary);
|
| 831 |
+
box-shadow: var(--focus-ring);
|
| 832 |
+
}
|
| 833 |
+
|
| 834 |
+
.save-keys-btn {
|
| 835 |
+
width: 100%;
|
| 836 |
+
padding: 10px;
|
| 837 |
+
background: var(--accent-primary);
|
| 838 |
+
color: #fff;
|
| 839 |
+
border: none;
|
| 840 |
+
border-radius: 6px;
|
| 841 |
+
font-size: 14px;
|
| 842 |
+
font-weight: 600;
|
| 843 |
+
cursor: pointer;
|
| 844 |
+
margin-top: 4px;
|
| 845 |
+
}
|
| 846 |
+
|
| 847 |
+
.save-keys-btn:hover {
|
| 848 |
+
opacity: 0.9;
|
| 849 |
+
}
|
| 850 |
+
|
| 851 |
+
.save-keys-btn:disabled {
|
| 852 |
+
opacity: 0.5;
|
| 853 |
+
cursor: not-allowed;
|
| 854 |
}
|
web/static/js/chat.js
CHANGED
|
@@ -8,6 +8,8 @@ class EurusChat {
|
|
| 8 |
this.messageId = 0;
|
| 9 |
this.currentAssistantMessage = null;
|
| 10 |
this.isConnected = false;
|
|
|
|
|
|
|
| 11 |
this.reconnectAttempts = 0;
|
| 12 |
this.maxReconnectAttempts = 5;
|
| 13 |
this.reconnectDelay = 1000;
|
|
@@ -20,6 +22,10 @@ class EurusChat {
|
|
| 20 |
this.clearBtn = document.getElementById('clear-btn');
|
| 21 |
this.cacheBtn = document.getElementById('cache-btn');
|
| 22 |
this.cacheModal = document.getElementById('cache-modal');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
marked.setOptions({
|
| 25 |
highlight: (code, lang) => {
|
|
@@ -37,10 +43,82 @@ class EurusChat {
|
|
| 37 |
}
|
| 38 |
|
| 39 |
init() {
|
|
|
|
| 40 |
this.connect();
|
| 41 |
this.setupEventListeners();
|
| 42 |
this.setupImageModal();
|
| 43 |
this.setupTheme();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
setupTheme() {
|
|
@@ -83,7 +161,21 @@ class EurusChat {
|
|
| 83 |
this.isConnected = true;
|
| 84 |
this.reconnectAttempts = 0;
|
| 85 |
this.updateConnectionStatus('connected');
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
};
|
| 88 |
|
| 89 |
this.ws.onclose = () => {
|
|
@@ -277,6 +369,18 @@ class EurusChat {
|
|
| 277 |
|
| 278 |
handleMessage(data) {
|
| 279 |
switch (data.type) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
case 'thinking':
|
| 281 |
this.showThinkingIndicator();
|
| 282 |
break;
|
|
|
|
| 8 |
this.messageId = 0;
|
| 9 |
this.currentAssistantMessage = null;
|
| 10 |
this.isConnected = false;
|
| 11 |
+
this.keysConfigured = false;
|
| 12 |
+
this.serverKeysPresent = { openai: false, arraylake: false };
|
| 13 |
this.reconnectAttempts = 0;
|
| 14 |
this.maxReconnectAttempts = 5;
|
| 15 |
this.reconnectDelay = 1000;
|
|
|
|
| 22 |
this.clearBtn = document.getElementById('clear-btn');
|
| 23 |
this.cacheBtn = document.getElementById('cache-btn');
|
| 24 |
this.cacheModal = document.getElementById('cache-modal');
|
| 25 |
+
this.apiKeysPanel = document.getElementById('api-keys-panel');
|
| 26 |
+
this.saveKeysBtn = document.getElementById('save-keys-btn');
|
| 27 |
+
this.openaiKeyInput = document.getElementById('openai-key');
|
| 28 |
+
this.arraylakeKeyInput = document.getElementById('arraylake-key');
|
| 29 |
|
| 30 |
marked.setOptions({
|
| 31 |
highlight: (code, lang) => {
|
|
|
|
| 43 |
}
|
| 44 |
|
| 45 |
init() {
|
| 46 |
+
this.checkKeysStatus();
|
| 47 |
this.connect();
|
| 48 |
this.setupEventListeners();
|
| 49 |
this.setupImageModal();
|
| 50 |
this.setupTheme();
|
| 51 |
+
this.setupKeysPanel();
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
async checkKeysStatus() {
|
| 55 |
+
try {
|
| 56 |
+
const resp = await fetch('/api/keys-status');
|
| 57 |
+
const data = await resp.json();
|
| 58 |
+
this.serverKeysPresent = data;
|
| 59 |
+
|
| 60 |
+
if (data.openai) {
|
| 61 |
+
// Keys pre-configured on server — hide the panel
|
| 62 |
+
this.apiKeysPanel.style.display = 'none';
|
| 63 |
+
this.keysConfigured = true;
|
| 64 |
+
} else {
|
| 65 |
+
// No server keys — check localStorage for saved keys
|
| 66 |
+
const savedOpenai = localStorage.getItem('eurus-openai-key');
|
| 67 |
+
const savedArraylake = localStorage.getItem('eurus-arraylake-key');
|
| 68 |
+
if (savedOpenai) {
|
| 69 |
+
this.openaiKeyInput.value = savedOpenai;
|
| 70 |
+
}
|
| 71 |
+
if (savedArraylake) {
|
| 72 |
+
this.arraylakeKeyInput.value = savedArraylake;
|
| 73 |
+
}
|
| 74 |
+
this.apiKeysPanel.style.display = 'block';
|
| 75 |
+
this.keysConfigured = false;
|
| 76 |
+
}
|
| 77 |
+
} catch (e) {
|
| 78 |
+
// Can't reach server yet, show panel
|
| 79 |
+
this.apiKeysPanel.style.display = 'block';
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
setupKeysPanel() {
|
| 84 |
+
this.saveKeysBtn.addEventListener('click', () => this.saveAndSendKeys());
|
| 85 |
+
|
| 86 |
+
// Allow Enter in key fields to submit
|
| 87 |
+
[this.openaiKeyInput, this.arraylakeKeyInput].forEach(input => {
|
| 88 |
+
input.addEventListener('keydown', (e) => {
|
| 89 |
+
if (e.key === 'Enter') {
|
| 90 |
+
e.preventDefault();
|
| 91 |
+
this.saveAndSendKeys();
|
| 92 |
+
}
|
| 93 |
+
});
|
| 94 |
+
});
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
saveAndSendKeys() {
|
| 98 |
+
const openaiKey = this.openaiKeyInput.value.trim();
|
| 99 |
+
const arraylakeKey = this.arraylakeKeyInput.value.trim();
|
| 100 |
+
|
| 101 |
+
if (!openaiKey) {
|
| 102 |
+
this.openaiKeyInput.focus();
|
| 103 |
+
return;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// Save to localStorage (client-side only)
|
| 107 |
+
localStorage.setItem('eurus-openai-key', openaiKey);
|
| 108 |
+
if (arraylakeKey) {
|
| 109 |
+
localStorage.setItem('eurus-arraylake-key', arraylakeKey);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// Send keys via WebSocket
|
| 113 |
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
| 114 |
+
this.saveKeysBtn.disabled = true;
|
| 115 |
+
this.saveKeysBtn.textContent = 'Connecting...';
|
| 116 |
+
this.ws.send(JSON.stringify({
|
| 117 |
+
type: 'configure_keys',
|
| 118 |
+
openai_api_key: openaiKey,
|
| 119 |
+
arraylake_api_key: arraylakeKey,
|
| 120 |
+
}));
|
| 121 |
+
}
|
| 122 |
}
|
| 123 |
|
| 124 |
setupTheme() {
|
|
|
|
| 161 |
this.isConnected = true;
|
| 162 |
this.reconnectAttempts = 0;
|
| 163 |
this.updateConnectionStatus('connected');
|
| 164 |
+
|
| 165 |
+
// If server has no keys, auto-send saved keys from localStorage
|
| 166 |
+
if (!this.serverKeysPresent.openai) {
|
| 167 |
+
const savedOpenai = localStorage.getItem('eurus-openai-key');
|
| 168 |
+
if (savedOpenai) {
|
| 169 |
+
const savedArraylake = localStorage.getItem('eurus-arraylake-key') || '';
|
| 170 |
+
this.ws.send(JSON.stringify({
|
| 171 |
+
type: 'configure_keys',
|
| 172 |
+
openai_api_key: savedOpenai,
|
| 173 |
+
arraylake_api_key: savedArraylake,
|
| 174 |
+
}));
|
| 175 |
+
}
|
| 176 |
+
} else {
|
| 177 |
+
this.sendBtn.disabled = false;
|
| 178 |
+
}
|
| 179 |
};
|
| 180 |
|
| 181 |
this.ws.onclose = () => {
|
|
|
|
| 369 |
|
| 370 |
handleMessage(data) {
|
| 371 |
switch (data.type) {
|
| 372 |
+
case 'keys_configured':
|
| 373 |
+
this.keysConfigured = data.ready;
|
| 374 |
+
if (data.ready) {
|
| 375 |
+
this.apiKeysPanel.style.display = 'none';
|
| 376 |
+
this.sendBtn.disabled = false;
|
| 377 |
+
} else {
|
| 378 |
+
this.saveKeysBtn.disabled = false;
|
| 379 |
+
this.saveKeysBtn.textContent = 'Connect';
|
| 380 |
+
this.showError('Failed to initialize agent. Check your API keys.');
|
| 381 |
+
}
|
| 382 |
+
break;
|
| 383 |
+
|
| 384 |
case 'thinking':
|
| 385 |
this.showThinkingIndicator();
|
| 386 |
break;
|
web/templates/index.html
CHANGED
|
@@ -4,6 +4,25 @@
|
|
| 4 |
|
| 5 |
{% block content %}
|
| 6 |
<div class="chat-container">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
<div id="chat-messages" class="chat-messages">
|
| 8 |
<div class="message system-message">
|
| 9 |
<h3>Welcome to Eurus</h3>
|
|
|
|
| 4 |
|
| 5 |
{% block content %}
|
| 6 |
<div class="chat-container">
|
| 7 |
+
<!-- API Keys panel — hidden when keys are pre-configured via env -->
|
| 8 |
+
<div id="api-keys-panel" class="api-keys-panel" style="display: none;">
|
| 9 |
+
<div class="api-keys-header">
|
| 10 |
+
<span>API Keys Required</span>
|
| 11 |
+
</div>
|
| 12 |
+
<div class="api-keys-body">
|
| 13 |
+
<p class="api-keys-note">Enter your API keys to use Eurus. Keys are stored in your browser only and never saved on the server.</p>
|
| 14 |
+
<div class="api-key-field">
|
| 15 |
+
<label for="openai-key">OpenAI API Key <span class="required">*</span></label>
|
| 16 |
+
<input type="password" id="openai-key" placeholder="sk-..." autocomplete="off">
|
| 17 |
+
</div>
|
| 18 |
+
<div class="api-key-field">
|
| 19 |
+
<label for="arraylake-key">Arraylake API Key</label>
|
| 20 |
+
<input type="password" id="arraylake-key" placeholder="ema_..." autocomplete="off">
|
| 21 |
+
</div>
|
| 22 |
+
<button id="save-keys-btn" class="save-keys-btn">Connect</button>
|
| 23 |
+
</div>
|
| 24 |
+
</div>
|
| 25 |
+
|
| 26 |
<div id="chat-messages" class="chat-messages">
|
| 27 |
<div class="message system-message">
|
| 28 |
<h3>Welcome to Eurus</h3>
|