diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 4614b6f..4f6f185 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -13,13 +13,16 @@
"@tanstack/react-query-devtools": "^5.91.1",
"antd": "^6.0.0",
"axios": "^1.13.2",
+ "prismjs": "^1.30.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
- "react-router-dom": "^7.9.4"
+ "react-router-dom": "^7.9.4",
+ "react-simple-code-editor": "^0.14.1"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@types/node": "^24.9.1",
+ "@types/prismjs": "^1.26.5",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@types/react-router-dom": "^5.3.3",
@@ -2316,6 +2319,13 @@
"undici-types": "~7.16.0"
}
},
+ "node_modules/@types/prismjs": {
+ "version": "1.26.5",
+ "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
+ "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/react": {
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
@@ -5204,6 +5214,15 @@
"url": "https://github.com/prettier/prettier?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/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -5305,6 +5324,16 @@
"react-dom": ">=18"
}
},
+ "node_modules/react-simple-code-editor": {
+ "version": "0.14.1",
+ "resolved": "https://registry.npmjs.org/react-simple-code-editor/-/react-simple-code-editor-0.14.1.tgz",
+ "integrity": "sha512-BR5DtNRy+AswWJECyA17qhUDvrrCZ6zXOCfkQY5zSmb96BVUbpVAv03WpcjcwtCwiLbIANx3gebHOcXYn1EHow==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index e0eef19..4f2f4fd 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -17,13 +17,16 @@
"@tanstack/react-query-devtools": "^5.91.1",
"antd": "^6.0.0",
"axios": "^1.13.2",
+ "prismjs": "^1.30.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
- "react-router-dom": "^7.9.4"
+ "react-router-dom": "^7.9.4",
+ "react-simple-code-editor": "^0.14.1"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@types/node": "^24.9.1",
+ "@types/prismjs": "^1.26.5",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@types/react-router-dom": "^5.3.3",
diff --git a/frontend/src/Layout/Sidebar.tsx b/frontend/src/Layout/Sidebar.tsx
index 6127c0c..2856d7b 100644
--- a/frontend/src/Layout/Sidebar.tsx
+++ b/frontend/src/Layout/Sidebar.tsx
@@ -3,6 +3,7 @@ import { ApartmentOutlined, ApiOutlined, HomeOutlined, InfoCircleOutlined, Setti
import { Layout, Menu } from 'antd';
import React from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
+import TerminalIcon from '../icons/TerminalIcon';
import { PATHS } from '../routes';
import '../theme/layout.less';
@@ -12,6 +13,11 @@ const menuItems = [
{ key: PATHS.HOME, icon: , label: 'Home' },
{ key: PATHS.NETWORK, icon: , label: 'Network' },
{ key: PATHS.SNIFFING, icon: , label: 'Sniffing' },
+ {
+ key: PATHS.SCRIPTING,
+ icon: ,
+ label: 'Scripting',
+ },
{ key: '/about', icon: , label: 'About' },
{ key: '/settings', icon: , label: 'Settings' },
];
diff --git a/frontend/src/appRouter.tsx b/frontend/src/appRouter.tsx
index 57c46d8..e94dabf 100644
--- a/frontend/src/appRouter.tsx
+++ b/frontend/src/appRouter.tsx
@@ -3,6 +3,7 @@ import { Navigate, Route, Routes } from 'react-router-dom';
import App from './App'; // your layout component (has )
import Home from './pages/Home';
import Network from './pages/Network';
+import Scripting from './pages/Scripting';
import Sniffing from './pages/Sniffing';
import { PATHS } from './routes';
@@ -18,6 +19,7 @@ export default function AppRouter() {
} />
} />
} />
+ } />
{/* Fallback (renders inside layout too) */}
} />
diff --git a/frontend/src/components/PythonEditor.tsx b/frontend/src/components/PythonEditor.tsx
new file mode 100644
index 0000000..064d0d2
--- /dev/null
+++ b/frontend/src/components/PythonEditor.tsx
@@ -0,0 +1,54 @@
+import Prism from 'prismjs';
+import 'prismjs/components/prism-python';
+import 'prismjs/themes/prism.css';
+import { useCallback, useMemo } from 'react';
+import Editor from 'react-simple-code-editor';
+
+// Props for the editor
+export type PythonEditorProps = {
+ value: string;
+ onChange: (code: string) => void;
+ readOnly?: boolean;
+ className?: string;
+ height?: string | number; // e.g. '200px' or 200
+};
+
+export default function PythonEditor({
+ value,
+ onChange,
+ readOnly = false,
+ height = 480,
+ className = '',
+}: PythonEditorProps) {
+ const highlight = useCallback((code: string) => {
+ try {
+ return Prism.highlight(code, Prism.languages.python, 'python');
+ } catch (e) {
+ return code;
+ }
+ }, []);
+
+ const editorStyle = useMemo(
+ () => ({
+ fontFamily: '"JetBrains Mono", "Fira Code", monospace',
+ fontSize: 14,
+ height: typeof height === 'number' ? `${height}px` : height,
+ }),
+ [height],
+ );
+
+ return (
+
+ onChange(code)}
+ highlight={highlight}
+ padding={12}
+ preClassName="language-python"
+ textareaId="python-editor"
+ style={editorStyle}
+ readOnly={readOnly}
+ />
+
+ );
+}
diff --git a/frontend/src/icons/TerminalIcon.tsx b/frontend/src/icons/TerminalIcon.tsx
new file mode 100644
index 0000000..4298b6b
--- /dev/null
+++ b/frontend/src/icons/TerminalIcon.tsx
@@ -0,0 +1,57 @@
+import React, { forwardRef } from 'react';
+
+export type IconProps = React.SVGProps & {
+ /**
+ * Width/height of the icon. If a number is provided it will be used as px.
+ * Default: 24
+ */
+ size?: number | string;
+ /**
+ * Icon color — will be used as the fill for paths.
+ * Default: 'currentColor' so color can be controlled via CSS.
+ */
+ color?: string;
+ /**
+ * Accessible title. If provided, title will be rendered and aria-hidden will be false.
+ */
+ title?: string;
+};
+
+const TerminalIcon = forwardRef(
+ ({ size = 24, color = 'currentColor', title, ...rest }, ref) => {
+ // If user passed a numeric size, treat as px
+ const sizeValue = typeof size === 'number' ? `${size}px` : size;
+
+ return (
+
+ );
+ },
+);
+
+TerminalIcon.displayName = 'TerminalIcon';
+
+export default TerminalIcon;
diff --git a/frontend/src/index.less b/frontend/src/index.less
index f787941..ce08f1e 100644
--- a/frontend/src/index.less
+++ b/frontend/src/index.less
@@ -76,10 +76,6 @@ a:hover {
/* === Code blocks / preformatted text === */
pre,
code {
- background-color: #f0f0f0;
- color: #262626;
- padding: 0.25rem 0.5rem;
- border-radius: 4px;
font-family: "JetBrains Mono", "Fira Code", monospace;
}
diff --git a/frontend/src/pages/Scripting.tsx b/frontend/src/pages/Scripting.tsx
new file mode 100644
index 0000000..18bd14f
--- /dev/null
+++ b/frontend/src/pages/Scripting.tsx
@@ -0,0 +1,33 @@
+import { Card, Tabs } from 'antd';
+import { useState } from 'react';
+import PythonEditor from '../components/PythonEditor';
+
+const TwoTabsPage: React.FC = () => {
+ const [code, setCode] = useState("print('hello world')\n");
+
+ const tabItems = [
+ {
+ key: '1',
+ label: 'Saved Scripts',
+ children: <>>,
+ },
+ {
+ key: '2',
+ label: 'Custom Script',
+ children: (
+
+ ),
+ },
+ ];
+
+ return (
+
+
+
+ );
+};
+
+export default TwoTabsPage;
diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts
index 1215334..b0b80b9 100644
--- a/frontend/src/routes.ts
+++ b/frontend/src/routes.ts
@@ -3,4 +3,5 @@ export const PATHS = {
HOME: '/home',
NETWORK: '/network',
SNIFFING: '/sniffing',
+ SCRIPTING: '/scripting',
};
\ No newline at end of file