opc-agent 0.8.0 → 1.0.0

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.
Files changed (59) hide show
  1. package/README.md +145 -144
  2. package/dist/channels/web.js +118 -39
  3. package/dist/cli.js +153 -17
  4. package/dist/core/analytics-engine.d.ts +51 -0
  5. package/dist/core/analytics-engine.js +186 -0
  6. package/dist/core/cache.d.ts +47 -0
  7. package/dist/core/cache.js +156 -0
  8. package/dist/core/errors.d.ts +68 -0
  9. package/dist/core/errors.js +149 -0
  10. package/dist/core/rate-limiter.d.ts +47 -0
  11. package/dist/core/rate-limiter.js +92 -0
  12. package/dist/core/security.d.ts +48 -0
  13. package/dist/core/security.js +146 -0
  14. package/dist/i18n/index.d.ts +6 -1
  15. package/dist/i18n/index.js +86 -0
  16. package/dist/index.d.ts +15 -0
  17. package/dist/index.js +42 -1
  18. package/dist/plugins/index.d.ts +24 -3
  19. package/dist/plugins/index.js +109 -4
  20. package/dist/schema/oad.d.ts +54 -0
  21. package/dist/schema/oad.js +6 -1
  22. package/dist/templates/data-analyst.d.ts +53 -0
  23. package/dist/templates/data-analyst.js +70 -0
  24. package/dist/templates/teacher.d.ts +58 -0
  25. package/dist/templates/teacher.js +78 -0
  26. package/dist/testing/index.d.ts +37 -0
  27. package/dist/testing/index.js +176 -0
  28. package/docs/.vitepress/config.ts +92 -0
  29. package/docs/api/cli.md +48 -0
  30. package/docs/api/sdk.md +80 -0
  31. package/docs/guide/configuration.md +79 -0
  32. package/docs/guide/deployment.md +42 -0
  33. package/docs/guide/testing.md +84 -0
  34. package/docs/index.md +27 -0
  35. package/docs/zh/api/oad-schema.md +3 -0
  36. package/docs/zh/guide/concepts.md +28 -0
  37. package/docs/zh/guide/configuration.md +39 -0
  38. package/docs/zh/guide/deployment.md +3 -0
  39. package/docs/zh/guide/getting-started.md +58 -0
  40. package/docs/zh/guide/templates.md +22 -0
  41. package/docs/zh/guide/testing.md +18 -0
  42. package/docs/zh/index.md +27 -0
  43. package/package.json +7 -3
  44. package/src/channels/web.ts +118 -39
  45. package/src/cli.ts +152 -19
  46. package/src/core/analytics-engine.ts +186 -0
  47. package/src/core/cache.ts +141 -0
  48. package/src/core/errors.ts +148 -0
  49. package/src/core/rate-limiter.ts +128 -0
  50. package/src/core/security.ts +171 -0
  51. package/src/i18n/index.ts +87 -1
  52. package/src/index.ts +19 -0
  53. package/src/plugins/index.ts +128 -7
  54. package/src/schema/oad.ts +6 -0
  55. package/src/templates/data-analyst.ts +70 -0
  56. package/src/templates/teacher.ts +79 -0
  57. package/src/testing/index.ts +181 -0
  58. package/tests/errors.test.ts +83 -0
  59. package/tests/security.test.ts +60 -0
package/README.md CHANGED
@@ -1,188 +1,189 @@
1
- # OPC Agent
1
+ <p align="center">
2
+ <h1 align="center">🤖 OPC Agent</h1>
3
+ <p align="center"><strong>Open Agent Framework — Build, test, and run AI Agents for business workstations</strong></p>
4
+ <p align="center">
5
+ <a href="https://www.npmjs.com/package/opc-agent"><img src="https://img.shields.io/npm/v/opc-agent?color=blue" alt="npm"></a>
6
+ <a href="https://github.com/anthropic-lab/opc-agent/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-green" alt="license"></a>
7
+ <a href="https://github.com/anthropic-lab/opc-agent/actions"><img src="https://img.shields.io/badge/tests-passing-brightgreen" alt="tests"></a>
8
+ <a href="https://www.npmjs.com/package/opc-agent"><img src="https://img.shields.io/npm/dm/opc-agent?color=orange" alt="downloads"></a>
9
+ </p>
10
+ </p>
2
11
 
3
- **Open Agent Framework** — Build, test, and run AI Agents for business workstations.
12
+ ---
4
13
 
5
- [![npm version](https://img.shields.io/npm/v/opc-agent.svg)](https://www.npmjs.com/package/opc-agent)
6
- [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
14
+ OPC Agent is a **TypeScript-first framework** for building production AI agents. Define your agent in a single YAML file (OAD — Open Agent Definition), connect any LLM provider, deploy to any channel.
7
15
 
8
- ## Features
9
-
10
- - 🤖 **Agent Framework** — BaseAgent with lifecycle management, skills, and LLM integration
11
- - 📋 **OAD Schema** — Declarative agent definition (YAML/JSON) with validation
12
- - 🧠 **Memory System** — Short-term + long-term memory with DeepBrain integration
13
- - 🔌 **Multi-Channel** — Web, WebSocket, and Telegram channels
14
- - 🛡️ **DTV Framework** — Data, Trust, and Value tracking for agents
15
- - 🎯 **Skill System** — Pluggable skills with registry and priority execution
16
- - 📦 **Templates** — Customer service, sales assistant, knowledge base, code reviewer
17
- - 🚀 **CLI** — Interactive project creation, dev mode, build, test, run
18
-
19
- ## Quick Start
16
+ ## ⚡ Quick Start (30 seconds)
20
17
 
21
18
  ```bash
22
- # Install globally
19
+ # Install
23
20
  npm install -g opc-agent
24
21
 
25
- # Create a new agent project (interactive)
22
+ # Create your first agent
26
23
  opc init my-agent
27
-
28
- # Or with a specific template
29
- opc init my-bot --template sales-assistant
30
-
31
- # Run the agent
32
24
  cd my-agent
25
+
26
+ # Run it
33
27
  opc run
34
28
  ```
35
29
 
36
- ## Templates
37
-
38
- | Template | Description |
39
- |----------|-------------|
40
- | `customer-service` | FAQ lookup + human handoff |
41
- | `sales-assistant` | Product Q&A + lead capture + appointment booking |
42
- | `knowledge-base` | RAG with DeepBrain semantic search |
43
- | `code-reviewer` | Bug detection + style checking |
30
+ Your agent is now live at `http://localhost:3000` with a beautiful web chat UI.
44
31
 
45
- ## 🚀 Deploy to OpenClaw
32
+ ## Features
46
33
 
47
- OPC Agent is a **development framework**. [OpenClaw](https://github.com/nicepkg/openclaw) is the **runtime**. Design your agent with OPC, deploy it to OpenClaw, and it runs on Telegram, Discord, or any channel OpenClaw supports.
48
-
49
- ```bash
50
- # Generate OpenClaw workspace from your OAD
51
- opc deploy --target openclaw --output ./my-agent-workspace
34
+ ### 🔌 Multi-Provider LLM Support
35
+ ```yaml
36
+ # oad.yaml
37
+ spec:
38
+ provider:
39
+ default: deepseek
40
+ allowed: [openai, deepseek, qwen, anthropic, ollama]
41
+ model: deepseek-chat
42
+ ```
52
43
 
53
- # Or deploy AND auto-register in OpenClaw
54
- opc deploy --target openclaw --install
44
+ Supports **OpenAI**, **DeepSeek**, **Anthropic**, **Qwen**, **Ollama** (local), and any OpenAI-compatible API.
55
45
 
56
- # Then restart OpenClaw to pick it up
57
- openclaw gateway restart
46
+ ### 📡 Multi-Channel Deployment
47
+ ```yaml
48
+ spec:
49
+ channels:
50
+ - type: web # Beautiful chat UI
51
+ port: 3000
52
+ - type: telegram # Telegram bot
53
+ - type: websocket # Real-time WebSocket
54
+ - type: slack # Slack integration
55
+ - type: email # Email channel
56
+ - type: wechat # WeChat Official Account
57
+ - type: voice # Voice (STT/TTS)
58
+ - type: webhook # Incoming webhooks
58
59
  ```
59
60
 
60
- This generates `IDENTITY.md`, `SOUL.md`, `AGENTS.md`, `USER.md`, and `MEMORY.md` — everything OpenClaw needs to run your agent.
61
+ ### 🧠 Knowledge Base (RAG)
62
+ ```typescript
63
+ import { KnowledgeBase } from 'opc-agent';
61
64
 
62
- See [`examples/customer-service-demo/`](examples/customer-service-demo/) for a complete walkthrough.
65
+ const kb = new KnowledgeBase('./docs');
66
+ await kb.addFile('product-manual.pdf');
67
+ // Agent automatically uses KB for context
68
+ ```
63
69
 
64
- ## CLI Commands
70
+ ### 🔧 Plugin System
71
+ ```yaml
72
+ spec:
73
+ plugins:
74
+ - name: logging
75
+ - name: analytics
76
+ - name: rate-limit
77
+ config: { maxPerMinute: 60 }
78
+ ```
65
79
 
66
- | Command | Description |
67
- |---------|-------------|
68
- | `opc init [name]` | Create new project (interactive) |
69
- | `opc create <name>` | Create agent from template |
70
- | `opc info` | Show agent info from OAD |
71
- | `opc build` | Validate OAD |
72
- | `opc test` | Run in sandbox mode |
73
- | `opc run` | Start agent with channels |
74
- | `opc dev` | Hot-reload development mode |
75
- | `opc deploy` | **Deploy to OpenClaw runtime** |
76
- | `opc publish` | Validate and generate manifest |
77
- | `opc search <query>` | Search OPC Registry (coming soon) |
80
+ Built-in plugins: `logging`, `analytics`, `rate-limit`. Custom plugins support lifecycle hooks: `onInit`, `onMessage`, `onResponse`, `onError`, `onShutdown`.
78
81
 
79
- ## OAD Schema
82
+ ### 🔒 Security
83
+ - Input sanitization (XSS, injection prevention)
84
+ - API key rotation & management
85
+ - CORS configuration
86
+ - Helmet-style security headers
87
+ - Content Security Policy
88
+ - Auth middleware with session isolation
80
89
 
81
- OAD (Open Agent Definition) is a declarative schema for defining agents:
90
+ ### 🧪 Agent Testing
91
+ ```bash
92
+ opc test # Run test cases
93
+ opc test --watch # Watch mode
94
+ ```
82
95
 
83
96
  ```yaml
84
- apiVersion: opc/v1
85
- kind: Agent
86
- metadata:
87
- name: my-agent
88
- version: 1.0.0
89
- description: "My AI agent"
90
- marketplace:
91
- category: support
92
- pricing: free
93
- tags: [ai, support]
94
- spec:
95
- provider:
96
- default: deepseek
97
- allowed: [openai, deepseek, qwen]
98
- model: deepseek-chat
99
- systemPrompt: "You are a helpful assistant."
100
- skills:
101
- - name: faq-lookup
102
- description: "Answer FAQs"
103
- channels:
104
- - type: web
105
- port: 3000
106
- - type: telegram
107
- config:
108
- token: "BOT_TOKEN"
109
- - type: websocket
110
- port: 3002
111
- memory:
112
- shortTerm: true
113
- longTerm:
114
- provider: deepbrain
115
- collection: my-knowledge
116
- dtv:
117
- trust:
118
- level: sandbox
119
- value:
120
- metrics: [response_time]
97
+ # tests/greeting.yaml
98
+ - input: "Hello"
99
+ expect:
100
+ contains: ["hello", "hi"]
101
+ maxLatencyMs: 5000
121
102
  ```
122
103
 
123
- ## Memory Providers
104
+ ### 🎭 Multi-Agent Orchestration
105
+ ```typescript
106
+ import { Orchestrator } from 'opc-agent';
124
107
 
125
- ### In-Memory (default)
126
- Simple key-value store. Data lost on restart.
108
+ const orchestrator = new Orchestrator({
109
+ agents: [triageAgent, salesAgent, supportAgent],
110
+ strategy: 'route-by-intent',
111
+ });
112
+ ```
127
113
 
128
- ### DeepBrain (optional)
129
- Semantic search over past conversations and knowledge. Install `deepbrain` package:
114
+ ### 📊 Built-in Analytics & Monitoring
115
+ - `/api/health` Health check
116
+ - `/api/metrics` — Prometheus-compatible metrics
117
+ - `/api/dashboard` — Real-time dashboard UI
118
+ - Conversation export (JSON, Markdown, CSV)
130
119
 
131
- ```bash
132
- npm install deepbrain
133
- ```
120
+ ## 🏗️ Architecture
134
121
 
135
- Configure in OAD:
136
- ```yaml
137
- memory:
138
- longTerm:
139
- provider: deepbrain
140
- collection: my-collection
122
+ ```
123
+ ┌─────────────────────────────────────────────────┐
124
+ │ OAD (YAML) │
125
+ │ Agent Definition & Config │
126
+ ├─────────────────────────────────────────────────┤
127
+ │ │
128
+ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
129
+ │ │ Channels │ │ Plugins │ │ Security │ │
130
+ │ │ web,tg, │ │ logging, │ │ sanitize, │ │
131
+ │ │ ws,slack │ │ analytics│ │ CORS, auth │ │
132
+ │ └────┬─────┘ └────┬─────┘ └──────┬───────┘ │
133
+ │ │ │ │ │
134
+ │ ┌────▼──────────────▼───────────────▼────────┐ │
135
+ │ │ Agent Runtime │ │
136
+ │ │ ┌─────────┐ ┌────────┐ ┌─────────────┐ │ │
137
+ │ │ │ Memory │ │ Skills │ │ Knowledge │ │ │
138
+ │ │ └─────────┘ └────────┘ └─────────────┘ │ │
139
+ │ └────────────────────┬───────────────────────┘ │
140
+ │ │ │
141
+ │ ┌────────────────────▼───────────────────────┐ │
142
+ │ │ LLM Providers │ │
143
+ │ │ OpenAI · DeepSeek · Anthropic · Ollama │ │
144
+ │ └─────────────────────────────────────────────┘│
145
+ └─────────────────────────────────────────────────┘
141
146
  ```
142
147
 
143
- Falls back to in-memory if deepbrain is not installed.
148
+ ## 📖 CLI Reference
144
149
 
145
- ## Channels
150
+ | Command | Description |
151
+ |---------|-------------|
152
+ | `opc init [name]` | Create a new agent project |
153
+ | `opc run` | Start the agent |
154
+ | `opc dev` | Start in development mode (auto-reload) |
155
+ | `opc test` | Run agent test cases |
156
+ | `opc validate` | Validate OAD configuration |
157
+ | `opc deploy hermes` | Deploy to Hermes cloud |
158
+ | `opc plugin list` | List available plugins |
159
+ | `opc plugin add <name>` | Add a plugin to config |
160
+ | `opc migrate` | Migrate OAD to latest schema |
161
+ | `opc marketplace publish` | Publish to marketplace |
146
162
 
147
- - **Web** — Express HTTP server with `/chat` endpoint and SSE streaming
148
- - **WebSocket** — Real-time bidirectional communication with broadcast
149
- - **Telegram** — Webhook handler for Telegram Bot API
163
+ ## 🤝 Contributing
150
164
 
151
- ## Programmatic Usage
165
+ We welcome contributions! Here's how:
152
166
 
153
- ```typescript
154
- import { BaseAgent, AgentRuntime } from 'opc-agent';
167
+ 1. Fork the repository
168
+ 2. Create a feature branch: `git checkout -b feat/my-feature`
169
+ 3. Make your changes with tests
170
+ 4. Run tests: `npm test`
171
+ 5. Submit a pull request
155
172
 
156
- // Quick start
157
- const agent = new BaseAgent({
158
- name: 'my-agent',
159
- systemPrompt: 'You are helpful.',
160
- });
161
- await agent.init();
162
-
163
- // With skills
164
- agent.registerSkill({
165
- name: 'greeter',
166
- description: 'Greet users',
167
- execute: async (ctx, msg) => {
168
- if (msg.content.includes('hello')) {
169
- return { handled: true, response: 'Hi!', confidence: 1.0 };
170
- }
171
- return { handled: false, confidence: 0 };
172
- },
173
- });
173
+ ### Development Setup
174
174
 
175
- // From OAD config
176
- const runtime = new AgentRuntime();
177
- await runtime.loadConfig('oad.yaml');
178
- await runtime.initialize();
179
- await runtime.start();
175
+ ```bash
176
+ git clone https://github.com/anthropic-lab/opc-agent.git
177
+ cd opc-agent
178
+ npm install
179
+ npm run build
180
+ npm test
180
181
  ```
181
182
 
182
- ## Contributing
183
+ ## 📄 License
183
184
 
184
- See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
185
+ [Apache License 2.0](LICENSE) Use it freely in commercial and open source projects.
185
186
 
186
- ## License
187
+ ---
187
188
 
188
- Apache-2.0 see [LICENSE](LICENSE).
189
+ <p align="center">Built with ❤️ by the OPC team</p>
@@ -62,52 +62,127 @@ const CHAT_HTML = `<!DOCTYPE html>
62
62
  <html lang="en">
63
63
  <head>
64
64
  <meta charset="UTF-8">
65
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
65
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
66
66
  <title>OPC Agent</title>
67
67
  <style>
68
+ :root{--bg:#0a0a0f;--surface:#12121a;--border:#1e1e2e;--text:#e0e0e0;--text-dim:#888;--accent:#818cf8;--accent-hover:#6366f1;--user-bg:#2563eb;--user-hover:#1d4ed8;--error-bg:#7f1d1d;--error-text:#fca5a5;--success:#22c55e;--radius:12px}
68
69
  *{margin:0;padding:0;box-sizing:border-box}
69
- body{background:#0a0a0f;color:#e0e0e0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;height:100vh;display:flex;flex-direction:column}
70
- header{background:#12121a;padding:16px 24px;border-bottom:1px solid #1e1e2e;display:flex;align-items:center;gap:12px}
71
- header h1{font-size:18px;font-weight:600;color:#fff}
72
- header .dot{width:8px;height:8px;border-radius:50%;background:#22c55e;animation:pulse 2s infinite}
73
- @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
74
- #messages{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:16px}
75
- .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}
76
- .msg.user{align-self:flex-end;background:#2563eb;color:#fff;border-bottom-right-radius:4px}
77
- .msg.assistant{align-self:flex-start;background:#1e1e2e;color:#d4d4d8;border-bottom-left-radius:4px}
78
- .msg.assistant .cursor{display:inline-block;width:2px;height:14px;background:#818cf8;animation:blink .6s infinite;vertical-align:text-bottom;margin-left:2px}
70
+ body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',sans-serif;height:100vh;height:100dvh;display:flex;flex-direction:column;overflow:hidden}
71
+ header{background:var(--surface);padding:14px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;flex-shrink:0;backdrop-filter:blur(12px)}
72
+ header .avatar{width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,var(--accent),#6366f1);display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0}
73
+ header .info{flex:1;min-width:0}
74
+ header h1{font-size:16px;font-weight:600;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
75
+ header .status{font-size:12px;color:var(--success);display:flex;align-items:center;gap:4px}
76
+ header .status .dot{width:6px;height:6px;border-radius:50%;background:var(--success);animation:pulse 2s infinite}
77
+ nav.header-nav{display:flex;gap:4px}
78
+ nav.header-nav a{color:var(--text-dim);text-decoration:none;font-size:12px;padding:4px 10px;border-radius:6px;transition:all .2s}
79
+ nav.header-nav a:hover{color:#fff;background:rgba(255,255,255,.06)}
80
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
81
+ @keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
82
+ @keyframes slideIn{from{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}
83
+ #messages{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:12px;scroll-behavior:smooth}
84
+ #messages::-webkit-scrollbar{width:4px}
85
+ #messages::-webkit-scrollbar-track{background:transparent}
86
+ #messages::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px}
87
+ .msg-wrap{display:flex;flex-direction:column;animation:fadeIn .3s ease-out}
88
+ .msg-wrap.user{align-items:flex-end}
89
+ .msg-wrap.assistant{align-items:flex-start}
90
+ .msg{max-width:min(720px,85%);padding:10px 14px;border-radius:var(--radius);line-height:1.7;font-size:14px;word-break:break-word;position:relative;transition:all .2s}
91
+ .msg.user{background:var(--user-bg);color:#fff;border-bottom-right-radius:4px}
92
+ .msg.assistant{background:var(--surface);color:var(--text);border:1px solid var(--border);border-bottom-left-radius:4px}
93
+ .msg.error{background:var(--error-bg);color:var(--error-text);border:1px solid rgba(239,68,68,.3)}
94
+ .msg pre{background:rgba(0,0,0,.4);padding:12px;border-radius:8px;overflow-x:auto;margin:8px 0;font-size:13px;font-family:'JetBrains Mono','Fira Code','Cascadia Code',monospace;line-height:1.5}
95
+ .msg code{font-family:'JetBrains Mono','Fira Code','Cascadia Code',monospace;font-size:13px;background:rgba(0,0,0,.3);padding:1px 5px;border-radius:4px}
96
+ .msg pre code{background:none;padding:0}
97
+ .msg .cursor{display:inline-block;width:2px;height:14px;background:var(--accent);animation:blink .6s infinite;vertical-align:text-bottom;margin-left:2px}
79
98
  @keyframes blink{0%,100%{opacity:1}50%{opacity:0}}
80
- .msg.error{background:#7f1d1d;color:#fca5a5}
81
- #input-area{background:#12121a;padding:16px 24px;border-top:1px solid #1e1e2e;display:flex;gap:12px}
82
- #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}
83
- #input:focus{border-color:#818cf8}
84
- #send{background:#2563eb;color:#fff;border:none;border-radius:10px;padding:12px 20px;font-size:14px;cursor:pointer;font-weight:500;transition:background .2s}
85
- #send:hover{background:#1d4ed8}
86
- #send:disabled{background:#334155;cursor:not-allowed}
99
+ .typing{display:flex;gap:4px;padding:12px 16px;align-items:center}
100
+ .typing span{width:6px;height:6px;border-radius:50%;background:var(--text-dim);animation:typingDot 1.4s infinite}
101
+ .typing span:nth-child(2){animation-delay:.2s}
102
+ .typing span:nth-child(3){animation-delay:.4s}
103
+ @keyframes typingDot{0%,60%,100%{opacity:.3;transform:scale(.8)}30%{opacity:1;transform:scale(1)}}
104
+ .reactions{display:flex;gap:4px;margin-top:4px}
105
+ .reactions button{background:rgba(255,255,255,.06);border:1px solid transparent;border-radius:16px;padding:2px 8px;font-size:13px;cursor:pointer;transition:all .15s;color:var(--text-dim)}
106
+ .reactions button:hover{background:rgba(255,255,255,.12);border-color:var(--border)}
107
+ .reactions button.active{background:rgba(99,102,241,.2);border-color:var(--accent);color:var(--accent)}
108
+ .msg-time{font-size:11px;color:var(--text-dim);margin-top:2px;opacity:0;transition:opacity .2s}
109
+ .msg-wrap:hover .msg-time{opacity:1}
110
+ .attachment{display:flex;align-items:center;gap:8px;background:rgba(0,0,0,.3);padding:8px 12px;border-radius:8px;margin-top:6px;font-size:13px}
111
+ .attachment .icon{font-size:18px}
112
+ #input-area{background:var(--surface);padding:12px 20px 16px;border-top:1px solid var(--border);display:flex;gap:10px;align-items:flex-end;flex-shrink:0}
113
+ #input{flex:1;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);padding:10px 14px;color:#fff;font-size:14px;outline:none;resize:none;max-height:150px;min-height:42px;font-family:inherit;line-height:1.5;transition:border-color .2s}
114
+ #input:focus{border-color:var(--accent)}
115
+ #input::placeholder{color:var(--text-dim)}
116
+ #send{background:var(--user-bg);color:#fff;border:none;border-radius:var(--radius);width:42px;height:42px;font-size:18px;cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;flex-shrink:0}
117
+ #send:hover{background:var(--user-hover);transform:scale(1.05)}
118
+ #send:disabled{background:#334155;cursor:not-allowed;transform:none}
119
+ .empty-state{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--text-dim);gap:12px;padding:40px;text-align:center}
120
+ .empty-state .logo{font-size:48px;opacity:.6}
121
+ .empty-state h2{color:var(--text);font-size:20px;font-weight:500}
122
+ .empty-state p{font-size:14px;max-width:400px;line-height:1.6}
123
+ @media(max-width:640px){
124
+ header{padding:10px 14px}
125
+ #messages{padding:12px}
126
+ #input-area{padding:10px 14px 14px}
127
+ .msg{max-width:90%;font-size:14px}
128
+ nav.header-nav{display:none}
129
+ }
87
130
  </style>
88
131
  </head>
89
132
  <body>
90
- <header><div class="dot"></div><h1 id="title">OPC Agent</h1></header>
91
- <div id="messages"></div>
133
+ <header>
134
+ <div class="avatar" id="avatar">🤖</div>
135
+ <div class="info"><h1 id="title">OPC Agent</h1><div class="status"><span class="dot"></span>Online</div></div>
136
+ <nav class="header-nav"><a href="/dashboard">Dashboard</a><a href="/templates">Templates</a></nav>
137
+ </header>
138
+ <div id="messages">
139
+ <div class="empty-state" id="empty"><div class="logo">💬</div><h2>Start a conversation</h2><p>Type a message below to chat with your AI agent.</p></div>
140
+ </div>
92
141
  <div id="input-area">
93
- <textarea id="input" rows="1" placeholder="Type a message..." autocomplete="off"></textarea>
94
- <button id="send">Send</button>
142
+ <textarea id="input" rows="1" placeholder="Type a message" autocomplete="off"></textarea>
143
+ <button id="send" aria-label="Send">↑</button>
95
144
  </div>
96
145
  <script>
97
- const msgs=document.getElementById('messages'),input=document.getElementById('input'),btn=document.getElementById('send');
146
+ const msgs=document.getElementById('messages'),input=document.getElementById('input'),btn=document.getElementById('send'),empty=document.getElementById('empty');
98
147
  let sessionId=crypto.randomUUID(),sending=false;
99
148
 
100
- function addMsg(role,text){
149
+ function esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML}
150
+ function fmtTime(){return new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}
151
+ function renderMd(text){
152
+ let h=esc(text);
153
+ h=h.replace(/\`\`\`(\\w*)\\n([\\s\\S]*?)\`\`\`/g,'<pre><code>$2</code></pre>');
154
+ h=h.replace(/\`([^\`]+)\`/g,'<code>$1</code>');
155
+ h=h.replace(/\\*\\*(.+?)\\*\\*/g,'<strong>$1</strong>');
156
+ h=h.replace(/\\n/g,'<br>');
157
+ return h;
158
+ }
159
+
160
+ function addMsg(role,text,opts){
161
+ if(empty)empty.remove();
162
+ const wrap=document.createElement('div');
163
+ wrap.className='msg-wrap '+role;
101
164
  const d=document.createElement('div');
102
165
  d.className='msg '+role;
103
- d.textContent=text;
104
- msgs.appendChild(d);
166
+ if(opts?.html)d.innerHTML=text;else if(role==='assistant'&&text)d.innerHTML=renderMd(text);else d.textContent=text;
167
+ wrap.appendChild(d);
168
+ const time=document.createElement('div');
169
+ time.className='msg-time';
170
+ time.textContent=fmtTime();
171
+ wrap.appendChild(time);
172
+ if(role==='assistant'&&text){
173
+ const rx=document.createElement('div');rx.className='reactions';
174
+ rx.innerHTML='<button data-r="👍" onclick="react(this)">👍</button><button data-r="👎" onclick="react(this)">👎</button>';
175
+ wrap.appendChild(rx);
176
+ }
177
+ msgs.appendChild(wrap);
105
178
  msgs.scrollTop=msgs.scrollHeight;
106
179
  return d;
107
180
  }
108
181
 
182
+ window.react=function(el){el.classList.toggle('active')};
183
+
109
184
  input.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send()}});
110
- input.addEventListener('input',()=>{input.style.height='auto';input.style.height=Math.min(input.scrollHeight,120)+'px'});
185
+ input.addEventListener('input',()=>{input.style.height='auto';input.style.height=Math.min(input.scrollHeight,150)+'px'});
111
186
  btn.addEventListener('click',send);
112
187
 
113
188
  async function send(){
@@ -116,8 +191,13 @@ async function send(){
116
191
  sending=true;btn.disabled=true;
117
192
  input.value='';input.style.height='auto';
118
193
  addMsg('user',text);
119
- const el=addMsg('assistant','');
120
- el.innerHTML='<span class="cursor"></span>';
194
+ const wrap=document.createElement('div');wrap.className='msg-wrap assistant';
195
+ const d=document.createElement('div');d.className='msg assistant';
196
+ d.innerHTML='<div class="typing"><span></span><span></span><span></span></div>';
197
+ wrap.appendChild(d);
198
+ const time=document.createElement('div');time.className='msg-time';time.textContent=fmtTime();
199
+ wrap.appendChild(time);
200
+ msgs.appendChild(wrap);msgs.scrollTop=msgs.scrollHeight;
121
201
  try{
122
202
  const res=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:text,sessionId})});
123
203
  if(!res.ok)throw new Error('HTTP '+res.status);
@@ -130,21 +210,20 @@ async function send(){
130
210
  const lines=chunk.split('\\n');
131
211
  for(const line of lines){
132
212
  if(!line.startsWith('data: '))continue;
133
- const d=line.slice(6);
134
- if(d==='[DONE]')continue;
135
- try{const j=JSON.parse(d);if(j.content)full+=j.content;if(j.error)full='Error: '+j.error;}catch{}
213
+ const dd=line.slice(6);if(dd==='[DONE]')continue;
214
+ try{const j=JSON.parse(dd);if(j.content)full+=j.content;if(j.error)full='Error: '+j.error;}catch{}
136
215
  }
137
- el.textContent=full;
216
+ d.innerHTML=renderMd(full)+'<span class="cursor"></span>';
138
217
  msgs.scrollTop=msgs.scrollHeight;
139
218
  }
140
- if(!full)el.textContent='(empty response)';
141
- }catch(e){
142
- el.className='msg error';el.textContent='Error: '+e.message;
143
- }
219
+ if(!full){d.textContent='(empty response)';}else{d.innerHTML=renderMd(full);}
220
+ const rx=document.createElement('div');rx.className='reactions';
221
+ rx.innerHTML='<button data-r="👍" onclick="react(this)">👍</button><button data-r="👎" onclick="react(this)">👎</button>';
222
+ wrap.appendChild(rx);
223
+ }catch(e){d.className='msg error';d.textContent='Error: '+e.message;}
144
224
  sending=false;btn.disabled=false;input.focus();
145
225
  }
146
226
 
147
- // Fetch agent info
148
227
  fetch('/api/info').then(r=>r.json()).then(d=>{if(d.name)document.getElementById('title').textContent=d.name}).catch(()=>{});
149
228
  </script>
150
229
  </body>
@@ -365,7 +444,7 @@ class WebChannel extends index_1.BaseChannel {
365
444
  timestamp: Date.now(),
366
445
  uptime: uptimeMs,
367
446
  uptimeHuman: `${Math.floor(uptimeMs / 3600000)}h ${Math.floor((uptimeMs % 3600000) / 60000)}m`,
368
- version: '0.7.0',
447
+ version: '1.0.0',
369
448
  agent: this.agentName,
370
449
  stats: {
371
450
  sessions: this.stats.sessions,