
    BjP                        d Z ddlZddlZddlZddlZddlZddlZddl	Zddl
Z
ddlZddlmZ  ej        dd          Z e ej        dd                    Zde d	e Z ej        d
d          Z e ej        dd                    ZdZdZ ed ee           d            G d dej        j                  Zd Zedk    r e             dS dS )uO   Hermes Web Chat — a lightweight web frontend for Hermes Agent (FIXED VERSION)    N)
HTTPServerHERMES_API_HOST	localhostHERMES_API_PORT8642zhttp://:HERMES_WEB_HOSTz0.0.0.0HERMES_WEB_PORT8084;hms_server_806b81a7e1f79ab04ee61f56c9eb357168400ebfe6daf651u=  <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Iris — Hermes Web Chat</title>
<style>
  :root {
    --bg: #0d1117; --surface: #161b22; --border: #30363d;
    --text: #c9d1d9; --text-dim: #8b949e; --accent: #58a6ff;
    --user-bg: #1f6feb; --bot-bg: #161b22; --input-bg: #0d1117;
    --scrollbar: #30363d; --scrollbar-bg: #161b22;
  }
  * { margin:0; padding:0; box-sizing:border-box; }
  html, body { height:100%; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--text); }
  #app { display:flex; flex-direction:column; height:100%; max-width: 800px; margin: 0 auto; }

  /* Header */
  #header {
    display:flex; align-items:center; justify-content:space-between;
    padding: 16px 20px; border-bottom:1px solid var(--border);
    background: var(--surface); flex-shrink:0;
  }
  #header .brand { font-size: 18px; font-weight: 700; color: var(--accent); letter-spacing: -0.5px; }
  #header .brand span { color: var(--text-dim); font-weight: 400; font-size: 13px; margin-left: 8px; }
  .header-btns { display:flex; gap:8px; }
  .header-btns button {
    background: transparent; border: 1px solid var(--border); color: var(--text-dim);
    padding: 6px 12px; border-radius: 6px; cursor:pointer; font-size: 13px;
    transition: all .15s;
  }
  .header-btns button:hover { border-color: var(--accent); color: var(--accent); }

  /* Messages */
  #messages {
    flex: 1; overflow-y: auto; padding: 20px;
    display: flex; flex-direction: column; gap: 16px;
    scrollbar-width: thin; scrollbar-color: var(--scrollbar) var(--scrollbar-bg);
  }
  #messages::-webkit-scrollbar { width: 6px; }
  #messages::-webkit-scrollbar-thumb { background: var(--scrollbar); border-radius: 3px; }

  .msg { display: flex; gap: 10px; max-width: 85%; animation: fadeIn .2s ease; }
  .msg.user { align-self: flex-end; flex-direction: row-reverse; }
  .msg.bot { align-self: flex-start; }
  @keyframes fadeIn { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:translateY(0)} }

  .msg .avatar {
    width: 32px; height: 32px; border-radius: 50%; flex-shrink: 0;
    display:flex; align-items:center; justify-content:center; font-size: 15px; font-weight:600;
  }
  .msg.user .avatar { background: var(--user-bg); color: #fff; }
  .msg.bot .avatar { background: #2d333b; color: var(--accent); }

  .msg .bubble {
    padding: 10px 14px; border-radius: 12px; line-height: 1.55; font-size: 14px;
    word-wrap: break-word; white-space: pre-wrap;
  }
  .msg.user .bubble {
    background: var(--user-bg); color: #fff; border-bottom-right-radius: 4px;
  }
  .msg.bot .bubble {
    background: var(--bot-bg); border: 1px solid var(--border); border-bottom-left-radius: 4px;
  }
  .msg.bot .bubble code {
    background: #0d419d; padding: 2px 5px; border-radius: 4px; font-size: 13px;
    font-family: 'SF Mono', 'Fira Code', monospace;
  }
  .msg.bot .bubble pre {
    background: #0d1117; border: 1px solid var(--border); border-radius: 8px;
    padding: 12px; margin: 8px 0; overflow-x: auto;
  }
  .msg.bot .bubble pre code {
    background: none; padding: 0;
  }

  /* Typing indicator */
  .typing { display:flex; gap:4px; padding:10px 14px; }
  .typing span {
    width:7px; height:7px; border-radius:50%; background: var(--text-dim);
    animation: typDot 1.2s infinite;
  }
  .typing span:nth-child(2) { animation-delay: .2s; }
  .typing span:nth-child(3) { animation-delay: .4s; }
  @keyframes typDot { 0%,60%,100%{opacity:.3;transform:translateY(0)} 30%{opacity:1;transform:translateY(-4px)} }

  /* Input */
  #input-area {
    border-top: 1px solid var(--border); background: var(--surface);
    padding: 12px 20px; flex-shrink: 0;
  }
  #input-wrap {
    display:flex; gap:8px; align-items:flex-end;
  }
  #input-wrap textarea {
    flex: 1; background: var(--input-bg); border: 1px solid var(--border);
    color: var(--text); padding: 12px; border-radius: 10px; resize:none;
    font-size: 14px; font-family: inherit; line-height: 1.5;
    outline:none; transition: border-color .15s;
    max-height: 160px; min-height: 44px;
  }
  #input-wrap textarea:focus { border-color: var(--accent); }
  #input-wrap textarea::placeholder { color: var(--text-dim); }
  #send-btn {
    width: 44px; height: 44px; border-radius: 10px; border: none;
    background: var(--user-bg); color: #fff; cursor: pointer;
    display:flex; align-items:center; justify-content:center;
    transition: background .15s, transform .1s;
  }
  #send-btn:hover { background: #388bfd; }
  #send-btn:active { transform: scale(.95); }
  #send-btn:disabled { opacity: .5; cursor: not-allowed; }
  #send-btn svg { width: 20px; height: 20px; }

  /* New chat welcome */
  .welcome {
    display:flex; flex-direction:column; align-items:center; justify-content:center;
    height: 100%; text-align:center; gap:8px; opacity:.5;
  }
  .welcome h2 { font-size: 22px; font-weight: 600; color: var(--text); }
  .welcome p { font-size: 14px; color: var(--text-dim); }

  /* Timestamps */
  .timestamp { font-size: 11px; color: var(--text-dim); margin-top: 4px; }
  .msg.user .timestamp { text-align: right; }

</style>
</head>
<body>
<div id="app">
  <div id="header">
    <div>
      <div class="brand">Iris <span>Via Hermes Web Chat</span></div>
    </div>
    <div class="header-btns">
      <button onclick="newChat()" title="New Chat">＋</button>
      <button onclick="toggleTheme()" title="Toggle Theme" id="theme-btn">🌙</button>
    </div>
  </div>
  <div id="messages"></div>
  <div id="input-area">
    <div id="input-wrap">
      <textarea id="msg-input" placeholder="Message Iris..." rows="1"></textarea>
      <button id="send-btn" onclick="sendMessage()">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
          <line x1="22" y1="2" x2="11" y2="13"></line>
          <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
        </svg>
      </button>
    </div>
  </div>
</div>

<script>
// Session management
let sessionId = localStorage.getItem('hermes_sid') || '';
let sessionKey = localStorage.getItem('hermes_skey') || '';

// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
  const ta = document.getElementById('msg-input');
  if (ta) {
    ta.addEventListener('keydown', function(e) {
      if (e.key === 'Enter' && !e.shiftKey) {
        e.preventDefault();
        sendMessage();
      }
    });
    ta.addEventListener('input', function() {
      ta.style.height = 'auto';
      ta.style.height = Math.min(ta.scrollHeight, 160) + 'px';
    });
  }
  
  // Load history if we have a session
  if (sessionId) {
    loadHistory();
  } else {
    showWelcome();
  }
  logDebug('UI initialized');
});

function genId() { return 'sid_' + Math.random().toString(36).slice(2, 10); }
function genKey() { return 'web-' + Math.random().toString(36).slice(2, 12); }

async function loadHistory() {
  logDebug('Loading history for session: ' + sessionId);
  try {
    const resp = await fetch('/api/history', {
      headers: { 'X-Hermes-Session-Id': sessionId }
    });
    if (!resp.ok) {
      logDebug('History load failed with status: ' + resp.status);
      showWelcome();
      return;
    }
    const data = await resp.json();
    logDebug('History response: ' + JSON.stringify(data).substring(0, 100));
    const messages = data.messages || [];
    const messagesDiv = document.getElementById('messages');
    messagesDiv.innerHTML = '';
    if (!messages.length) {
      showWelcome();
    } else {
      messages.forEach(function(m) { appendMessage(m.role, m.content, false); });
    }
  } catch(e) {
    logDebug('Error loading history: ' + e.message);
    showWelcome();
  }
}

async function sendMessage() {
  const input = document.getElementById('msg-input');
  const text = input.value.trim();
  logDebug('sendMessage called with text: ' + text);
  
  if (!text) {
    logDebug('No text, returning early');
    return;
  }

  const messagesDiv = document.getElementById('messages');
  if (!sessionId) {
    sessionId = genId();
    sessionKey = genKey();
    localStorage.setItem('hermes_sid', sessionId);
    localStorage.setItem('hermes_skey', sessionKey);
    logDebug('Created new session: ' + sessionId);
  }

  // Add user message
  appendMessage('user', text, false);
  input.value = '';
  input.style.height = 'auto';

  const sendBtn = document.getElementById('send-btn');
  sendBtn.disabled = true;

  // Show typing indicator
  const typingEl = document.createElement('div');
  typingEl.className = 'msg bot';
  typingEl.innerHTML = '<div class="avatar">✦</div><div class="bubble"><div class="typing"><span></span><span></span><span></span></div></div>';
  messagesDiv.appendChild(typingEl);
  messagesDiv.scrollTop = messagesDiv.scrollHeight;

  // Prepare the request to our proxy server (not to Ollama directly)
  const payload = {
    model: "qwen3.6:latest",
    messages: [{ role: "user", content: text }],
    stream: false
  };
  
  logDebug('Sending proxy request to /api/chat with payload: ' + JSON.stringify(payload).substring(0, 100));

  try {
    const resp = await fetch('/api/chat', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Hermes-Session-Id': sessionId,
        'X-Hermes-Session-Key': sessionKey
      },
      body: JSON.stringify(payload)
    });

    logDebug('Proxy response status: ' + resp.status);

    // Remove typing indicator
    const typingDiv = typingEl.parentNode;
    if (typingDiv) typingDiv.remove();

    const respText = await resp.text();
    logDebug('Proxy response body: ' + respText.substring(0, 200));

    if (!resp.ok) {
      appendMessage('bot', 'Error: ' + respText, true);
      return;
    }
    
    // Parse the response
    let content = '';
    try {
      const data = JSON.parse(respText);
      logDebug('Response parsed: ' + JSON.stringify(data).substring(0, 100));
      content = data.content || data.message || data.reply || data.text || '';
      
      // Handle if content is nested
      if (content && typeof content === 'object') {
        content = JSON.stringify(content);
      }
    } catch(parseErr) {
      logDebug('JSON parse error: ' + parseErr.message);
      content = 'Invalid response format';
    }
    
    if (!content || content === '') {
      logDebug('Empty content, showing raw response');
      content = 'Received empty response from server';
    }
    
    appendMessage('bot', content, true);

  } catch(e) {
    logDebug('Network error: ' + e.message);
    // Remove typing if it exists
    const typingDiv = typingEl.parentNode;
    if (typingDiv) typingDiv.remove();
    
    appendMessage('bot', 'Connection error: ' + e.message, true);
  }

  sendBtn.disabled = false;
  input.focus();
}

function escapeHtml(text) {
  return text
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

function appendMessage(role, content, animate) {
  const messagesDiv = document.getElementById('messages');
  const div = document.createElement('div');
  div.className = 'msg ' + role;
  const avatar = role === 'user' ? 'J' : '✦';
  
  // Get current time
  const now = new Date();
  const timeStr = now.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
  
  // Escape and format content safely
  let safeContent;
  if (content) {
    safeContent = escapeHtml(content);
  } else {
    safeContent = escapeHtml('');
  }

  // Check if content looks like code (starts with newline + indentation)
  if (typeof content === 'string' && content.trim().startsWith('<pre>')) {
    // Already HTML
    div.innerHTML = '<div class="avatar">' + avatar + '</div><div class="bubble">' + content + '<div class="timestamp">' + timeStr + '</div></div>';
  } else if (typeof content === 'string' && content.indexOf('```') > -1) {
    // Has code blocks - create pre element directly
    const codeMatch = content.match(/```(\w*)\n([\s\S]*?)```/g);
    if (codeMatch) {
      safeContent = content.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
        .replace(/```(\w*)\n([\s\S]*?)```/g, '<pre style="background:#0d1117;padding:12px;border-radius:8px;margin:8px 0;overflow-x:auto"><pre style="background:#0d1117;padding:12px;border-radius:8px;margin:8px 0;overflow-x:auto;position:relative;"><code>$2</code></pre></pre>');
      div.innerHTML = '<div class="avatar">' + avatar + '</div><div class="bubble">' + safeContent + '<div class="timestamp">' + timeStr + '</div></div>';
    } else {
      div.innerHTML = '<div class="avatar">' + avatar + '</div><div class="bubble">' + safeContent + '<div class="timestamp">' + timeStr + '</div></div>';
    }
  } else {
    // Plain text
    div.innerHTML = '<div class="avatar">' + avatar + '</div><div class="bubble">' + safeContent + '<div class="timestamp">' + timeStr + '</div></div>';
  }
  
  messagesDiv.appendChild(div);
  messagesDiv.scrollTop = messagesDiv.scrollHeight;
  logDebug('Appended message, total messages: ' + messagesDiv.children.length);
}

function showWelcome() {
  const messagesDiv = document.getElementById('messages');
  messagesDiv.innerHTML = '<div class="welcome"><h2>🌸 Hello, Josh!</h2><p>I\'m Iris, your Iris boss\'s AI assistant.</p><p>What can I help you with today?</p></div>';
  logDebug('Welcome screen shown');
}

function newChat() {
  sessionId = '';
  sessionKey = '';
  localStorage.setItem('hermes_sid','');
  localStorage.setItem('hermes_skey','');
  const messagesDiv = document.getElementById('messages');
  messagesDiv.innerHTML = '';
  showWelcome();
  logDebug('New chat started');
}

// Theme toggle
let darkTheme = true;
let lastTheme = localStorage.getItem('hermes_theme');
if (lastTheme === 'light') {
  darkTheme = false;
}

function toggleTheme() {
  darkTheme = !darkTheme;
  localStorage.setItem('hermes_theme', darkTheme ? 'dark' : 'light');
  document.getElementById('theme-btn').textContent = darkTheme ? '🌙' : '☀️';
  
  if (darkTheme) {
    document.documentElement.style.setProperty('--bg', '#0d1117');
    document.documentElement.style.setProperty('--surface', '#161b22');
    document.documentElement.style.setProperty('--border', '#30363d');
    document.documentElement.style.setProperty('--text', '#c9d1d9');
    document.documentElement.style.setProperty('--text-dim', '#8b949e');
    document.documentElement.style.setProperty('--input-bg', '#0d1117');
    document.documentElement.style.setProperty('--bot-bg', '#161b22');
    document.documentElement.style.setProperty('--scrollbar', '#30363d');
    document.documentElement.style.setProperty('--scrollbar-bg', '#161b22');
  } else {
    document.documentElement.style.setProperty('--bg', '#f6f8fa');
    document.documentElement.style.setProperty('--surface', '#ffffff');
    document.documentElement.style.setProperty('--border', '#d0d7de');
    document.documentElement.style.setProperty('--text', '#1f2328');
    document.documentElement.style.setProperty('--text-dim', '#656d76');
    document.documentElement.style.setProperty('--input-bg', '#f6f8fa');
    document.documentElement.style.setProperty('--bot-bg', '#f6f8fa');
    document.documentElement.style.setProperty('--scrollbar', '#d0d7de');
    document.documentElement.style.setProperty('--scrollbar-bg', '#ffffff');
  }
  logDebug('Theme toggled to ' + (darkTheme ? 'dark' : 'light'));
}

// Debug logging helper
function logDebug(msg) {
  console.log('[Iris] ' + msg);
}
</script>
</body>
</html>zGenerated UI size: z bytesc                   2    e Zd Zd Zd Zd Zd Zd Zd ZdS )ChatHandlerc                     d S )N )selfformatargss      (/home/jworkman/hermes-web-chat/server.pylog_messagezChatHandler.log_message  s        c                 d   t          j        |                                          }|                     |           |                     dd           |                     dt          t          |                               |                                  | j        	                    |           d S )NContent-Typeapplication/jsonContent-Length)
jsondumpsencodesend_responsesend_headerstrlenend_headerswfilewrite)r   codedatabodys       r   
_send_jsonzChatHandler._send_json  s    z$&&((4   );<<<)3s4yy>>:::
r   c           	      *   | j         dk    s| j         dk    r|                     d           |                     dd           |                     dt          t	          t
                                                                         |                                  | j        	                    t
                                                     d S | j         dk    r#| 
                    ddt           d	d
           d S |                     d           d S )N/z/index.html   r   z	text/htmlr   z/healthokz/v1)statusapi  )pathr   r   r    r!   UI_INDEXr   r"   r#   r$   r(   HERMES_API_BASE
send_errorr   s    r   do_GETzChatHandler.do_GET  s    9tyM99s###^[999-s3x7H7H3I3I/J/JKKKJX__../////Y)##OOCDO9P9P9P!Q!QRRRRROOC     r   c                     | j         dk    r|                                  d S | j         dk    r|                                  d S |                     d           d S )Nz	/api/chatz/api/historyr/   )r0   _handle_chat_handle_historyr3   r4   s    r   do_POSTzChatHandler.do_POST  sd    9##Y.((  """""OOC     r   c           	      \   | j                             dd          }| j                             dd          }	 t          | j                             dd                    }| j                            |          }t          j        |          }n #  |                     dddi           Y d S xY wt           d	}t          j
                            ||d
          }|                    dd           |r|                    d|           |r|                    d|           t          r|                    dt                     	 t          j
                            |d          5 }t          j        |                                          }	|	                    di g          d                             di                               dd          }
|                     d|
|	                    dd          |pdd           d d d            d S # 1 swxY w Y   d S # t          j        j        $ rN}|                                                                }|                     |j        d|i           Y d }~d S d }~wt&          $ r&}|                     ddd| i           Y d }~d S d }~ww xY w)NX-Hermes-Session-Id zX-Hermes-Session-Keyr   r   i  errorzinvalid JSONz/v1/chat/completionsPOST)r&   methodr   r   X-Hermes-API-Keyi,  timeoutchoicesmessagecontentr+   modelirisnew)rE   rF   
session_idi  zproxy error: )headersgetintrfilereadr   loadsr(   r2   urllibrequestRequest
add_headerAPI_KEYurlopenr=   	HTTPErrordecoder%   	Exception)r   sess_idsess_keylengthraw_bodyr'   api_urlreqrespr&   rE   eerr_bodys                r   r7   zChatHandler._handle_chat  s   ,""#8"==<##$:B??	))*:A>>??Fzv..H:h''DD	OOC'>!:;;;FF %::: n$$ % 
 
 	~'9::: 	;NN0'::: 	=NN18<<< 	8NN-w777	A''S'99 Tz$))++..((9rd33A6::9bIIMMiY[\\&!XXgv66")"2U& &                     |% 	9 	9 	9vvxx((HOOAFWh$7888888888 	A 	A 	AOOC'+>1+>+>!?@@@@@@@@@	AsV   AB B,!H %BHH HH HH J+/AI88J+J&&J+c                 F   | j                             dd          }t           d}t          j                            |d          }|r|                    d|           t          r|                    dt                     	 t          j                            |d          5 }t          j
        |                                          }|                     d	|           d d d            d S # 1 swxY w Y   d S # t          $ r |                     d	d
g i           Y d S w xY w)Nr;   r<   z/v1/messagesGET)r?   r@   
   rA   r+   messages)rJ   rK   r2   rP   rQ   rR   rS   rT   rU   r   rO   rN   r(   rX   )r   rY   r]   r^   r_   r&   s         r   r8   zChatHandler._handle_history  sg   ,""#8"==$222n$$WU$;; 	;NN0'::: 	8NN-w777	3''R'88 +Dz$))++..T***+ + + + + + + + + + + + + + + + + +  	3 	3 	3OOC*b!1222222	3s6   !C: #=C- C: -C11C: 4C15C: :"D D N)	__name__
__module____qualname__r   r(   r5   r9   r7   r8   r   r   r   r   r     sr            
! 
! 
!! ! !+A +A +AZ3 3 3 3 3r   r   c                     t          t          t          ft                    } t	          d            t	          d           t	          dt                      t	          dt
                      t	          dt          d d          d           t	          d            	 |                                  d S # t          $ r | 	                                 Y d S w xY w)Nz2==================================================z  Iris Web Chatz  Access: http://localhost:z
  Proxy:  z  API_KEY: rd   z...)
r   LISTEN_HOSTLISTEN_PORTr   printr2   rT   serve_foreverKeyboardInterruptshutdown)servers    r   mainrq   )  s    k2K@@F	V+	
	
5
5
5666	
(
(
()))	
)
)
)
)***	V+   s    B6 6CC__main__)__doc__http.serverhttpr   ossysurllib.requestrP   urllib.errorurllib.parsessl	threadingr   getenvr   rL   r   r2   rj   rk   rT   r1   rl   r!   rp   BaseHTTPRequestHandlerr   rq   rf   r   r   r   <module>r      s   U U      				 



             



     " " " " " "")-{;;#ibi 16::;;?O??o??bi)955c)")-v6677 HkZ 1CCMM111 2 2 2]3 ]3 ]3 ]3 ]3$+4 ]3 ]3 ]3@   zDFFFFF r   