@octo-cyber/workflow 0.5.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 (297) hide show
  1. package/dist/controllers/ai-workflow-session.controller.d.ts +19 -0
  2. package/dist/controllers/ai-workflow-session.controller.d.ts.map +1 -0
  3. package/dist/controllers/ai-workflow-session.controller.js +135 -0
  4. package/dist/controllers/ai-workflow-session.controller.js.map +1 -0
  5. package/dist/controllers/credential.controller.d.ts +68 -0
  6. package/dist/controllers/credential.controller.d.ts.map +1 -0
  7. package/dist/controllers/credential.controller.js +303 -0
  8. package/dist/controllers/credential.controller.js.map +1 -0
  9. package/dist/controllers/index.d.ts +3 -0
  10. package/dist/controllers/index.d.ts.map +1 -0
  11. package/dist/controllers/index.js +8 -0
  12. package/dist/controllers/index.js.map +1 -0
  13. package/dist/controllers/workflow-ai.controller.d.ts +23 -0
  14. package/dist/controllers/workflow-ai.controller.d.ts.map +1 -0
  15. package/dist/controllers/workflow-ai.controller.js +164 -0
  16. package/dist/controllers/workflow-ai.controller.js.map +1 -0
  17. package/dist/controllers/workflow.controller.d.ts +66 -0
  18. package/dist/controllers/workflow.controller.d.ts.map +1 -0
  19. package/dist/controllers/workflow.controller.js +239 -0
  20. package/dist/controllers/workflow.controller.js.map +1 -0
  21. package/dist/core/expression-resolver.d.ts +49 -0
  22. package/dist/core/expression-resolver.d.ts.map +1 -0
  23. package/dist/core/expression-resolver.js +113 -0
  24. package/dist/core/expression-resolver.js.map +1 -0
  25. package/dist/core/node-registry.d.ts +24 -0
  26. package/dist/core/node-registry.d.ts.map +1 -0
  27. package/dist/core/node-registry.js +62 -0
  28. package/dist/core/node-registry.js.map +1 -0
  29. package/dist/core/workflow-executor.d.ts +50 -0
  30. package/dist/core/workflow-executor.d.ts.map +1 -0
  31. package/dist/core/workflow-executor.js +458 -0
  32. package/dist/core/workflow-executor.js.map +1 -0
  33. package/dist/entities/ai-workflow-session.entity.d.ts +17 -0
  34. package/dist/entities/ai-workflow-session.entity.d.ts.map +1 -0
  35. package/dist/entities/ai-workflow-session.entity.js +70 -0
  36. package/dist/entities/ai-workflow-session.entity.js.map +1 -0
  37. package/dist/entities/ai-workflow-version.entity.d.ts +18 -0
  38. package/dist/entities/ai-workflow-version.entity.d.ts.map +1 -0
  39. package/dist/entities/ai-workflow-version.entity.js +71 -0
  40. package/dist/entities/ai-workflow-version.entity.js.map +1 -0
  41. package/dist/entities/credential-definition.entity.d.ts +17 -0
  42. package/dist/entities/credential-definition.entity.d.ts.map +1 -0
  43. package/dist/entities/credential-definition.entity.js +66 -0
  44. package/dist/entities/credential-definition.entity.js.map +1 -0
  45. package/dist/entities/index.d.ts +9 -0
  46. package/dist/entities/index.d.ts.map +1 -0
  47. package/dist/entities/index.js +22 -0
  48. package/dist/entities/index.js.map +1 -0
  49. package/dist/entities/workflow-definition.entity.d.ts +20 -0
  50. package/dist/entities/workflow-definition.entity.d.ts.map +1 -0
  51. package/dist/entities/workflow-definition.entity.js +85 -0
  52. package/dist/entities/workflow-definition.entity.js.map +1 -0
  53. package/dist/entities/workflow-execution.entity.d.ts +26 -0
  54. package/dist/entities/workflow-execution.entity.d.ts.map +1 -0
  55. package/dist/entities/workflow-execution.entity.js +99 -0
  56. package/dist/entities/workflow-execution.entity.js.map +1 -0
  57. package/dist/index.d.ts +52 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +96 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/n8n/cipher/cipher.d.ts +11 -0
  62. package/dist/n8n/cipher/cipher.d.ts.map +1 -0
  63. package/dist/n8n/cipher/cipher.js +54 -0
  64. package/dist/n8n/cipher/cipher.js.map +1 -0
  65. package/dist/n8n/controllers/n8n-session.controller.d.ts +20 -0
  66. package/dist/n8n/controllers/n8n-session.controller.d.ts.map +1 -0
  67. package/dist/n8n/controllers/n8n-session.controller.js +84 -0
  68. package/dist/n8n/controllers/n8n-session.controller.js.map +1 -0
  69. package/dist/n8n/index.d.ts +8 -0
  70. package/dist/n8n/index.d.ts.map +1 -0
  71. package/dist/n8n/index.js +14 -0
  72. package/dist/n8n/index.js.map +1 -0
  73. package/dist/n8n/launcher/n8n-launcher.d.ts +41 -0
  74. package/dist/n8n/launcher/n8n-launcher.d.ts.map +1 -0
  75. package/dist/n8n/launcher/n8n-launcher.js +186 -0
  76. package/dist/n8n/launcher/n8n-launcher.js.map +1 -0
  77. package/dist/n8n/nodes/OctoClaudeCliTool.node.d.ts +13 -0
  78. package/dist/n8n/nodes/OctoClaudeCliTool.node.d.ts.map +1 -0
  79. package/dist/n8n/nodes/OctoClaudeCliTool.node.js +103 -0
  80. package/dist/n8n/nodes/OctoClaudeCliTool.node.js.map +1 -0
  81. package/dist/n8n/nodes/OctoCliChatModel.node.d.ts +14 -0
  82. package/dist/n8n/nodes/OctoCliChatModel.node.d.ts.map +1 -0
  83. package/dist/n8n/nodes/OctoCliChatModel.node.js +431 -0
  84. package/dist/n8n/nodes/OctoCliChatModel.node.js.map +1 -0
  85. package/dist/n8n/nodes/OctoCodexCliTool.node.d.ts +13 -0
  86. package/dist/n8n/nodes/OctoCodexCliTool.node.d.ts.map +1 -0
  87. package/dist/n8n/nodes/OctoCodexCliTool.node.js +100 -0
  88. package/dist/n8n/nodes/OctoCodexCliTool.node.js.map +1 -0
  89. package/dist/n8n/nodes/OctoGeminiCliTool.node.d.ts +13 -0
  90. package/dist/n8n/nodes/OctoGeminiCliTool.node.d.ts.map +1 -0
  91. package/dist/n8n/nodes/OctoGeminiCliTool.node.js +91 -0
  92. package/dist/n8n/nodes/OctoGeminiCliTool.node.js.map +1 -0
  93. package/dist/n8n/nodes/OctoKnowledge.node.d.ts +6 -0
  94. package/dist/n8n/nodes/OctoKnowledge.node.d.ts.map +1 -0
  95. package/dist/n8n/nodes/OctoKnowledge.node.js +95 -0
  96. package/dist/n8n/nodes/OctoKnowledge.node.js.map +1 -0
  97. package/dist/n8n/nodes/OctoNotification.node.d.ts +6 -0
  98. package/dist/n8n/nodes/OctoNotification.node.d.ts.map +1 -0
  99. package/dist/n8n/nodes/OctoNotification.node.js +72 -0
  100. package/dist/n8n/nodes/OctoNotification.node.js.map +1 -0
  101. package/dist/n8n/nodes/OctoSyncBridge.node.d.ts +6 -0
  102. package/dist/n8n/nodes/OctoSyncBridge.node.d.ts.map +1 -0
  103. package/dist/n8n/nodes/OctoSyncBridge.node.js +60 -0
  104. package/dist/n8n/nodes/OctoSyncBridge.node.js.map +1 -0
  105. package/dist/n8n/nodes/n8n-node-types.d.ts +111 -0
  106. package/dist/n8n/nodes/n8n-node-types.d.ts.map +1 -0
  107. package/dist/n8n/nodes/n8n-node-types.js +30 -0
  108. package/dist/n8n/nodes/n8n-node-types.js.map +1 -0
  109. package/dist/n8n/nodes/octo-cli-chat-model.svg +23 -0
  110. package/dist/n8n/nodes/package.json +16 -0
  111. package/dist/n8n/proxy/css-injector.d.ts +3 -0
  112. package/dist/n8n/proxy/css-injector.d.ts.map +1 -0
  113. package/dist/n8n/proxy/css-injector.js +62 -0
  114. package/dist/n8n/proxy/css-injector.js.map +1 -0
  115. package/dist/n8n/proxy/n8n-proxy.middleware.d.ts +12 -0
  116. package/dist/n8n/proxy/n8n-proxy.middleware.d.ts.map +1 -0
  117. package/dist/n8n/proxy/n8n-proxy.middleware.js +131 -0
  118. package/dist/n8n/proxy/n8n-proxy.middleware.js.map +1 -0
  119. package/dist/n8n/proxy/n8n-theme.css +45 -0
  120. package/dist/n8n/sync/n8n-user-sync.service.d.ts +75 -0
  121. package/dist/n8n/sync/n8n-user-sync.service.d.ts.map +1 -0
  122. package/dist/n8n/sync/n8n-user-sync.service.js +286 -0
  123. package/dist/n8n/sync/n8n-user-sync.service.js.map +1 -0
  124. package/dist/n8n/watcher/n8n-execution-watcher.d.ts +39 -0
  125. package/dist/n8n/watcher/n8n-execution-watcher.d.ts.map +1 -0
  126. package/dist/n8n/watcher/n8n-execution-watcher.js +110 -0
  127. package/dist/n8n/watcher/n8n-execution-watcher.js.map +1 -0
  128. package/dist/nodes/code.node.d.ts +24 -0
  129. package/dist/nodes/code.node.d.ts.map +1 -0
  130. package/dist/nodes/code.node.js +150 -0
  131. package/dist/nodes/code.node.js.map +1 -0
  132. package/dist/nodes/error-trigger.node.d.ts +15 -0
  133. package/dist/nodes/error-trigger.node.d.ts.map +1 -0
  134. package/dist/nodes/error-trigger.node.js +53 -0
  135. package/dist/nodes/error-trigger.node.js.map +1 -0
  136. package/dist/nodes/execute-command.node.d.ts +9 -0
  137. package/dist/nodes/execute-command.node.d.ts.map +1 -0
  138. package/dist/nodes/execute-command.node.js +81 -0
  139. package/dist/nodes/execute-command.node.js.map +1 -0
  140. package/dist/nodes/filter.node.d.ts +10 -0
  141. package/dist/nodes/filter.node.d.ts.map +1 -0
  142. package/dist/nodes/filter.node.js +95 -0
  143. package/dist/nodes/filter.node.js.map +1 -0
  144. package/dist/nodes/http-request.node.d.ts +11 -0
  145. package/dist/nodes/http-request.node.d.ts.map +1 -0
  146. package/dist/nodes/http-request.node.js +139 -0
  147. package/dist/nodes/http-request.node.js.map +1 -0
  148. package/dist/nodes/if.node.d.ts +13 -0
  149. package/dist/nodes/if.node.d.ts.map +1 -0
  150. package/dist/nodes/if.node.js +137 -0
  151. package/dist/nodes/if.node.js.map +1 -0
  152. package/dist/nodes/index.d.ts +12 -0
  153. package/dist/nodes/index.d.ts.map +1 -0
  154. package/dist/nodes/index.js +26 -0
  155. package/dist/nodes/index.js.map +1 -0
  156. package/dist/nodes/manual-trigger.node.d.ts +10 -0
  157. package/dist/nodes/manual-trigger.node.d.ts.map +1 -0
  158. package/dist/nodes/manual-trigger.node.js +36 -0
  159. package/dist/nodes/manual-trigger.node.js.map +1 -0
  160. package/dist/nodes/merge.node.d.ts +15 -0
  161. package/dist/nodes/merge.node.d.ts.map +1 -0
  162. package/dist/nodes/merge.node.js +99 -0
  163. package/dist/nodes/merge.node.js.map +1 -0
  164. package/dist/nodes/noop.node.d.ts +12 -0
  165. package/dist/nodes/noop.node.d.ts.map +1 -0
  166. package/dist/nodes/noop.node.js +32 -0
  167. package/dist/nodes/noop.node.js.map +1 -0
  168. package/dist/nodes/placeholder.node.d.ts +10 -0
  169. package/dist/nodes/placeholder.node.d.ts.map +1 -0
  170. package/dist/nodes/placeholder.node.js +50 -0
  171. package/dist/nodes/placeholder.node.js.map +1 -0
  172. package/dist/nodes/remove-duplicates.node.d.ts +9 -0
  173. package/dist/nodes/remove-duplicates.node.d.ts.map +1 -0
  174. package/dist/nodes/remove-duplicates.node.js +68 -0
  175. package/dist/nodes/remove-duplicates.node.js.map +1 -0
  176. package/dist/nodes/respond-to-webhook.node.d.ts +14 -0
  177. package/dist/nodes/respond-to-webhook.node.d.ts.map +1 -0
  178. package/dist/nodes/respond-to-webhook.node.js +116 -0
  179. package/dist/nodes/respond-to-webhook.node.js.map +1 -0
  180. package/dist/nodes/schedule-trigger.node.d.ts +9 -0
  181. package/dist/nodes/schedule-trigger.node.d.ts.map +1 -0
  182. package/dist/nodes/schedule-trigger.node.js +67 -0
  183. package/dist/nodes/schedule-trigger.node.js.map +1 -0
  184. package/dist/nodes/set.node.d.ts +12 -0
  185. package/dist/nodes/set.node.d.ts.map +1 -0
  186. package/dist/nodes/set.node.js +81 -0
  187. package/dist/nodes/set.node.js.map +1 -0
  188. package/dist/nodes/sort.node.d.ts +9 -0
  189. package/dist/nodes/sort.node.d.ts.map +1 -0
  190. package/dist/nodes/sort.node.js +61 -0
  191. package/dist/nodes/sort.node.js.map +1 -0
  192. package/dist/nodes/split-in-batches.node.d.ts +9 -0
  193. package/dist/nodes/split-in-batches.node.d.ts.map +1 -0
  194. package/dist/nodes/split-in-batches.node.js +53 -0
  195. package/dist/nodes/split-in-batches.node.js.map +1 -0
  196. package/dist/nodes/split-out.node.d.ts +9 -0
  197. package/dist/nodes/split-out.node.d.ts.map +1 -0
  198. package/dist/nodes/split-out.node.js +76 -0
  199. package/dist/nodes/split-out.node.js.map +1 -0
  200. package/dist/nodes/switch.node.d.ts +16 -0
  201. package/dist/nodes/switch.node.d.ts.map +1 -0
  202. package/dist/nodes/switch.node.js +156 -0
  203. package/dist/nodes/switch.node.js.map +1 -0
  204. package/dist/nodes/wait.node.d.ts +12 -0
  205. package/dist/nodes/wait.node.d.ts.map +1 -0
  206. package/dist/nodes/wait.node.js +81 -0
  207. package/dist/nodes/wait.node.js.map +1 -0
  208. package/dist/nodes/webhook.node.d.ts +9 -0
  209. package/dist/nodes/webhook.node.d.ts.map +1 -0
  210. package/dist/nodes/webhook.node.js +69 -0
  211. package/dist/nodes/webhook.node.js.map +1 -0
  212. package/dist/services/ai-workflow-session.service.d.ts +31 -0
  213. package/dist/services/ai-workflow-session.service.d.ts.map +1 -0
  214. package/dist/services/ai-workflow-session.service.js +118 -0
  215. package/dist/services/ai-workflow-session.service.js.map +1 -0
  216. package/dist/services/credential.service.d.ts +57 -0
  217. package/dist/services/credential.service.d.ts.map +1 -0
  218. package/dist/services/credential.service.js +155 -0
  219. package/dist/services/credential.service.js.map +1 -0
  220. package/dist/services/index.d.ts +10 -0
  221. package/dist/services/index.d.ts.map +1 -0
  222. package/dist/services/index.js +14 -0
  223. package/dist/services/index.js.map +1 -0
  224. package/dist/services/push.service.d.ts +60 -0
  225. package/dist/services/push.service.d.ts.map +1 -0
  226. package/dist/services/push.service.js +121 -0
  227. package/dist/services/push.service.js.map +1 -0
  228. package/dist/services/workflow-ai.service.d.ts +61 -0
  229. package/dist/services/workflow-ai.service.d.ts.map +1 -0
  230. package/dist/services/workflow-ai.service.js +219 -0
  231. package/dist/services/workflow-ai.service.js.map +1 -0
  232. package/dist/services/workflow-context.service.d.ts +32 -0
  233. package/dist/services/workflow-context.service.d.ts.map +1 -0
  234. package/dist/services/workflow-context.service.js +155 -0
  235. package/dist/services/workflow-context.service.js.map +1 -0
  236. package/dist/services/workflow-engine.service.d.ts +90 -0
  237. package/dist/services/workflow-engine.service.d.ts.map +1 -0
  238. package/dist/services/workflow-engine.service.js +305 -0
  239. package/dist/services/workflow-engine.service.js.map +1 -0
  240. package/dist/services/workflow.service.d.ts +84 -0
  241. package/dist/services/workflow.service.d.ts.map +1 -0
  242. package/dist/services/workflow.service.js +241 -0
  243. package/dist/services/workflow.service.js.map +1 -0
  244. package/dist/triggers/cron-trigger.d.ts +39 -0
  245. package/dist/triggers/cron-trigger.d.ts.map +1 -0
  246. package/dist/triggers/cron-trigger.js +137 -0
  247. package/dist/triggers/cron-trigger.js.map +1 -0
  248. package/dist/triggers/index.d.ts +3 -0
  249. package/dist/triggers/index.d.ts.map +1 -0
  250. package/dist/triggers/index.js +8 -0
  251. package/dist/triggers/index.js.map +1 -0
  252. package/dist/triggers/webhook-trigger.d.ts +51 -0
  253. package/dist/triggers/webhook-trigger.d.ts.map +1 -0
  254. package/dist/triggers/webhook-trigger.js +122 -0
  255. package/dist/triggers/webhook-trigger.js.map +1 -0
  256. package/dist/types/index.d.ts +5 -0
  257. package/dist/types/index.d.ts.map +1 -0
  258. package/dist/types/index.js +8 -0
  259. package/dist/types/index.js.map +1 -0
  260. package/dist/types/node.types.d.ts +313 -0
  261. package/dist/types/node.types.d.ts.map +1 -0
  262. package/dist/types/node.types.js +23 -0
  263. package/dist/types/node.types.js.map +1 -0
  264. package/dist/types/workflow.types.d.ts +153 -0
  265. package/dist/types/workflow.types.d.ts.map +1 -0
  266. package/dist/types/workflow.types.js +44 -0
  267. package/dist/types/workflow.types.js.map +1 -0
  268. package/dist/utils/n8n-converter.d.ts +52 -0
  269. package/dist/utils/n8n-converter.d.ts.map +1 -0
  270. package/dist/utils/n8n-converter.js +107 -0
  271. package/dist/utils/n8n-converter.js.map +1 -0
  272. package/dist/utils/n8n-node-map.d.ts +6 -0
  273. package/dist/utils/n8n-node-map.d.ts.map +1 -0
  274. package/dist/utils/n8n-node-map.js +59 -0
  275. package/dist/utils/n8n-node-map.js.map +1 -0
  276. package/dist/workflow.module.d.ts +40 -0
  277. package/dist/workflow.module.d.ts.map +1 -0
  278. package/dist/workflow.module.js +240 -0
  279. package/dist/workflow.module.js.map +1 -0
  280. package/package.json +97 -0
  281. package/web/components/ChatPanel.tsx +344 -0
  282. package/web/components/N8nIframe.tsx +119 -0
  283. package/web/components/SessionPanel.tsx +301 -0
  284. package/web/components/ToolPanel.tsx +404 -0
  285. package/web/components/WorkflowDiff.tsx +161 -0
  286. package/web/components/WorkflowGraph.tsx +158 -0
  287. package/web/components/WorkflowPreviewPanel.tsx +186 -0
  288. package/web/hooks/use-n8n-session.ts +46 -0
  289. package/web/index.ts +29 -0
  290. package/web/manifest.ts +16 -0
  291. package/web/messages/en-US.json +94 -0
  292. package/web/messages/zh-CN.json +94 -0
  293. package/web/pages/AiDesignPage.tsx +215 -0
  294. package/web/pages/CredentialsPage.tsx +7 -0
  295. package/web/pages/ExecutionsPage.tsx +7 -0
  296. package/web/pages/WorkflowsPage.tsx +7 -0
  297. package/web/services/workflow-ai-service.ts +173 -0
@@ -0,0 +1,301 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect } from 'react'
4
+ import { useTranslations } from 'next-intl'
5
+ import { toast } from 'sonner'
6
+ import {
7
+ Plus,
8
+ Trash2,
9
+ Loader2,
10
+ Workflow,
11
+ FileText,
12
+ ChevronDown,
13
+ ChevronRight,
14
+ } from 'lucide-react'
15
+ import { cn } from '@octo-cyber/ui/lib/utils'
16
+ import { Button } from '@octo-cyber/ui/components/ui/button'
17
+ import { Input } from '@octo-cyber/ui/components/ui/input'
18
+ import {
19
+ Dialog,
20
+ DialogContent,
21
+ DialogHeader,
22
+ DialogTitle,
23
+ DialogFooter,
24
+ } from '@octo-cyber/ui/components/ui/dialog'
25
+ import {
26
+ workflowAiService,
27
+ type AiWorkflowSession,
28
+ type N8nWorkflowSummary,
29
+ } from '../services/workflow-ai-service'
30
+
31
+ interface SessionPanelProps {
32
+ activeSessionId: number | null
33
+ onSelectSession: (session: AiWorkflowSession) => void
34
+ className?: string
35
+ }
36
+
37
+ export function SessionPanel({
38
+ activeSessionId,
39
+ onSelectSession,
40
+ className,
41
+ }: SessionPanelProps) {
42
+ const t = useTranslations('workflows.aiDesign')
43
+ const [sessions, setSessions] = useState<AiWorkflowSession[]>([])
44
+ const [n8nWorkflows, setN8nWorkflows] = useState<N8nWorkflowSummary[]>([])
45
+ const [isLoading, setIsLoading] = useState(true)
46
+ const [isDialogOpen, setIsDialogOpen] = useState(false)
47
+ const [isN8nExpanded, setIsN8nExpanded] = useState(true)
48
+ const [isSessionsExpanded, setIsSessionsExpanded] = useState(true)
49
+
50
+ useEffect(() => {
51
+ loadData()
52
+ }, [])
53
+
54
+ async function loadData() {
55
+ try {
56
+ const [sessionsData, workflowsData] = await Promise.all([
57
+ workflowAiService.listSessions(),
58
+ workflowAiService.listN8nWorkflows().catch(() => []),
59
+ ])
60
+ setSessions(sessionsData)
61
+ setN8nWorkflows(workflowsData)
62
+ } catch {
63
+ toast.error(t('loadSessionsFailed'))
64
+ } finally {
65
+ setIsLoading(false)
66
+ }
67
+ }
68
+
69
+ async function handleDeleteSession(id: number, e: React.MouseEvent) {
70
+ e.stopPropagation()
71
+ try {
72
+ await workflowAiService.deleteSession(id)
73
+ setSessions((prev) => prev.filter((s) => s.id !== id))
74
+ toast.success(t('sessionDeleted'))
75
+ } catch {
76
+ toast.error(t('deleteSessionFailed'))
77
+ }
78
+ }
79
+
80
+ function handleSessionCreated(session: AiWorkflowSession) {
81
+ setSessions((prev) => [session, ...prev])
82
+ onSelectSession(session)
83
+ setIsDialogOpen(false)
84
+ }
85
+
86
+ async function handleN8nWorkflowClick(wf: N8nWorkflowSummary) {
87
+ // Check if a session already exists for this n8n workflow
88
+ const existing = sessions.find((s) => s.n8nWorkflowId === wf.id)
89
+ if (existing) {
90
+ onSelectSession(existing)
91
+ return
92
+ }
93
+ // Create a new session from this n8n workflow
94
+ try {
95
+ const fullWf = await workflowAiService.getN8nWorkflow(wf.id)
96
+ const session = await workflowAiService.createSession({
97
+ workflowName: wf.name,
98
+ n8nWorkflowId: wf.id,
99
+ initialJson: JSON.stringify(fullWf),
100
+ })
101
+ handleSessionCreated(session)
102
+ } catch {
103
+ toast.error(t('createSessionFailed'))
104
+ }
105
+ }
106
+
107
+ return (
108
+ <div className={cn('flex flex-col border-r border-border', className)}>
109
+ <div className="flex items-center justify-between border-b border-border px-3 py-2">
110
+ <span className="text-sm font-medium">{t('sessions')}</span>
111
+ <Button variant="ghost" size="sm" onClick={() => setIsDialogOpen(true)}>
112
+ <Plus className="h-4 w-4" />
113
+ </Button>
114
+ </div>
115
+
116
+ <div className="flex-1 overflow-y-auto">
117
+ {isLoading ? (
118
+ <div className="flex items-center justify-center py-8">
119
+ <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
120
+ </div>
121
+ ) : (
122
+ <>
123
+ {/* n8n Workflows Section */}
124
+ <SectionHeader
125
+ label={t('n8nWorkflows')}
126
+ count={n8nWorkflows.length}
127
+ isExpanded={isN8nExpanded}
128
+ onToggle={() => setIsN8nExpanded(!isN8nExpanded)}
129
+ />
130
+ {isN8nExpanded && (
131
+ n8nWorkflows.length === 0 ? (
132
+ <div className="px-3 py-3 text-center text-xs text-muted-foreground">
133
+ {t('noN8nWorkflows')}
134
+ </div>
135
+ ) : (
136
+ n8nWorkflows.map((wf) => (
137
+ <button
138
+ key={`n8n-${wf.id}`}
139
+ onClick={() => handleN8nWorkflowClick(wf)}
140
+ className={cn(
141
+ 'group flex w-full items-start gap-2 border-b border-border/50 px-3 py-2 text-left transition-colors hover:bg-accent',
142
+ sessions.some((s) => s.n8nWorkflowId === wf.id && activeSessionId === s.id) && 'bg-accent',
143
+ )}
144
+ >
145
+ <Workflow className="mt-0.5 h-4 w-4 shrink-0 text-blue-500" />
146
+ <div className="min-w-0 flex-1">
147
+ <p className="truncate text-sm">{wf.name}</p>
148
+ <p className="text-[10px] text-muted-foreground">
149
+ {wf.active ? '● Active' : '○ Inactive'}
150
+ </p>
151
+ </div>
152
+ </button>
153
+ ))
154
+ )
155
+ )}
156
+
157
+ {/* Design Sessions Section */}
158
+ <SectionHeader
159
+ label={t('sessions')}
160
+ count={sessions.length}
161
+ isExpanded={isSessionsExpanded}
162
+ onToggle={() => setIsSessionsExpanded(!isSessionsExpanded)}
163
+ />
164
+ {isSessionsExpanded && (
165
+ sessions.length === 0 ? (
166
+ <div className="px-3 py-3 text-center text-xs text-muted-foreground">
167
+ {t('noSessions')}
168
+ </div>
169
+ ) : (
170
+ sessions.map((session) => (
171
+ <button
172
+ key={session.id}
173
+ onClick={() => onSelectSession(session)}
174
+ className={cn(
175
+ 'group flex w-full items-start gap-2 border-b border-border/50 px-3 py-2.5 text-left transition-colors hover:bg-accent',
176
+ activeSessionId === session.id && 'bg-accent',
177
+ )}
178
+ >
179
+ <FileText className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
180
+ <div className="min-w-0 flex-1">
181
+ <p className="truncate text-sm font-medium">{session.workflowName}</p>
182
+ <p className="text-xs text-muted-foreground">
183
+ v{session.currentVersion} · {session.status}
184
+ </p>
185
+ </div>
186
+ <Button
187
+ variant="ghost"
188
+ size="sm"
189
+ className="h-6 w-6 shrink-0 p-0 opacity-0 group-hover:opacity-100"
190
+ onClick={(e) => handleDeleteSession(session.id, e)}
191
+ >
192
+ <Trash2 className="h-3.5 w-3.5" />
193
+ </Button>
194
+ </button>
195
+ ))
196
+ )
197
+ )}
198
+ </>
199
+ )}
200
+ </div>
201
+
202
+ <NewSessionDialog
203
+ isOpen={isDialogOpen}
204
+ onClose={() => setIsDialogOpen(false)}
205
+ onCreated={handleSessionCreated}
206
+ />
207
+ </div>
208
+ )
209
+ }
210
+
211
+ // ─── Section Header ──────────────────────────────────────────
212
+
213
+ function SectionHeader({
214
+ label,
215
+ count,
216
+ isExpanded,
217
+ onToggle,
218
+ }: {
219
+ label: string
220
+ count: number
221
+ isExpanded: boolean
222
+ onToggle: () => void
223
+ }) {
224
+ return (
225
+ <button
226
+ onClick={onToggle}
227
+ className="flex w-full items-center gap-1 bg-muted/30 px-3 py-1.5 text-xs font-medium text-muted-foreground hover:bg-muted/50"
228
+ >
229
+ {isExpanded ? (
230
+ <ChevronDown className="h-3 w-3" />
231
+ ) : (
232
+ <ChevronRight className="h-3 w-3" />
233
+ )}
234
+ {label}
235
+ <span className="ml-auto text-[10px]">({count})</span>
236
+ </button>
237
+ )
238
+ }
239
+
240
+ // ─── New Session Dialog ──────────────────────────────────────
241
+
242
+ function NewSessionDialog({
243
+ isOpen,
244
+ onClose,
245
+ onCreated,
246
+ }: {
247
+ isOpen: boolean
248
+ onClose: () => void
249
+ onCreated: (session: AiWorkflowSession) => void
250
+ }) {
251
+ const t = useTranslations('workflows.aiDesign')
252
+ const [name, setName] = useState('')
253
+ const [isCreating, setIsCreating] = useState(false)
254
+
255
+ async function handleCreate() {
256
+ if (!name.trim()) return
257
+ setIsCreating(true)
258
+ try {
259
+ const session = await workflowAiService.createSession({
260
+ workflowName: name.trim(),
261
+ })
262
+ onCreated(session)
263
+ setName('')
264
+ } catch {
265
+ toast.error(t('createSessionFailed'))
266
+ } finally {
267
+ setIsCreating(false)
268
+ }
269
+ }
270
+
271
+ return (
272
+ <Dialog open={isOpen} onOpenChange={onClose}>
273
+ <DialogContent>
274
+ <DialogHeader>
275
+ <DialogTitle>{t('newSession')}</DialogTitle>
276
+ </DialogHeader>
277
+ <div className="space-y-4 py-2">
278
+ <div>
279
+ <label className="text-sm font-medium">{t('sessionName')}</label>
280
+ <Input
281
+ value={name}
282
+ onChange={(e) => setName(e.target.value)}
283
+ placeholder={t('sessionNamePlaceholder')}
284
+ className="mt-1"
285
+ onKeyDown={(e) => {
286
+ if (e.key === 'Enter') handleCreate()
287
+ }}
288
+ />
289
+ </div>
290
+ </div>
291
+ <DialogFooter>
292
+ <Button variant="outline" onClick={onClose}>{t('cancel')}</Button>
293
+ <Button onClick={handleCreate} disabled={!name.trim() || isCreating}>
294
+ {isCreating && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
295
+ {t('create')}
296
+ </Button>
297
+ </DialogFooter>
298
+ </DialogContent>
299
+ </Dialog>
300
+ )
301
+ }
@@ -0,0 +1,404 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect } from 'react'
4
+ import { useTranslations } from 'next-intl'
5
+ import { toast } from 'sonner'
6
+ import {
7
+ History,
8
+ FileText,
9
+ Cpu,
10
+ RotateCcw,
11
+ Loader2,
12
+ Eye,
13
+ ScrollText,
14
+ } from 'lucide-react'
15
+ import { Button } from '@octo-cyber/ui/components/ui/button'
16
+ import { Badge } from '@octo-cyber/ui/components/ui/badge'
17
+ import { Checkbox } from '@octo-cyber/ui/components/ui/checkbox'
18
+ import {
19
+ Tabs,
20
+ TabsContent,
21
+ TabsList,
22
+ TabsTrigger,
23
+ } from '@octo-cyber/ui/components/ui/tabs'
24
+ import {
25
+ Select,
26
+ SelectContent,
27
+ SelectItem,
28
+ SelectTrigger,
29
+ SelectValue,
30
+ } from '@octo-cyber/ui/components/ui/select'
31
+ import {
32
+ Dialog,
33
+ DialogContent,
34
+ DialogHeader,
35
+ DialogTitle,
36
+ DialogFooter,
37
+ } from '@octo-cyber/ui/components/ui/dialog'
38
+ import {
39
+ workflowAiService,
40
+ type AiWorkflowVersion,
41
+ type AiToolInfo,
42
+ type ApiDocModule,
43
+ } from '../services/workflow-ai-service'
44
+
45
+ interface ToolPanelProps {
46
+ sessionId: number | null
47
+ selectedTool: string
48
+ selectedModel: string
49
+ onToolChange: (tool: string) => void
50
+ onModelChange: (model: string) => void
51
+ selectedDocs: string[]
52
+ onDocsChange: (docs: string[]) => void
53
+ onRollback: (versionNumber: number) => void
54
+ onVersionSelect?: (version: AiWorkflowVersion) => void
55
+ refreshKey: number
56
+ className?: string
57
+ }
58
+
59
+ export function ToolPanel({
60
+ sessionId,
61
+ selectedTool,
62
+ selectedModel,
63
+ onToolChange,
64
+ onModelChange,
65
+ selectedDocs,
66
+ onDocsChange,
67
+ onRollback,
68
+ onVersionSelect,
69
+ refreshKey,
70
+ className,
71
+ }: ToolPanelProps) {
72
+ const t = useTranslations('workflows.aiDesign')
73
+
74
+ return (
75
+ <div className={`flex flex-col border-l border-border ${className ?? ''}`}>
76
+ <Tabs defaultValue="tools" className="flex flex-1 flex-col">
77
+ <TabsList className="mx-2 mt-2 grid grid-cols-3">
78
+ <TabsTrigger value="tools" className="text-xs">
79
+ <Cpu className="mr-1 h-3.5 w-3.5" />
80
+ {t('tools')}
81
+ </TabsTrigger>
82
+ <TabsTrigger value="versions" className="text-xs">
83
+ <History className="mr-1 h-3.5 w-3.5" />
84
+ {t('versions')}
85
+ </TabsTrigger>
86
+ <TabsTrigger value="docs" className="text-xs">
87
+ <FileText className="mr-1 h-3.5 w-3.5" />
88
+ {t('apiDocs')}
89
+ </TabsTrigger>
90
+ </TabsList>
91
+
92
+ <TabsContent value="tools" className="flex-1 overflow-y-auto px-3 py-2">
93
+ <AiToolSelector
94
+ selectedTool={selectedTool}
95
+ selectedModel={selectedModel}
96
+ onToolChange={onToolChange}
97
+ onModelChange={onModelChange}
98
+ />
99
+ </TabsContent>
100
+
101
+ <TabsContent value="versions" className="flex-1 overflow-y-auto px-3 py-2">
102
+ <VersionHistory
103
+ sessionId={sessionId}
104
+ onRollback={onRollback}
105
+ onVersionSelect={onVersionSelect}
106
+ refreshKey={refreshKey}
107
+ />
108
+ </TabsContent>
109
+
110
+ <TabsContent value="docs" className="flex-1 overflow-y-auto px-3 py-2">
111
+ <ApiDocSelector
112
+ selectedDocs={selectedDocs}
113
+ onDocsChange={onDocsChange}
114
+ />
115
+ </TabsContent>
116
+ </Tabs>
117
+ </div>
118
+ )
119
+ }
120
+
121
+ // ─── AI Tool Selector ────────────────────────────────────────
122
+
123
+ function AiToolSelector({
124
+ selectedTool,
125
+ selectedModel,
126
+ onToolChange,
127
+ onModelChange,
128
+ }: {
129
+ selectedTool: string
130
+ selectedModel: string
131
+ onToolChange: (tool: string) => void
132
+ onModelChange: (model: string) => void
133
+ }) {
134
+ const t = useTranslations('workflows.aiDesign')
135
+ const [tools, setTools] = useState<AiToolInfo[]>([])
136
+ const [isPromptDialogOpen, setIsPromptDialogOpen] = useState(false)
137
+
138
+ useEffect(() => {
139
+ workflowAiService.listAiTools().then(setTools).catch(() => {})
140
+ }, [])
141
+
142
+ const activeTool = tools.find((tool) => tool.name === selectedTool)
143
+
144
+ return (
145
+ <div className="space-y-4">
146
+ <div>
147
+ <label className="text-xs font-medium text-muted-foreground">
148
+ {t('aiTool')}
149
+ </label>
150
+ <Select value={selectedTool} onValueChange={onToolChange}>
151
+ <SelectTrigger className="mt-1">
152
+ <SelectValue />
153
+ </SelectTrigger>
154
+ <SelectContent>
155
+ {tools.map((tool) => (
156
+ <SelectItem key={tool.name} value={tool.name} disabled={!tool.isAvailable}>
157
+ <div className="flex items-center gap-2">
158
+ {tool.displayName}
159
+ {!tool.isAvailable && (
160
+ <Badge variant="secondary" className="text-[10px]">
161
+ {t('unavailable')}
162
+ </Badge>
163
+ )}
164
+ </div>
165
+ </SelectItem>
166
+ ))}
167
+ </SelectContent>
168
+ </Select>
169
+ </div>
170
+
171
+ {activeTool && activeTool.models.length > 0 && (
172
+ <div>
173
+ <label className="text-xs font-medium text-muted-foreground">
174
+ {t('model')}
175
+ </label>
176
+ <Select value={selectedModel} onValueChange={onModelChange}>
177
+ <SelectTrigger className="mt-1">
178
+ <SelectValue placeholder={t('defaultModel')} />
179
+ </SelectTrigger>
180
+ <SelectContent>
181
+ {activeTool.models.map((model) => (
182
+ <SelectItem key={model} value={model}>
183
+ {model}
184
+ </SelectItem>
185
+ ))}
186
+ </SelectContent>
187
+ </Select>
188
+ </div>
189
+ )}
190
+
191
+ {/* System Prompt Viewer */}
192
+ <div className="border-t border-border pt-3">
193
+ <Button
194
+ variant="outline"
195
+ size="sm"
196
+ className="w-full gap-2 text-xs"
197
+ onClick={() => setIsPromptDialogOpen(true)}
198
+ >
199
+ <ScrollText className="h-3.5 w-3.5" />
200
+ {t('viewPrompt')}
201
+ </Button>
202
+ </div>
203
+
204
+ <SystemPromptDialog
205
+ isOpen={isPromptDialogOpen}
206
+ onClose={() => setIsPromptDialogOpen(false)}
207
+ />
208
+ </div>
209
+ )
210
+ }
211
+
212
+ // ─── System Prompt Dialog ────────────────────────────────────
213
+
214
+ function SystemPromptDialog({
215
+ isOpen,
216
+ onClose,
217
+ }: {
218
+ isOpen: boolean
219
+ onClose: () => void
220
+ }) {
221
+ const t = useTranslations('workflows.aiDesign')
222
+ const [prompt, setPrompt] = useState('')
223
+ const [isLoading, setIsLoading] = useState(false)
224
+
225
+ useEffect(() => {
226
+ if (!isOpen) return
227
+ setIsLoading(true)
228
+ workflowAiService
229
+ .getSystemPrompt()
230
+ .then(setPrompt)
231
+ .catch(() => toast.error(t('loadPromptFailed')))
232
+ .finally(() => setIsLoading(false))
233
+ }, [isOpen, t])
234
+
235
+ return (
236
+ <Dialog open={isOpen} onOpenChange={onClose}>
237
+ <DialogContent className="max-w-2xl max-h-[80vh]">
238
+ <DialogHeader>
239
+ <DialogTitle>{t('systemPrompt')}</DialogTitle>
240
+ </DialogHeader>
241
+ <p className="text-xs text-muted-foreground">{t('systemPromptDesc')}</p>
242
+ <div className="max-h-[50vh] overflow-y-auto rounded-md border border-border bg-muted/30 p-3">
243
+ {isLoading ? (
244
+ <div className="flex items-center justify-center py-8">
245
+ <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
246
+ </div>
247
+ ) : (
248
+ <pre className="whitespace-pre-wrap text-xs leading-relaxed">
249
+ {prompt}
250
+ </pre>
251
+ )}
252
+ </div>
253
+ <DialogFooter>
254
+ <Button variant="outline" onClick={onClose}>{t('close')}</Button>
255
+ </DialogFooter>
256
+ </DialogContent>
257
+ </Dialog>
258
+ )
259
+ }
260
+
261
+ // ─── Version History ─────────────────────────────────────────
262
+
263
+ function VersionHistory({
264
+ sessionId,
265
+ onRollback,
266
+ onVersionSelect,
267
+ refreshKey,
268
+ }: {
269
+ sessionId: number | null
270
+ onRollback: (versionNumber: number) => void
271
+ onVersionSelect?: (version: AiWorkflowVersion) => void
272
+ refreshKey: number
273
+ }) {
274
+ const t = useTranslations('workflows.aiDesign')
275
+ const [versions, setVersions] = useState<AiWorkflowVersion[]>([])
276
+ const [isLoading, setIsLoading] = useState(false)
277
+
278
+ useEffect(() => {
279
+ if (!sessionId) {
280
+ setVersions([])
281
+ return
282
+ }
283
+ setIsLoading(true)
284
+ workflowAiService
285
+ .listVersions(sessionId)
286
+ .then(setVersions)
287
+ .catch(() => {})
288
+ .finally(() => setIsLoading(false))
289
+ }, [sessionId, refreshKey])
290
+
291
+ if (!sessionId) {
292
+ return <p className="py-4 text-center text-sm text-muted-foreground">{t('selectSessionFirst')}</p>
293
+ }
294
+
295
+ if (isLoading) {
296
+ return (
297
+ <div className="flex items-center justify-center py-8">
298
+ <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
299
+ </div>
300
+ )
301
+ }
302
+
303
+ return (
304
+ <div className="space-y-2">
305
+ {versions.map((v) => (
306
+ <div
307
+ key={v.id}
308
+ className="rounded-md border border-border p-2.5 text-sm"
309
+ >
310
+ <div className="flex items-center justify-between">
311
+ <Badge variant="outline" className="text-xs">
312
+ v{v.versionNumber}
313
+ </Badge>
314
+ <div className="flex gap-1">
315
+ {onVersionSelect && (
316
+ <Button
317
+ variant="ghost"
318
+ size="sm"
319
+ className="h-6 px-2 text-xs"
320
+ onClick={() => onVersionSelect(v)}
321
+ >
322
+ <Eye className="mr-1 h-3 w-3" />
323
+ </Button>
324
+ )}
325
+ <Button
326
+ variant="ghost"
327
+ size="sm"
328
+ className="h-6 px-2 text-xs"
329
+ onClick={() => onRollback(v.versionNumber)}
330
+ >
331
+ <RotateCcw className="mr-1 h-3 w-3" />
332
+ {t('rollback')}
333
+ </Button>
334
+ </div>
335
+ </div>
336
+ {v.changeDescription && (
337
+ <p className="mt-1 truncate text-xs text-muted-foreground">
338
+ {v.changeDescription}
339
+ </p>
340
+ )}
341
+ <div className="mt-1 flex items-center gap-2 text-[10px] text-muted-foreground">
342
+ {v.aiTool && <span>{v.aiTool}</span>}
343
+ <span>{new Date(v.createdAt).toLocaleString()}</span>
344
+ </div>
345
+ </div>
346
+ ))}
347
+ </div>
348
+ )
349
+ }
350
+
351
+ // ─── API Doc Selector ────────────────────────────────────────
352
+
353
+ function ApiDocSelector({
354
+ selectedDocs,
355
+ onDocsChange,
356
+ }: {
357
+ selectedDocs: string[]
358
+ onDocsChange: (docs: string[]) => void
359
+ }) {
360
+ const t = useTranslations('workflows.aiDesign')
361
+ const [docs, setDocs] = useState<ApiDocModule[]>([])
362
+
363
+ useEffect(() => {
364
+ workflowAiService.listApiDocs().then(setDocs).catch(() => {})
365
+ }, [])
366
+
367
+ function toggleDoc(moduleId: string) {
368
+ if (selectedDocs.includes(moduleId)) {
369
+ onDocsChange(selectedDocs.filter((d) => d !== moduleId))
370
+ } else {
371
+ onDocsChange([...selectedDocs, moduleId])
372
+ }
373
+ }
374
+
375
+ const totalSize = docs
376
+ .filter((d) => selectedDocs.includes(d.moduleId))
377
+ .reduce((sum, d) => sum + d.contentLength, 0)
378
+
379
+ return (
380
+ <div className="space-y-3">
381
+ <p className="text-xs text-muted-foreground">{t('apiDocsDesc')}</p>
382
+ {docs.map((doc) => (
383
+ <label
384
+ key={doc.moduleId}
385
+ className="flex cursor-pointer items-center gap-2 rounded-md p-1.5 hover:bg-accent"
386
+ >
387
+ <Checkbox
388
+ checked={selectedDocs.includes(doc.moduleId)}
389
+ onCheckedChange={() => toggleDoc(doc.moduleId)}
390
+ />
391
+ <span className="text-sm">{doc.moduleName}</span>
392
+ <span className="ml-auto text-xs text-muted-foreground">
393
+ {(doc.contentLength / 1024).toFixed(1)}KB
394
+ </span>
395
+ </label>
396
+ ))}
397
+ {selectedDocs.length > 0 && (
398
+ <p className="text-xs text-muted-foreground">
399
+ {t('estimatedTokens')}: ~{Math.round(totalSize / 4)}
400
+ </p>
401
+ )}
402
+ </div>
403
+ )
404
+ }