spec-gen-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1078 -0
  3. package/dist/api/analyze.d.ts +17 -0
  4. package/dist/api/analyze.d.ts.map +1 -0
  5. package/dist/api/analyze.js +109 -0
  6. package/dist/api/analyze.js.map +1 -0
  7. package/dist/api/drift.d.ts +21 -0
  8. package/dist/api/drift.d.ts.map +1 -0
  9. package/dist/api/drift.js +145 -0
  10. package/dist/api/drift.js.map +1 -0
  11. package/dist/api/generate.d.ts +18 -0
  12. package/dist/api/generate.d.ts.map +1 -0
  13. package/dist/api/generate.js +251 -0
  14. package/dist/api/generate.js.map +1 -0
  15. package/dist/api/index.d.ts +39 -0
  16. package/dist/api/index.d.ts.map +1 -0
  17. package/dist/api/index.js +32 -0
  18. package/dist/api/index.js.map +1 -0
  19. package/dist/api/init.d.ts +18 -0
  20. package/dist/api/init.d.ts.map +1 -0
  21. package/dist/api/init.js +82 -0
  22. package/dist/api/init.js.map +1 -0
  23. package/dist/api/run.d.ts +19 -0
  24. package/dist/api/run.d.ts.map +1 -0
  25. package/dist/api/run.js +291 -0
  26. package/dist/api/run.js.map +1 -0
  27. package/dist/api/specs.d.ts +49 -0
  28. package/dist/api/specs.d.ts.map +1 -0
  29. package/dist/api/specs.js +136 -0
  30. package/dist/api/specs.js.map +1 -0
  31. package/dist/api/types.d.ts +176 -0
  32. package/dist/api/types.d.ts.map +1 -0
  33. package/dist/api/types.js +9 -0
  34. package/dist/api/types.js.map +1 -0
  35. package/dist/api/verify.d.ts +20 -0
  36. package/dist/api/verify.d.ts.map +1 -0
  37. package/dist/api/verify.js +117 -0
  38. package/dist/api/verify.js.map +1 -0
  39. package/dist/cli/commands/analyze.d.ts +27 -0
  40. package/dist/cli/commands/analyze.d.ts.map +1 -0
  41. package/dist/cli/commands/analyze.js +485 -0
  42. package/dist/cli/commands/analyze.js.map +1 -0
  43. package/dist/cli/commands/drift.d.ts +9 -0
  44. package/dist/cli/commands/drift.d.ts.map +1 -0
  45. package/dist/cli/commands/drift.js +540 -0
  46. package/dist/cli/commands/drift.js.map +1 -0
  47. package/dist/cli/commands/generate.d.ts +9 -0
  48. package/dist/cli/commands/generate.d.ts.map +1 -0
  49. package/dist/cli/commands/generate.js +633 -0
  50. package/dist/cli/commands/generate.js.map +1 -0
  51. package/dist/cli/commands/init.d.ts +9 -0
  52. package/dist/cli/commands/init.d.ts.map +1 -0
  53. package/dist/cli/commands/init.js +171 -0
  54. package/dist/cli/commands/init.js.map +1 -0
  55. package/dist/cli/commands/mcp.d.ts +638 -0
  56. package/dist/cli/commands/mcp.d.ts.map +1 -0
  57. package/dist/cli/commands/mcp.js +574 -0
  58. package/dist/cli/commands/mcp.js.map +1 -0
  59. package/dist/cli/commands/run.d.ts +24 -0
  60. package/dist/cli/commands/run.d.ts.map +1 -0
  61. package/dist/cli/commands/run.js +546 -0
  62. package/dist/cli/commands/run.js.map +1 -0
  63. package/dist/cli/commands/verify.d.ts +9 -0
  64. package/dist/cli/commands/verify.d.ts.map +1 -0
  65. package/dist/cli/commands/verify.js +417 -0
  66. package/dist/cli/commands/verify.js.map +1 -0
  67. package/dist/cli/commands/view.d.ts +9 -0
  68. package/dist/cli/commands/view.d.ts.map +1 -0
  69. package/dist/cli/commands/view.js +511 -0
  70. package/dist/cli/commands/view.js.map +1 -0
  71. package/dist/cli/index.d.ts +9 -0
  72. package/dist/cli/index.d.ts.map +1 -0
  73. package/dist/cli/index.js +83 -0
  74. package/dist/cli/index.js.map +1 -0
  75. package/dist/core/analyzer/architecture-writer.d.ts +67 -0
  76. package/dist/core/analyzer/architecture-writer.d.ts.map +1 -0
  77. package/dist/core/analyzer/architecture-writer.js +209 -0
  78. package/dist/core/analyzer/architecture-writer.js.map +1 -0
  79. package/dist/core/analyzer/artifact-generator.d.ts +222 -0
  80. package/dist/core/analyzer/artifact-generator.d.ts.map +1 -0
  81. package/dist/core/analyzer/artifact-generator.js +726 -0
  82. package/dist/core/analyzer/artifact-generator.js.map +1 -0
  83. package/dist/core/analyzer/call-graph.d.ts +83 -0
  84. package/dist/core/analyzer/call-graph.d.ts.map +1 -0
  85. package/dist/core/analyzer/call-graph.js +827 -0
  86. package/dist/core/analyzer/call-graph.js.map +1 -0
  87. package/dist/core/analyzer/code-shaper.d.ts +33 -0
  88. package/dist/core/analyzer/code-shaper.d.ts.map +1 -0
  89. package/dist/core/analyzer/code-shaper.js +149 -0
  90. package/dist/core/analyzer/code-shaper.js.map +1 -0
  91. package/dist/core/analyzer/dependency-graph.d.ts +179 -0
  92. package/dist/core/analyzer/dependency-graph.d.ts.map +1 -0
  93. package/dist/core/analyzer/dependency-graph.js +574 -0
  94. package/dist/core/analyzer/dependency-graph.js.map +1 -0
  95. package/dist/core/analyzer/duplicate-detector.d.ts +52 -0
  96. package/dist/core/analyzer/duplicate-detector.d.ts.map +1 -0
  97. package/dist/core/analyzer/duplicate-detector.js +279 -0
  98. package/dist/core/analyzer/duplicate-detector.js.map +1 -0
  99. package/dist/core/analyzer/embedding-service.d.ts +50 -0
  100. package/dist/core/analyzer/embedding-service.d.ts.map +1 -0
  101. package/dist/core/analyzer/embedding-service.js +104 -0
  102. package/dist/core/analyzer/embedding-service.js.map +1 -0
  103. package/dist/core/analyzer/file-walker.d.ts +78 -0
  104. package/dist/core/analyzer/file-walker.d.ts.map +1 -0
  105. package/dist/core/analyzer/file-walker.js +531 -0
  106. package/dist/core/analyzer/file-walker.js.map +1 -0
  107. package/dist/core/analyzer/import-parser.d.ts +91 -0
  108. package/dist/core/analyzer/import-parser.d.ts.map +1 -0
  109. package/dist/core/analyzer/import-parser.js +720 -0
  110. package/dist/core/analyzer/import-parser.js.map +1 -0
  111. package/dist/core/analyzer/index.d.ts +10 -0
  112. package/dist/core/analyzer/index.d.ts.map +1 -0
  113. package/dist/core/analyzer/index.js +10 -0
  114. package/dist/core/analyzer/index.js.map +1 -0
  115. package/dist/core/analyzer/refactor-analyzer.d.ts +80 -0
  116. package/dist/core/analyzer/refactor-analyzer.d.ts.map +1 -0
  117. package/dist/core/analyzer/refactor-analyzer.js +339 -0
  118. package/dist/core/analyzer/refactor-analyzer.js.map +1 -0
  119. package/dist/core/analyzer/repository-mapper.d.ts +150 -0
  120. package/dist/core/analyzer/repository-mapper.d.ts.map +1 -0
  121. package/dist/core/analyzer/repository-mapper.js +731 -0
  122. package/dist/core/analyzer/repository-mapper.js.map +1 -0
  123. package/dist/core/analyzer/signature-extractor.d.ts +31 -0
  124. package/dist/core/analyzer/signature-extractor.d.ts.map +1 -0
  125. package/dist/core/analyzer/signature-extractor.js +387 -0
  126. package/dist/core/analyzer/signature-extractor.js.map +1 -0
  127. package/dist/core/analyzer/significance-scorer.d.ts +79 -0
  128. package/dist/core/analyzer/significance-scorer.d.ts.map +1 -0
  129. package/dist/core/analyzer/significance-scorer.js +407 -0
  130. package/dist/core/analyzer/significance-scorer.js.map +1 -0
  131. package/dist/core/analyzer/subgraph-extractor.d.ts +43 -0
  132. package/dist/core/analyzer/subgraph-extractor.d.ts.map +1 -0
  133. package/dist/core/analyzer/subgraph-extractor.js +129 -0
  134. package/dist/core/analyzer/subgraph-extractor.js.map +1 -0
  135. package/dist/core/analyzer/vector-index.d.ts +63 -0
  136. package/dist/core/analyzer/vector-index.d.ts.map +1 -0
  137. package/dist/core/analyzer/vector-index.js +169 -0
  138. package/dist/core/analyzer/vector-index.js.map +1 -0
  139. package/dist/core/drift/drift-detector.d.ts +102 -0
  140. package/dist/core/drift/drift-detector.d.ts.map +1 -0
  141. package/dist/core/drift/drift-detector.js +597 -0
  142. package/dist/core/drift/drift-detector.js.map +1 -0
  143. package/dist/core/drift/git-diff.d.ts +55 -0
  144. package/dist/core/drift/git-diff.d.ts.map +1 -0
  145. package/dist/core/drift/git-diff.js +356 -0
  146. package/dist/core/drift/git-diff.js.map +1 -0
  147. package/dist/core/drift/index.d.ts +12 -0
  148. package/dist/core/drift/index.d.ts.map +1 -0
  149. package/dist/core/drift/index.js +9 -0
  150. package/dist/core/drift/index.js.map +1 -0
  151. package/dist/core/drift/spec-mapper.d.ts +73 -0
  152. package/dist/core/drift/spec-mapper.d.ts.map +1 -0
  153. package/dist/core/drift/spec-mapper.js +353 -0
  154. package/dist/core/drift/spec-mapper.js.map +1 -0
  155. package/dist/core/generator/adr-generator.d.ts +32 -0
  156. package/dist/core/generator/adr-generator.d.ts.map +1 -0
  157. package/dist/core/generator/adr-generator.js +192 -0
  158. package/dist/core/generator/adr-generator.js.map +1 -0
  159. package/dist/core/generator/index.d.ts +9 -0
  160. package/dist/core/generator/index.d.ts.map +1 -0
  161. package/dist/core/generator/index.js +12 -0
  162. package/dist/core/generator/index.js.map +1 -0
  163. package/dist/core/generator/mapping-generator.d.ts +54 -0
  164. package/dist/core/generator/mapping-generator.d.ts.map +1 -0
  165. package/dist/core/generator/mapping-generator.js +239 -0
  166. package/dist/core/generator/mapping-generator.js.map +1 -0
  167. package/dist/core/generator/openspec-compat.d.ts +160 -0
  168. package/dist/core/generator/openspec-compat.d.ts.map +1 -0
  169. package/dist/core/generator/openspec-compat.js +523 -0
  170. package/dist/core/generator/openspec-compat.js.map +1 -0
  171. package/dist/core/generator/openspec-format-generator.d.ts +111 -0
  172. package/dist/core/generator/openspec-format-generator.d.ts.map +1 -0
  173. package/dist/core/generator/openspec-format-generator.js +817 -0
  174. package/dist/core/generator/openspec-format-generator.js.map +1 -0
  175. package/dist/core/generator/openspec-writer.d.ts +131 -0
  176. package/dist/core/generator/openspec-writer.d.ts.map +1 -0
  177. package/dist/core/generator/openspec-writer.js +379 -0
  178. package/dist/core/generator/openspec-writer.js.map +1 -0
  179. package/dist/core/generator/prompts.d.ts +35 -0
  180. package/dist/core/generator/prompts.d.ts.map +1 -0
  181. package/dist/core/generator/prompts.js +212 -0
  182. package/dist/core/generator/prompts.js.map +1 -0
  183. package/dist/core/generator/spec-pipeline.d.ts +94 -0
  184. package/dist/core/generator/spec-pipeline.d.ts.map +1 -0
  185. package/dist/core/generator/spec-pipeline.js +474 -0
  186. package/dist/core/generator/spec-pipeline.js.map +1 -0
  187. package/dist/core/generator/stages/stage1-survey.d.ts +19 -0
  188. package/dist/core/generator/stages/stage1-survey.d.ts.map +1 -0
  189. package/dist/core/generator/stages/stage1-survey.js +105 -0
  190. package/dist/core/generator/stages/stage1-survey.js.map +1 -0
  191. package/dist/core/generator/stages/stage2-entities.d.ts +11 -0
  192. package/dist/core/generator/stages/stage2-entities.d.ts.map +1 -0
  193. package/dist/core/generator/stages/stage2-entities.js +67 -0
  194. package/dist/core/generator/stages/stage2-entities.js.map +1 -0
  195. package/dist/core/generator/stages/stage3-services.d.ts +11 -0
  196. package/dist/core/generator/stages/stage3-services.d.ts.map +1 -0
  197. package/dist/core/generator/stages/stage3-services.js +75 -0
  198. package/dist/core/generator/stages/stage3-services.js.map +1 -0
  199. package/dist/core/generator/stages/stage4-api.d.ts +11 -0
  200. package/dist/core/generator/stages/stage4-api.d.ts.map +1 -0
  201. package/dist/core/generator/stages/stage4-api.js +65 -0
  202. package/dist/core/generator/stages/stage4-api.js.map +1 -0
  203. package/dist/core/generator/stages/stage5-architecture.d.ts +10 -0
  204. package/dist/core/generator/stages/stage5-architecture.d.ts.map +1 -0
  205. package/dist/core/generator/stages/stage5-architecture.js +62 -0
  206. package/dist/core/generator/stages/stage5-architecture.js.map +1 -0
  207. package/dist/core/generator/stages/stage6-adr.d.ts +8 -0
  208. package/dist/core/generator/stages/stage6-adr.d.ts.map +1 -0
  209. package/dist/core/generator/stages/stage6-adr.js +41 -0
  210. package/dist/core/generator/stages/stage6-adr.js.map +1 -0
  211. package/dist/core/services/chat-agent.d.ts +45 -0
  212. package/dist/core/services/chat-agent.d.ts.map +1 -0
  213. package/dist/core/services/chat-agent.js +310 -0
  214. package/dist/core/services/chat-agent.js.map +1 -0
  215. package/dist/core/services/chat-tools.d.ts +32 -0
  216. package/dist/core/services/chat-tools.d.ts.map +1 -0
  217. package/dist/core/services/chat-tools.js +270 -0
  218. package/dist/core/services/chat-tools.js.map +1 -0
  219. package/dist/core/services/config-manager.d.ts +61 -0
  220. package/dist/core/services/config-manager.d.ts.map +1 -0
  221. package/dist/core/services/config-manager.js +143 -0
  222. package/dist/core/services/config-manager.js.map +1 -0
  223. package/dist/core/services/gitignore-manager.d.ts +29 -0
  224. package/dist/core/services/gitignore-manager.d.ts.map +1 -0
  225. package/dist/core/services/gitignore-manager.js +106 -0
  226. package/dist/core/services/gitignore-manager.js.map +1 -0
  227. package/dist/core/services/index.d.ts +8 -0
  228. package/dist/core/services/index.d.ts.map +1 -0
  229. package/dist/core/services/index.js +8 -0
  230. package/dist/core/services/index.js.map +1 -0
  231. package/dist/core/services/llm-service.d.ts +336 -0
  232. package/dist/core/services/llm-service.d.ts.map +1 -0
  233. package/dist/core/services/llm-service.js +1155 -0
  234. package/dist/core/services/llm-service.js.map +1 -0
  235. package/dist/core/services/mcp-handlers/analysis.d.ts +42 -0
  236. package/dist/core/services/mcp-handlers/analysis.d.ts.map +1 -0
  237. package/dist/core/services/mcp-handlers/analysis.js +300 -0
  238. package/dist/core/services/mcp-handlers/analysis.js.map +1 -0
  239. package/dist/core/services/mcp-handlers/graph.d.ts +65 -0
  240. package/dist/core/services/mcp-handlers/graph.d.ts.map +1 -0
  241. package/dist/core/services/mcp-handlers/graph.js +509 -0
  242. package/dist/core/services/mcp-handlers/graph.js.map +1 -0
  243. package/dist/core/services/mcp-handlers/semantic.d.ts +38 -0
  244. package/dist/core/services/mcp-handlers/semantic.d.ts.map +1 -0
  245. package/dist/core/services/mcp-handlers/semantic.js +172 -0
  246. package/dist/core/services/mcp-handlers/semantic.js.map +1 -0
  247. package/dist/core/services/mcp-handlers/utils.d.ts +21 -0
  248. package/dist/core/services/mcp-handlers/utils.d.ts.map +1 -0
  249. package/dist/core/services/mcp-handlers/utils.js +62 -0
  250. package/dist/core/services/mcp-handlers/utils.js.map +1 -0
  251. package/dist/core/services/project-detector.d.ts +32 -0
  252. package/dist/core/services/project-detector.d.ts.map +1 -0
  253. package/dist/core/services/project-detector.js +111 -0
  254. package/dist/core/services/project-detector.js.map +1 -0
  255. package/dist/core/verifier/index.d.ts +5 -0
  256. package/dist/core/verifier/index.d.ts.map +1 -0
  257. package/dist/core/verifier/index.js +5 -0
  258. package/dist/core/verifier/index.js.map +1 -0
  259. package/dist/core/verifier/verification-engine.d.ts +226 -0
  260. package/dist/core/verifier/verification-engine.d.ts.map +1 -0
  261. package/dist/core/verifier/verification-engine.js +681 -0
  262. package/dist/core/verifier/verification-engine.js.map +1 -0
  263. package/dist/types/index.d.ts +252 -0
  264. package/dist/types/index.d.ts.map +1 -0
  265. package/dist/types/index.js +5 -0
  266. package/dist/types/index.js.map +1 -0
  267. package/dist/types/pipeline.d.ts +148 -0
  268. package/dist/types/pipeline.d.ts.map +1 -0
  269. package/dist/types/pipeline.js +5 -0
  270. package/dist/types/pipeline.js.map +1 -0
  271. package/dist/utils/errors.d.ts +51 -0
  272. package/dist/utils/errors.d.ts.map +1 -0
  273. package/dist/utils/errors.js +128 -0
  274. package/dist/utils/errors.js.map +1 -0
  275. package/dist/utils/logger.d.ts +149 -0
  276. package/dist/utils/logger.d.ts.map +1 -0
  277. package/dist/utils/logger.js +331 -0
  278. package/dist/utils/logger.js.map +1 -0
  279. package/dist/utils/progress.d.ts +142 -0
  280. package/dist/utils/progress.d.ts.map +1 -0
  281. package/dist/utils/progress.js +280 -0
  282. package/dist/utils/progress.js.map +1 -0
  283. package/dist/utils/prompts.d.ts +53 -0
  284. package/dist/utils/prompts.d.ts.map +1 -0
  285. package/dist/utils/prompts.js +199 -0
  286. package/dist/utils/prompts.js.map +1 -0
  287. package/dist/utils/shutdown.d.ts +89 -0
  288. package/dist/utils/shutdown.d.ts.map +1 -0
  289. package/dist/utils/shutdown.js +237 -0
  290. package/dist/utils/shutdown.js.map +1 -0
  291. package/package.json +114 -0
  292. package/src/viewer/InteractiveGraphViewer.jsx +1486 -0
  293. package/src/viewer/app/index.html +17 -0
  294. package/src/viewer/app/main.jsx +13 -0
  295. package/src/viewer/components/ArchitectureView.jsx +177 -0
  296. package/src/viewer/components/ChatPanel.jsx +448 -0
  297. package/src/viewer/components/ClusterGraph.jsx +441 -0
  298. package/src/viewer/components/FilterBar.jsx +179 -0
  299. package/src/viewer/components/FlatGraph.jsx +275 -0
  300. package/src/viewer/components/MicroComponents.jsx +83 -0
  301. package/src/viewer/hooks/usePanZoom.js +79 -0
  302. package/src/viewer/utils/constants.js +47 -0
  303. package/src/viewer/utils/graph-helpers.js +291 -0
@@ -0,0 +1,17 @@
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>spec-gen · Graph Viewer</title>
7
+ <style>
8
+ html, body { margin: 0; height: 100%; background: #080b18; }
9
+ #root { height: 100%; }
10
+ </style>
11
+ </head>
12
+ <body>
13
+ <div id="root"></div>
14
+ <script type="module" src="/main.jsx"></script>
15
+ </body>
16
+ </html>
17
+
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import InteractiveGraphViewer from '../InteractiveGraphViewer.jsx';
4
+
5
+ ReactDOM.createRoot(document.getElementById('root')).render(
6
+ <React.StrictMode>
7
+ <InteractiveGraphViewer
8
+ graphUrl="/api/dependency-graph"
9
+ mappingUrl="/api/mapping"
10
+ specUrl="/api/spec"
11
+ />
12
+ </React.StrictMode>
13
+ );
@@ -0,0 +1,177 @@
1
+ import { useState, useMemo } from 'react';
2
+ import { ROLE_COLOR, ROLE_LABEL } from '../utils/constants.js';
3
+ import { computeArchOverview } from '../utils/graph-helpers.js';
4
+
5
+ export function ArchitectureView({ graph, llmCtx, focusedIds }) {
6
+ const overview = useMemo(() => computeArchOverview(graph, llmCtx), [graph, llmCtx]);
7
+ const [hovered, setHovered] = useState(null);
8
+ const focusedClusterIds = useMemo(() => {
9
+ if (!focusedIds?.length || !graph) return null;
10
+ return new Set(graph.nodes.filter(n => focusedIds.includes(n.id)).map(n => n.cluster.id));
11
+ }, [focusedIds, graph]);
12
+
13
+ if (!overview) return <div style={{ color: '#3a3f5c', padding: 24, fontSize: 11 }}>No graph loaded.</div>;
14
+
15
+ const { summary, clusters, globalEntryPoints, criticalHubs } = overview;
16
+
17
+ const COLS = Math.min(4, clusters.length);
18
+ const BOX_W = 140, BOX_H = 64, GAP_X = 30, GAP_Y = 40;
19
+ const ROWS = Math.ceil(clusters.length / COLS);
20
+ const SVG_W = COLS * (BOX_W + GAP_X) + GAP_X;
21
+ const SVG_H = ROWS * (BOX_H + GAP_Y) + GAP_Y;
22
+
23
+ const pos = {};
24
+ clusters.forEach((cl, i) => {
25
+ const col = i % COLS;
26
+ const row = Math.floor(i / COLS);
27
+ pos[cl.id] = {
28
+ x: GAP_X + col * (BOX_W + GAP_X),
29
+ y: GAP_Y + row * (BOX_H + GAP_Y),
30
+ };
31
+ });
32
+
33
+ const arrows = [];
34
+ clusters.forEach(cl => {
35
+ (cl.dependsOn ?? []).forEach(toId => {
36
+ const from = pos[cl.id];
37
+ const to = pos[toId];
38
+ if (!from || !to) return;
39
+ const fx = from.x + BOX_W / 2, fy = from.y + BOX_H / 2;
40
+ const tx = to.x + BOX_W / 2, ty = to.y + BOX_H / 2;
41
+ const dx = tx - fx, dy = ty - fy;
42
+ const len = Math.sqrt(dx * dx + dy * dy) || 1;
43
+ const sx = fx + (dx / len) * (BOX_W / 2 + 4);
44
+ const sy = fy + (dy / len) * (BOX_H / 2 + 4);
45
+ const ex = tx - (dx / len) * (BOX_W / 2 + 4);
46
+ const ey = ty - (dy / len) * (BOX_H / 2 + 4);
47
+ const isHov = hovered === cl.id || hovered === toId;
48
+ arrows.push(
49
+ <line
50
+ key={`${cl.id}->${toId}`}
51
+ x1={sx} y1={sy} x2={ex} y2={ey}
52
+ stroke={isHov ? '#7c6af7' : '#1e2240'}
53
+ strokeWidth={isHov ? 1.5 : 1}
54
+ markerEnd="url(#arrowhead)"
55
+ opacity={isHov ? 1 : 0.6}
56
+ />
57
+ );
58
+ });
59
+ });
60
+
61
+ const S = { fontSize: 9, fontFamily: 'inherit' };
62
+
63
+ return (
64
+ <div style={{ display: 'flex', height: '100%', overflow: 'hidden' }}>
65
+ <div style={{ flex: 1, overflow: 'auto', padding: 12 }}>
66
+ <div style={{ display: 'flex', gap: 12, marginBottom: 12, flexWrap: 'wrap' }}>
67
+ {[
68
+ ['files', summary.totalFiles],
69
+ ['clusters', summary.totalClusters],
70
+ ['edges', summary.totalEdges],
71
+ summary.cycles > 0 ? ['⚠ cycles', summary.cycles] : null,
72
+ summary.layerViolations > 0 ? ['⚠ violations', summary.layerViolations] : null,
73
+ ].filter(Boolean).map(([l, v]) => (
74
+ <div key={l} style={{ fontSize: 9, color: '#6a70a0', background: '#0e1028', borderRadius: 4, padding: '2px 8px', border: '1px solid #141830' }}>
75
+ <span style={{ color: l.startsWith('⚠') ? '#f97316' : '#c8cde8' }}>{v}</span> {l}
76
+ </div>
77
+ ))}
78
+ </div>
79
+
80
+ <svg width={SVG_W} height={SVG_H} style={{ display: 'block', minWidth: SVG_W }}>
81
+ <defs>
82
+ <marker id="arrowhead" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto">
83
+ <path d="M0,0 L0,6 L6,3 z" fill="#3a3f6c" />
84
+ </marker>
85
+ </defs>
86
+
87
+ {arrows}
88
+
89
+ {clusters.map(cl => {
90
+ const { x, y } = pos[cl.id] ?? { x: 0, y: 0 };
91
+ const color = ROLE_COLOR[cl.role] ?? '#475569';
92
+ const isHov = hovered === cl.id;
93
+ const isDimmed = focusedClusterIds && !focusedClusterIds.has(cl.id);
94
+ return (
95
+ <g
96
+ key={cl.id}
97
+ onMouseEnter={() => setHovered(cl.id)}
98
+ onMouseLeave={() => setHovered(null)}
99
+ style={{ cursor: 'default' }}
100
+ opacity={isDimmed ? 0.15 : 1}
101
+ >
102
+ <rect
103
+ x={x} y={y} width={BOX_W} height={BOX_H}
104
+ rx={6} ry={6}
105
+ fill={isHov ? '#12163a' : '#0b0e28'}
106
+ stroke={isHov ? color : isDimmed ? '#0e1028' : '#1e2240'}
107
+ strokeWidth={isHov ? 1.5 : 1}
108
+ />
109
+ <rect x={x} y={y} width={4} height={BOX_H} rx={3} ry={3} fill={color} opacity={0.8} />
110
+ <text x={x + 12} y={y + 18} fill="#c8cde8" fontSize={10} fontWeight="600" fontFamily="inherit">
111
+ {cl.name.length > 18 ? cl.name.slice(0, 17) + '...' : cl.name}
112
+ </text>
113
+ <text x={x + 12} y={y + 31} fill={color} fontSize={8} fontFamily="inherit" opacity={0.9}>
114
+ {ROLE_LABEL[cl.role] ?? cl.role}
115
+ </text>
116
+ <text x={x + 12} y={y + 44} fill="#3a4060" fontSize={8} fontFamily="inherit">
117
+ {cl.fileCount} files
118
+ {cl.hubCount > 0 ? ` · ${cl.hubCount} hub${cl.hubCount > 1 ? 's' : ''}` : ''}
119
+ {cl.entryPointCount > 0 ? ` · ${cl.entryPointCount} entry` : ''}
120
+ </text>
121
+ {cl.dependsOn.length > 0 && (
122
+ <text x={x + BOX_W - 6} y={y + 18} fill="#3a4060" fontSize={7} fontFamily="inherit" textAnchor="end">
123
+ {'->'}{ cl.dependsOn.length}
124
+ </text>
125
+ )}
126
+ </g>
127
+ );
128
+ })}
129
+ </svg>
130
+
131
+ <div style={{ display: 'flex', gap: 12, marginTop: 12, flexWrap: 'wrap' }}>
132
+ {Object.entries(ROLE_LABEL).map(([role, label]) => (
133
+ <div key={role} style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 8, color: '#3a4060' }}>
134
+ <div style={{ width: 8, height: 8, borderRadius: 2, background: ROLE_COLOR[role] }} />
135
+ {label}
136
+ </div>
137
+ ))}
138
+ </div>
139
+ </div>
140
+
141
+ <div style={{ width: 220, borderLeft: '1px solid #0f1224', overflow: 'auto', padding: '12px 10px', flexShrink: 0 }}>
142
+ {globalEntryPoints.length > 0 && (
143
+ <>
144
+ <div style={{ ...S, color: '#4ade80', fontWeight: 600, marginBottom: 6, letterSpacing: '0.08em', textTransform: 'uppercase' }}>
145
+ Entry Points
146
+ </div>
147
+ {globalEntryPoints.map((ep, i) => (
148
+ <div key={i} style={{ marginBottom: 6 }}>
149
+ <div style={{ ...S, color: '#c8cde8', fontWeight: 600 }}>{ep.name}</div>
150
+ <div style={{ ...S, color: '#3a4060', wordBreak: 'break-all' }}>{ep.file}</div>
151
+ </div>
152
+ ))}
153
+ </>
154
+ )}
155
+
156
+ {criticalHubs.length > 0 && (
157
+ <>
158
+ <div style={{ ...S, color: '#f97316', fontWeight: 600, marginBottom: 6, marginTop: 16, letterSpacing: '0.08em', textTransform: 'uppercase' }}>
159
+ Critical Hubs
160
+ </div>
161
+ {criticalHubs.map((hub, i) => (
162
+ <div key={i} style={{ marginBottom: 6 }}>
163
+ <div style={{ ...S, color: '#c8cde8', fontWeight: 600 }}>{hub.name}</div>
164
+ <div style={{ ...S, color: '#3a4060', wordBreak: 'break-all' }}>{hub.file}</div>
165
+ <div style={{ ...S, color: '#f97316', opacity: 0.7 }}>fanIn {hub.fanIn} · fanOut {hub.fanOut}</div>
166
+ </div>
167
+ ))}
168
+ </>
169
+ )}
170
+
171
+ {globalEntryPoints.length === 0 && criticalHubs.length === 0 && (
172
+ <div style={{ ...S, color: '#3a3f5c' }}>Run <code style={{ color: '#7c6af7' }}>spec-gen analyze</code> to populate call graph data.</div>
173
+ )}
174
+ </div>
175
+ </div>
176
+ );
177
+ }
@@ -0,0 +1,448 @@
1
+ import { useState, useRef, useEffect } from 'react';
2
+
3
+ // ============================================================================
4
+ // SIMPLE MARKDOWN RENDERER
5
+ // ============================================================================
6
+
7
+ function escapeHtml(str) {
8
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
9
+ }
10
+
11
+ function renderInline(text) {
12
+ // Escape HTML first to prevent XSS, then apply markdown formatting
13
+ return escapeHtml(text)
14
+ .replace(/`([^`]+)`/g, '<code style="background:#1a1f38;padding:1px 4px;border-radius:3px;font-family:JetBrains Mono,monospace;font-size:8px;color:#c8cde8">$1</code>')
15
+ .replace(/\*\*([^*]+)\*\*/g, '<strong style="color:#c8cde8">$1</strong>')
16
+ .replace(/\*([^*]+)\*/g, '<em style="color:#9b96c8">$1</em>');
17
+ }
18
+
19
+ function MarkdownBlock({ text }) {
20
+ const lines = text.split('\n');
21
+ const elements = [];
22
+ let i = 0;
23
+
24
+ while (i < lines.length) {
25
+ const line = lines[i];
26
+
27
+ // Fenced code block
28
+ if (line.startsWith('```')) {
29
+ const codeLines = [];
30
+ i++;
31
+ while (i < lines.length && !lines[i].startsWith('```')) {
32
+ codeLines.push(lines[i]);
33
+ i++;
34
+ }
35
+ elements.push(
36
+ <pre key={i} style={{
37
+ background: '#0c0e22', border: '1px solid #141830', borderRadius: 4,
38
+ padding: '6px 8px', margin: '4px 0', overflowX: 'auto',
39
+ fontSize: 8, color: '#9b96c8', fontFamily: 'JetBrains Mono,monospace', lineHeight: 1.5,
40
+ }}>
41
+ {codeLines.join('\n')}
42
+ </pre>
43
+ );
44
+ i++;
45
+ continue;
46
+ }
47
+
48
+ // H3 / H4 headings
49
+ if (/^#{3,4}\s+/.test(line)) {
50
+ const content = line.replace(/^#{3,4}\s+/, '');
51
+ elements.push(
52
+ <div key={i} style={{ fontSize: 9, color: '#7c6af7', fontWeight: 600, margin: '8px 0 3px', letterSpacing: '0.04em' }}
53
+ dangerouslySetInnerHTML={{ __html: renderInline(content) }} />
54
+ );
55
+ i++;
56
+ continue;
57
+ }
58
+
59
+ // H1 / H2 headings
60
+ if (/^#{1,2}\s+/.test(line)) {
61
+ const content = line.replace(/^#{1,2}\s+/, '');
62
+ elements.push(
63
+ <div key={i} style={{ fontSize: 10, color: '#c8cde8', fontWeight: 700, margin: '10px 0 4px' }}
64
+ dangerouslySetInnerHTML={{ __html: renderInline(content) }} />
65
+ );
66
+ i++;
67
+ continue;
68
+ }
69
+
70
+ // Bullet list
71
+ if (/^[*-]\s+/.test(line)) {
72
+ const items = [];
73
+ while (i < lines.length && /^[*-]\s+/.test(lines[i])) {
74
+ items.push(lines[i].replace(/^[*-]\s+/, ''));
75
+ i++;
76
+ }
77
+ elements.push(
78
+ <ul key={i} style={{ margin: '3px 0', paddingLeft: 14, listStyle: 'none' }}>
79
+ {items.map((item, j) => (
80
+ <li key={j} style={{ fontSize: 9, color: '#9b96c8', lineHeight: 1.6, position: 'relative', paddingLeft: 8 }}>
81
+ <span style={{ position: 'absolute', left: 0, color: '#7c6af7' }}>·</span>
82
+ <span dangerouslySetInnerHTML={{ __html: renderInline(item) }} />
83
+ </li>
84
+ ))}
85
+ </ul>
86
+ );
87
+ continue;
88
+ }
89
+
90
+ // Numbered list
91
+ if (/^\d+\.\s+/.test(line)) {
92
+ const items = [];
93
+ while (i < lines.length && /^\d+\.\s+/.test(lines[i])) {
94
+ const match = lines[i].match(/^(\d+)\.\s+(.*)/);
95
+ if (match) items.push({ num: match[1], text: match[2] });
96
+ i++;
97
+ }
98
+ elements.push(
99
+ <ol key={i} style={{ margin: '3px 0', paddingLeft: 0, listStyle: 'none' }}>
100
+ {items.map((item, j) => (
101
+ <li key={j} style={{ fontSize: 9, color: '#9b96c8', lineHeight: 1.6, display: 'flex', gap: 5, margin: '1px 0' }}>
102
+ <span style={{ color: '#7c6af7', flexShrink: 0, fontVariantNumeric: 'tabular-nums' }}>{item.num}.</span>
103
+ <span dangerouslySetInnerHTML={{ __html: renderInline(item.text) }} />
104
+ </li>
105
+ ))}
106
+ </ol>
107
+ );
108
+ continue;
109
+ }
110
+
111
+ // Horizontal rule
112
+ if (/^---+$/.test(line.trim())) {
113
+ elements.push(<hr key={i} style={{ border: 'none', borderTop: '1px solid #141830', margin: '6px 0' }} />);
114
+ i++;
115
+ continue;
116
+ }
117
+
118
+ // Empty line -> spacer
119
+ if (line.trim() === '') {
120
+ elements.push(<div key={i} style={{ height: 4 }} />);
121
+ i++;
122
+ continue;
123
+ }
124
+
125
+ // Plain paragraph
126
+ elements.push(
127
+ <div key={i} style={{ fontSize: 9, color: '#9b96c8', lineHeight: 1.6 }}
128
+ dangerouslySetInnerHTML={{ __html: renderInline(line) }} />
129
+ );
130
+ i++;
131
+ }
132
+
133
+ return <div style={{ display: 'flex', flexDirection: 'column', gap: 1 }}>{elements}</div>;
134
+ }
135
+
136
+ // ============================================================================
137
+ // TOOL SPINNER
138
+ // ============================================================================
139
+
140
+ function ToolSpinner() {
141
+ const [frame, setFrame] = useState(0);
142
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
143
+ useEffect(() => {
144
+ const id = setInterval(() => setFrame((f) => (f + 1) % frames.length), 80);
145
+ return () => clearInterval(id);
146
+ }, []);
147
+ return <span style={{ color: '#7c6af7', fontFamily: 'monospace', fontSize: 10, lineHeight: 1 }}>{frames[frame]}</span>;
148
+ }
149
+
150
+ // ============================================================================
151
+ // CHAT PANEL
152
+ // ============================================================================
153
+
154
+ /**
155
+ * ChatPanel -- agentic chatbot panel for the dependency graph viewer.
156
+ *
157
+ * Props:
158
+ * onHighlight(ids: string[]) -- called when the agent returns node IDs to highlight
159
+ * onClose() -- called when the panel is closed
160
+ */
161
+ export function ChatPanel({ onHighlight, onClose }) {
162
+ const [messages, setMessages] = useState([
163
+ {
164
+ role: 'assistant',
165
+ content:
166
+ 'Hi! Ask me anything about this codebase. For example:\n' +
167
+ '- "What are the most critical functions?"\n' +
168
+ '- "What is the impact of changing the embedding service?"\n' +
169
+ '- "Where would I add a new API endpoint?"',
170
+ },
171
+ ]);
172
+ const [input, setInput] = useState('');
173
+ const [loading, setLoading] = useState(false);
174
+ const [activeTools, setActiveTools] = useState([]); // tools currently running
175
+ const [error, setError] = useState(null);
176
+ const [modelInfo, setModelInfo] = useState(null); // { provider, currentModel, models }
177
+ const bottomRef = useRef(null);
178
+
179
+ useEffect(() => {
180
+ fetch('/api/chat/models')
181
+ .then((r) => r.json())
182
+ .then((data) => {
183
+ if (data.error) console.warn('[chat/models]', data.error);
184
+ else setModelInfo(data);
185
+ })
186
+ .catch((e) => console.warn('[chat/models] fetch failed:', e.message));
187
+ }, []);
188
+
189
+ useEffect(() => {
190
+ bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
191
+ }, [messages, loading, activeTools]);
192
+
193
+ const send = async () => {
194
+ const text = input.trim();
195
+ if (!text || loading) return;
196
+
197
+ const history = messages
198
+ .slice(1) // drop initial greeting
199
+ .map((m) => ({ role: m.role, content: m.content }));
200
+
201
+ setMessages((prev) => [...prev, { role: 'user', content: text }]);
202
+ setInput('');
203
+ setLoading(true);
204
+ setActiveTools([]);
205
+ setError(null);
206
+
207
+ try {
208
+ const res = await fetch('/api/chat', {
209
+ method: 'POST',
210
+ headers: { 'Content-Type': 'application/json' },
211
+ body: JSON.stringify({ message: text, history, model: modelInfo?.currentModel }),
212
+ });
213
+
214
+ if (!res.ok) {
215
+ const err = await res.json().catch(() => ({ error: res.statusText }));
216
+ throw new Error(err.error ?? res.statusText);
217
+ }
218
+
219
+ // Parse SSE stream
220
+ const reader = res.body.getReader();
221
+ const decoder = new TextDecoder();
222
+ let buf = '';
223
+
224
+ while (true) {
225
+ const { done, value } = await reader.read();
226
+ if (done) break;
227
+ buf += decoder.decode(value, { stream: true });
228
+ const parts = buf.split('\n\n');
229
+ buf = parts.pop() ?? '';
230
+ for (const part of parts) {
231
+ const line = part.trim();
232
+ if (!line.startsWith('data:')) continue;
233
+ let evt;
234
+ try { evt = JSON.parse(line.slice(5).trim()); } catch { continue; }
235
+
236
+ if (evt.type === 'tool_start') {
237
+ setActiveTools((prev) => [...prev, evt.name]);
238
+ } else if (evt.type === 'tool_end') {
239
+ setActiveTools((prev) => prev.filter((n) => n !== evt.name));
240
+ } else if (evt.type === 'reply') {
241
+ setMessages((prev) => [...prev, { role: 'assistant', content: evt.reply }]);
242
+ onHighlight(evt.highlightIds ?? []);
243
+ } else if (evt.type === 'error') {
244
+ throw new Error(evt.error);
245
+ }
246
+ }
247
+ }
248
+ } catch (err) {
249
+ setError(err.message);
250
+ } finally {
251
+ setLoading(false);
252
+ setActiveTools([]);
253
+ }
254
+ };
255
+
256
+ const handleKeyDown = (e) => {
257
+ if (e.key === 'Enter' && !e.shiftKey) {
258
+ e.preventDefault();
259
+ send();
260
+ }
261
+ };
262
+
263
+ const clear = () => {
264
+ setMessages([{ role: 'assistant', content: 'Conversation cleared. What would you like to know?' }]);
265
+ setError(null);
266
+ onHighlight([]);
267
+ };
268
+
269
+ return (
270
+ <div
271
+ style={{
272
+ width: 340,
273
+ borderLeft: '1px solid #0f1224',
274
+ background: '#080b1e',
275
+ display: 'flex',
276
+ flexDirection: 'column',
277
+ overflow: 'hidden',
278
+ flexShrink: 0,
279
+ }}
280
+ >
281
+ {/* Header */}
282
+ <div
283
+ style={{
284
+ display: 'flex',
285
+ flexDirection: 'column',
286
+ padding: '6px 10px',
287
+ borderBottom: '1px solid #0f1224',
288
+ flexShrink: 0,
289
+ gap: 5,
290
+ }}
291
+ >
292
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
293
+ <span style={{ fontSize: 9, color: '#7c6af7', letterSpacing: '0.1em', fontFamily: 'inherit' }}>
294
+ DIAGRAM CHAT
295
+ </span>
296
+ <div style={{ display: 'flex', gap: 6 }}>
297
+ <button
298
+ onClick={clear}
299
+ title="Clear conversation"
300
+ style={{
301
+ background: 'none', border: '1px solid #1a1f38', borderRadius: 3,
302
+ color: '#3a3f5c', fontSize: 8, padding: '2px 6px', cursor: 'pointer', fontFamily: 'inherit',
303
+ }}
304
+ >
305
+ CLEAR
306
+ </button>
307
+ <button
308
+ onClick={onClose}
309
+ title="Close chat"
310
+ style={{
311
+ background: 'none', border: '1px solid #1a1f38', borderRadius: 3,
312
+ color: '#3a3f5c', fontSize: 10, padding: '2px 6px', cursor: 'pointer', fontFamily: 'inherit', lineHeight: 1,
313
+ }}
314
+ >
315
+ x
316
+ </button>
317
+ </div>
318
+ </div>
319
+ {/* Model selector */}
320
+ {modelInfo && (
321
+ <div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
322
+ <span style={{ fontSize: 8, color: '#3a3f5c', letterSpacing: '0.06em', flexShrink: 0 }}>
323
+ {modelInfo.provider.toUpperCase()}
324
+ </span>
325
+ <select
326
+ value={modelInfo.currentModel}
327
+ onChange={(e) => setModelInfo((prev) => ({ ...prev, currentModel: e.target.value }))}
328
+ style={{
329
+ flex: 1, background: '#0c0e22', border: '1px solid #2a2f4c', borderRadius: 3,
330
+ color: '#9b96c8', fontSize: 8, padding: '2px 4px', fontFamily: 'inherit',
331
+ cursor: 'pointer', outline: 'none',
332
+ }}
333
+ >
334
+ {modelInfo.models.length === 0 && (
335
+ <option value={modelInfo.currentModel}>{modelInfo.currentModel}</option>
336
+ )}
337
+ {modelInfo.models.map((m) => (
338
+ <option key={m} value={m}>{m}</option>
339
+ ))}
340
+ </select>
341
+ </div>
342
+ )}
343
+ </div>
344
+
345
+ {/* Message list */}
346
+ <div
347
+ style={{
348
+ flex: 1, overflowY: 'auto', padding: '8px 10px',
349
+ display: 'flex', flexDirection: 'column', gap: 8,
350
+ }}
351
+ >
352
+ {messages.map((m, i) => (
353
+ <div key={i} style={{ alignSelf: m.role === 'user' ? 'flex-end' : 'flex-start', maxWidth: '96%' }}>
354
+ <div
355
+ style={{
356
+ background: m.role === 'user' ? '#141830' : '#0c0e22',
357
+ border: `1px solid ${m.role === 'user' ? '#1a1f38' : '#0f1224'}`,
358
+ borderRadius: m.role === 'user' ? '8px 8px 2px 8px' : '8px 8px 8px 2px',
359
+ padding: '6px 10px',
360
+ fontFamily: 'inherit',
361
+ }}
362
+ >
363
+ {m.role === 'user' ? (
364
+ <div style={{ fontSize: 9, color: '#c8cde8', lineHeight: 1.5, whiteSpace: 'pre-wrap' }}>
365
+ {m.content}
366
+ </div>
367
+ ) : (
368
+ <MarkdownBlock text={m.content} />
369
+ )}
370
+ </div>
371
+ </div>
372
+ ))}
373
+
374
+ {loading && (
375
+ <div style={{ alignSelf: 'flex-start' }}>
376
+ <div style={{
377
+ fontSize: 9, color: '#7c6af7', background: '#0c0e22',
378
+ border: '1px solid #0f1224', borderRadius: '8px 8px 8px 2px',
379
+ padding: '6px 10px', fontFamily: 'inherit',
380
+ display: 'flex', flexDirection: 'column', gap: 4,
381
+ }}>
382
+ {activeTools.length === 0 ? (
383
+ <span>thinking...</span>
384
+ ) : (
385
+ activeTools.map((name, i) => (
386
+ <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
387
+ <ToolSpinner />
388
+ <span style={{ color: '#9b96c8' }}>{name.replace(/_/g, ' ')}</span>
389
+ </div>
390
+ ))
391
+ )}
392
+ </div>
393
+ </div>
394
+ )}
395
+
396
+ {error && (
397
+ <div style={{
398
+ fontSize: 9, color: '#f87171', background: '#1a0a0a',
399
+ border: '1px solid #3a1a1a', borderRadius: 4,
400
+ padding: '5px 8px', fontFamily: 'inherit',
401
+ }}>
402
+ Error: {error}
403
+ </div>
404
+ )}
405
+
406
+ <div ref={bottomRef} />
407
+ </div>
408
+
409
+ {/* Input area */}
410
+ <div
411
+ style={{
412
+ borderTop: '1px solid #0f1224', padding: '8px 10px',
413
+ display: 'flex', flexDirection: 'column', gap: 6, flexShrink: 0,
414
+ }}
415
+ >
416
+ <textarea
417
+ value={input}
418
+ onChange={(e) => setInput(e.target.value)}
419
+ onKeyDown={handleKeyDown}
420
+ placeholder="Ask about the codebase... (Enter to send, Shift+Enter for newline)"
421
+ rows={3}
422
+ style={{
423
+ background: '#0c0e22', border: '1px solid #141830', borderRadius: 4,
424
+ color: '#c8cde8', fontSize: 9, padding: '6px 8px',
425
+ resize: 'none', outline: 'none', fontFamily: 'inherit', lineHeight: 1.4,
426
+ }}
427
+ />
428
+ <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
429
+ <button
430
+ onClick={send}
431
+ disabled={!input.trim() || loading}
432
+ style={{
433
+ background: input.trim() && !loading ? '#1a1050' : 'transparent',
434
+ border: `1px solid ${input.trim() && !loading ? '#7c6af7' : '#1a1f38'}`,
435
+ borderRadius: 4,
436
+ color: input.trim() && !loading ? '#c8cde8' : '#3a3f5c',
437
+ fontSize: 8, padding: '4px 12px',
438
+ cursor: input.trim() && !loading ? 'pointer' : 'default',
439
+ fontFamily: 'inherit', letterSpacing: '0.06em',
440
+ }}
441
+ >
442
+ SEND
443
+ </button>
444
+ </div>
445
+ </div>
446
+ </div>
447
+ );
448
+ }