opc-agent 2.0.2 → 3.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 (151) hide show
  1. package/README.md +603 -545
  2. package/dist/channels/voice.d.ts +59 -0
  3. package/dist/channels/voice.js +351 -1
  4. package/dist/cli.js +284 -5
  5. package/dist/core/agent.d.ts +9 -0
  6. package/dist/core/agent.js +49 -0
  7. package/dist/core/collaboration.d.ts +89 -0
  8. package/dist/core/collaboration.js +201 -0
  9. package/dist/deploy/index.d.ts +40 -0
  10. package/dist/deploy/index.js +261 -0
  11. package/dist/index.d.ts +7 -1
  12. package/dist/index.js +47 -3
  13. package/dist/mcp/servers/calculator-mcp.d.ts +3 -0
  14. package/dist/mcp/servers/calculator-mcp.js +65 -0
  15. package/dist/mcp/servers/crypto-mcp.d.ts +3 -0
  16. package/dist/mcp/servers/crypto-mcp.js +108 -0
  17. package/dist/mcp/servers/database-mcp.d.ts +3 -0
  18. package/dist/mcp/servers/database-mcp.js +73 -0
  19. package/dist/mcp/servers/datetime-mcp.d.ts +3 -0
  20. package/dist/mcp/servers/datetime-mcp.js +71 -0
  21. package/dist/mcp/servers/filesystem.d.ts +3 -0
  22. package/dist/mcp/servers/filesystem.js +101 -0
  23. package/dist/mcp/servers/github-mcp.d.ts +3 -0
  24. package/dist/mcp/servers/github-mcp.js +60 -0
  25. package/dist/mcp/servers/index.d.ts +21 -0
  26. package/dist/mcp/servers/index.js +50 -0
  27. package/dist/mcp/servers/json-mcp.d.ts +3 -0
  28. package/dist/mcp/servers/json-mcp.js +126 -0
  29. package/dist/mcp/servers/memory-mcp.d.ts +3 -0
  30. package/dist/mcp/servers/memory-mcp.js +60 -0
  31. package/dist/mcp/servers/regex-mcp.d.ts +3 -0
  32. package/dist/mcp/servers/regex-mcp.js +56 -0
  33. package/dist/mcp/servers/web-mcp.d.ts +3 -0
  34. package/dist/mcp/servers/web-mcp.js +51 -0
  35. package/dist/memory/index.d.ts +2 -0
  36. package/dist/memory/index.js +4 -1
  37. package/dist/memory/seed-loader.d.ts +51 -0
  38. package/dist/memory/seed-loader.js +200 -0
  39. package/dist/schema/oad.d.ts +292 -12
  40. package/dist/schema/oad.js +12 -1
  41. package/dist/security/guardrails.d.ts +50 -0
  42. package/dist/security/guardrails.js +197 -0
  43. package/dist/studio/server.d.ts +31 -1
  44. package/dist/studio/server.js +154 -3
  45. package/dist/studio-ui/index.html +1278 -662
  46. package/dist/tools/integrations/calendar.d.ts +3 -0
  47. package/dist/tools/integrations/calendar.js +73 -0
  48. package/dist/tools/integrations/code-exec.d.ts +3 -0
  49. package/dist/tools/integrations/code-exec.js +42 -0
  50. package/dist/tools/integrations/csv-analyzer.d.ts +3 -0
  51. package/dist/tools/integrations/csv-analyzer.js +142 -0
  52. package/dist/tools/integrations/database.d.ts +3 -0
  53. package/dist/tools/integrations/database.js +44 -0
  54. package/dist/tools/integrations/email-send.d.ts +3 -0
  55. package/dist/tools/integrations/email-send.js +104 -0
  56. package/dist/tools/integrations/git-tool.d.ts +3 -0
  57. package/dist/tools/integrations/git-tool.js +49 -0
  58. package/dist/tools/integrations/github-tool.d.ts +3 -0
  59. package/dist/tools/integrations/github-tool.js +77 -0
  60. package/dist/tools/integrations/image-gen.d.ts +3 -0
  61. package/dist/tools/integrations/image-gen.js +58 -0
  62. package/dist/tools/integrations/index.d.ts +30 -0
  63. package/dist/tools/integrations/index.js +107 -0
  64. package/dist/tools/integrations/jira.d.ts +3 -0
  65. package/dist/tools/integrations/jira.js +85 -0
  66. package/dist/tools/integrations/notion.d.ts +3 -0
  67. package/dist/tools/integrations/notion.js +71 -0
  68. package/dist/tools/integrations/npm-tool.d.ts +3 -0
  69. package/dist/tools/integrations/npm-tool.js +49 -0
  70. package/dist/tools/integrations/pdf-reader.d.ts +3 -0
  71. package/dist/tools/integrations/pdf-reader.js +91 -0
  72. package/dist/tools/integrations/slack.d.ts +3 -0
  73. package/dist/tools/integrations/slack.js +67 -0
  74. package/dist/tools/integrations/summarizer.d.ts +3 -0
  75. package/dist/tools/integrations/summarizer.js +49 -0
  76. package/dist/tools/integrations/translator.d.ts +3 -0
  77. package/dist/tools/integrations/translator.js +48 -0
  78. package/dist/tools/integrations/trello.d.ts +3 -0
  79. package/dist/tools/integrations/trello.js +60 -0
  80. package/dist/tools/integrations/vector-search.d.ts +3 -0
  81. package/dist/tools/integrations/vector-search.js +44 -0
  82. package/dist/tools/integrations/web-scraper.d.ts +3 -0
  83. package/dist/tools/integrations/web-scraper.js +48 -0
  84. package/dist/tools/integrations/web-search.d.ts +3 -0
  85. package/dist/tools/integrations/web-search.js +60 -0
  86. package/dist/tools/integrations/webhook.d.ts +3 -0
  87. package/dist/tools/integrations/webhook.js +39 -0
  88. package/dist/ui/components.d.ts +10 -0
  89. package/dist/ui/components.js +123 -0
  90. package/package.json +1 -1
  91. package/src/channels/voice.ts +365 -0
  92. package/src/cli.ts +294 -6
  93. package/src/core/agent.ts +56 -0
  94. package/src/core/collaboration.ts +275 -0
  95. package/src/deploy/index.ts +255 -0
  96. package/src/index.ts +21 -1
  97. package/src/mcp/servers/calculator-mcp.ts +65 -0
  98. package/src/mcp/servers/crypto-mcp.ts +73 -0
  99. package/src/mcp/servers/database-mcp.ts +72 -0
  100. package/src/mcp/servers/datetime-mcp.ts +69 -0
  101. package/src/mcp/servers/filesystem.ts +66 -0
  102. package/src/mcp/servers/github-mcp.ts +58 -0
  103. package/src/mcp/servers/index.ts +63 -0
  104. package/src/mcp/servers/json-mcp.ts +102 -0
  105. package/src/mcp/servers/memory-mcp.ts +56 -0
  106. package/src/mcp/servers/regex-mcp.ts +53 -0
  107. package/src/mcp/servers/web-mcp.ts +49 -0
  108. package/src/memory/index.ts +3 -0
  109. package/src/memory/seed-loader.ts +212 -0
  110. package/src/schema/oad.ts +13 -0
  111. package/src/security/guardrails.ts +248 -0
  112. package/src/studio/server.ts +166 -4
  113. package/src/studio-ui/index.html +1278 -662
  114. package/src/tools/integrations/calendar.ts +73 -0
  115. package/src/tools/integrations/code-exec.ts +39 -0
  116. package/src/tools/integrations/csv-analyzer.ts +92 -0
  117. package/src/tools/integrations/database.ts +44 -0
  118. package/src/tools/integrations/email-send.ts +76 -0
  119. package/src/tools/integrations/git-tool.ts +42 -0
  120. package/src/tools/integrations/github-tool.ts +76 -0
  121. package/src/tools/integrations/image-gen.ts +56 -0
  122. package/src/tools/integrations/index.ts +92 -0
  123. package/src/tools/integrations/jira.ts +83 -0
  124. package/src/tools/integrations/notion.ts +71 -0
  125. package/src/tools/integrations/npm-tool.ts +48 -0
  126. package/src/tools/integrations/pdf-reader.ts +58 -0
  127. package/src/tools/integrations/slack.ts +65 -0
  128. package/src/tools/integrations/summarizer.ts +49 -0
  129. package/src/tools/integrations/translator.ts +48 -0
  130. package/src/tools/integrations/trello.ts +60 -0
  131. package/src/tools/integrations/vector-search.ts +42 -0
  132. package/src/tools/integrations/web-scraper.ts +47 -0
  133. package/src/tools/integrations/web-search.ts +58 -0
  134. package/src/tools/integrations/webhook.ts +38 -0
  135. package/src/ui/components.ts +127 -0
  136. package/tests/brain-seed-extended.test.ts +490 -0
  137. package/tests/brain-seed.test.ts +239 -0
  138. package/tests/collaboration.test.ts +319 -0
  139. package/tests/deploy-and-dag.test.ts +196 -0
  140. package/tests/guardrails.test.ts +177 -0
  141. package/tests/integrations.test.ts +249 -0
  142. package/tests/mcp-servers.test.ts +260 -0
  143. package/tests/voice-enhanced.test.ts +169 -0
  144. package/dist/dtv/data.d.ts +0 -18
  145. package/dist/dtv/data.js +0 -25
  146. package/dist/dtv/trust.d.ts +0 -19
  147. package/dist/dtv/trust.js +0 -40
  148. package/dist/dtv/value.d.ts +0 -23
  149. package/dist/dtv/value.js +0 -38
  150. package/dist/marketplace/index.d.ts +0 -34
  151. package/dist/marketplace/index.js +0 -202
@@ -1,662 +1,1278 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>OPC Studio</title>
7
- <style>
8
- /* === Global === */
9
- * { margin: 0; padding: 0; box-sizing: border-box; }
10
- :root {
11
- --bg: #0a0a0a;
12
- --bg-card: #141414;
13
- --bg-hover: #1a1a1a;
14
- --border: #262626;
15
- --text: #e5e5e5;
16
- --text-muted: #737373;
17
- --accent: #3b82f6;
18
- --accent-hover: #2563eb;
19
- --green: #22c55e;
20
- --red: #ef4444;
21
- --yellow: #eab308;
22
- --purple: #a855f7;
23
- --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
24
- --mono: 'SF Mono', 'Fira Code', monospace;
25
- --radius: 8px;
26
- }
27
- body { font-family: var(--font); background: var(--bg); color: var(--text); min-height: 100vh; }
28
-
29
- /* === Layout === */
30
- .app { display: flex; min-height: 100vh; }
31
-
32
- /* Sidebar */
33
- .sidebar {
34
- width: 240px; background: var(--bg-card); border-right: 1px solid var(--border);
35
- padding: 16px; display: flex; flex-direction: column; position: fixed; height: 100vh;
36
- }
37
- .sidebar-logo { font-size: 18px; font-weight: 700; padding: 8px 12px; margin-bottom: 24px; display: flex; align-items: center; gap: 8px; }
38
- .sidebar-logo span { color: var(--accent); }
39
- .sidebar-nav { flex: 1; }
40
- .nav-item {
41
- display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: var(--radius);
42
- cursor: pointer; color: var(--text-muted); transition: all 0.15s; font-size: 14px; margin-bottom: 2px;
43
- }
44
- .nav-item:hover { background: var(--bg-hover); color: var(--text); }
45
- .nav-item.active { background: var(--bg-hover); color: var(--text); font-weight: 500; }
46
- .nav-item .icon { width: 18px; text-align: center; }
47
-
48
- /* Main content */
49
- .main { flex: 1; margin-left: 240px; padding: 32px; max-width: 1200px; }
50
-
51
- /* Header */
52
- .page-header { margin-bottom: 32px; }
53
- .page-title { font-size: 24px; font-weight: 700; margin-bottom: 4px; }
54
- .page-subtitle { color: var(--text-muted); font-size: 14px; }
55
-
56
- /* Cards */
57
- .card {
58
- background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius);
59
- padding: 20px; margin-bottom: 16px;
60
- }
61
- .card-title { font-size: 14px; font-weight: 600; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
62
- .card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; margin-bottom: 24px; }
63
-
64
- /* Stats */
65
- .stat { text-align: center; padding: 16px; }
66
- .stat-value { font-size: 32px; font-weight: 700; color: var(--accent); }
67
- .stat-label { font-size: 12px; color: var(--text-muted); margin-top: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
68
-
69
- /* Status badge */
70
- .badge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 500; }
71
- .badge-green { background: rgba(34,197,94,0.1); color: var(--green); }
72
- .badge-red { background: rgba(239,68,68,0.1); color: var(--red); }
73
- .badge-yellow { background: rgba(234,179,8,0.1); color: var(--yellow); }
74
- .badge-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
75
-
76
- /* Table */
77
- .table { width: 100%; border-collapse: collapse; }
78
- .table th { text-align: left; padding: 10px 12px; font-size: 12px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid var(--border); }
79
- .table td { padding: 10px 12px; font-size: 14px; border-bottom: 1px solid var(--border); }
80
- .table tr:hover { background: var(--bg-hover); }
81
-
82
- /* Chat */
83
- .chat-container { height: 500px; display: flex; flex-direction: column; }
84
- .chat-messages { flex: 1; overflow-y: auto; padding: 16px; }
85
- .chat-input-row { display: flex; gap: 8px; padding: 16px; border-top: 1px solid var(--border); }
86
- .chat-input { flex: 1; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 14px; color: var(--text); font-size: 14px; outline: none; }
87
- .chat-input:focus { border-color: var(--accent); }
88
- .chat-send { background: var(--accent); color: white; border: none; border-radius: var(--radius); padding: 10px 20px; font-weight: 500; cursor: pointer; }
89
- .chat-send:hover { background: var(--accent-hover); }
90
- .message { margin-bottom: 16px; }
91
- .message-user { text-align: right; }
92
- .message-user .bubble { background: var(--accent); color: white; display: inline-block; padding: 10px 14px; border-radius: 14px 14px 4px 14px; max-width: 70%; text-align: left; }
93
- .message-agent .bubble { background: var(--bg-hover); display: inline-block; padding: 10px 14px; border-radius: 14px 14px 14px 4px; max-width: 70%; }
94
- .message-label { font-size: 11px; color: var(--text-muted); margin-bottom: 4px; }
95
-
96
- /* Config editor */
97
- .editor { width: 100%; min-height: 400px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; color: var(--text); font-family: var(--mono); font-size: 13px; line-height: 1.6; resize: vertical; outline: none; }
98
- .editor:focus { border-color: var(--accent); }
99
-
100
- /* Button */
101
- .btn { padding: 8px 16px; border-radius: var(--radius); font-size: 13px; font-weight: 500; cursor: pointer; border: 1px solid var(--border); background: var(--bg-card); color: var(--text); transition: all 0.15s; }
102
- .btn:hover { background: var(--bg-hover); }
103
- .btn-primary { background: var(--accent); border-color: var(--accent); color: white; }
104
- .btn-primary:hover { background: var(--accent-hover); }
105
-
106
- /* Doctor checks */
107
- .check-item { display: flex; align-items: center; gap: 10px; padding: 8px 0; }
108
- .check-icon { font-size: 16px; }
109
- .check-name { font-weight: 500; min-width: 180px; }
110
- .check-detail { color: var(--text-muted); font-size: 13px; }
111
- .check-fix { color: var(--yellow); font-size: 12px; font-style: italic; }
112
-
113
- /* Memory list */
114
- .memory-item { padding: 12px; border-bottom: 1px solid var(--border); cursor: pointer; }
115
- .memory-item:hover { background: var(--bg-hover); }
116
- .memory-slug { font-weight: 500; font-family: var(--mono); font-size: 13px; }
117
- .memory-preview { color: var(--text-muted); font-size: 13px; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
118
- .memory-meta { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
119
-
120
- /* Search */
121
- .search-bar { display: flex; gap: 8px; margin-bottom: 16px; }
122
- .search-input { flex: 1; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 8px 14px; color: var(--text); font-size: 14px; outline: none; }
123
-
124
- /* Page sections (hidden by default) */
125
- .page { display: none; }
126
- .page.active { display: block; }
127
-
128
- /* Scrollbar */
129
- ::-webkit-scrollbar { width: 6px; }
130
- ::-webkit-scrollbar-track { background: transparent; }
131
- ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
132
-
133
- /* Loading */
134
- .loading { color: var(--text-muted); text-align: center; padding: 40px; }
135
- .spinner { display: inline-block; width: 20px; height: 20px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; }
136
- @keyframes spin { to { transform: rotate(360deg); } }
137
- </style>
138
- </head>
139
- <body>
140
- <div class="app">
141
- <!-- Sidebar -->
142
- <nav class="sidebar">
143
- <div class="sidebar-logo">⚡ <span>OPC</span> Studio</div>
144
- <div class="sidebar-nav">
145
- <div class="nav-item active" data-page="dashboard"><span class="icon">📊</span> Dashboard</div>
146
- <div class="nav-item" data-page="chat"><span class="icon">💬</span> Chat</div>
147
- <div class="nav-item" data-page="config"><span class="icon">⚙️</span> Config</div>
148
- <div class="nav-item" data-page="memory"><span class="icon">🧠</span> Memory</div>
149
- <div class="nav-item" data-page="skills"><span class="icon">🛠</span> Skills</div>
150
- <div class="nav-item" data-page="tools"><span class="icon">🔧</span> Tools</div>
151
- <div class="nav-item" data-page="channels"><span class="icon">📡</span> Channels</div>
152
- <div class="nav-item" data-page="workflows"><span class="icon">🔀</span> Workflows</div>
153
- <div class="nav-item" data-page="jobs"><span class="icon">⏰</span> Jobs</div>
154
- <div class="nav-item" data-page="plugins"><span class="icon">🔌</span> Plugins</div>
155
- <div class="nav-item" data-page="protocols"><span class="icon">📡</span> Protocols</div>
156
- <div class="nav-item" data-page="doctor"><span class="icon">🩺</span> Doctor</div>
157
- <div class="nav-item" data-page="evals"><span class="icon">🧪</span> Evals</div>
158
- <div class="nav-item" data-page="telemetry"><span class="icon">📈</span> Telemetry</div>
159
- <div class="nav-item" data-page="logs"><span class="icon">📜</span> Logs</div>
160
- <div style="padding: 8px 12px; margin-top: 16px; font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px;">Modules</div>
161
- <div class="nav-item" data-page="modules"><span class="icon">🔌</span> Modules</div>
162
- <div class="nav-item" data-page="brain-module"><span class="icon">🧠</span> DeepBrain</div>
163
- <div class="nav-item" data-page="kits-module"><span class="icon">📊</span> AgentKits</div>
164
- <div class="nav-item" data-page="workstation-module"><span class="icon">👤</span> Workstation</div>
165
- </div>
166
- <div style="padding: 8px 12px; font-size: 11px; color: var(--text-muted);">OPC Agent v2.1</div>
167
- </nav>
168
-
169
- <!-- Main Content -->
170
- <main class="main">
171
- <!-- Dashboard -->
172
- <div class="page active" id="page-dashboard">
173
- <div class="page-header">
174
- <div class="page-title">Dashboard</div>
175
- <div class="page-subtitle">Agent overview and health status</div>
176
- </div>
177
- <div class="card-grid">
178
- <div class="card stat"><div class="stat-value" id="agent-name">—</div><div class="stat-label">Agent Name</div></div>
179
- <div class="card stat"><div class="stat-value" id="agent-model">—</div><div class="stat-label">Model</div></div>
180
- <div class="card stat"><div class="stat-value" id="agent-channels">—</div><div class="stat-label">Channels</div></div>
181
- <div class="card stat"><div class="stat-value" id="agent-skills">—</div><div class="stat-label">Skills</div></div>
182
- </div>
183
- <div class="card">
184
- <div class="card-title">🟢 Agent Status</div>
185
- <div id="agent-status-detail">Loading...</div>
186
- </div>
187
- <div class="card">
188
- <div class="card-title">🧠 Memory Stats</div>
189
- <div id="memory-stats">Loading...</div>
190
- </div>
191
- </div>
192
-
193
- <!-- Chat -->
194
- <div class="page" id="page-chat">
195
- <div class="page-header">
196
- <div class="page-title">Chat</div>
197
- <div class="page-subtitle">Test your agent in real-time</div>
198
- </div>
199
- <div class="card chat-container">
200
- <div class="chat-messages" id="chat-messages">
201
- <div class="message message-agent"><div class="message-label">Agent</div><div class="bubble">Hi! I'm ready to chat. Type a message below.</div></div>
202
- </div>
203
- <div class="chat-input-row">
204
- <input class="chat-input" id="chat-input" placeholder="Type a message..." onkeydown="if(event.key==='Enter')sendChat()">
205
- <button class="chat-send" onclick="sendChat()">Send</button>
206
- </div>
207
- </div>
208
- </div>
209
-
210
- <!-- Config -->
211
- <div class="page" id="page-config">
212
- <div class="page-header">
213
- <div class="page-title">Configuration</div>
214
- <div class="page-subtitle">Edit agent.yaml</div>
215
- </div>
216
- <div class="card">
217
- <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
218
- <div class="card-title" style="margin:0">agent.yaml</div>
219
- <button class="btn btn-primary" onclick="saveConfig()">Save</button>
220
- </div>
221
- <textarea class="editor" id="config-editor">Loading...</textarea>
222
- </div>
223
- </div>
224
-
225
- <!-- Memory -->
226
- <div class="page" id="page-memory">
227
- <div class="page-header">
228
- <div class="page-title">Memory</div>
229
- <div class="page-subtitle">DeepBrain knowledge pages</div>
230
- </div>
231
- <div class="search-bar">
232
- <input class="search-input" id="memory-search" placeholder="Search memories..." onkeydown="if(event.key==='Enter')searchMemory()">
233
- <button class="btn" onclick="searchMemory()">Search</button>
234
- </div>
235
- <div class="card" id="memory-list">Loading...</div>
236
- </div>
237
-
238
- <!-- Skills -->
239
- <div class="page" id="page-skills">
240
- <div class="page-header">
241
- <div class="page-title">Skills</div>
242
- <div class="page-subtitle">Agent capabilities</div>
243
- </div>
244
- <div class="card" id="skills-list">Loading...</div>
245
- </div>
246
-
247
- <!-- Tools -->
248
- <div class="page" id="page-tools">
249
- <div class="page-header">
250
- <div class="page-title">Tools</div>
251
- <div class="page-subtitle">Built-in and MCP tools</div>
252
- </div>
253
- <div class="card" id="tools-list">Loading...</div>
254
- </div>
255
-
256
- <!-- Channels -->
257
- <div class="page" id="page-channels">
258
- <div class="page-header">
259
- <div class="page-title">Channels</div>
260
- <div class="page-subtitle">Communication endpoints</div>
261
- </div>
262
- <div class="card" id="channels-list">Loading...</div>
263
- </div>
264
-
265
- <!-- Workflows -->
266
- <div class="page" id="page-workflows">
267
- <div class="page-header">
268
- <div class="page-title">Workflows</div>
269
- <div class="page-subtitle">Agent workflow definitions</div>
270
- </div>
271
- <div class="card" id="workflows-list">Loading...</div>
272
- </div>
273
-
274
- <!-- Jobs -->
275
- <div class="page" id="page-jobs">
276
- <div class="page-header">
277
- <div class="page-title">Scheduled Jobs</div>
278
- <div class="page-subtitle">Cron tasks</div>
279
- </div>
280
- <div class="card" id="jobs-list">Loading...</div>
281
- </div>
282
-
283
- <!-- Plugins -->
284
- <div class="page" id="page-plugins">
285
- <div class="page-header">
286
- <div class="page-title">Plugins</div>
287
- <div class="page-subtitle">Middleware and extensions</div>
288
- </div>
289
- <div class="card" id="plugins-list">Loading...</div>
290
- </div>
291
-
292
- <!-- Doctor -->
293
- <div class="page" id="page-doctor">
294
- <div class="page-header">
295
- <div class="page-title">Doctor</div>
296
- <div class="page-subtitle">Environment health check</div>
297
- </div>
298
- <div class="card" id="doctor-results">
299
- <button class="btn btn-primary" onclick="runDoctor()">Run Diagnostic</button>
300
- </div>
301
- </div>
302
-
303
- <!-- Logs -->
304
- <div class="page" id="page-evals">
305
- <div class="page-header">
306
- <div class="page-title">🧪 Evals</div>
307
- <div class="page-subtitle">Agent quality evaluation</div>
308
- </div>
309
- <div class="card" id="eval-panel">
310
- <div style="margin-bottom:16px">
311
- <label>Suite: </label>
312
- <select id="eval-suite-select" style="padding:6px 12px;border-radius:6px;border:1px solid var(--border);background:var(--bg-card);color:var(--text)">
313
- <option value="basic">basic</option>
314
- <option value="safety">safety</option>
315
- <option value="memory">memory</option>
316
- </select>
317
- <button class="btn btn-primary" onclick="runEval()" style="margin-left:8px">Run Suite</button>
318
- </div>
319
- <div id="eval-results"></div>
320
- </div>
321
- </div>
322
-
323
- <!-- Telemetry -->
324
- <div class="page" id="page-telemetry">
325
- <div class="page-header">
326
- <div class="page-title">Telemetry</div>
327
- <div class="page-subtitle">OTel-compatible tracing & metrics</div>
328
- </div>
329
- <div class="card-grid" style="grid-template-columns: repeat(4,1fr); margin-bottom:16px">
330
- <div class="card" id="tel-total-spans"><div style="font-size:11px;color:var(--text-muted)">Total Spans</div><div style="font-size:24px;font-weight:700" id="tel-stat-spans">—</div></div>
331
- <div class="card" id="tel-total-traces"><div style="font-size:11px;color:var(--text-muted)">Total Traces</div><div style="font-size:24px;font-weight:700" id="tel-stat-traces">—</div></div>
332
- <div class="card" id="tel-error-rate"><div style="font-size:11px;color:var(--text-muted)">Error Rate</div><div style="font-size:24px;font-weight:700" id="tel-stat-errors">—</div></div>
333
- <div class="card" id="tel-p95"><div style="font-size:11px;color:var(--text-muted)">P95 Latency</div><div style="font-size:24px;font-weight:700" id="tel-stat-p95">—</div></div>
334
- </div>
335
- <div class="card" style="margin-bottom:16px">
336
- <h3 style="margin:0 0 12px">Recent Traces</h3>
337
- <div id="tel-traces-list" style="font-family:var(--mono);font-size:12px">Loading...</div>
338
- </div>
339
- <div class="card">
340
- <h3 style="margin:0 0 12px">Trace Waterfall</h3>
341
- <div id="tel-waterfall" style="font-family:var(--mono);font-size:12px;min-height:100px;color:var(--text-muted)">Click a trace above to view spans</div>
342
- </div>
343
- </div>
344
-
345
- <!-- Logs -->
346
- <div class="page" id="page-logs">
347
- <div class="page-header">
348
- <div class="page-title">Logs</div>
349
- <div class="page-subtitle">Recent agent activity</div>
350
- </div>
351
- <div class="card"><pre id="logs-content" style="font-family:var(--mono);font-size:12px;max-height:600px;overflow:auto;color:var(--text-muted)">Loading...</pre></div>
352
- </div>
353
-
354
- <!-- Modules Status -->
355
- <div class="page" id="page-modules">
356
- <div class="page-header">
357
- <div class="page-title">Modules</div>
358
- <div class="page-subtitle">Sub-module status and health</div>
359
- </div>
360
- <div id="modules-grid" class="card-grid">Loading...</div>
361
- </div>
362
-
363
- <!-- DeepBrain Module -->
364
- <div class="page" id="page-brain-module">
365
- <iframe src="/brain/" style="width:100%;height:calc(100vh - 32px);border:none;border-radius:8px;"></iframe>
366
- </div>
367
-
368
- <!-- AgentKits Module -->
369
- <div class="page" id="page-kits-module">
370
- <iframe src="/kits/" style="width:100%;height:calc(100vh - 32px);border:none;border-radius:8px;"></iframe>
371
- </div>
372
-
373
- <!-- Workstation Module -->
374
- <div class="page" id="page-workstation-module">
375
- <iframe src="/workstation/" style="width:100%;height:calc(100vh - 32px);border:none;border-radius:8px;"></iframe>
376
- </div>
377
- </main>
378
- </div>
379
-
380
- <script>
381
- const API = ''; // same origin
382
-
383
- // Navigation
384
- document.querySelectorAll('.nav-item').forEach(item => {
385
- item.addEventListener('click', () => {
386
- document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
387
- document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
388
- item.classList.add('active');
389
- const page = item.dataset.page;
390
- document.getElementById('page-' + page).classList.add('active');
391
- loadPage(page);
392
- });
393
- });
394
-
395
- // Page loaders
396
- async function loadPage(page) {
397
- switch (page) {
398
- case 'dashboard': loadDashboard(); break;
399
- case 'config': loadConfig(); break;
400
- case 'memory': loadMemory(); break;
401
- case 'skills': loadSkills(); break;
402
- case 'tools': loadTools(); break;
403
- case 'channels': loadChannels(); break;
404
- case 'workflows': loadWorkflows(); break;
405
- case 'jobs': loadJobs(); break;
406
- case 'plugins': loadPlugins(); break;
407
- case 'protocols': loadProtocols(); break;
408
- case 'logs': loadLogs(); break;
409
- case 'modules': loadModules(); break;
410
- case 'telemetry': loadTelemetry(); break;
411
- case 'evals': break; // static page, run via button
412
- }
413
- }
414
-
415
- async function api(path) {
416
- const r = await fetch(API + '/api/' + path);
417
- return r.json();
418
- }
419
-
420
- async function apiPost(path, body) {
421
- const r = await fetch(API + '/api/' + path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
422
- return r.json();
423
- }
424
-
425
- async function apiPut(path, body) {
426
- const r = await fetch(API + '/api/' + path, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
427
- return r.json();
428
- }
429
-
430
- // Dashboard
431
- async function loadDashboard() {
432
- try {
433
- const info = await api('agent/info');
434
- document.getElementById('agent-name').textContent = info.name || '—';
435
- document.getElementById('agent-model').textContent = info.model || '—';
436
- document.getElementById('agent-channels').textContent = info.channels?.length || 0;
437
- document.getElementById('agent-skills').textContent = info.skills?.length || 0;
438
- document.getElementById('agent-status-detail').innerHTML =
439
- `<span class="badge badge-green"><span class="badge-dot"></span> ${info.status || 'unknown'}</span>` +
440
- ` &nbsp; Provider: ${info.provider || '—'} &nbsp; Version: ${info.version || '—'}`;
441
-
442
- const stats = await api('memory/stats');
443
- document.getElementById('memory-stats').innerHTML =
444
- `Pages: <strong>${stats.pages || 0}</strong> &nbsp; Chunks: <strong>${stats.chunks || 0}</strong>`;
445
- } catch (e) {
446
- document.getElementById('agent-status-detail').textContent = 'Failed to connect to agent';
447
- }
448
- }
449
-
450
- // Chat
451
- async function sendChat() {
452
- const input = document.getElementById('chat-input');
453
- const msg = input.value.trim();
454
- if (!msg) return;
455
- input.value = '';
456
-
457
- const messages = document.getElementById('chat-messages');
458
- messages.innerHTML += `<div class="message message-user"><div class="message-label">You</div><div class="bubble">${escapeHtml(msg)}</div></div>`;
459
- messages.innerHTML += `<div class="message message-agent" id="pending"><div class="message-label">Agent</div><div class="bubble"><span class="spinner"></span> Thinking...</div></div>`;
460
- messages.scrollTop = messages.scrollHeight;
461
-
462
- try {
463
- const result = await apiPost('agent/chat', { message: msg, sessionId: 'studio' });
464
- document.getElementById('pending').innerHTML = `<div class="message-label">Agent</div><div class="bubble">${escapeHtml(result.response || 'No response')}</div>`;
465
- document.getElementById('pending').id = '';
466
- } catch (e) {
467
- document.getElementById('pending').innerHTML = `<div class="message-label">Agent</div><div class="bubble" style="color:var(--red)">Error: ${e.message}</div>`;
468
- document.getElementById('pending').id = '';
469
- }
470
- messages.scrollTop = messages.scrollHeight;
471
- }
472
-
473
- // Config
474
- async function loadConfig() {
475
- const data = await api('agent/config');
476
- document.getElementById('config-editor').value = data.content || '# No config found';
477
- }
478
- async function saveConfig() {
479
- const content = document.getElementById('config-editor').value;
480
- await apiPut('agent/config', { content });
481
- alert('Config saved!');
482
- }
483
-
484
- // Memory
485
- async function loadMemory() {
486
- const data = await api('memory/list');
487
- const el = document.getElementById('memory-list');
488
- if (!data.pages?.length) { el.innerHTML = '<div class="loading">No memories yet</div>'; return; }
489
- el.innerHTML = data.pages.map(p => `
490
- <div class="memory-item">
491
- <div class="memory-slug">${escapeHtml(p.slug || p.title || '—')}</div>
492
- <div class="memory-preview">${escapeHtml((p.compiled_truth || p.content || '').slice(0, 100))}</div>
493
- <div class="memory-meta">Type: ${p.type || '—'} | Tags: ${(p.tags || []).join(', ') || '—'}</div>
494
- </div>
495
- `).join('');
496
- }
497
- async function searchMemory() {
498
- const q = document.getElementById('memory-search').value;
499
- if (!q) return loadMemory();
500
- const data = await api('memory/search?q=' + encodeURIComponent(q));
501
- const el = document.getElementById('memory-list');
502
- if (!data.results?.length) { el.innerHTML = '<div class="loading">No results</div>'; return; }
503
- el.innerHTML = data.results.map(r => `
504
- <div class="memory-item">
505
- <div class="memory-slug">${escapeHtml(r.slug || r.title || '—')}</div>
506
- <div class="memory-preview">${escapeHtml((r.compiled_truth || r.content || '').slice(0, 100))}</div>
507
- </div>
508
- `).join('');
509
- }
510
-
511
- // Generic list loaders
512
- async function loadSkills() {
513
- const data = await api('skills/list');
514
- renderList('skills-list', data.skills || [], s => `<strong>${s.name}</strong> — ${s.description || '—'} (used ${s.usageCount || 0} times)`);
515
- }
516
- async function loadTools() {
517
- const data = await api('tools/list');
518
- renderList('tools-list', data.tools || [], t => `<strong>🔧 ${t.name}</strong> — ${t.description || '—'}`);
519
- }
520
- async function loadChannels() {
521
- const data = await api('channels/list');
522
- renderList('channels-list', data.channels || [], c => `<span class="badge badge-green"><span class="badge-dot"></span> ${c.type}</span> ${c.port ? 'Port ' + c.port : ''} ${c.mode || ''}`);
523
- }
524
- async function loadWorkflows() {
525
- const data = await api('workflows/list');
526
- renderList('workflows-list', data.workflows || [], w => `<strong>${w.name}</strong> — ${w.steps?.length || 0} steps`);
527
- }
528
- async function loadJobs() {
529
- const data = await api('jobs/list');
530
- renderList('jobs-list', data.jobs || [], j => `<strong>⏰ ${j.name}</strong> — <code>${j.schedule}</code> — ${j.task || ''}`);
531
- }
532
- async function loadPlugins() {
533
- const data = await api('plugins/list');
534
- renderList('plugins-list', data.plugins || [], p => `<strong>🔌 ${p.name}</strong> ${p.config ? JSON.stringify(p.config) : ''}`);
535
- }
536
- async function loadProtocols() {
537
- const data = await api('protocols');
538
- const list = data.protocols || [];
539
- const content = document.getElementById('content');
540
- content.innerHTML = `<h2>📡 Protocols</h2><div class="grid">${list.map(p =>
541
- `<div class="card stat"><div class="stat-value">${p.enabled ? '🟢' : ''} ${p.name}</div><div class="stat-label">${p.description}</div></div>`
542
- ).join('')}</div>`;
543
- }
544
- async function loadLogs() {
545
- const data = await api('logs/recent');
546
- document.getElementById('logs-content').textContent = (data.lines || []).join('\n') || 'No logs';
547
- }
548
-
549
- // Modules
550
- async function loadModules() {
551
- const data = await api('modules');
552
- const grid = document.getElementById('modules-grid');
553
- if (data.modules) {
554
- grid.innerHTML = data.modules.map(m => `
555
- <div class="card stat" style="cursor:pointer" onclick="document.querySelector('[data-page=${m.path.replace(/\//g,'')}-module]')?.click()">
556
- <div class="stat-value">${m.icon} ${m.name}</div>
557
- <div class="stat-label">${m.running ? '🟢 Running on port ' + m.port : '⚫ Not running'}</div>
558
- </div>`).join('');
559
- } else {
560
- grid.innerHTML = '<div class="card">Failed to load module status</div>';
561
- }
562
- }
563
-
564
- // Telemetry
565
- async function loadTelemetry() {
566
- const stats = await api('telemetry/stats');
567
- if (stats && !stats.error) {
568
- document.getElementById('tel-stat-spans').textContent = stats.totalSpans;
569
- document.getElementById('tel-stat-traces').textContent = stats.totalTraces;
570
- document.getElementById('tel-stat-errors').textContent = (stats.errorRate * 100).toFixed(1) + '%';
571
- document.getElementById('tel-stat-p95').textContent = stats.p95Latency.toFixed(0) + 'ms';
572
- }
573
- const tracesData = await api('telemetry/traces?limit=50');
574
- const el = document.getElementById('tel-traces-list');
575
- if (tracesData.traces && tracesData.traces.length > 0) {
576
- el.innerHTML = '<table style="width:100%;border-collapse:collapse"><tr style="color:var(--text-muted);text-align:left"><th>Trace ID</th><th>Root Span</th><th>Time</th><th>Spans</th><th>Status</th></tr>' +
577
- tracesData.traces.map(t => {
578
- const time = new Date(t.startTime).toLocaleTimeString();
579
- const statusColor = t.status === 'ok' ? '#4ade80' : t.status === 'error' ? '#f87171' : '#9ca3af';
580
- return `<tr style="cursor:pointer;border-top:1px solid var(--border)" onclick="loadTraceWaterfall('${t.traceId}')"><td style="padding:6px;color:var(--accent)">${t.traceId.slice(0,12)}</td><td>${t.rootSpan}</td><td>${time}</td><td>${t.spanCount}</td><td style="color:${statusColor}">${t.status}</td></tr>`;
581
- }).join('') + '</table>';
582
- } else {
583
- el.innerHTML = '<div style="color:var(--text-muted)">No traces yet. Enable telemetry: spec.telemetry.enabled: true</div>';
584
- }
585
- }
586
-
587
- async function loadTraceWaterfall(traceId) {
588
- const data = await api('telemetry/traces?id=' + traceId);
589
- const el = document.getElementById('tel-waterfall');
590
- if (!data.spans || data.spans.length === 0) { el.innerHTML = 'No spans found'; return; }
591
- const spans = data.spans;
592
- const minTime = Math.min(...spans.map(s => s.startTime));
593
- const maxTime = Math.max(...spans.map(s => s.endTime || s.startTime));
594
- const totalDur = maxTime - minTime || 1;
595
- const depthMap = {};
596
- spans.forEach(s => { depthMap[s.spanId] = s.parentSpanId && depthMap[s.parentSpanId] !== undefined ? depthMap[s.parentSpanId] + 1 : 0; });
597
- el.innerHTML = spans.map(s => {
598
- const left = ((s.startTime - minTime) / totalDur * 70).toFixed(1);
599
- const width = Math.max(1, ((s.endTime || s.startTime) - s.startTime) / totalDur * 70).toFixed(1);
600
- const color = s.status === 'ok' ? '#4ade80' : s.status === 'error' ? '#f87171' : '#60a5fa';
601
- const depth = depthMap[s.spanId] || 0;
602
- const dur = s.endTime ? (s.endTime - s.startTime) + 'ms' : '—';
603
- return `<div style="display:flex;align-items:center;margin:2px 0;padding-left:${depth*20}px"><span style="width:180px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${s.name}</span><div style="flex:1;position:relative;height:18px;background:var(--bg-hover);border-radius:3px"><div style="position:absolute;left:${left}%;width:${width}%;height:100%;background:${color};border-radius:3px;opacity:0.8" title="${dur}"></div></div><span style="width:60px;text-align:right;flex-shrink:0;margin-left:8px">${dur}</span></div>`;
604
- }).join('');
605
- }
606
- window.loadTraceWaterfall = loadTraceWaterfall;
607
-
608
- // Doctor
609
- async function runDoctor() {
610
- const el = document.getElementById('doctor-results');
611
- el.innerHTML = '<div class="loading"><span class="spinner"></span> Running checks...</div>';
612
- const data = await api('doctor/check');
613
- if (data.checks) {
614
- el.innerHTML = data.checks.map(c => `
615
- <div class="check-item">
616
- <span class="check-icon">${c.ok ? '✅' : '❌'}</span>
617
- <span class="check-name">${c.name}</span>
618
- <span class="check-detail">${c.detail || ''}</span>
619
- ${c.fix ? `<span class="check-fix">→ ${c.fix}</span>` : ''}
620
- </div>
621
- `).join('') + `<div style="margin-top:16px"><button class="btn btn-primary" onclick="runDoctor()">Re-run</button></div>`;
622
- } else {
623
- el.innerHTML = `<pre>${JSON.stringify(data, null, 2)}</pre><button class="btn btn-primary" onclick="runDoctor()">Re-run</button>`;
624
- }
625
- }
626
-
627
- // Helpers
628
- async function runEval() {
629
- const suite = document.getElementById('eval-suite-select').value;
630
- const el = document.getElementById('eval-results');
631
- el.innerHTML = '<div class="loading"><span class="spinner"></span> Running eval...</div>';
632
- try {
633
- const resp = await fetch('/api/eval/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ suite }) });
634
- const report = await resp.json();
635
- let html = `<div style="margin-bottom:12px;font-weight:600">${report.summary || ''}</div>`;
636
- html += '<table style="width:100%;border-collapse:collapse">';
637
- html += '<tr style="border-bottom:1px solid var(--border)"><th style="text-align:left;padding:6px">Case</th><th>Status</th><th>Latency</th></tr>';
638
- for (const r of (report.results || [])) {
639
- const color = r.passed ? '#4ade80' : '#f87171';
640
- html += `<tr style="border-bottom:1px solid var(--border)"><td style="padding:6px">${r.caseId}</td><td style="color:${color};text-align:center">${r.passed ? 'PASS' : 'FAIL'}</td><td style="text-align:center">${r.scores?.latency_ms || 0}ms</td></tr>`;
641
- }
642
- html += '</table>';
643
- el.innerHTML = html;
644
- } catch (e) { el.innerHTML = `<div style="color:#f87171">Error: ${e.message}</div>`; }
645
- }
646
-
647
- function renderList(id, items, renderFn) {
648
- const el = document.getElementById(id);
649
- if (!items.length) { el.innerHTML = '<div class="loading">None configured</div>'; return; }
650
- el.innerHTML = '<table class="table"><tbody>' +
651
- items.map(i => `<tr><td>${renderFn(i)}</td></tr>`).join('') +
652
- '</tbody></table>';
653
- }
654
- function escapeHtml(s) {
655
- return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
656
- }
657
-
658
- // Initial load
659
- loadDashboard();
660
- </script>
661
- </body>
662
- </html>
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>OPC Studio</title>
7
+ <style>
8
+ /* === Global === */
9
+ * { margin: 0; padding: 0; box-sizing: border-box; }
10
+ :root {
11
+ --bg: #0a0a0a;
12
+ --bg-card: #141414;
13
+ --bg-hover: #1a1a1a;
14
+ --border: #262626;
15
+ --text: #e5e5e5;
16
+ --text-muted: #737373;
17
+ --accent: #3b82f6;
18
+ --accent-hover: #2563eb;
19
+ --green: #22c55e;
20
+ --red: #ef4444;
21
+ --yellow: #eab308;
22
+ --purple: #a855f7;
23
+ --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
24
+ --mono: 'SF Mono', 'Fira Code', monospace;
25
+ --radius: 8px;
26
+ }
27
+ body { font-family: var(--font); background: var(--bg); color: var(--text); min-height: 100vh; }
28
+
29
+ /* === Layout === */
30
+ .app { display: flex; min-height: 100vh; }
31
+
32
+ /* Sidebar */
33
+ .sidebar {
34
+ width: 240px; background: var(--bg-card); border-right: 1px solid var(--border);
35
+ padding: 16px; display: flex; flex-direction: column; position: fixed; height: 100vh;
36
+ }
37
+ .sidebar-logo { font-size: 18px; font-weight: 700; padding: 8px 12px; margin-bottom: 24px; display: flex; align-items: center; gap: 8px; }
38
+ .sidebar-logo span { color: var(--accent); }
39
+ .sidebar-nav { flex: 1; }
40
+ .nav-item {
41
+ display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: var(--radius);
42
+ cursor: pointer; color: var(--text-muted); transition: all 0.15s; font-size: 14px; margin-bottom: 2px;
43
+ }
44
+ .nav-item:hover { background: var(--bg-hover); color: var(--text); }
45
+ .nav-item.active { background: var(--bg-hover); color: var(--text); font-weight: 500; }
46
+ .nav-item .icon { width: 18px; text-align: center; }
47
+
48
+ /* Main content */
49
+ .main { flex: 1; margin-left: 240px; padding: 32px; max-width: 1200px; }
50
+
51
+ /* Header */
52
+ .page-header { margin-bottom: 32px; }
53
+ .page-title { font-size: 24px; font-weight: 700; margin-bottom: 4px; }
54
+ .page-subtitle { color: var(--text-muted); font-size: 14px; }
55
+
56
+ /* Cards */
57
+ .card {
58
+ background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius);
59
+ padding: 20px; margin-bottom: 16px;
60
+ }
61
+ .card-title { font-size: 14px; font-weight: 600; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
62
+ .card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; margin-bottom: 24px; }
63
+
64
+ /* Stats */
65
+ .stat { text-align: center; padding: 16px; }
66
+ .stat-value { font-size: 32px; font-weight: 700; color: var(--accent); }
67
+ .stat-label { font-size: 12px; color: var(--text-muted); margin-top: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
68
+
69
+ /* Status badge */
70
+ .badge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 500; }
71
+ .badge-green { background: rgba(34,197,94,0.1); color: var(--green); }
72
+ .badge-red { background: rgba(239,68,68,0.1); color: var(--red); }
73
+ .badge-yellow { background: rgba(234,179,8,0.1); color: var(--yellow); }
74
+ .badge-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
75
+
76
+ /* Table */
77
+ .table { width: 100%; border-collapse: collapse; }
78
+ .table th { text-align: left; padding: 10px 12px; font-size: 12px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid var(--border); }
79
+ .table td { padding: 10px 12px; font-size: 14px; border-bottom: 1px solid var(--border); }
80
+ .table tr:hover { background: var(--bg-hover); }
81
+
82
+ /* Chat */
83
+ .chat-container { height: 500px; display: flex; flex-direction: column; }
84
+ .chat-messages { flex: 1; overflow-y: auto; padding: 16px; }
85
+ .chat-input-row { display: flex; gap: 8px; padding: 16px; border-top: 1px solid var(--border); }
86
+ .chat-input { flex: 1; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 14px; color: var(--text); font-size: 14px; outline: none; }
87
+ .chat-input:focus { border-color: var(--accent); }
88
+ .chat-send { background: var(--accent); color: white; border: none; border-radius: var(--radius); padding: 10px 20px; font-weight: 500; cursor: pointer; }
89
+ .chat-send:hover { background: var(--accent-hover); }
90
+ .message { margin-bottom: 16px; }
91
+ .message-user { text-align: right; }
92
+ .message-user .bubble { background: var(--accent); color: white; display: inline-block; padding: 10px 14px; border-radius: 14px 14px 4px 14px; max-width: 70%; text-align: left; }
93
+ .message-agent .bubble { background: var(--bg-hover); display: inline-block; padding: 10px 14px; border-radius: 14px 14px 14px 4px; max-width: 70%; }
94
+ .message-label { font-size: 11px; color: var(--text-muted); margin-bottom: 4px; }
95
+
96
+ /* Config editor */
97
+ .editor { width: 100%; min-height: 400px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; color: var(--text); font-family: var(--mono); font-size: 13px; line-height: 1.6; resize: vertical; outline: none; }
98
+ .editor:focus { border-color: var(--accent); }
99
+
100
+ /* Button */
101
+ .btn { padding: 8px 16px; border-radius: var(--radius); font-size: 13px; font-weight: 500; cursor: pointer; border: 1px solid var(--border); background: var(--bg-card); color: var(--text); transition: all 0.15s; }
102
+ .btn:hover { background: var(--bg-hover); }
103
+ .btn-primary { background: var(--accent); border-color: var(--accent); color: white; }
104
+ .btn-primary:hover { background: var(--accent-hover); }
105
+
106
+ /* Doctor checks */
107
+ .check-item { display: flex; align-items: center; gap: 10px; padding: 8px 0; }
108
+ .check-icon { font-size: 16px; }
109
+ .check-name { font-weight: 500; min-width: 180px; }
110
+ .check-detail { color: var(--text-muted); font-size: 13px; }
111
+ .check-fix { color: var(--yellow); font-size: 12px; font-style: italic; }
112
+
113
+ /* Memory list */
114
+ .memory-item { padding: 12px; border-bottom: 1px solid var(--border); cursor: pointer; }
115
+ .memory-item:hover { background: var(--bg-hover); }
116
+ .memory-slug { font-weight: 500; font-family: var(--mono); font-size: 13px; }
117
+ .memory-preview { color: var(--text-muted); font-size: 13px; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
118
+ .memory-meta { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
119
+
120
+ /* Search */
121
+ .search-bar { display: flex; gap: 8px; margin-bottom: 16px; }
122
+ .search-input { flex: 1; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 8px 14px; color: var(--text); font-size: 14px; outline: none; }
123
+
124
+ /* Page sections (hidden by default) */
125
+ .page { display: none; }
126
+ .page.active { display: block; }
127
+
128
+ /* Scrollbar */
129
+ ::-webkit-scrollbar { width: 6px; }
130
+ ::-webkit-scrollbar-track { background: transparent; }
131
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
132
+
133
+ /* Loading */
134
+ .loading { color: var(--text-muted); text-align: center; padding: 40px; }
135
+ .spinner { display: inline-block; width: 20px; height: 20px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; }
136
+ @keyframes spin { to { transform: rotate(360deg); } }
137
+ </style>
138
+ </head>
139
+ <body>
140
+ <div class="app">
141
+ <!-- Sidebar -->
142
+ <nav class="sidebar">
143
+ <div class="sidebar-logo">⚡ <span>OPC</span> Studio</div>
144
+ <div class="sidebar-nav">
145
+ <div class="nav-item active" data-page="dashboard"><span class="icon">📊</span> Dashboard</div>
146
+ <div class="nav-item" data-page="chat"><span class="icon">💬</span> Chat</div>
147
+ <div class="nav-item" data-page="config"><span class="icon">⚙️</span> Config</div>
148
+ <div class="nav-item" data-page="memory"><span class="icon">🧠</span> Memory</div>
149
+ <div class="nav-item" data-page="skills"><span class="icon">🛠</span> Skills</div>
150
+ <div class="nav-item" data-page="tools"><span class="icon">🔧</span> Tools</div>
151
+ <div class="nav-item" data-page="channels"><span class="icon">📡</span> Channels</div>
152
+ <div class="nav-item" data-page="workflows"><span class="icon">🔀</span> Workflows</div>
153
+ <div class="nav-item" data-page="jobs"><span class="icon">⏰</span> Jobs</div>
154
+ <div class="nav-item" data-page="plugins"><span class="icon">🔌</span> Plugins</div>
155
+ <div class="nav-item" data-page="protocols"><span class="icon">📡</span> Protocols</div>
156
+ <div class="nav-item" data-page="doctor"><span class="icon">🩺</span> Doctor</div>
157
+ <div class="nav-item" data-page="evals"><span class="icon">🧪</span> Evals</div>
158
+ <div class="nav-item" data-page="telemetry"><span class="icon">📈</span> Telemetry</div>
159
+ <div class="nav-item" data-page="logs"><span class="icon">📜</span> Logs</div>
160
+ <div class="nav-item" data-page="playground"><span class="icon">🎮</span> Playground</div>
161
+ <div style="padding: 8px 12px; margin-top: 16px; font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px;">Modules</div>
162
+ <div class="nav-item" data-page="modules"><span class="icon">🔌</span> Modules</div>
163
+ <div class="nav-item" data-page="brain-module"><span class="icon">🧠</span> DeepBrain</div>
164
+ <div class="nav-item" data-page="kits-module"><span class="icon">📊</span> AgentKits</div>
165
+ <div class="nav-item" data-page="workstation-module"><span class="icon">👤</span> Workstation</div>
166
+ </div>
167
+ <div style="padding: 8px 12px; font-size: 11px; color: var(--text-muted);">OPC Agent v2.1</div>
168
+ </nav>
169
+
170
+ <!-- Main Content -->
171
+ <main class="main">
172
+ <!-- Dashboard -->
173
+ <div class="page active" id="page-dashboard">
174
+ <div class="page-header">
175
+ <div class="page-title">Dashboard</div>
176
+ <div class="page-subtitle">Agent overview and health status</div>
177
+ </div>
178
+ <div class="card-grid">
179
+ <div class="card stat"><div class="stat-value" id="agent-name">—</div><div class="stat-label">Agent Name</div></div>
180
+ <div class="card stat"><div class="stat-value" id="agent-model">—</div><div class="stat-label">Model</div></div>
181
+ <div class="card stat"><div class="stat-value" id="agent-channels">—</div><div class="stat-label">Channels</div></div>
182
+ <div class="card stat"><div class="stat-value" id="agent-skills">—</div><div class="stat-label">Skills</div></div>
183
+ </div>
184
+ <div class="card">
185
+ <div class="card-title">🟢 Agent Status</div>
186
+ <div id="agent-status-detail">Loading...</div>
187
+ </div>
188
+ <div class="card">
189
+ <div class="card-title">🧠 Memory Stats</div>
190
+ <div id="memory-stats">Loading...</div>
191
+ </div>
192
+ </div>
193
+
194
+ <!-- Chat -->
195
+ <div class="page" id="page-chat">
196
+ <div class="page-header">
197
+ <div class="page-title">Chat</div>
198
+ <div class="page-subtitle">Test your agent in real-time</div>
199
+ </div>
200
+ <div class="card chat-container">
201
+ <div class="chat-messages" id="chat-messages">
202
+ <div class="message message-agent"><div class="message-label">Agent</div><div class="bubble">Hi! I'm ready to chat. Type a message below.</div></div>
203
+ </div>
204
+ <div class="chat-input-row">
205
+ <input class="chat-input" id="chat-input" placeholder="Type a message..." onkeydown="if(event.key==='Enter')sendChat()">
206
+ <button class="chat-send" onclick="sendChat()">Send</button>
207
+ </div>
208
+ </div>
209
+ </div>
210
+
211
+ <!-- Config -->
212
+ <div class="page" id="page-config">
213
+ <div class="page-header">
214
+ <div class="page-title">Configuration</div>
215
+ <div class="page-subtitle">Edit agent.yaml</div>
216
+ </div>
217
+ <div class="card">
218
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
219
+ <div class="card-title" style="margin:0">agent.yaml</div>
220
+ <button class="btn btn-primary" onclick="saveConfig()">Save</button>
221
+ </div>
222
+ <textarea class="editor" id="config-editor">Loading...</textarea>
223
+ </div>
224
+ </div>
225
+
226
+ <!-- Memory -->
227
+ <div class="page" id="page-memory">
228
+ <div class="page-header">
229
+ <div class="page-title">Memory</div>
230
+ <div class="page-subtitle">DeepBrain knowledge pages</div>
231
+ </div>
232
+ <div class="search-bar">
233
+ <input class="search-input" id="memory-search" placeholder="Search memories..." onkeydown="if(event.key==='Enter')searchMemory()">
234
+ <button class="btn" onclick="searchMemory()">Search</button>
235
+ </div>
236
+ <div class="card" id="memory-list">Loading...</div>
237
+ </div>
238
+
239
+ <!-- Skills -->
240
+ <div class="page" id="page-skills">
241
+ <div class="page-header">
242
+ <div class="page-title">Skills</div>
243
+ <div class="page-subtitle">Agent capabilities</div>
244
+ </div>
245
+ <div class="card" id="skills-list">Loading...</div>
246
+ </div>
247
+
248
+ <!-- Tools -->
249
+ <div class="page" id="page-tools">
250
+ <div class="page-header">
251
+ <div class="page-title">Tools</div>
252
+ <div class="page-subtitle">Built-in and MCP tools</div>
253
+ </div>
254
+ <div class="card" id="tools-list">Loading...</div>
255
+ </div>
256
+
257
+ <!-- Channels -->
258
+ <div class="page" id="page-channels">
259
+ <div class="page-header">
260
+ <div class="page-title">Channels</div>
261
+ <div class="page-subtitle">Communication endpoints</div>
262
+ </div>
263
+ <div class="card" id="channels-list">Loading...</div>
264
+ </div>
265
+
266
+ <!-- Workflows — Visual DAG Editor -->
267
+ <div class="page" id="page-workflows">
268
+ <div class="page-header" style="display:flex;justify-content:space-between;align-items:flex-start">
269
+ <div>
270
+ <div class="page-title">Workflows</div>
271
+ <div class="page-subtitle">Visual DAG workflow editor</div>
272
+ </div>
273
+ <div style="display:flex;gap:8px;flex-wrap:wrap">
274
+ <select id="wf-list-select" style="padding:6px 12px;border-radius:6px;border:1px solid var(--border);background:var(--bg-card);color:var(--text);font-size:13px">
275
+ <option value="">— New Workflow —</option>
276
+ </select>
277
+ <button class="btn" onclick="dagEditor.loadSelected()">Load</button>
278
+ <button class="btn btn-primary" onclick="dagEditor.save()">💾 Save</button>
279
+ <button class="btn" onclick="dagEditor.exportJSON()">📤 Export</button>
280
+ <button class="btn" onclick="dagEditor.importJSON()">📥 Import</button>
281
+ <button class="btn" style="background:var(--green);border-color:var(--green);color:#000" onclick="dagEditor.run()">▶ Run</button>
282
+ </div>
283
+ </div>
284
+ <div style="display:flex;gap:12px;margin-bottom:12px">
285
+ <div class="card" style="width:180px;padding:12px;flex-shrink:0">
286
+ <div style="font-size:12px;color:var(--text-muted);margin-bottom:8px;text-transform:uppercase;letter-spacing:0.5px">Node Palette</div>
287
+ <div class="dag-palette-item" draggable="true" data-type="input" style="cursor:grab;padding:6px 10px;margin:4px 0;border-radius:6px;border:1px solid var(--border);font-size:13px;background:var(--bg)" ondragstart="dagEditor.onPaletteDrag(event,'input')">📥 Input</div>
288
+ <div class="dag-palette-item" draggable="true" data-type="agent" style="cursor:grab;padding:6px 10px;margin:4px 0;border-radius:6px;border:1px solid var(--border);font-size:13px;background:var(--bg)" ondragstart="dagEditor.onPaletteDrag(event,'agent')">🤖 Agent</div>
289
+ <div class="dag-palette-item" draggable="true" data-type="tool" style="cursor:grab;padding:6px 10px;margin:4px 0;border-radius:6px;border:1px solid var(--border);font-size:13px;background:var(--bg)" ondragstart="dagEditor.onPaletteDrag(event,'tool')">🔧 Tool</div>
290
+ <div class="dag-palette-item" draggable="true" data-type="condition" style="cursor:grab;padding:6px 10px;margin:4px 0;border-radius:6px;border:1px solid var(--border);font-size:13px;background:var(--bg)" ondragstart="dagEditor.onPaletteDrag(event,'condition')">❓ Condition</div>
291
+ <div class="dag-palette-item" draggable="true" data-type="loop" style="cursor:grab;padding:6px 10px;margin:4px 0;border-radius:6px;border:1px solid var(--border);font-size:13px;background:var(--bg)" ondragstart="dagEditor.onPaletteDrag(event,'loop')">🔁 Loop</div>
292
+ <div class="dag-palette-item" draggable="true" data-type="parallel" style="cursor:grab;padding:6px 10px;margin:4px 0;border-radius:6px;border:1px solid var(--border);font-size:13px;background:var(--bg)" ondragstart="dagEditor.onPaletteDrag(event,'parallel')">⚡ Parallel</div>
293
+ <div class="dag-palette-item" draggable="true" data-type="output" style="cursor:grab;padding:6px 10px;margin:4px 0;border-radius:6px;border:1px solid var(--border);font-size:13px;background:var(--bg)" ondragstart="dagEditor.onPaletteDrag(event,'output')">📤 Output</div>
294
+ <hr style="border-color:var(--border);margin:12px 0">
295
+ <div style="font-size:12px;color:var(--text-muted);margin-bottom:8px;text-transform:uppercase;letter-spacing:0.5px">Actions</div>
296
+ <button class="btn" style="width:100%;margin:3px 0;font-size:12px" onclick="dagEditor.undo()">↩ Undo</button>
297
+ <button class="btn" style="width:100%;margin:3px 0;font-size:12px" onclick="dagEditor.redo()">↪ Redo</button>
298
+ <button class="btn" style="width:100%;margin:3px 0;font-size:12px" onclick="dagEditor.deleteSelected()">🗑 Delete</button>
299
+ <button class="btn" style="width:100%;margin:3px 0;font-size:12px" onclick="dagEditor.zoomIn()">🔍+ Zoom In</button>
300
+ <button class="btn" style="width:100%;margin:3px 0;font-size:12px" onclick="dagEditor.zoomOut()">🔍- Zoom Out</button>
301
+ <button class="btn" style="width:100%;margin:3px 0;font-size:12px" onclick="dagEditor.fitView()">⊞ Fit</button>
302
+ </div>
303
+ <div style="flex:1;position:relative">
304
+ <canvas id="dag-canvas" style="width:100%;height:600px;border-radius:8px;border:1px solid var(--border);background:#0d0d0d;cursor:default"></canvas>
305
+ </div>
306
+ </div>
307
+ <!-- Node properties panel -->
308
+ <div class="card" id="dag-props" style="display:none">
309
+ <div class="card-title">Node Properties</div>
310
+ <div id="dag-props-content"></div>
311
+ </div>
312
+ <!-- Run output -->
313
+ <div class="card" id="dag-run-output" style="display:none">
314
+ <div class="card-title">▶ Execution Results</div>
315
+ <pre id="dag-run-results" style="font-family:var(--mono);font-size:12px;max-height:300px;overflow:auto;color:var(--text-muted)"></pre>
316
+ </div>
317
+ </div>
318
+
319
+ <!-- Jobs -->
320
+ <div class="page" id="page-jobs">
321
+ <div class="page-header">
322
+ <div class="page-title">Scheduled Jobs</div>
323
+ <div class="page-subtitle">Cron tasks</div>
324
+ </div>
325
+ <div class="card" id="jobs-list">Loading...</div>
326
+ </div>
327
+
328
+ <!-- Plugins -->
329
+ <div class="page" id="page-plugins">
330
+ <div class="page-header">
331
+ <div class="page-title">Plugins</div>
332
+ <div class="page-subtitle">Middleware and extensions</div>
333
+ </div>
334
+ <div class="card" id="plugins-list">Loading...</div>
335
+ </div>
336
+
337
+ <!-- Doctor -->
338
+ <div class="page" id="page-doctor">
339
+ <div class="page-header">
340
+ <div class="page-title">Doctor</div>
341
+ <div class="page-subtitle">Environment health check</div>
342
+ </div>
343
+ <div class="card" id="doctor-results">
344
+ <button class="btn btn-primary" onclick="runDoctor()">Run Diagnostic</button>
345
+ </div>
346
+ </div>
347
+
348
+ <!-- Logs -->
349
+ <div class="page" id="page-evals">
350
+ <div class="page-header">
351
+ <div class="page-title">🧪 Evals</div>
352
+ <div class="page-subtitle">Agent quality evaluation</div>
353
+ </div>
354
+ <div class="card" id="eval-panel">
355
+ <div style="margin-bottom:16px">
356
+ <label>Suite: </label>
357
+ <select id="eval-suite-select" style="padding:6px 12px;border-radius:6px;border:1px solid var(--border);background:var(--bg-card);color:var(--text)">
358
+ <option value="basic">basic</option>
359
+ <option value="safety">safety</option>
360
+ <option value="memory">memory</option>
361
+ </select>
362
+ <button class="btn btn-primary" onclick="runEval()" style="margin-left:8px">Run Suite</button>
363
+ </div>
364
+ <div id="eval-results"></div>
365
+ </div>
366
+ </div>
367
+
368
+ <!-- Telemetry -->
369
+ <div class="page" id="page-telemetry">
370
+ <div class="page-header">
371
+ <div class="page-title">Telemetry</div>
372
+ <div class="page-subtitle">OTel-compatible tracing & metrics</div>
373
+ </div>
374
+ <div class="card-grid" style="grid-template-columns: repeat(4,1fr); margin-bottom:16px">
375
+ <div class="card" id="tel-total-spans"><div style="font-size:11px;color:var(--text-muted)">Total Spans</div><div style="font-size:24px;font-weight:700" id="tel-stat-spans">—</div></div>
376
+ <div class="card" id="tel-total-traces"><div style="font-size:11px;color:var(--text-muted)">Total Traces</div><div style="font-size:24px;font-weight:700" id="tel-stat-traces">—</div></div>
377
+ <div class="card" id="tel-error-rate"><div style="font-size:11px;color:var(--text-muted)">Error Rate</div><div style="font-size:24px;font-weight:700" id="tel-stat-errors">—</div></div>
378
+ <div class="card" id="tel-p95"><div style="font-size:11px;color:var(--text-muted)">P95 Latency</div><div style="font-size:24px;font-weight:700" id="tel-stat-p95">—</div></div>
379
+ </div>
380
+ <div class="card" style="margin-bottom:16px">
381
+ <h3 style="margin:0 0 12px">Recent Traces</h3>
382
+ <div id="tel-traces-list" style="font-family:var(--mono);font-size:12px">Loading...</div>
383
+ </div>
384
+ <div class="card">
385
+ <h3 style="margin:0 0 12px">Trace Waterfall</h3>
386
+ <div id="tel-waterfall" style="font-family:var(--mono);font-size:12px;min-height:100px;color:var(--text-muted)">Click a trace above to view spans</div>
387
+ </div>
388
+ </div>
389
+
390
+ <!-- Logs -->
391
+ <div class="page" id="page-logs">
392
+ <div class="page-header">
393
+ <div class="page-title">Logs</div>
394
+ <div class="page-subtitle">Recent agent activity</div>
395
+ </div>
396
+ <div class="card"><pre id="logs-content" style="font-family:var(--mono);font-size:12px;max-height:600px;overflow:auto;color:var(--text-muted)">Loading...</pre></div>
397
+ </div>
398
+
399
+ <!-- Modules Status -->
400
+ <div class="page" id="page-modules">
401
+ <div class="page-header">
402
+ <div class="page-title">Modules</div>
403
+ <div class="page-subtitle">Sub-module status and health</div>
404
+ </div>
405
+ <div id="modules-grid" class="card-grid">Loading...</div>
406
+ </div>
407
+
408
+ <!-- DeepBrain Module -->
409
+ <div class="page" id="page-brain-module">
410
+ <iframe src="/brain/" style="width:100%;height:calc(100vh - 32px);border:none;border-radius:8px;"></iframe>
411
+ </div>
412
+
413
+ <!-- AgentKits Module -->
414
+ <div class="page" id="page-kits-module">
415
+ <iframe src="/kits/" style="width:100%;height:calc(100vh - 32px);border:none;border-radius:8px;"></iframe>
416
+ </div>
417
+
418
+ <!-- Workstation Module -->
419
+ <div class="page" id="page-workstation-module">
420
+ <iframe src="/workstation/" style="width:100%;height:calc(100vh - 32px);border:none;border-radius:8px;"></iframe>
421
+ </div>
422
+
423
+ <!-- Playground Page -->
424
+ <div class="page" id="page-playground">
425
+ <div class="page-header">
426
+ <h1 class="page-title">🎮 Playground</h1>
427
+ <p class="page-subtitle">Interactive chat with model selection, system prompts, and streaming</p>
428
+ </div>
429
+ <div style="display:flex;gap:16px;margin-bottom:16px;flex-wrap:wrap;align-items:flex-end;">
430
+ <div style="flex:1;min-width:150px;">
431
+ <label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:4px;">Model</label>
432
+ <select id="pg-model" style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-card);color:var(--text);font-size:13px;">
433
+ <option>gpt-4o</option><option>gpt-4o-mini</option><option>claude-sonnet-4</option><option>claude-haiku</option><option>gemini-2.0-flash</option><option>deepseek-v3</option>
434
+ </select>
435
+ </div>
436
+ <div style="flex:0 0 160px;">
437
+ <label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:4px;">Temperature: <span id="pg-temp-val">0.7</span></label>
438
+ <input type="range" id="pg-temp" min="0" max="2" step="0.1" value="0.7" style="width:100%;" oninput="document.getElementById('pg-temp-val').textContent=this.value">
439
+ </div>
440
+ </div>
441
+ <div style="margin-bottom:16px;">
442
+ <label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:4px;">System Prompt</label>
443
+ <textarea id="pg-system" rows="2" placeholder="You are a helpful assistant..." style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-card);color:var(--text);font-size:13px;resize:vertical;font-family:inherit;"></textarea>
444
+ </div>
445
+ <div id="pg-messages" style="flex:1;min-height:300px;max-height:50vh;overflow-y:auto;border:1px solid var(--border);border-radius:8px;padding:16px;margin-bottom:16px;background:var(--bg-card);display:flex;flex-direction:column;gap:10px;">
446
+ <div style="color:var(--text-muted);font-size:13px;text-align:center;padding:40px;">Send a message to start chatting</div>
447
+ </div>
448
+ <div style="display:flex;gap:8px;">
449
+ <textarea id="pg-input" rows="2" placeholder="Type your message..." style="flex:1;padding:10px;border-radius:8px;border:1px solid var(--border);background:var(--bg-card);color:var(--text);font-size:14px;resize:none;font-family:inherit;" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();pgSend()}"></textarea>
450
+ <button onclick="pgSend()" id="pg-send-btn" style="padding:10px 24px;border:none;border-radius:8px;background:var(--accent);color:#000;font-weight:600;cursor:pointer;font-size:14px;">Send</button>
451
+ <button onclick="pgClear()" style="padding:10px 16px;border:1px solid var(--border);border-radius:8px;background:transparent;color:var(--text-muted);cursor:pointer;font-size:13px;">Clear</button>
452
+ </div>
453
+ </div>
454
+
455
+ </main>
456
+ </div>
457
+
458
+ <script>
459
+ const API = ''; // same origin
460
+
461
+ // Navigation
462
+ document.querySelectorAll('.nav-item').forEach(item => {
463
+ item.addEventListener('click', () => {
464
+ document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
465
+ document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
466
+ item.classList.add('active');
467
+ const page = item.dataset.page;
468
+ document.getElementById('page-' + page).classList.add('active');
469
+ loadPage(page);
470
+ });
471
+ });
472
+
473
+ // Page loaders
474
+ async function loadPage(page) {
475
+ switch (page) {
476
+ case 'dashboard': loadDashboard(); break;
477
+ case 'config': loadConfig(); break;
478
+ case 'memory': loadMemory(); break;
479
+ case 'skills': loadSkills(); break;
480
+ case 'tools': loadTools(); break;
481
+ case 'channels': loadChannels(); break;
482
+ case 'workflows': loadWorkflows(); break;
483
+ case 'jobs': loadJobs(); break;
484
+ case 'plugins': loadPlugins(); break;
485
+ case 'protocols': loadProtocols(); break;
486
+ case 'logs': loadLogs(); break;
487
+ case 'modules': loadModules(); break;
488
+ case 'telemetry': loadTelemetry(); break;
489
+ case 'evals': break; // static page, run via button
490
+ }
491
+ }
492
+
493
+ async function api(path) {
494
+ const r = await fetch(API + '/api/' + path);
495
+ return r.json();
496
+ }
497
+
498
+ async function apiPost(path, body) {
499
+ const r = await fetch(API + '/api/' + path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
500
+ return r.json();
501
+ }
502
+
503
+ async function apiPut(path, body) {
504
+ const r = await fetch(API + '/api/' + path, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
505
+ return r.json();
506
+ }
507
+
508
+ // Dashboard
509
+ async function loadDashboard() {
510
+ try {
511
+ const info = await api('agent/info');
512
+ document.getElementById('agent-name').textContent = info.name || '—';
513
+ document.getElementById('agent-model').textContent = info.model || '';
514
+ document.getElementById('agent-channels').textContent = info.channels?.length || 0;
515
+ document.getElementById('agent-skills').textContent = info.skills?.length || 0;
516
+ document.getElementById('agent-status-detail').innerHTML =
517
+ `<span class="badge badge-green"><span class="badge-dot"></span> ${info.status || 'unknown'}</span>` +
518
+ ` &nbsp; Provider: ${info.provider || ''} &nbsp; Version: ${info.version || '—'}`;
519
+
520
+ const stats = await api('memory/stats');
521
+ document.getElementById('memory-stats').innerHTML =
522
+ `Pages: <strong>${stats.pages || 0}</strong> &nbsp; Chunks: <strong>${stats.chunks || 0}</strong>`;
523
+ } catch (e) {
524
+ document.getElementById('agent-status-detail').textContent = 'Failed to connect to agent';
525
+ }
526
+ }
527
+
528
+ // Chat
529
+ async function sendChat() {
530
+ const input = document.getElementById('chat-input');
531
+ const msg = input.value.trim();
532
+ if (!msg) return;
533
+ input.value = '';
534
+
535
+ const messages = document.getElementById('chat-messages');
536
+ messages.innerHTML += `<div class="message message-user"><div class="message-label">You</div><div class="bubble">${escapeHtml(msg)}</div></div>`;
537
+ messages.innerHTML += `<div class="message message-agent" id="pending"><div class="message-label">Agent</div><div class="bubble"><span class="spinner"></span> Thinking...</div></div>`;
538
+ messages.scrollTop = messages.scrollHeight;
539
+
540
+ try {
541
+ const result = await apiPost('agent/chat', { message: msg, sessionId: 'studio' });
542
+ document.getElementById('pending').innerHTML = `<div class="message-label">Agent</div><div class="bubble">${escapeHtml(result.response || 'No response')}</div>`;
543
+ document.getElementById('pending').id = '';
544
+ } catch (e) {
545
+ document.getElementById('pending').innerHTML = `<div class="message-label">Agent</div><div class="bubble" style="color:var(--red)">Error: ${e.message}</div>`;
546
+ document.getElementById('pending').id = '';
547
+ }
548
+ messages.scrollTop = messages.scrollHeight;
549
+ }
550
+
551
+ // Config
552
+ async function loadConfig() {
553
+ const data = await api('agent/config');
554
+ document.getElementById('config-editor').value = data.content || '# No config found';
555
+ }
556
+ async function saveConfig() {
557
+ const content = document.getElementById('config-editor').value;
558
+ await apiPut('agent/config', { content });
559
+ alert('Config saved!');
560
+ }
561
+
562
+ // Memory
563
+ async function loadMemory() {
564
+ const data = await api('memory/list');
565
+ const el = document.getElementById('memory-list');
566
+ if (!data.pages?.length) { el.innerHTML = '<div class="loading">No memories yet</div>'; return; }
567
+ el.innerHTML = data.pages.map(p => `
568
+ <div class="memory-item">
569
+ <div class="memory-slug">${escapeHtml(p.slug || p.title || '—')}</div>
570
+ <div class="memory-preview">${escapeHtml((p.compiled_truth || p.content || '').slice(0, 100))}</div>
571
+ <div class="memory-meta">Type: ${p.type || ''} | Tags: ${(p.tags || []).join(', ') || ''}</div>
572
+ </div>
573
+ `).join('');
574
+ }
575
+ async function searchMemory() {
576
+ const q = document.getElementById('memory-search').value;
577
+ if (!q) return loadMemory();
578
+ const data = await api('memory/search?q=' + encodeURIComponent(q));
579
+ const el = document.getElementById('memory-list');
580
+ if (!data.results?.length) { el.innerHTML = '<div class="loading">No results</div>'; return; }
581
+ el.innerHTML = data.results.map(r => `
582
+ <div class="memory-item">
583
+ <div class="memory-slug">${escapeHtml(r.slug || r.title || '—')}</div>
584
+ <div class="memory-preview">${escapeHtml((r.compiled_truth || r.content || '').slice(0, 100))}</div>
585
+ </div>
586
+ `).join('');
587
+ }
588
+
589
+ // Generic list loaders
590
+ async function loadSkills() {
591
+ const data = await api('skills/list');
592
+ renderList('skills-list', data.skills || [], s => `<strong>${s.name}</strong> — ${s.description || '—'} (used ${s.usageCount || 0} times)`);
593
+ }
594
+ async function loadTools() {
595
+ const data = await api('tools/list');
596
+ renderList('tools-list', data.tools || [], t => `<strong>🔧 ${t.name}</strong> ${t.description || '—'}`);
597
+ }
598
+ async function loadChannels() {
599
+ const data = await api('channels/list');
600
+ renderList('channels-list', data.channels || [], c => `<span class="badge badge-green"><span class="badge-dot"></span> ${c.type}</span> ${c.port ? 'Port ' + c.port : ''} ${c.mode || ''}`);
601
+ }
602
+ async function loadWorkflows() {
603
+ // Load saved workflows into dropdown
604
+ try {
605
+ const data = await api('workflows');
606
+ const select = document.getElementById('wf-list-select');
607
+ select.innerHTML = '<option value="">— New Workflow —</option>';
608
+ for (const wf of (data.workflows || [])) {
609
+ const opt = document.createElement('option');
610
+ opt.value = wf.id;
611
+ opt.textContent = wf.name || wf.id;
612
+ select.appendChild(opt);
613
+ }
614
+ } catch {}
615
+ dagEditor.initCanvas();
616
+ }
617
+ async function loadJobs() {
618
+ const data = await api('jobs/list');
619
+ renderList('jobs-list', data.jobs || [], j => `<strong>⏰ ${j.name}</strong> <code>${j.schedule}</code> — ${j.task || ''}`);
620
+ }
621
+ async function loadPlugins() {
622
+ const data = await api('plugins/list');
623
+ renderList('plugins-list', data.plugins || [], p => `<strong>🔌 ${p.name}</strong> ${p.config ? JSON.stringify(p.config) : ''}`);
624
+ }
625
+ async function loadProtocols() {
626
+ const data = await api('protocols');
627
+ const list = data.protocols || [];
628
+ const content = document.getElementById('content');
629
+ content.innerHTML = `<h2>📡 Protocols</h2><div class="grid">${list.map(p =>
630
+ `<div class="card stat"><div class="stat-value">${p.enabled ? '🟢' : '⚫'} ${p.name}</div><div class="stat-label">${p.description}</div></div>`
631
+ ).join('')}</div>`;
632
+ }
633
+ async function loadLogs() {
634
+ const data = await api('logs/recent');
635
+ document.getElementById('logs-content').textContent = (data.lines || []).join('\n') || 'No logs';
636
+ }
637
+
638
+ // Modules
639
+ async function loadModules() {
640
+ const data = await api('modules');
641
+ const grid = document.getElementById('modules-grid');
642
+ if (data.modules) {
643
+ grid.innerHTML = data.modules.map(m => `
644
+ <div class="card stat" style="cursor:pointer" onclick="document.querySelector('[data-page=${m.path.replace(/\//g,'')}-module]')?.click()">
645
+ <div class="stat-value">${m.icon} ${m.name}</div>
646
+ <div class="stat-label">${m.running ? '🟢 Running on port ' + m.port : '⚫ Not running'}</div>
647
+ </div>`).join('');
648
+ } else {
649
+ grid.innerHTML = '<div class="card">Failed to load module status</div>';
650
+ }
651
+ }
652
+
653
+ // Telemetry
654
+ async function loadTelemetry() {
655
+ const stats = await api('telemetry/stats');
656
+ if (stats && !stats.error) {
657
+ document.getElementById('tel-stat-spans').textContent = stats.totalSpans;
658
+ document.getElementById('tel-stat-traces').textContent = stats.totalTraces;
659
+ document.getElementById('tel-stat-errors').textContent = (stats.errorRate * 100).toFixed(1) + '%';
660
+ document.getElementById('tel-stat-p95').textContent = stats.p95Latency.toFixed(0) + 'ms';
661
+ }
662
+ const tracesData = await api('telemetry/traces?limit=50');
663
+ const el = document.getElementById('tel-traces-list');
664
+ if (tracesData.traces && tracesData.traces.length > 0) {
665
+ el.innerHTML = '<table style="width:100%;border-collapse:collapse"><tr style="color:var(--text-muted);text-align:left"><th>Trace ID</th><th>Root Span</th><th>Time</th><th>Spans</th><th>Status</th></tr>' +
666
+ tracesData.traces.map(t => {
667
+ const time = new Date(t.startTime).toLocaleTimeString();
668
+ const statusColor = t.status === 'ok' ? '#4ade80' : t.status === 'error' ? '#f87171' : '#9ca3af';
669
+ return `<tr style="cursor:pointer;border-top:1px solid var(--border)" onclick="loadTraceWaterfall('${t.traceId}')"><td style="padding:6px;color:var(--accent)">${t.traceId.slice(0,12)}</td><td>${t.rootSpan}</td><td>${time}</td><td>${t.spanCount}</td><td style="color:${statusColor}">${t.status}</td></tr>`;
670
+ }).join('') + '</table>';
671
+ } else {
672
+ el.innerHTML = '<div style="color:var(--text-muted)">No traces yet. Enable telemetry: spec.telemetry.enabled: true</div>';
673
+ }
674
+ }
675
+
676
+ async function loadTraceWaterfall(traceId) {
677
+ const data = await api('telemetry/traces?id=' + traceId);
678
+ const el = document.getElementById('tel-waterfall');
679
+ if (!data.spans || data.spans.length === 0) { el.innerHTML = 'No spans found'; return; }
680
+ const spans = data.spans;
681
+ const minTime = Math.min(...spans.map(s => s.startTime));
682
+ const maxTime = Math.max(...spans.map(s => s.endTime || s.startTime));
683
+ const totalDur = maxTime - minTime || 1;
684
+ const depthMap = {};
685
+ spans.forEach(s => { depthMap[s.spanId] = s.parentSpanId && depthMap[s.parentSpanId] !== undefined ? depthMap[s.parentSpanId] + 1 : 0; });
686
+ el.innerHTML = spans.map(s => {
687
+ const left = ((s.startTime - minTime) / totalDur * 70).toFixed(1);
688
+ const width = Math.max(1, ((s.endTime || s.startTime) - s.startTime) / totalDur * 70).toFixed(1);
689
+ const color = s.status === 'ok' ? '#4ade80' : s.status === 'error' ? '#f87171' : '#60a5fa';
690
+ const depth = depthMap[s.spanId] || 0;
691
+ const dur = s.endTime ? (s.endTime - s.startTime) + 'ms' : '—';
692
+ return `<div style="display:flex;align-items:center;margin:2px 0;padding-left:${depth*20}px"><span style="width:180px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${s.name}</span><div style="flex:1;position:relative;height:18px;background:var(--bg-hover);border-radius:3px"><div style="position:absolute;left:${left}%;width:${width}%;height:100%;background:${color};border-radius:3px;opacity:0.8" title="${dur}"></div></div><span style="width:60px;text-align:right;flex-shrink:0;margin-left:8px">${dur}</span></div>`;
693
+ }).join('');
694
+ }
695
+ window.loadTraceWaterfall = loadTraceWaterfall;
696
+
697
+ // Doctor
698
+ async function runDoctor() {
699
+ const el = document.getElementById('doctor-results');
700
+ el.innerHTML = '<div class="loading"><span class="spinner"></span> Running checks...</div>';
701
+ const data = await api('doctor/check');
702
+ if (data.checks) {
703
+ el.innerHTML = data.checks.map(c => `
704
+ <div class="check-item">
705
+ <span class="check-icon">${c.ok ? '✅' : '❌'}</span>
706
+ <span class="check-name">${c.name}</span>
707
+ <span class="check-detail">${c.detail || ''}</span>
708
+ ${c.fix ? `<span class="check-fix">→ ${c.fix}</span>` : ''}
709
+ </div>
710
+ `).join('') + `<div style="margin-top:16px"><button class="btn btn-primary" onclick="runDoctor()">Re-run</button></div>`;
711
+ } else {
712
+ el.innerHTML = `<pre>${JSON.stringify(data, null, 2)}</pre><button class="btn btn-primary" onclick="runDoctor()">Re-run</button>`;
713
+ }
714
+ }
715
+
716
+ // Helpers
717
+ async function runEval() {
718
+ const suite = document.getElementById('eval-suite-select').value;
719
+ const el = document.getElementById('eval-results');
720
+ el.innerHTML = '<div class="loading"><span class="spinner"></span> Running eval...</div>';
721
+ try {
722
+ const resp = await fetch('/api/eval/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ suite }) });
723
+ const report = await resp.json();
724
+ let html = `<div style="margin-bottom:12px;font-weight:600">${report.summary || ''}</div>`;
725
+ html += '<table style="width:100%;border-collapse:collapse">';
726
+ html += '<tr style="border-bottom:1px solid var(--border)"><th style="text-align:left;padding:6px">Case</th><th>Status</th><th>Latency</th></tr>';
727
+ for (const r of (report.results || [])) {
728
+ const color = r.passed ? '#4ade80' : '#f87171';
729
+ html += `<tr style="border-bottom:1px solid var(--border)"><td style="padding:6px">${r.caseId}</td><td style="color:${color};text-align:center">${r.passed ? 'PASS' : 'FAIL'}</td><td style="text-align:center">${r.scores?.latency_ms || 0}ms</td></tr>`;
730
+ }
731
+ html += '</table>';
732
+ el.innerHTML = html;
733
+ } catch (e) { el.innerHTML = `<div style="color:#f87171">Error: ${e.message}</div>`; }
734
+ }
735
+
736
+ function renderList(id, items, renderFn) {
737
+ const el = document.getElementById(id);
738
+ if (!items.length) { el.innerHTML = '<div class="loading">None configured</div>'; return; }
739
+ el.innerHTML = '<table class="table"><tbody>' +
740
+ items.map(i => `<tr><td>${renderFn(i)}</td></tr>`).join('') +
741
+ '</tbody></table>';
742
+ }
743
+ function escapeHtml(s) {
744
+ return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
745
+ }
746
+
747
+ // ═══════════════════════════════════════════════
748
+ // DAG Visual Editor
749
+ // ═══════════════════════════════════════════════
750
+ const dagEditor = (() => {
751
+ let canvas, ctx;
752
+ let nodes = [], edges = [];
753
+ let selected = null, dragging = null, dragOffset = { x: 0, y: 0 };
754
+ let connecting = null; // { nodeId, port:'out', mx, my }
755
+ let pan = { x: 0, y: 0 }, zoom = 1;
756
+ let isPanning = false, panStart = { x: 0, y: 0 };
757
+ let undoStack = [], redoStack = [];
758
+ let workflowId = null, workflowName = 'Untitled';
759
+ let initialized = false;
760
+ const GRID = 20, NODE_W = 160, NODE_H = 60, PORT_R = 7;
761
+ const NODE_COLORS = {
762
+ input: '#22c55e', output: '#ef4444', agent: '#3b82f6',
763
+ tool: '#eab308', condition: '#a855f7', loop: '#f97316', parallel: '#06b6d4'
764
+ };
765
+ const NODE_ICONS = {
766
+ input: '📥', output: '📤', agent: '🤖', tool: '🔧',
767
+ condition: '❓', loop: '🔁', parallel: '⚡'
768
+ };
769
+
770
+ function snap(v) { return Math.round(v / GRID) * GRID; }
771
+
772
+ function initCanvas() {
773
+ canvas = document.getElementById('dag-canvas');
774
+ if (!canvas) return;
775
+ const rect = canvas.parentElement.getBoundingClientRect();
776
+ canvas.width = rect.width * (window.devicePixelRatio || 1);
777
+ canvas.height = 600 * (window.devicePixelRatio || 1);
778
+ canvas.style.width = rect.width + 'px';
779
+ canvas.style.height = '600px';
780
+ ctx = canvas.getContext('2d');
781
+ ctx.scale(window.devicePixelRatio || 1, window.devicePixelRatio || 1);
782
+
783
+ if (!initialized) {
784
+ canvas.addEventListener('mousedown', onMouseDown);
785
+ canvas.addEventListener('mousemove', onMouseMove);
786
+ canvas.addEventListener('mouseup', onMouseUp);
787
+ canvas.addEventListener('dblclick', onDblClick);
788
+ canvas.addEventListener('wheel', onWheel);
789
+ canvas.addEventListener('dragover', e => e.preventDefault());
790
+ canvas.addEventListener('drop', onDrop);
791
+ canvas.addEventListener('contextmenu', e => e.preventDefault());
792
+ document.addEventListener('keydown', onKey);
793
+ initialized = true;
794
+ }
795
+ render();
796
+ }
797
+
798
+ function toCanvas(e) {
799
+ const r = canvas.getBoundingClientRect();
800
+ return { x: (e.clientX - r.left - pan.x) / zoom, y: (e.clientY - r.top - pan.y) / zoom };
801
+ }
802
+
803
+ function nodeAt(x, y) {
804
+ for (let i = nodes.length - 1; i >= 0; i--) {
805
+ const n = nodes[i];
806
+ if (x >= n.x && x <= n.x + NODE_W && y >= n.y && y <= n.y + NODE_H) return n;
807
+ }
808
+ return null;
809
+ }
810
+
811
+ function portAt(x, y) {
812
+ for (const n of nodes) {
813
+ // Input port (left center)
814
+ if (n.type !== 'input') {
815
+ const px = n.x, py = n.y + NODE_H / 2;
816
+ if (Math.hypot(x - px, y - py) < PORT_R + 4) return { node: n, port: 'in' };
817
+ }
818
+ // Output port (right center)
819
+ if (n.type !== 'output') {
820
+ const px = n.x + NODE_W, py = n.y + NODE_H / 2;
821
+ if (Math.hypot(x - px, y - py) < PORT_R + 4) return { node: n, port: 'out' };
822
+ }
823
+ }
824
+ return null;
825
+ }
826
+
827
+ function pushUndo() {
828
+ undoStack.push(JSON.stringify({ nodes, edges }));
829
+ if (undoStack.length > 50) undoStack.shift();
830
+ redoStack = [];
831
+ }
832
+
833
+ function addNode(type, x, y) {
834
+ pushUndo();
835
+ const id = 'n' + Date.now() + Math.random().toString(36).slice(2, 6);
836
+ const node = { id, type, name: type.charAt(0).toUpperCase() + type.slice(1), x: snap(x), y: snap(y), config: {} };
837
+ if (type === 'agent') { node.config = { systemPrompt: '', model: 'gpt-4o' }; }
838
+ if (type === 'tool') { node.config = { toolName: '' }; }
839
+ if (type === 'condition') { node.config = { expression: '' }; }
840
+ if (type === 'loop') { node.config = { maxIterations: 10, condition: '' }; }
841
+ if (type === 'parallel') { node.config = { branches: [] }; }
842
+ nodes.push(node);
843
+ selected = node;
844
+ showProps(node);
845
+ render();
846
+ }
847
+
848
+ function addEdge(fromId, toId) {
849
+ if (fromId === toId) return;
850
+ if (edges.find(e => e.from === fromId && e.to === toId)) return;
851
+ pushUndo();
852
+ edges.push({ id: 'e' + Date.now(), from: fromId, to: toId, fromPort: 'out', toPort: 'in' });
853
+ render();
854
+ }
855
+
856
+ // ── Rendering ──
857
+ function render() {
858
+ if (!ctx) return;
859
+ const w = canvas.width / (window.devicePixelRatio || 1);
860
+ const h = canvas.height / (window.devicePixelRatio || 1);
861
+ ctx.clearRect(0, 0, w, h);
862
+ ctx.save();
863
+ ctx.translate(pan.x, pan.y);
864
+ ctx.scale(zoom, zoom);
865
+
866
+ // Grid
867
+ ctx.strokeStyle = '#1a1a1a';
868
+ ctx.lineWidth = 0.5;
869
+ const gs = GRID;
870
+ const startX = Math.floor(-pan.x / zoom / gs) * gs - gs;
871
+ const startY = Math.floor(-pan.y / zoom / gs) * gs - gs;
872
+ const endX = startX + w / zoom + gs * 2;
873
+ const endY = startY + h / zoom + gs * 2;
874
+ for (let x = startX; x < endX; x += gs) { ctx.beginPath(); ctx.moveTo(x, startY); ctx.lineTo(x, endY); ctx.stroke(); }
875
+ for (let y = startY; y < endY; y += gs) { ctx.beginPath(); ctx.moveTo(startX, y); ctx.lineTo(endX, y); ctx.stroke(); }
876
+
877
+ // Edges
878
+ for (const e of edges) {
879
+ const from = nodes.find(n => n.id === e.from);
880
+ const to = nodes.find(n => n.id === e.to);
881
+ if (!from || !to) continue;
882
+ const x1 = from.x + NODE_W, y1 = from.y + NODE_H / 2;
883
+ const x2 = to.x, y2 = to.y + NODE_H / 2;
884
+ const cp = Math.abs(x2 - x1) * 0.5 + 40;
885
+ ctx.beginPath();
886
+ ctx.moveTo(x1, y1);
887
+ ctx.bezierCurveTo(x1 + cp, y1, x2 - cp, y2, x2, y2);
888
+ ctx.strokeStyle = '#555';
889
+ ctx.lineWidth = 2;
890
+ ctx.stroke();
891
+ // Arrow
892
+ const angle = Math.atan2(y2 - (y2 - 0.1), x2 - (x2 - cp * 0.1));
893
+ ctx.fillStyle = '#555';
894
+ ctx.beginPath();
895
+ ctx.moveTo(x2, y2);
896
+ ctx.lineTo(x2 - 8, y2 - 4);
897
+ ctx.lineTo(x2 - 8, y2 + 4);
898
+ ctx.fill();
899
+ }
900
+
901
+ // Connecting line (in progress)
902
+ if (connecting) {
903
+ const from = nodes.find(n => n.id === connecting.nodeId);
904
+ if (from) {
905
+ const x1 = from.x + NODE_W, y1 = from.y + NODE_H / 2;
906
+ ctx.beginPath();
907
+ ctx.moveTo(x1, y1);
908
+ const cp = Math.abs(connecting.mx - x1) * 0.5 + 40;
909
+ ctx.bezierCurveTo(x1 + cp, y1, connecting.mx - cp, connecting.my, connecting.mx, connecting.my);
910
+ ctx.strokeStyle = '#3b82f6';
911
+ ctx.lineWidth = 2;
912
+ ctx.setLineDash([6, 3]);
913
+ ctx.stroke();
914
+ ctx.setLineDash([]);
915
+ }
916
+ }
917
+
918
+ // Nodes
919
+ for (const n of nodes) {
920
+ const isSelected = selected && selected.id === n.id;
921
+ const color = NODE_COLORS[n.type] || '#666';
922
+ // Shadow
923
+ ctx.fillStyle = 'rgba(0,0,0,0.3)';
924
+ ctx.beginPath();
925
+ ctx.roundRect(n.x + 2, n.y + 2, NODE_W, NODE_H, 10);
926
+ ctx.fill();
927
+ // Body
928
+ ctx.fillStyle = isSelected ? '#252525' : '#1e1e1e';
929
+ ctx.strokeStyle = isSelected ? color : '#333';
930
+ ctx.lineWidth = isSelected ? 2.5 : 1;
931
+ ctx.beginPath();
932
+ ctx.roundRect(n.x, n.y, NODE_W, NODE_H, 10);
933
+ ctx.fill();
934
+ ctx.stroke();
935
+ // Color bar top
936
+ ctx.fillStyle = color;
937
+ ctx.beginPath();
938
+ ctx.roundRect(n.x, n.y, NODE_W, 4, [10, 10, 0, 0]);
939
+ ctx.fill();
940
+ // Icon + name
941
+ ctx.fillStyle = '#e5e5e5';
942
+ ctx.font = '13px -apple-system, sans-serif';
943
+ ctx.textBaseline = 'middle';
944
+ const icon = NODE_ICONS[n.type] || '⬜';
945
+ ctx.fillText(icon + ' ' + n.name, n.x + 12, n.y + 28);
946
+ // Type label
947
+ ctx.fillStyle = '#737373';
948
+ ctx.font = '10px -apple-system, sans-serif';
949
+ ctx.fillText(n.type, n.x + 12, n.y + 46);
950
+ // Input port
951
+ if (n.type !== 'input') {
952
+ ctx.beginPath();
953
+ ctx.arc(n.x, n.y + NODE_H / 2, PORT_R, 0, Math.PI * 2);
954
+ ctx.fillStyle = '#333';
955
+ ctx.fill();
956
+ ctx.strokeStyle = color;
957
+ ctx.lineWidth = 1.5;
958
+ ctx.stroke();
959
+ }
960
+ // Output port
961
+ if (n.type !== 'output') {
962
+ ctx.beginPath();
963
+ ctx.arc(n.x + NODE_W, n.y + NODE_H / 2, PORT_R, 0, Math.PI * 2);
964
+ ctx.fillStyle = '#333';
965
+ ctx.fill();
966
+ ctx.strokeStyle = color;
967
+ ctx.lineWidth = 1.5;
968
+ ctx.stroke();
969
+ }
970
+ }
971
+ ctx.restore();
972
+ }
973
+
974
+ // ── Events ──
975
+ function onMouseDown(e) {
976
+ const p = toCanvas(e);
977
+ // Check port first
978
+ const port = portAt(p.x, p.y);
979
+ if (port && port.port === 'out') {
980
+ connecting = { nodeId: port.node.id, mx: p.x, my: p.y };
981
+ return;
982
+ }
983
+ const node = nodeAt(p.x, p.y);
984
+ if (node) {
985
+ selected = node;
986
+ dragging = node;
987
+ dragOffset = { x: p.x - node.x, y: p.y - node.y };
988
+ pushUndo();
989
+ showProps(node);
990
+ render();
991
+ } else {
992
+ selected = null;
993
+ hideProps();
994
+ isPanning = true;
995
+ panStart = { x: e.clientX - pan.x, y: e.clientY - pan.y };
996
+ render();
997
+ }
998
+ }
999
+
1000
+ function onMouseMove(e) {
1001
+ const p = toCanvas(e);
1002
+ if (connecting) {
1003
+ connecting.mx = p.x;
1004
+ connecting.my = p.y;
1005
+ render();
1006
+ return;
1007
+ }
1008
+ if (dragging) {
1009
+ dragging.x = snap(p.x - dragOffset.x);
1010
+ dragging.y = snap(p.y - dragOffset.y);
1011
+ render();
1012
+ return;
1013
+ }
1014
+ if (isPanning) {
1015
+ pan.x = e.clientX - panStart.x;
1016
+ pan.y = e.clientY - panStart.y;
1017
+ render();
1018
+ }
1019
+ }
1020
+
1021
+ function onMouseUp(e) {
1022
+ if (connecting) {
1023
+ const p = toCanvas(e);
1024
+ const port = portAt(p.x, p.y);
1025
+ if (port && port.port === 'in' && port.node.id !== connecting.nodeId) {
1026
+ addEdge(connecting.nodeId, port.node.id);
1027
+ }
1028
+ connecting = null;
1029
+ render();
1030
+ }
1031
+ dragging = null;
1032
+ isPanning = false;
1033
+ }
1034
+
1035
+ function onDblClick(e) {
1036
+ const p = toCanvas(e);
1037
+ const node = nodeAt(p.x, p.y);
1038
+ if (node) {
1039
+ const newName = prompt('Node name:', node.name);
1040
+ if (newName !== null) { pushUndo(); node.name = newName; render(); showProps(node); }
1041
+ }
1042
+ }
1043
+
1044
+ function onWheel(e) {
1045
+ e.preventDefault();
1046
+ const r = canvas.getBoundingClientRect();
1047
+ const mx = e.clientX - r.left, my = e.clientY - r.top;
1048
+ const oldZoom = zoom;
1049
+ zoom *= e.deltaY < 0 ? 1.1 : 0.9;
1050
+ zoom = Math.max(0.2, Math.min(3, zoom));
1051
+ pan.x = mx - (mx - pan.x) * (zoom / oldZoom);
1052
+ pan.y = my - (my - pan.y) * (zoom / oldZoom);
1053
+ render();
1054
+ }
1055
+
1056
+ function onDrop(e) {
1057
+ e.preventDefault();
1058
+ const type = e.dataTransfer.getData('node-type');
1059
+ if (!type) return;
1060
+ const p = toCanvas(e);
1061
+ addNode(type, p.x - NODE_W / 2, p.y - NODE_H / 2);
1062
+ }
1063
+
1064
+ function onKey(e) {
1065
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
1066
+ if ((e.key === 'Delete' || e.key === 'Backspace') && selected) { deleteSelected(); }
1067
+ if (e.ctrlKey && e.key === 'z') { undo(); }
1068
+ if (e.ctrlKey && e.key === 'y') { redo(); }
1069
+ }
1070
+
1071
+ function showProps(node) {
1072
+ const panel = document.getElementById('dag-props');
1073
+ const content = document.getElementById('dag-props-content');
1074
+ panel.style.display = 'block';
1075
+ let html = `<div style="margin-bottom:8px"><strong>${NODE_ICONS[node.type]} ${node.name}</strong> <span style="color:var(--text-muted)">(${node.type})</span></div>`;
1076
+ html += `<label style="font-size:12px;color:var(--text-muted)">Name</label><br><input class="search-input" value="${escapeHtml(node.name)}" onchange="dagEditor.updateProp('${node.id}','name',this.value)" style="margin-bottom:8px;width:300px"><br>`;
1077
+ for (const [key, val] of Object.entries(node.config)) {
1078
+ html += `<label style="font-size:12px;color:var(--text-muted)">${key}</label><br>`;
1079
+ if (typeof val === 'number') {
1080
+ html += `<input class="search-input" type="number" value="${val}" onchange="dagEditor.updateConfig('${node.id}','${key}',Number(this.value))" style="margin-bottom:8px;width:300px"><br>`;
1081
+ } else {
1082
+ html += `<input class="search-input" value="${escapeHtml(String(val))}" onchange="dagEditor.updateConfig('${node.id}','${key}',this.value)" style="margin-bottom:8px;width:300px"><br>`;
1083
+ }
1084
+ }
1085
+ content.innerHTML = html;
1086
+ }
1087
+
1088
+ function hideProps() {
1089
+ document.getElementById('dag-props').style.display = 'none';
1090
+ }
1091
+
1092
+ // ── Public API ──
1093
+ return {
1094
+ initCanvas,
1095
+ onPaletteDrag(e, type) { e.dataTransfer.setData('node-type', type); },
1096
+ deleteSelected() {
1097
+ if (!selected) return;
1098
+ pushUndo();
1099
+ edges = edges.filter(e => e.from !== selected.id && e.to !== selected.id);
1100
+ nodes = nodes.filter(n => n.id !== selected.id);
1101
+ selected = null;
1102
+ hideProps();
1103
+ render();
1104
+ },
1105
+ undo() {
1106
+ if (!undoStack.length) return;
1107
+ redoStack.push(JSON.stringify({ nodes, edges }));
1108
+ const state = JSON.parse(undoStack.pop());
1109
+ nodes = state.nodes; edges = state.edges; selected = null; hideProps(); render();
1110
+ },
1111
+ redo() {
1112
+ if (!redoStack.length) return;
1113
+ undoStack.push(JSON.stringify({ nodes, edges }));
1114
+ const state = JSON.parse(redoStack.pop());
1115
+ nodes = state.nodes; edges = state.edges; selected = null; hideProps(); render();
1116
+ },
1117
+ zoomIn() { zoom = Math.min(3, zoom * 1.2); render(); },
1118
+ zoomOut() { zoom = Math.max(0.2, zoom * 0.8); render(); },
1119
+ fitView() {
1120
+ if (!nodes.length) { pan = { x: 50, y: 50 }; zoom = 1; render(); return; }
1121
+ const minX = Math.min(...nodes.map(n => n.x));
1122
+ const minY = Math.min(...nodes.map(n => n.y));
1123
+ const maxX = Math.max(...nodes.map(n => n.x + NODE_W));
1124
+ const maxY = Math.max(...nodes.map(n => n.y + NODE_H));
1125
+ const cw = canvas.width / (window.devicePixelRatio || 1);
1126
+ const ch = canvas.height / (window.devicePixelRatio || 1);
1127
+ zoom = Math.min(cw / (maxX - minX + 100), ch / (maxY - minY + 100), 2);
1128
+ pan.x = (cw - (maxX + minX) * zoom) / 2;
1129
+ pan.y = (ch - (maxY + minY) * zoom) / 2;
1130
+ render();
1131
+ },
1132
+ updateProp(nodeId, key, value) {
1133
+ const n = nodes.find(n => n.id === nodeId);
1134
+ if (n) { pushUndo(); n[key] = value; render(); }
1135
+ },
1136
+ updateConfig(nodeId, key, value) {
1137
+ const n = nodes.find(n => n.id === nodeId);
1138
+ if (n) { pushUndo(); n.config[key] = value; }
1139
+ },
1140
+ async save() {
1141
+ const name = prompt('Workflow name:', workflowName);
1142
+ if (!name) return;
1143
+ workflowName = name;
1144
+ const wf = { id: workflowId || undefined, name, nodes, edges };
1145
+ try {
1146
+ const result = await apiPost('workflows', wf);
1147
+ workflowId = result.id;
1148
+ alert('Saved: ' + result.id);
1149
+ loadWorkflows();
1150
+ } catch (e) { alert('Save failed: ' + e.message); }
1151
+ },
1152
+ async loadSelected() {
1153
+ const id = document.getElementById('wf-list-select').value;
1154
+ if (!id) { nodes = []; edges = []; workflowId = null; workflowName = 'Untitled'; selected = null; hideProps(); render(); return; }
1155
+ try {
1156
+ const wf = await api('workflows/' + id);
1157
+ if (wf.error) { alert(wf.error); return; }
1158
+ nodes = wf.nodes || []; edges = wf.edges || [];
1159
+ workflowId = wf.id; workflowName = wf.name;
1160
+ selected = null; hideProps(); undoStack = []; redoStack = [];
1161
+ render();
1162
+ } catch (e) { alert('Load failed: ' + e.message); }
1163
+ },
1164
+ exportJSON() {
1165
+ const json = JSON.stringify({ name: workflowName, nodes, edges }, null, 2);
1166
+ const blob = new Blob([json], { type: 'application/json' });
1167
+ const a = document.createElement('a');
1168
+ a.href = URL.createObjectURL(blob);
1169
+ a.download = (workflowName || 'workflow') + '.json';
1170
+ a.click();
1171
+ },
1172
+ importJSON() {
1173
+ const input = document.createElement('input');
1174
+ input.type = 'file';
1175
+ input.accept = '.json';
1176
+ input.onchange = async (e) => {
1177
+ const file = e.target.files[0];
1178
+ if (!file) return;
1179
+ const text = await file.text();
1180
+ try {
1181
+ const wf = JSON.parse(text);
1182
+ pushUndo();
1183
+ nodes = wf.nodes || []; edges = wf.edges || [];
1184
+ workflowName = wf.name || 'Imported';
1185
+ workflowId = null; selected = null; hideProps();
1186
+ render();
1187
+ } catch { alert('Invalid JSON'); }
1188
+ };
1189
+ input.click();
1190
+ },
1191
+ async run() {
1192
+ if (!workflowId) { alert('Save workflow first'); return; }
1193
+ const outEl = document.getElementById('dag-run-output');
1194
+ const resEl = document.getElementById('dag-run-results');
1195
+ outEl.style.display = 'block';
1196
+ resEl.textContent = 'Running...';
1197
+ try {
1198
+ const result = await apiPost('workflows/' + workflowId + '/run', {});
1199
+ resEl.textContent = JSON.stringify(result, null, 2);
1200
+ } catch (e) { resEl.textContent = 'Error: ' + e.message; }
1201
+ },
1202
+ // For serialization tests
1203
+ serialize() { return { name: workflowName, nodes, edges }; },
1204
+ deserialize(wf) { nodes = wf.nodes || []; edges = wf.edges || []; workflowName = wf.name || ''; render(); },
1205
+ getNodes() { return nodes; },
1206
+ getEdges() { return edges; },
1207
+ };
1208
+ })();
1209
+
1210
+ // Initial load
1211
+ loadDashboard();
1212
+
1213
+ // ─── Playground ───────────────────────────────────
1214
+ const pgMessages = [];
1215
+ function pgAddMsg(role, text) {
1216
+ const el = document.createElement('div');
1217
+ el.style.cssText = role === 'user'
1218
+ ? 'align-self:flex-end;background:var(--accent);color:#000;padding:8px 12px;border-radius:12px 12px 4px 12px;max-width:75%;font-size:14px;white-space:pre-wrap;'
1219
+ : 'align-self:flex-start;background:var(--bg-hover);padding:8px 12px;border-radius:12px 12px 12px 4px;max-width:75%;font-size:14px;white-space:pre-wrap;';
1220
+ el.textContent = text;
1221
+ const container = document.getElementById('pg-messages');
1222
+ // Remove placeholder
1223
+ if (pgMessages.length === 0) container.innerHTML = '';
1224
+ container.appendChild(el);
1225
+ container.scrollTop = container.scrollHeight;
1226
+ return el;
1227
+ }
1228
+ async function pgSend() {
1229
+ const input = document.getElementById('pg-input');
1230
+ const text = input.value.trim();
1231
+ if (!text) return;
1232
+ input.value = '';
1233
+ document.getElementById('pg-send-btn').disabled = true;
1234
+ pgMessages.push({ role: 'user', content: text });
1235
+ pgAddMsg('user', text);
1236
+ const el = pgAddMsg('assistant', '');
1237
+ let full = '';
1238
+ try {
1239
+ const res = await fetch(API + '/api/playground/chat', {
1240
+ method: 'POST',
1241
+ headers: { 'Content-Type': 'application/json' },
1242
+ body: JSON.stringify({
1243
+ messages: pgMessages,
1244
+ model: document.getElementById('pg-model').value,
1245
+ temperature: parseFloat(document.getElementById('pg-temp').value),
1246
+ systemPrompt: document.getElementById('pg-system').value || undefined,
1247
+ }),
1248
+ });
1249
+ const reader = res.body.getReader();
1250
+ const decoder = new TextDecoder();
1251
+ let buf = '';
1252
+ while (true) {
1253
+ const { done, value } = await reader.read();
1254
+ if (done) break;
1255
+ buf += decoder.decode(value, { stream: true });
1256
+ const lines = buf.split('\n');
1257
+ buf = lines.pop() || '';
1258
+ for (const line of lines) {
1259
+ if (line.startsWith('data: ')) {
1260
+ const d = line.slice(6);
1261
+ if (d === '[DONE]') break;
1262
+ try { const j = JSON.parse(d); full += j.content || ''; el.textContent = full; } catch {}
1263
+ }
1264
+ }
1265
+ document.getElementById('pg-messages').scrollTop = document.getElementById('pg-messages').scrollHeight;
1266
+ }
1267
+ } catch (e) { full = 'Error: ' + e.message; el.textContent = full; }
1268
+ pgMessages.push({ role: 'assistant', content: full });
1269
+ document.getElementById('pg-send-btn').disabled = false;
1270
+ input.focus();
1271
+ }
1272
+ function pgClear() {
1273
+ pgMessages.length = 0;
1274
+ document.getElementById('pg-messages').innerHTML = '<div style="color:var(--text-muted);font-size:13px;text-align:center;padding:40px;">Send a message to start chatting</div>';
1275
+ }
1276
+ </script>
1277
+ </body>
1278
+ </html>