diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 16618a66..f259c01d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -18,6 +18,7 @@ "@sentry/vite-plugin": "^5.1.1", "@stripe/stripe-js": "^8.7.0", "@tailwindcss/vite": "^4.2.1", + "@types/react-syntax-highlighter": "^15.5.13", "@xyflow/react": "^12.10.0", "axios": "^1.13.4", "class-variance-authority": "^0.7.1", @@ -33,6 +34,7 @@ "react-helmet-async": "^3.0.0", "react-markdown": "^10.1.0", "react-router-dom": "^7.13.0", + "react-syntax-highlighter": "^16.1.1", "recharts": "^3.7.0", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", @@ -362,7 +364,6 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2991,6 +2992,12 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/prismjs": { + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.10", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", @@ -3010,6 +3017,15 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-syntax-highlighter": { + "version": "15.5.13", + "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", + "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -4837,6 +4853,19 @@ "dev": true, "license": "MIT" }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -4946,6 +4975,14 @@ "node": ">= 6" } }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -5171,6 +5208,19 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", @@ -5211,6 +5261,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -5228,6 +5295,21 @@ "hermes-estree": "0.25.1" } }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", + "license": "CC0-1.0" + }, "node_modules/html-encoding-sniffer": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", @@ -5998,6 +6080,20 @@ "loose-envify": "cli.js" } }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -7196,6 +7292,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -7428,6 +7533,26 @@ "react-dom": ">=18" } }, + "node_modules/react-syntax-highlighter": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz", + "integrity": "sha512-PjVawBGy80C6YbC5DDZJeUjBmC7skaoEUdvfFQediQHgCL7aKyVHe57SaJGfQsloGDac+gCpTfRdtxzWWKmCXA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", + "lowlight": "^1.17.0", + "prismjs": "^1.30.0", + "refractor": "^5.0.0" + }, + "engines": { + "node": ">= 16.20.2" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, "node_modules/recharts": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz", @@ -7497,6 +7622,22 @@ "redux": "^5.0.0" } }, + "node_modules/refractor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz", + "integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/prismjs": "^1.0.0", + "hastscript": "^9.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 721a2590..16079d27 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,6 +31,7 @@ "@sentry/vite-plugin": "^5.1.1", "@stripe/stripe-js": "^8.7.0", "@tailwindcss/vite": "^4.2.1", + "@types/react-syntax-highlighter": "^15.5.13", "@xyflow/react": "^12.10.0", "axios": "^1.13.4", "class-variance-authority": "^0.7.1", @@ -46,6 +47,7 @@ "react-helmet-async": "^3.0.0", "react-markdown": "^10.1.0", "react-router-dom": "^7.13.0", + "react-syntax-highlighter": "^16.1.1", "recharts": "^3.7.0", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", @@ -62,8 +64,8 @@ "@types/node": "^24.10.9", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", - "@vitest/coverage-v8": "^4.0.18", "@vitejs/plugin-react": "^5.1.1", + "@vitest/coverage-v8": "^4.0.18", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", diff --git a/frontend/src/components/layout/AppLayout.tsx b/frontend/src/components/layout/AppLayout.tsx index dd813cf9..cc870ab0 100644 --- a/frontend/src/components/layout/AppLayout.tsx +++ b/frontend/src/components/layout/AppLayout.tsx @@ -1,6 +1,6 @@ import { useEffect, useState, useCallback } from 'react' import { useLocation, useNavigate, Link } from 'react-router-dom' -import { Menu, X, LayoutGrid, Clock, Network, AlertTriangle, Code2, BarChart3, Settings, LogOut, Shield, Library } from 'lucide-react' +import { Menu, X, LayoutGrid, Clock, Network, AlertTriangle, Code2, Wand2, BarChart3, Settings, LogOut, Shield, Library } from 'lucide-react' import { useAuthStore } from '@/store/authStore' import { usePermissions } from '@/hooks/usePermissions' import { useUserPreferencesStore } from '@/store/userPreferencesStore' @@ -57,6 +57,7 @@ export function AppLayout() { { path: '/trees', label: 'Flows', icon: Network }, { path: '/step-library', label: 'Step Library', icon: Library }, { path: '/scripts', label: 'Scripts', icon: Code2 }, + { path: '/script-builder', label: 'Script Builder', icon: Wand2 }, { path: '/analytics', label: 'Analytics', icon: BarChart3 }, { path: '/account', label: 'Account', icon: Settings }, ] diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index abe4cd08..796034f7 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -3,7 +3,7 @@ import { useLocation } from 'react-router-dom' import { LayoutGrid, Network, Clock, FileOutput, BarChart3, TrendingUp, Settings, PanelLeftClose, PanelLeftOpen, MessageSquareText, ListChecks, - BookOpen, Code2, Library, AlertTriangle, + BookOpen, Code2, Library, AlertTriangle, Wand2, } from 'lucide-react' import { cn } from '@/lib/utils' import { useUserPreferencesStore } from '@/store/userPreferencesStore' @@ -19,6 +19,7 @@ const NAV_COLORS = { exports: '#60a5fa', // blue-400 stepLib: '#fb923c', // orange-400 scripts: '#2dd4bf', // teal-400 + scriptBuilder: '#e879f9', // fuchsia-400 analytics: '#38bdf8', // sky-400 guides: '#a3e635', // lime-400 feedback: '#818cf8', // indigo-400 @@ -83,6 +84,7 @@ export function Sidebar() { + @@ -124,6 +126,7 @@ export function Sidebar() { /> + {/* Insights */} diff --git a/frontend/src/components/script-builder/SaveToLibraryDialog.tsx b/frontend/src/components/script-builder/SaveToLibraryDialog.tsx new file mode 100644 index 00000000..45713177 --- /dev/null +++ b/frontend/src/components/script-builder/SaveToLibraryDialog.tsx @@ -0,0 +1,186 @@ +import { useState, useEffect } from 'react' +import { X, Loader2 } from 'lucide-react' +import { cn } from '@/lib/utils' +import { scriptBuilderApi, scriptsApi } from '@/api' +import type { ScriptCategoryResponse } from '@/types' + +interface SaveToLibraryDialogProps { + sessionId: string + defaultName: string + defaultDescription?: string + onClose: () => void + onSaved: () => void +} + +export function SaveToLibraryDialog({ + sessionId, + defaultName, + defaultDescription, + onClose, + onSaved, +}: SaveToLibraryDialogProps) { + const [name, setName] = useState(defaultName) + const [description, setDescription] = useState(defaultDescription || '') + const [categoryId, setCategoryId] = useState('') + const [shareWithTeam, setShareWithTeam] = useState(false) + const [categories, setCategories] = useState([]) + const [isSaving, setIsSaving] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + scriptsApi.getCategories().then(setCategories).catch(() => {}) + }, []) + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose() + } + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [onClose]) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!name.trim()) return + + setIsSaving(true) + setError(null) + + try { + await scriptBuilderApi.saveToLibrary(sessionId, { + name: name.trim(), + description: description.trim() || undefined, + category_id: categoryId || undefined, + share_with_team: shareWithTeam, + }) + onSaved() + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to save script. Please try again.') + } finally { + setIsSaving(false) + } + } + + return ( +
{ if (e.target === e.currentTarget) onClose() }} + > +
+ {/* Header */} +
+

Save to Library

+ +
+ + {/* Form */} +
+ {/* Name */} +
+ + setName(e.target.value)} + required + className={cn( + "w-full rounded-[10px] px-3 py-2 text-sm", + "border border-border bg-card text-foreground placeholder:text-muted-foreground", + "focus:outline-none focus:border-[rgba(6,182,212,0.3)] transition-colors" + )} + placeholder="Script name" + /> +
+ + {/* Description */} +
+ +