vibepulse 0.1.0 → 0.1.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/README.md +7 -13
- package/bin/vibepulse.js +1 -0
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/docs/session-status-detection.md +258 -0
- package/next.config.ts +11 -0
- package/package.json +17 -11
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/readme-cover.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/opencode-config/route.ts +304 -0
- package/src/app/api/opencode-config/status/route.ts +31 -0
- package/src/app/api/opencode-events/route.ts +86 -0
- package/src/app/api/opencode-models/route.test.ts +135 -0
- package/src/app/api/opencode-models/route.ts +58 -0
- package/src/app/api/profiles/[id]/apply/route.ts +49 -0
- package/src/app/api/profiles/[id]/route.ts +160 -0
- package/src/app/api/profiles/route.ts +107 -0
- package/src/app/api/sessions/[id]/archive/route.ts +35 -0
- package/src/app/api/sessions/[id]/delete/route.ts +26 -0
- package/src/app/api/sessions/[id]/route.ts +45 -0
- package/src/app/api/sessions/route.ts +596 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +66 -0
- package/src/app/layout.tsx +37 -0
- package/src/app/page.tsx +239 -0
- package/src/components/ErrorBoundary.tsx +72 -0
- package/src/components/KanbanBoard.tsx +442 -0
- package/src/components/LoadingState.tsx +37 -0
- package/src/components/ProjectCard.tsx +382 -0
- package/src/components/QueryProvider.tsx +25 -0
- package/src/components/SessionCard.tsx +291 -0
- package/src/components/SessionList.tsx +60 -0
- package/src/components/opencode-config/AgentConfigForm.test.tsx +66 -0
- package/src/components/opencode-config/AgentConfigForm.tsx +445 -0
- package/src/components/opencode-config/AgentModelSelector.tsx +284 -0
- package/src/components/opencode-config/AgentsConfigPanel.tsx +162 -0
- package/src/components/opencode-config/ConfigButton.tsx +43 -0
- package/src/components/opencode-config/ConfigPanel.tsx +91 -0
- package/src/components/opencode-config/FullscreenConfigPanel.tsx +360 -0
- package/src/components/opencode-config/categories/CategoriesList.tsx +328 -0
- package/src/components/opencode-config/categories/CategoriesManager.test.tsx +97 -0
- package/src/components/opencode-config/categories/CategoriesManager.tsx +174 -0
- package/src/components/opencode-config/categories/CategoryConfigForm.tsx +384 -0
- package/src/components/opencode-config/profiles/ProfileCard.tsx +140 -0
- package/src/components/opencode-config/profiles/ProfileEditor.tsx +446 -0
- package/src/components/opencode-config/profiles/ProfileList.tsx +398 -0
- package/src/components/opencode-config/profiles/ProfileManager.test.tsx +122 -0
- package/src/components/opencode-config/profiles/ProfileManager.tsx +293 -0
- package/src/components/ui/Tabs.tsx +59 -0
- package/src/hooks/useOpencodeSync.ts +378 -0
- package/src/index.ts +2 -0
- package/src/lib/notificationSound.ts +266 -0
- package/src/lib/opencodeConfig.test.ts +81 -0
- package/src/lib/opencodeConfig.ts +48 -0
- package/src/lib/opencodeDiscovery.ts +154 -0
- package/src/lib/profiles/storage.ts +264 -0
- package/src/lib/transform.ts +84 -0
- package/src/test/setup.ts +8 -0
- package/src/types/index.ts +89 -0
- package/src/types/opencodeConfig.ts +133 -0
- package/src/types/testing-library-vitest.d.ts +17 -0
- package/tsconfig.json +34 -0
- package/tsconfig.lib.json +17 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Pencil, Trash2, Check, RefreshCw } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
interface Profile {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
emoji: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
isBuiltIn?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ProfileCardProps {
|
|
14
|
+
profile: Profile;
|
|
15
|
+
isActive: boolean;
|
|
16
|
+
isApplied: boolean;
|
|
17
|
+
onApply: () => void;
|
|
18
|
+
onEdit: () => void;
|
|
19
|
+
onDelete: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ProfileCard({
|
|
23
|
+
profile,
|
|
24
|
+
isActive,
|
|
25
|
+
isApplied,
|
|
26
|
+
onApply,
|
|
27
|
+
onEdit,
|
|
28
|
+
onDelete,
|
|
29
|
+
}: ProfileCardProps) {
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
className={`
|
|
33
|
+
group relative flex flex-col rounded-lg border
|
|
34
|
+
transition-all duration-200
|
|
35
|
+
bg-white border-zinc-200 dark:bg-zinc-900 dark:border-zinc-700
|
|
36
|
+
hover:border-zinc-300 dark:hover:border-zinc-600
|
|
37
|
+
`}
|
|
38
|
+
>
|
|
39
|
+
<div className="flex items-center justify-between p-4">
|
|
40
|
+
<div className="flex items-center gap-3 flex-1 min-w-0">
|
|
41
|
+
<span className="text-2xl" role="img" aria-label={profile.name}>
|
|
42
|
+
{profile.emoji}
|
|
43
|
+
</span>
|
|
44
|
+
|
|
45
|
+
<div className="flex-1 min-w-0">
|
|
46
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
47
|
+
<h4 className="text-sm font-medium text-zinc-900 dark:text-zinc-100 truncate">
|
|
48
|
+
{profile.name}
|
|
49
|
+
</h4>
|
|
50
|
+
|
|
51
|
+
{profile.isBuiltIn && (
|
|
52
|
+
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300">
|
|
53
|
+
Built-in
|
|
54
|
+
</span>
|
|
55
|
+
)}
|
|
56
|
+
|
|
57
|
+
{isActive && (
|
|
58
|
+
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300">
|
|
59
|
+
Active
|
|
60
|
+
</span>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
{isApplied && (
|
|
64
|
+
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-medium bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300">
|
|
65
|
+
<Check className="h-3 w-3" />
|
|
66
|
+
Last Applied
|
|
67
|
+
</span>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{profile.description && (
|
|
72
|
+
<p className="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400 truncate">
|
|
73
|
+
{profile.description}
|
|
74
|
+
</p>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div className="flex items-center gap-2 ml-4">
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
onClick={onApply}
|
|
83
|
+
className={`
|
|
84
|
+
inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium
|
|
85
|
+
transition-colors
|
|
86
|
+
${
|
|
87
|
+
isApplied
|
|
88
|
+
? 'bg-emerald-600 text-white hover:bg-emerald-700 dark:bg-emerald-600 dark:hover:bg-emerald-700'
|
|
89
|
+
: 'bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700'
|
|
90
|
+
}
|
|
91
|
+
`}
|
|
92
|
+
aria-label={isApplied ? `Re-apply ${profile.name} to reset configs` : `Apply ${profile.name}`}
|
|
93
|
+
title={isApplied ? 'Re-apply to reset all configs to this profile' : `Apply ${profile.name}`}
|
|
94
|
+
>
|
|
95
|
+
{isApplied ? (
|
|
96
|
+
<>
|
|
97
|
+
<RefreshCw className="h-3.5 w-3.5" />
|
|
98
|
+
Re-apply
|
|
99
|
+
</>
|
|
100
|
+
) : (
|
|
101
|
+
'Apply'
|
|
102
|
+
)}
|
|
103
|
+
</button>
|
|
104
|
+
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
onClick={onEdit}
|
|
108
|
+
className="p-1.5 rounded-md text-zinc-500 hover:text-zinc-900 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:text-zinc-100 dark:hover:bg-zinc-800 transition-colors"
|
|
109
|
+
aria-label={`Edit ${profile.name}`}
|
|
110
|
+
title={`Edit ${profile.name}`}
|
|
111
|
+
>
|
|
112
|
+
<Pencil className="h-4 w-4" />
|
|
113
|
+
</button>
|
|
114
|
+
|
|
115
|
+
{!profile.isBuiltIn && (
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
onClick={onDelete}
|
|
119
|
+
className="p-1.5 rounded-md text-zinc-500 hover:text-red-600 hover:bg-red-50 dark:text-zinc-400 dark:hover:text-red-400 dark:hover:bg-red-900/20 transition-colors"
|
|
120
|
+
aria-label={`Delete ${profile.name}`}
|
|
121
|
+
title={`Delete ${profile.name}`}
|
|
122
|
+
>
|
|
123
|
+
<Trash2 className="h-4 w-4" />
|
|
124
|
+
</button>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
{isApplied && (
|
|
130
|
+
<div className="border-t border-zinc-100 dark:border-zinc-800 px-4 py-2">
|
|
131
|
+
<p className="text-[11px] text-zinc-400 dark:text-zinc-500">
|
|
132
|
+
💡 Modified configs after applying? Click <strong>Re-apply</strong> to reset back to this profile.
|
|
133
|
+
</p>
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export default ProfileCard;
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { useForm, Controller } from 'react-hook-form';
|
|
5
|
+
import { Check, AlertCircle, Loader2, Upload, ChevronDown, RotateCcw } from 'lucide-react';
|
|
6
|
+
import { Profile, ProfileConfig } from '../../../types/opencodeConfig';
|
|
7
|
+
|
|
8
|
+
interface ProfileFormData {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
emoji: string;
|
|
12
|
+
description: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ProfileEditorProps {
|
|
16
|
+
profile?: Profile;
|
|
17
|
+
initialConfig?: ProfileConfig;
|
|
18
|
+
onSave: (data: { profile: Partial<Profile>; config: ProfileConfig }) => void;
|
|
19
|
+
onCancel: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const COMMON_EMOJIS = [
|
|
23
|
+
'⚡', '🔥', '💎', '🚀', '🎯', '💡', '🔧', '🎨', '📊', '🤖',
|
|
24
|
+
'👾', '💻', '⚙️', '🔍', '✨', '🌟', '🎭', '🎪', '🧩', '🎲',
|
|
25
|
+
'📚', '🔐', '🛠️', '⚡️', '🌊', '🔮', '📡', '🎸', '🏆', '🌈',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export function ProfileEditor({
|
|
29
|
+
profile,
|
|
30
|
+
initialConfig,
|
|
31
|
+
onSave,
|
|
32
|
+
onCancel,
|
|
33
|
+
}: ProfileEditorProps) {
|
|
34
|
+
const isEditing = !!profile;
|
|
35
|
+
const [toast, setToast] = React.useState<{
|
|
36
|
+
type: 'success' | 'error';
|
|
37
|
+
message: string;
|
|
38
|
+
} | null>(null);
|
|
39
|
+
const [config, setConfig] = React.useState<ProfileConfig>(
|
|
40
|
+
initialConfig || { agents: {} }
|
|
41
|
+
);
|
|
42
|
+
// Store original config for reset functionality
|
|
43
|
+
const [originalConfig, setOriginalConfig] = React.useState<ProfileConfig>(
|
|
44
|
+
initialConfig || { agents: {} }
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
React.useEffect(() => {
|
|
48
|
+
if (initialConfig) {
|
|
49
|
+
setConfig(initialConfig);
|
|
50
|
+
setOriginalConfig(initialConfig);
|
|
51
|
+
}
|
|
52
|
+
}, [initialConfig]);
|
|
53
|
+
const [isConfigExpanded, setIsConfigExpanded] = React.useState(false);
|
|
54
|
+
|
|
55
|
+
const {
|
|
56
|
+
control,
|
|
57
|
+
handleSubmit,
|
|
58
|
+
watch,
|
|
59
|
+
formState: { errors, isSubmitting },
|
|
60
|
+
} = useForm<ProfileFormData>({
|
|
61
|
+
defaultValues: {
|
|
62
|
+
id: profile?.id || '',
|
|
63
|
+
name: profile?.name || '',
|
|
64
|
+
emoji: profile?.emoji || '⚡',
|
|
65
|
+
description: profile?.description || '',
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const watchedName = watch('name');
|
|
70
|
+
const watchedEmoji = watch('emoji');
|
|
71
|
+
|
|
72
|
+
React.useEffect(() => {
|
|
73
|
+
if (toast) {
|
|
74
|
+
const timer = setTimeout(() => setToast(null), 3000);
|
|
75
|
+
return () => clearTimeout(timer);
|
|
76
|
+
}
|
|
77
|
+
}, [toast]);
|
|
78
|
+
|
|
79
|
+
const handleImportFromCurrent = async () => {
|
|
80
|
+
try {
|
|
81
|
+
const res = await fetch('/api/opencode-config');
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
throw new Error('Failed to fetch configuration');
|
|
84
|
+
}
|
|
85
|
+
const parsed = await res.json();
|
|
86
|
+
const importedConfig: ProfileConfig = {
|
|
87
|
+
agents: parsed.agents || {},
|
|
88
|
+
categories: parsed.categories,
|
|
89
|
+
};
|
|
90
|
+
setConfig(importedConfig);
|
|
91
|
+
setToast({ type: 'success', message: 'Configuration imported successfully' });
|
|
92
|
+
} catch {
|
|
93
|
+
setToast({ type: 'error', message: 'Failed to import configuration' });
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const handleResetToOriginal = () => {
|
|
98
|
+
setConfig(originalConfig);
|
|
99
|
+
setToast({ type: 'success', message: 'Configuration reset to original values' });
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const hasConfigChanged = React.useMemo(() => {
|
|
103
|
+
return JSON.stringify(config) !== JSON.stringify(originalConfig);
|
|
104
|
+
}, [config, originalConfig]);
|
|
105
|
+
|
|
106
|
+
const onSubmit = (data: ProfileFormData) => {
|
|
107
|
+
const now = new Date().toISOString();
|
|
108
|
+
|
|
109
|
+
const newProfile: Profile = {
|
|
110
|
+
id: isEditing ? profile.id : data.id,
|
|
111
|
+
name: data.name,
|
|
112
|
+
emoji: data.emoji,
|
|
113
|
+
description: data.description || undefined,
|
|
114
|
+
createdAt: isEditing ? profile.createdAt : now,
|
|
115
|
+
updatedAt: now,
|
|
116
|
+
isDefault: isEditing ? profile.isDefault : false,
|
|
117
|
+
isBuiltIn: isEditing ? profile.isBuiltIn : false,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
onSave({ profile: newProfile, config });
|
|
121
|
+
setToast({ type: 'success', message: `Profile ${isEditing ? 'updated' : 'created'} successfully` });
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const agentCount = Object.keys(config.agents || {}).length;
|
|
125
|
+
const categoryCount = Object.keys(config.categories || {}).length;
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6" aria-label="Profile editor form">
|
|
129
|
+
{toast && (
|
|
130
|
+
<div
|
|
131
|
+
role="alert"
|
|
132
|
+
className={`flex items-center gap-2 rounded-lg px-4 py-3 text-sm ${
|
|
133
|
+
toast.type === 'success'
|
|
134
|
+
? 'bg-emerald-50 text-emerald-800 dark:bg-emerald-900/20 dark:text-emerald-300'
|
|
135
|
+
: 'bg-red-50 text-red-800 dark:bg-red-900/20 dark:text-red-300'
|
|
136
|
+
}`}
|
|
137
|
+
>
|
|
138
|
+
{toast.type === 'success' ? (
|
|
139
|
+
<Check className="h-4 w-4 shrink-0" aria-hidden="true" />
|
|
140
|
+
) : (
|
|
141
|
+
<AlertCircle className="h-4 w-4 shrink-0" aria-hidden="true" />
|
|
142
|
+
)}
|
|
143
|
+
<span>{toast.message}</span>
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{!isEditing && (
|
|
148
|
+
<div className="space-y-2">
|
|
149
|
+
<label htmlFor="profile-id" className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
150
|
+
Profile ID
|
|
151
|
+
<span className="ml-1 text-red-500">*</span>
|
|
152
|
+
</label>
|
|
153
|
+
<Controller
|
|
154
|
+
name="id"
|
|
155
|
+
control={control}
|
|
156
|
+
rules={{
|
|
157
|
+
required: 'Profile ID is required',
|
|
158
|
+
pattern: {
|
|
159
|
+
value: /^[a-zA-Z0-9_-]+$/,
|
|
160
|
+
message: 'Only alphanumeric characters, hyphens, and underscores allowed',
|
|
161
|
+
},
|
|
162
|
+
}}
|
|
163
|
+
render={({ field }) => (
|
|
164
|
+
<input
|
|
165
|
+
id="profile-id"
|
|
166
|
+
type="text"
|
|
167
|
+
value={field.value}
|
|
168
|
+
onChange={(e) => field.onChange(e.target.value)}
|
|
169
|
+
placeholder="e.g., my-custom-profile"
|
|
170
|
+
className="w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm dark:border-zinc-800 dark:bg-zinc-950"
|
|
171
|
+
/>
|
|
172
|
+
)}
|
|
173
|
+
/>
|
|
174
|
+
{errors.id && (
|
|
175
|
+
<p className="text-xs text-red-600 dark:text-red-400" role="alert">
|
|
176
|
+
{errors.id.message}
|
|
177
|
+
</p>
|
|
178
|
+
)}
|
|
179
|
+
<p className="text-xs text-zinc-500 dark:text-zinc-400">
|
|
180
|
+
Unique identifier for this profile. Use only letters, numbers, hyphens, and underscores.
|
|
181
|
+
</p>
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
|
|
185
|
+
{isEditing && (
|
|
186
|
+
<div className="space-y-2">
|
|
187
|
+
<label htmlFor="profile-id-readonly" className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
188
|
+
Profile ID
|
|
189
|
+
</label>
|
|
190
|
+
<input
|
|
191
|
+
id="profile-id-readonly"
|
|
192
|
+
type="text"
|
|
193
|
+
value={profile.id}
|
|
194
|
+
disabled
|
|
195
|
+
className="w-full rounded-lg border border-zinc-200 bg-zinc-100 px-3 py-2 text-sm text-zinc-600 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-400"
|
|
196
|
+
/>
|
|
197
|
+
<p className="text-xs text-zinc-500 dark:text-zinc-400">
|
|
198
|
+
Profile ID cannot be changed after creation.
|
|
199
|
+
</p>
|
|
200
|
+
</div>
|
|
201
|
+
)}
|
|
202
|
+
|
|
203
|
+
<div className="space-y-2">
|
|
204
|
+
<label htmlFor="profile-name" className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
205
|
+
Name
|
|
206
|
+
<span className="ml-1 text-red-500">*</span>
|
|
207
|
+
</label>
|
|
208
|
+
<Controller
|
|
209
|
+
name="name"
|
|
210
|
+
control={control}
|
|
211
|
+
rules={{
|
|
212
|
+
required: 'Name is required',
|
|
213
|
+
minLength: {
|
|
214
|
+
value: 1,
|
|
215
|
+
message: 'Name cannot be empty',
|
|
216
|
+
},
|
|
217
|
+
}}
|
|
218
|
+
render={({ field }) => (
|
|
219
|
+
<input
|
|
220
|
+
id="profile-name"
|
|
221
|
+
type="text"
|
|
222
|
+
value={field.value}
|
|
223
|
+
onChange={(e) => field.onChange(e.target.value)}
|
|
224
|
+
placeholder="e.g., My Custom Profile"
|
|
225
|
+
className="w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm dark:border-zinc-800 dark:bg-zinc-950"
|
|
226
|
+
/>
|
|
227
|
+
)}
|
|
228
|
+
/>
|
|
229
|
+
{errors.name && (
|
|
230
|
+
<p className="text-xs text-red-600 dark:text-red-400" role="alert">
|
|
231
|
+
{errors.name.message}
|
|
232
|
+
</p>
|
|
233
|
+
)}
|
|
234
|
+
<p className="text-xs text-zinc-500 dark:text-zinc-400">
|
|
235
|
+
Display name for this profile.
|
|
236
|
+
</p>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<div className="space-y-2">
|
|
240
|
+
<p className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
241
|
+
Emoji Icon
|
|
242
|
+
</p>
|
|
243
|
+
<Controller
|
|
244
|
+
name="emoji"
|
|
245
|
+
control={control}
|
|
246
|
+
render={({ field }) => (
|
|
247
|
+
<div className="flex flex-wrap gap-2">
|
|
248
|
+
{COMMON_EMOJIS.map((emoji) => (
|
|
249
|
+
<button
|
|
250
|
+
key={emoji}
|
|
251
|
+
type="button"
|
|
252
|
+
onClick={() => field.onChange(emoji)}
|
|
253
|
+
className={`h-10 w-10 rounded-lg text-xl transition-all ${
|
|
254
|
+
field.value === emoji
|
|
255
|
+
? 'bg-blue-100 ring-2 ring-blue-500 dark:bg-blue-900/30 dark:ring-blue-400'
|
|
256
|
+
: 'bg-zinc-100 hover:bg-zinc-200 dark:bg-zinc-800 dark:hover:bg-zinc-700'
|
|
257
|
+
}`}
|
|
258
|
+
aria-label={`Select ${emoji} emoji`}
|
|
259
|
+
aria-pressed={field.value === emoji}
|
|
260
|
+
>
|
|
261
|
+
{emoji}
|
|
262
|
+
</button>
|
|
263
|
+
))}
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
/>
|
|
267
|
+
<p className="text-xs text-zinc-500 dark:text-zinc-400">
|
|
268
|
+
Choose an emoji to represent this profile.
|
|
269
|
+
</p>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<div className="space-y-2">
|
|
273
|
+
<label htmlFor="profile-description" className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
274
|
+
Description
|
|
275
|
+
</label>
|
|
276
|
+
<Controller
|
|
277
|
+
name="description"
|
|
278
|
+
control={control}
|
|
279
|
+
render={({ field }) => (
|
|
280
|
+
<textarea
|
|
281
|
+
id="profile-description"
|
|
282
|
+
value={field.value}
|
|
283
|
+
onChange={(e) => field.onChange(e.target.value)}
|
|
284
|
+
rows={3}
|
|
285
|
+
className="w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm resize-none dark:border-zinc-800 dark:bg-zinc-950"
|
|
286
|
+
placeholder="Optional description of this profile..."
|
|
287
|
+
/>
|
|
288
|
+
)}
|
|
289
|
+
/>
|
|
290
|
+
<p className="text-xs text-zinc-500 dark:text-zinc-400">
|
|
291
|
+
Optional description to help identify this profile.
|
|
292
|
+
</p>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
<div className="space-y-2">
|
|
296
|
+
<p className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
297
|
+
Configuration
|
|
298
|
+
</p>
|
|
299
|
+
<div className="flex flex-wrap gap-2">
|
|
300
|
+
<button
|
|
301
|
+
type="button"
|
|
302
|
+
onClick={handleImportFromCurrent}
|
|
303
|
+
className="inline-flex items-center gap-2 rounded-lg border border-zinc-200 bg-white px-4 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800"
|
|
304
|
+
>
|
|
305
|
+
<Upload className="h-4 w-4" aria-hidden="true" />
|
|
306
|
+
Import from Current Config
|
|
307
|
+
</button>
|
|
308
|
+
|
|
309
|
+
{isEditing && hasConfigChanged && (
|
|
310
|
+
<button
|
|
311
|
+
type="button"
|
|
312
|
+
onClick={handleResetToOriginal}
|
|
313
|
+
className="inline-flex items-center gap-2 rounded-lg border border-zinc-200 bg-white px-4 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50 hover:text-amber-600 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800 dark:hover:text-amber-400"
|
|
314
|
+
>
|
|
315
|
+
<RotateCcw className="h-4 w-4" aria-hidden="true" />
|
|
316
|
+
Reset to Original
|
|
317
|
+
</button>
|
|
318
|
+
)}
|
|
319
|
+
</div>
|
|
320
|
+
<p className="text-xs text-zinc-500 dark:text-zinc-400">
|
|
321
|
+
Import agent and category configurations from your current settings, or reset to the profile's original values.
|
|
322
|
+
</p>
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
<div className="rounded-lg border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-700 dark:bg-zinc-900">
|
|
326
|
+
<h4 className="text-sm font-medium text-zinc-900 dark:text-zinc-100 mb-2">
|
|
327
|
+
Profile Preview
|
|
328
|
+
</h4>
|
|
329
|
+
<div className="flex items-center gap-3">
|
|
330
|
+
<span className="text-3xl" aria-hidden="true">
|
|
331
|
+
{watchedEmoji || '⚡'}
|
|
332
|
+
</span>
|
|
333
|
+
<div>
|
|
334
|
+
<p className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
335
|
+
{watchedName || 'Untitled Profile'}
|
|
336
|
+
</p>
|
|
337
|
+
<p className="text-xs text-zinc-500 dark:text-zinc-400">
|
|
338
|
+
{agentCount} agent{agentCount !== 1 ? 's' : ''}, {categoryCount} categor{categoryCount !== 1 ? 'ies' : 'y'} configured
|
|
339
|
+
</p>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
<div className="rounded-lg border border-zinc-200 dark:border-zinc-700 overflow-hidden">
|
|
345
|
+
<button
|
|
346
|
+
type="button"
|
|
347
|
+
onClick={() => setIsConfigExpanded(!isConfigExpanded)}
|
|
348
|
+
className="w-full flex items-center justify-between p-4 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-colors"
|
|
349
|
+
aria-expanded={isConfigExpanded}
|
|
350
|
+
>
|
|
351
|
+
<span className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
352
|
+
Configuration Details
|
|
353
|
+
</span>
|
|
354
|
+
<ChevronDown
|
|
355
|
+
className={`h-4 w-4 text-zinc-500 transition-transform duration-200 ${isConfigExpanded ? 'rotate-180' : ''}`}
|
|
356
|
+
aria-hidden="true"
|
|
357
|
+
/>
|
|
358
|
+
</button>
|
|
359
|
+
{isConfigExpanded && (
|
|
360
|
+
<div className="border-t border-zinc-200 dark:border-zinc-700 p-4 space-y-4">
|
|
361
|
+
<div>
|
|
362
|
+
<h5 className="text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wide mb-2">
|
|
363
|
+
Agents ({agentCount})
|
|
364
|
+
</h5>
|
|
365
|
+
{agentCount === 0 ? (
|
|
366
|
+
<p className="text-sm text-zinc-400 dark:text-zinc-500 italic">
|
|
367
|
+
No agents configured
|
|
368
|
+
</p>
|
|
369
|
+
) : (
|
|
370
|
+
<ul className="space-y-1.5">
|
|
371
|
+
{Object.entries(config.agents || {}).map(([name, agentConfig]) => (
|
|
372
|
+
<li
|
|
373
|
+
key={name}
|
|
374
|
+
className="text-sm text-zinc-700 dark:text-zinc-300 flex items-center gap-2"
|
|
375
|
+
>
|
|
376
|
+
<span className="font-medium text-zinc-900 dark:text-zinc-100 min-w-[80px]">
|
|
377
|
+
{name}
|
|
378
|
+
</span>
|
|
379
|
+
<span className="text-zinc-400">→</span>
|
|
380
|
+
<span className="text-zinc-600 dark:text-zinc-400">
|
|
381
|
+
{agentConfig.model}
|
|
382
|
+
{agentConfig.temperature !== undefined && ` temp: ${agentConfig.temperature}`}
|
|
383
|
+
</span>
|
|
384
|
+
</li>
|
|
385
|
+
))}
|
|
386
|
+
</ul>
|
|
387
|
+
)}
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
<div>
|
|
391
|
+
<h5 className="text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wide mb-2">
|
|
392
|
+
Categories ({categoryCount})
|
|
393
|
+
</h5>
|
|
394
|
+
{categoryCount === 0 ? (
|
|
395
|
+
<p className="text-sm text-zinc-400 dark:text-zinc-500 italic">
|
|
396
|
+
No categories configured
|
|
397
|
+
</p>
|
|
398
|
+
) : (
|
|
399
|
+
<ul className="space-y-1.5">
|
|
400
|
+
{Object.entries(config.categories || {}).map(([name, categoryConfig]) => (
|
|
401
|
+
<li
|
|
402
|
+
key={name}
|
|
403
|
+
className="text-sm text-zinc-700 dark:text-zinc-300 flex items-center gap-2"
|
|
404
|
+
>
|
|
405
|
+
<span className="font-medium text-zinc-900 dark:text-zinc-100 min-w-[80px]">
|
|
406
|
+
{name}
|
|
407
|
+
</span>
|
|
408
|
+
<span className="text-zinc-400">→</span>
|
|
409
|
+
<span className="text-zinc-600 dark:text-zinc-400">
|
|
410
|
+
{categoryConfig.model}
|
|
411
|
+
{categoryConfig.variant && ` (${categoryConfig.variant})`}
|
|
412
|
+
{categoryConfig.temperature !== undefined && ` temp: ${categoryConfig.temperature}`}
|
|
413
|
+
</span>
|
|
414
|
+
</li>
|
|
415
|
+
))}
|
|
416
|
+
</ul>
|
|
417
|
+
)}
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
)}
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
<div className="flex items-center justify-end gap-3 pt-2">
|
|
424
|
+
<button
|
|
425
|
+
type="button"
|
|
426
|
+
onClick={onCancel}
|
|
427
|
+
className="inline-flex items-center gap-2 rounded-lg border border-zinc-200 bg-white px-4 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800"
|
|
428
|
+
>
|
|
429
|
+
Cancel
|
|
430
|
+
</button>
|
|
431
|
+
<button
|
|
432
|
+
type="submit"
|
|
433
|
+
disabled={isSubmitting || !watchedName.trim()}
|
|
434
|
+
className="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-blue-600 dark:hover:bg-blue-500"
|
|
435
|
+
>
|
|
436
|
+
{isSubmitting && (
|
|
437
|
+
<Loader2 className="h-4 w-4 animate-spin" aria-hidden="true" />
|
|
438
|
+
)}
|
|
439
|
+
{isEditing ? 'Save Changes' : 'Create Profile'}
|
|
440
|
+
</button>
|
|
441
|
+
</div>
|
|
442
|
+
</form>
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export default ProfileEditor;
|