opc-agent 0.5.0 → 0.5.1

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.
@@ -1,10 +1,16 @@
1
+ import { type Response } from 'express';
2
+ import type { Message } from '../core/types';
1
3
  import { BaseChannel } from './index';
2
4
  export declare class WebChannel extends BaseChannel {
3
5
  readonly type = "web";
4
6
  private app;
5
7
  private server;
6
8
  private port;
9
+ private streamHandler;
10
+ private agentName;
7
11
  constructor(port?: number);
12
+ setAgentName(name: string): void;
13
+ onStreamMessage(handler: (msg: Message, res: Response) => Promise<void>): void;
8
14
  private setupRoutes;
9
15
  start(): Promise<void>;
10
16
  stop(): Promise<void>;
@@ -6,11 +6,104 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.WebChannel = void 0;
7
7
  const express_1 = __importDefault(require("express"));
8
8
  const index_1 = require("./index");
9
+ const CHAT_HTML = `<!DOCTYPE html>
10
+ <html lang="en">
11
+ <head>
12
+ <meta charset="UTF-8">
13
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
14
+ <title>OPC Agent</title>
15
+ <style>
16
+ *{margin:0;padding:0;box-sizing:border-box}
17
+ body{background:#0a0a0f;color:#e0e0e0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;height:100vh;display:flex;flex-direction:column}
18
+ header{background:#12121a;padding:16px 24px;border-bottom:1px solid #1e1e2e;display:flex;align-items:center;gap:12px}
19
+ header h1{font-size:18px;font-weight:600;color:#fff}
20
+ header .dot{width:8px;height:8px;border-radius:50%;background:#22c55e;animation:pulse 2s infinite}
21
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
22
+ #messages{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:16px}
23
+ .msg{max-width:720px;padding:12px 16px;border-radius:12px;line-height:1.6;font-size:14px;white-space:pre-wrap;word-break:break-word}
24
+ .msg.user{align-self:flex-end;background:#2563eb;color:#fff;border-bottom-right-radius:4px}
25
+ .msg.assistant{align-self:flex-start;background:#1e1e2e;color:#d4d4d8;border-bottom-left-radius:4px}
26
+ .msg.assistant .cursor{display:inline-block;width:2px;height:14px;background:#818cf8;animation:blink .6s infinite;vertical-align:text-bottom;margin-left:2px}
27
+ @keyframes blink{0%,100%{opacity:1}50%{opacity:0}}
28
+ .msg.error{background:#7f1d1d;color:#fca5a5}
29
+ #input-area{background:#12121a;padding:16px 24px;border-top:1px solid #1e1e2e;display:flex;gap:12px}
30
+ #input{flex:1;background:#1e1e2e;border:1px solid #2e2e3e;border-radius:10px;padding:12px 16px;color:#fff;font-size:14px;outline:none;resize:none;max-height:120px;font-family:inherit}
31
+ #input:focus{border-color:#818cf8}
32
+ #send{background:#2563eb;color:#fff;border:none;border-radius:10px;padding:12px 20px;font-size:14px;cursor:pointer;font-weight:500;transition:background .2s}
33
+ #send:hover{background:#1d4ed8}
34
+ #send:disabled{background:#334155;cursor:not-allowed}
35
+ </style>
36
+ </head>
37
+ <body>
38
+ <header><div class="dot"></div><h1 id="title">OPC Agent</h1></header>
39
+ <div id="messages"></div>
40
+ <div id="input-area">
41
+ <textarea id="input" rows="1" placeholder="Type a message..." autocomplete="off"></textarea>
42
+ <button id="send">Send</button>
43
+ </div>
44
+ <script>
45
+ const msgs=document.getElementById('messages'),input=document.getElementById('input'),btn=document.getElementById('send');
46
+ let sessionId=crypto.randomUUID(),sending=false;
47
+
48
+ function addMsg(role,text){
49
+ const d=document.createElement('div');
50
+ d.className='msg '+role;
51
+ d.textContent=text;
52
+ msgs.appendChild(d);
53
+ msgs.scrollTop=msgs.scrollHeight;
54
+ return d;
55
+ }
56
+
57
+ input.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send()}});
58
+ input.addEventListener('input',()=>{input.style.height='auto';input.style.height=Math.min(input.scrollHeight,120)+'px'});
59
+ btn.addEventListener('click',send);
60
+
61
+ async function send(){
62
+ const text=input.value.trim();
63
+ if(!text||sending)return;
64
+ sending=true;btn.disabled=true;
65
+ input.value='';input.style.height='auto';
66
+ addMsg('user',text);
67
+ const el=addMsg('assistant','');
68
+ el.innerHTML='<span class="cursor"></span>';
69
+ try{
70
+ const res=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:text,sessionId})});
71
+ if(!res.ok)throw new Error('HTTP '+res.status);
72
+ const reader=res.body.getReader(),dec=new TextDecoder();
73
+ let full='';
74
+ while(true){
75
+ const{done,value}=await reader.read();
76
+ if(done)break;
77
+ const chunk=dec.decode(value,{stream:true});
78
+ const lines=chunk.split('\\n');
79
+ for(const line of lines){
80
+ if(!line.startsWith('data: '))continue;
81
+ const d=line.slice(6);
82
+ if(d==='[DONE]')continue;
83
+ try{const j=JSON.parse(d);if(j.content)full+=j.content;if(j.error)full='Error: '+j.error;}catch{}
84
+ }
85
+ el.textContent=full;
86
+ msgs.scrollTop=msgs.scrollHeight;
87
+ }
88
+ if(!full)el.textContent='(empty response)';
89
+ }catch(e){
90
+ el.className='msg error';el.textContent='Error: '+e.message;
91
+ }
92
+ sending=false;btn.disabled=false;input.focus();
93
+ }
94
+
95
+ // Fetch agent info
96
+ fetch('/api/info').then(r=>r.json()).then(d=>{if(d.name)document.getElementById('title').textContent=d.name}).catch(()=>{});
97
+ </script>
98
+ </body>
99
+ </html>`;
9
100
  class WebChannel extends index_1.BaseChannel {
10
101
  type = 'web';
11
102
  app;
12
103
  server = null;
13
104
  port;
105
+ streamHandler = null;
106
+ agentName = 'OPC Agent';
14
107
  constructor(port = 3000) {
15
108
  super();
16
109
  this.port = port;
@@ -18,10 +111,61 @@ class WebChannel extends index_1.BaseChannel {
18
111
  this.app.use(express_1.default.json());
19
112
  this.setupRoutes();
20
113
  }
114
+ setAgentName(name) {
115
+ this.agentName = name;
116
+ }
117
+ onStreamMessage(handler) {
118
+ this.streamHandler = handler;
119
+ }
21
120
  setupRoutes() {
121
+ this.app.get('/', (_req, res) => {
122
+ res.type('html').send(CHAT_HTML);
123
+ });
22
124
  this.app.get('/health', (_req, res) => {
23
125
  res.json({ status: 'ok', timestamp: Date.now() });
24
126
  });
127
+ this.app.get('/api/info', (_req, res) => {
128
+ res.json({ name: this.agentName });
129
+ });
130
+ // Streaming chat endpoint
131
+ this.app.post('/api/chat', async (req, res) => {
132
+ const { message, sessionId } = req.body;
133
+ if (!message) {
134
+ res.status(400).json({ error: 'message is required' });
135
+ return;
136
+ }
137
+ const msg = {
138
+ id: `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
139
+ role: 'user',
140
+ content: message,
141
+ timestamp: Date.now(),
142
+ metadata: { sessionId: sessionId ?? 'default' },
143
+ };
144
+ if (this.streamHandler) {
145
+ try {
146
+ await this.streamHandler(msg, res);
147
+ }
148
+ catch (err) {
149
+ if (!res.headersSent) {
150
+ res.status(500).json({ error: 'Internal error' });
151
+ }
152
+ }
153
+ return;
154
+ }
155
+ // Fallback: non-streaming
156
+ if (!this.handler) {
157
+ res.status(503).json({ error: 'Agent not ready' });
158
+ return;
159
+ }
160
+ try {
161
+ const response = await this.handler(msg);
162
+ res.json({ response: response.content, id: response.id });
163
+ }
164
+ catch (err) {
165
+ res.status(500).json({ error: 'Internal error' });
166
+ }
167
+ });
168
+ // Legacy endpoint
25
169
  this.app.post('/chat', async (req, res) => {
26
170
  if (!this.handler) {
27
171
  res.status(503).json({ error: 'Agent not ready' });
@@ -43,28 +187,15 @@ class WebChannel extends index_1.BaseChannel {
43
187
  const response = await this.handler(msg);
44
188
  res.json({ response: response.content, id: response.id });
45
189
  }
46
- catch (err) {
190
+ catch {
47
191
  res.status(500).json({ error: 'Internal error' });
48
192
  }
49
193
  });
50
- // SSE streaming endpoint
51
- this.app.get('/stream', (req, res) => {
52
- res.writeHead(200, {
53
- 'Content-Type': 'text/event-stream',
54
- 'Cache-Control': 'no-cache',
55
- 'Connection': 'keep-alive',
56
- });
57
- res.write('data: {"type":"connected"}\n\n');
58
- const interval = setInterval(() => {
59
- res.write('data: {"type":"heartbeat"}\n\n');
60
- }, 30000);
61
- req.on('close', () => clearInterval(interval));
62
- });
63
194
  }
64
195
  async start() {
65
196
  return new Promise((resolve) => {
66
197
  this.server = this.app.listen(this.port, () => {
67
- console.log(`[WebChannel] Listening on port ${this.port}`);
198
+ console.log(`[WebChannel] Listening on http://localhost:${this.port}`);
68
199
  resolve();
69
200
  });
70
201
  });