@doppelgangerdev/doppelganger 0.2.2
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.
- package/.dockerignore +9 -0
- package/.github/workflows/docker-publish.yml +59 -0
- package/CODE_OF_CONDUCT.md +28 -0
- package/CONTRIBUTING.md +42 -0
- package/Dockerfile +44 -0
- package/LICENSE +163 -0
- package/README.md +133 -0
- package/TERMS.md +16 -0
- package/THIRD_PARTY_LICENSES.md +3502 -0
- package/agent.js +1240 -0
- package/headful.js +171 -0
- package/index.html +21 -0
- package/n8n-nodes-doppelganger/LICENSE +201 -0
- package/n8n-nodes-doppelganger/README.md +42 -0
- package/n8n-nodes-doppelganger/package-lock.json +6128 -0
- package/n8n-nodes-doppelganger/package.json +36 -0
- package/n8n-nodes-doppelganger/src/credentials/DoppelgangerApi.credentials.ts +35 -0
- package/n8n-nodes-doppelganger/src/index.ts +4 -0
- package/n8n-nodes-doppelganger/src/nodes/Doppelganger/Doppelganger.node.ts +147 -0
- package/n8n-nodes-doppelganger/src/nodes/Doppelganger/icon.png +0 -0
- package/n8n-nodes-doppelganger/tsconfig.json +14 -0
- package/package.json +45 -0
- package/postcss.config.js +6 -0
- package/public/icon.png +0 -0
- package/public/novnc.html +151 -0
- package/public/styles.css +86 -0
- package/scrape.js +389 -0
- package/server.js +875 -0
- package/src/App.tsx +722 -0
- package/src/components/AuthScreen.tsx +95 -0
- package/src/components/CodeEditor.tsx +70 -0
- package/src/components/DashboardScreen.tsx +133 -0
- package/src/components/EditorScreen.tsx +1519 -0
- package/src/components/ExecutionDetailScreen.tsx +115 -0
- package/src/components/ExecutionsScreen.tsx +156 -0
- package/src/components/LoadingScreen.tsx +26 -0
- package/src/components/NotFoundScreen.tsx +34 -0
- package/src/components/RichInput.tsx +68 -0
- package/src/components/SettingsScreen.tsx +228 -0
- package/src/components/Sidebar.tsx +61 -0
- package/src/components/app/CenterAlert.tsx +44 -0
- package/src/components/app/CenterConfirm.tsx +33 -0
- package/src/components/app/EditorLoader.tsx +89 -0
- package/src/components/editor/ActionPalette.tsx +79 -0
- package/src/components/editor/JsonEditorPane.tsx +71 -0
- package/src/components/editor/ResultsPane.tsx +641 -0
- package/src/components/editor/actionCatalog.ts +23 -0
- package/src/components/settings/AgentAiPanel.tsx +105 -0
- package/src/components/settings/ApiKeyPanel.tsx +68 -0
- package/src/components/settings/CookiesPanel.tsx +154 -0
- package/src/components/settings/LayoutPanel.tsx +46 -0
- package/src/components/settings/ScreenshotsPanel.tsx +64 -0
- package/src/components/settings/SettingsHeader.tsx +28 -0
- package/src/components/settings/StoragePanel.tsx +35 -0
- package/src/index.css +287 -0
- package/src/main.tsx +13 -0
- package/src/types.ts +114 -0
- package/src/utils/syntaxHighlight.ts +140 -0
- package/start-vnc.sh +52 -0
- package/tailwind.config.js +22 -0
- package/tsconfig.json +39 -0
- package/tsconfig.node.json +12 -0
- package/vite.config.mts +27 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
interface AuthScreenProps {
|
|
4
|
+
status: 'login' | 'setup';
|
|
5
|
+
onSubmit: (email: string, pass: string, name?: string, passConfirm?: string) => Promise<void>;
|
|
6
|
+
error: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const AuthScreen: React.FC<AuthScreenProps> = ({ status, onSubmit, error }) => {
|
|
10
|
+
const [name, setName] = useState('');
|
|
11
|
+
const [email, setEmail] = useState('');
|
|
12
|
+
const [pass, setPass] = useState('');
|
|
13
|
+
const [passConfirm, setPassConfirm] = useState('');
|
|
14
|
+
|
|
15
|
+
const handleSubmit = () => {
|
|
16
|
+
onSubmit(email, pass, name, passConfirm);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="fixed inset-0 z-[100] bg-[#020202] flex items-center justify-center">
|
|
21
|
+
<div className="absolute inset-0 opacity-[0.03] pointer-events-none"
|
|
22
|
+
style={{ backgroundImage: 'radial-gradient(#fff 1px, transparent 1px)', backgroundSize: '40px 40px' }} />
|
|
23
|
+
<div className="w-[400px] glass-card p-10 rounded-[48px] space-y-8 relative">
|
|
24
|
+
<div className="text-center space-y-2">
|
|
25
|
+
<h1 className="text-2xl font-bold tracking-tighter text-white uppercase">Doppelganger</h1>
|
|
26
|
+
<p className="text-[10px] font-bold text-gray-500 uppercase tracking-[0.3em]">
|
|
27
|
+
{status === 'setup' ? 'Initializing System' : 'Access Restricted'}
|
|
28
|
+
</p>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div className="space-y-4">
|
|
32
|
+
{status === 'setup' && (
|
|
33
|
+
<div className="space-y-2">
|
|
34
|
+
<label className="text-[9px] font-bold text-gray-400 uppercase tracking-[0.2em]">Name</label>
|
|
35
|
+
<input
|
|
36
|
+
type="text"
|
|
37
|
+
value={name}
|
|
38
|
+
onChange={(e) => setName(e.target.value)}
|
|
39
|
+
placeholder="Full Name"
|
|
40
|
+
className="w-full bg-white/[0.05] border border-white/10 rounded-2xl px-5 py-4 text-sm focus:outline-none focus:border-white/30 transition-all placeholder:text-gray-600"
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
)}
|
|
44
|
+
<div className="space-y-2">
|
|
45
|
+
<label className="text-[9px] font-bold text-gray-400 uppercase tracking-[0.2em]">Email</label>
|
|
46
|
+
<input
|
|
47
|
+
type="email"
|
|
48
|
+
value={email}
|
|
49
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
50
|
+
placeholder="user@example.com"
|
|
51
|
+
className="w-full bg-white/[0.05] border border-white/10 rounded-2xl px-5 py-4 text-sm focus:outline-none focus:border-white/30 transition-all placeholder:text-gray-600"
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
<div className="space-y-2">
|
|
55
|
+
<label className="text-[9px] font-bold text-gray-400 uppercase tracking-[0.2em]">Password</label>
|
|
56
|
+
<input
|
|
57
|
+
type="password"
|
|
58
|
+
value={pass}
|
|
59
|
+
onChange={(e) => setPass(e.target.value)}
|
|
60
|
+
placeholder="••••••••"
|
|
61
|
+
className="w-full bg-white/[0.05] border border-white/10 rounded-2xl px-5 py-4 text-sm focus:outline-none focus:border-white/30 transition-all placeholder:text-gray-600"
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
{status === 'setup' && (
|
|
65
|
+
<div className="space-y-2">
|
|
66
|
+
<label className="text-[9px] font-bold text-gray-400 uppercase tracking-[0.2em]">Confirm Password</label>
|
|
67
|
+
<input
|
|
68
|
+
type="password"
|
|
69
|
+
value={passConfirm}
|
|
70
|
+
onChange={(e) => setPassConfirm(e.target.value)}
|
|
71
|
+
placeholder="••••••••"
|
|
72
|
+
className="w-full bg-white/[0.05] border border-white/10 rounded-2xl px-5 py-4 text-sm focus:outline-none focus:border-white/30 transition-all placeholder:text-gray-600"
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<button
|
|
79
|
+
onClick={handleSubmit}
|
|
80
|
+
className="shine-effect w-full bg-white text-black py-4 rounded-2xl font-bold text-[10px] tracking-[0.3em] uppercase hover:scale-[1.02] active:scale-[0.98] transition-all"
|
|
81
|
+
>
|
|
82
|
+
{status === 'setup' ? 'Create Account' : 'Authenticate'}
|
|
83
|
+
</button>
|
|
84
|
+
|
|
85
|
+
{error && (
|
|
86
|
+
<div className="text-[9px] font-bold text-red-500 text-center uppercase tracking-widest">
|
|
87
|
+
{error}
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export default AuthScreen;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { highlightCode, SyntaxLanguage } from '../utils/syntaxHighlight';
|
|
3
|
+
|
|
4
|
+
interface CodeEditorProps {
|
|
5
|
+
value: string;
|
|
6
|
+
onChange?: (val: string) => void;
|
|
7
|
+
language: SyntaxLanguage;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
readOnly?: boolean;
|
|
11
|
+
variables?: Record<string, any>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const CodeEditor: React.FC<CodeEditorProps> = ({ value, onChange, language, placeholder, className, readOnly, variables }) => {
|
|
15
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
16
|
+
const preRef = useRef<HTMLPreElement>(null);
|
|
17
|
+
|
|
18
|
+
const displayValue = value || placeholder || '';
|
|
19
|
+
const isPlaceholder = !value && !!placeholder;
|
|
20
|
+
const highlighted = useMemo(() => highlightCode(displayValue, language, variables), [displayValue, language, variables]);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const textarea = textareaRef.current;
|
|
24
|
+
const pre = preRef.current;
|
|
25
|
+
if (!textarea || !pre) return;
|
|
26
|
+
const syncScroll = () => {
|
|
27
|
+
pre.scrollTop = textarea.scrollTop;
|
|
28
|
+
pre.scrollLeft = textarea.scrollLeft;
|
|
29
|
+
};
|
|
30
|
+
textarea.addEventListener('scroll', syncScroll);
|
|
31
|
+
return () => {
|
|
32
|
+
textarea.removeEventListener('scroll', syncScroll);
|
|
33
|
+
};
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
className={`code-editor ${className || ''}`}
|
|
39
|
+
onWheel={(event) => {
|
|
40
|
+
const textarea = textareaRef.current;
|
|
41
|
+
if (!textarea) return;
|
|
42
|
+
if (textarea.scrollHeight <= textarea.clientHeight) return;
|
|
43
|
+
textarea.scrollTop += event.deltaY;
|
|
44
|
+
textarea.scrollLeft += event.deltaX;
|
|
45
|
+
textarea.focus();
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
<pre
|
|
50
|
+
ref={preRef}
|
|
51
|
+
className={`code-editor-pre ${isPlaceholder ? 'code-editor-placeholder' : ''}`}
|
|
52
|
+
aria-hidden
|
|
53
|
+
dangerouslySetInnerHTML={{ __html: highlighted }}
|
|
54
|
+
/>
|
|
55
|
+
<textarea
|
|
56
|
+
ref={textareaRef}
|
|
57
|
+
value={value}
|
|
58
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
59
|
+
spellCheck={false}
|
|
60
|
+
wrap="off"
|
|
61
|
+
readOnly={readOnly}
|
|
62
|
+
className={`code-editor-textarea ${readOnly ? 'code-editor-textarea-readonly' : ''}`}
|
|
63
|
+
aria-label="Code editor"
|
|
64
|
+
tabIndex={readOnly ? -1 : 0}
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default CodeEditor;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { X, Globe, Download, Upload } from 'lucide-react';
|
|
2
|
+
import { useRef } from 'react';
|
|
3
|
+
import { Task } from '../types';
|
|
4
|
+
|
|
5
|
+
interface DashboardScreenProps {
|
|
6
|
+
tasks: Task[];
|
|
7
|
+
onNewTask: () => void;
|
|
8
|
+
onEditTask: (task: Task) => void;
|
|
9
|
+
onDeleteTask: (id: string) => void;
|
|
10
|
+
onExportTasks: () => void;
|
|
11
|
+
onImportTasks: (file: File) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DashboardScreen: React.FC<DashboardScreenProps> = ({ tasks, onNewTask, onEditTask, onDeleteTask, onExportTasks, onImportTasks }) => {
|
|
15
|
+
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
16
|
+
const getFavicon = (url: string) => {
|
|
17
|
+
try {
|
|
18
|
+
if (!url) return null;
|
|
19
|
+
const domain = new URL(url).hostname;
|
|
20
|
+
return `https://www.google.com/s2/favicons?domain=${domain}&sz=64`;
|
|
21
|
+
} catch (e) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const handleImportClick = () => {
|
|
27
|
+
fileInputRef.current?.click();
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
31
|
+
const file = event.target.files?.[0];
|
|
32
|
+
if (file) {
|
|
33
|
+
onImportTasks(file);
|
|
34
|
+
}
|
|
35
|
+
event.target.value = '';
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="flex-1 overflow-hidden animate-in fade-in duration-500">
|
|
40
|
+
<div className="h-full flex flex-col px-12 py-12 max-w-7xl mx-auto space-y-12 w-full">
|
|
41
|
+
<div className="flex items-end justify-between">
|
|
42
|
+
<div className="space-y-2">
|
|
43
|
+
<p className="text-[10px] font-bold text-blue-500 uppercase tracking-[0.4em]">Status</p>
|
|
44
|
+
<h2 className="text-4xl font-bold tracking-tighter text-white">Dashboard</h2>
|
|
45
|
+
</div>
|
|
46
|
+
<div className="flex items-center gap-3">
|
|
47
|
+
<button
|
|
48
|
+
onClick={onExportTasks}
|
|
49
|
+
className="px-4 py-3 rounded-2xl border border-white/10 text-white text-[9px] font-bold uppercase tracking-[0.3em] hover:bg-white/5 transition-all"
|
|
50
|
+
>
|
|
51
|
+
<Download className="w-4 h-4 inline-block mr-2" />
|
|
52
|
+
Export
|
|
53
|
+
</button>
|
|
54
|
+
<button
|
|
55
|
+
onClick={handleImportClick}
|
|
56
|
+
className="px-4 py-3 rounded-2xl border border-white/10 text-white text-[9px] font-bold uppercase tracking-[0.3em] hover:bg-white/5 transition-all"
|
|
57
|
+
>
|
|
58
|
+
<Upload className="w-4 h-4 inline-block mr-2" />
|
|
59
|
+
Import
|
|
60
|
+
</button>
|
|
61
|
+
<button
|
|
62
|
+
onClick={onNewTask}
|
|
63
|
+
className="shine-effect bg-white text-black px-8 py-3 rounded-2xl font-bold text-[10px] tracking-[0.2em] uppercase transition-all hover:scale-105 active:scale-95"
|
|
64
|
+
>
|
|
65
|
+
+ New Task
|
|
66
|
+
</button>
|
|
67
|
+
<input
|
|
68
|
+
ref={fileInputRef}
|
|
69
|
+
type="file"
|
|
70
|
+
accept="application/json"
|
|
71
|
+
className="hidden"
|
|
72
|
+
onChange={handleFileChange}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 overflow-y-auto custom-scrollbar pb-12 pr-4">
|
|
78
|
+
{tasks.map(task => {
|
|
79
|
+
const favicon = getFavicon(task.url);
|
|
80
|
+
return (
|
|
81
|
+
<div key={task.id} className="glass-card p-8 rounded-[40px] flex flex-col gap-6 group hover:-translate-y-1">
|
|
82
|
+
<div className="flex justify-between items-start">
|
|
83
|
+
<div className="w-12 h-12 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center overflow-hidden">
|
|
84
|
+
{favicon ? (
|
|
85
|
+
<img
|
|
86
|
+
src={favicon}
|
|
87
|
+
alt=""
|
|
88
|
+
className="w-6 h-6 object-contain"
|
|
89
|
+
onError={(e) => {
|
|
90
|
+
(e.target as HTMLImageElement).style.display = 'none';
|
|
91
|
+
}}
|
|
92
|
+
/>
|
|
93
|
+
) : (
|
|
94
|
+
<Globe className="w-5 h-5 text-gray-500" />
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
<div className="px-3 py-1 rounded-full bg-white/5 text-[7px] font-bold uppercase tracking-widest text-gray-500">{task.mode}</div>
|
|
98
|
+
</div>
|
|
99
|
+
<div>
|
|
100
|
+
<h3 className="text-lg font-bold text-white truncate">{task.name || 'Untitled'}</h3>
|
|
101
|
+
<p className="text-[10px] text-gray-600 font-mono truncate mt-1">{task.url || 'Target undefined'}</p>
|
|
102
|
+
</div>
|
|
103
|
+
<div className="flex gap-3 pt-4 border-t border-white/5">
|
|
104
|
+
<button
|
|
105
|
+
onClick={() => onEditTask(task)}
|
|
106
|
+
className="flex-1 py-2 rounded-xl bg-white text-black text-[9px] font-bold uppercase tracking-widest hover:scale-105 transition-all"
|
|
107
|
+
>
|
|
108
|
+
Edit Task
|
|
109
|
+
</button>
|
|
110
|
+
<button
|
|
111
|
+
onClick={() => onDeleteTask(task.id!)}
|
|
112
|
+
className="w-10 h-10 rounded-xl bg-white/5 border border-white/10 flex items-center justify-center hover:bg-red-500/10 hover:text-red-500 transition-all"
|
|
113
|
+
>
|
|
114
|
+
<X className="w-4 h-4" />
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
})}
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{tasks.length === 0 && (
|
|
123
|
+
<div className="flex-1 flex flex-col items-center justify-center space-y-6 opacity-20">
|
|
124
|
+
<div className="w-20 h-20 border-2 border-dashed border-white rounded-[40px] flex items-center justify-center text-3xl">🚀</div>
|
|
125
|
+
<p className="text-[10px] font-bold uppercase tracking-[0.3em]">No Tasks Found</p>
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export default DashboardScreen;
|