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,97 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { CategoriesManager } from './CategoriesManager';
|
|
5
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
6
|
+
|
|
7
|
+
const mockFetch = vi.fn();
|
|
8
|
+
global.fetch = mockFetch;
|
|
9
|
+
|
|
10
|
+
describe('CategoriesManager', () => {
|
|
11
|
+
let queryClient: QueryClient;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
queryClient = new QueryClient({
|
|
15
|
+
defaultOptions: {
|
|
16
|
+
queries: { staleTime: 0 },
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
vi.clearAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should load and display category configurations correctly', async () => {
|
|
23
|
+
mockFetch.mockResolvedValueOnce({
|
|
24
|
+
json: async () => ({
|
|
25
|
+
agents: {},
|
|
26
|
+
categories: {
|
|
27
|
+
coding: { model: 'claude', variant: 'high' },
|
|
28
|
+
writing: { model: 'gpt-4', variant: 'max' },
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
ok: true,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
render(
|
|
35
|
+
<QueryClientProvider client={queryClient}>
|
|
36
|
+
<CategoriesManager />
|
|
37
|
+
</QueryClientProvider>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
await waitFor(() => {
|
|
41
|
+
expect(screen.getByText('Ultrabrain')).toBeInTheDocument();
|
|
42
|
+
expect(screen.getByText('Visual Engineering')).toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should save category correctly after editing', async () => {
|
|
47
|
+
const user = userEvent.setup();
|
|
48
|
+
|
|
49
|
+
mockFetch
|
|
50
|
+
.mockResolvedValueOnce({
|
|
51
|
+
json: async () => ({
|
|
52
|
+
agents: {},
|
|
53
|
+
categories: { ultrabrain: { model: 'claude' } },
|
|
54
|
+
}),
|
|
55
|
+
ok: true,
|
|
56
|
+
})
|
|
57
|
+
.mockResolvedValueOnce({
|
|
58
|
+
json: async () => ({
|
|
59
|
+
models: ['anthropic/claude-3-5-sonnet', 'openai/gpt-4'],
|
|
60
|
+
source: 'test'
|
|
61
|
+
}),
|
|
62
|
+
ok: true,
|
|
63
|
+
})
|
|
64
|
+
.mockResolvedValueOnce({ json: async () => ({ success: true }), ok: true })
|
|
65
|
+
.mockResolvedValueOnce({
|
|
66
|
+
json: async () => ({
|
|
67
|
+
agents: {},
|
|
68
|
+
categories: { ultrabrain: { model: 'anthropic/claude-3-5-sonnet' } },
|
|
69
|
+
}),
|
|
70
|
+
ok: true,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
render(
|
|
74
|
+
<QueryClientProvider client={queryClient}>
|
|
75
|
+
<CategoriesManager />
|
|
76
|
+
</QueryClientProvider>
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
await waitFor(() => {
|
|
80
|
+
expect(screen.getByText('Ultrabrain')).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const editButtons = screen.getAllByRole('button', { name: /edit/i });
|
|
84
|
+
await user.click(editButtons[0]);
|
|
85
|
+
|
|
86
|
+
await waitFor(() => {
|
|
87
|
+
expect(screen.getByText('Category')).toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const saveButton = screen.getByRole('button', { name: /save changes/i });
|
|
91
|
+
await user.click(saveButton);
|
|
92
|
+
|
|
93
|
+
await waitFor(() => {
|
|
94
|
+
expect(screen.getByText(/saved successfully/i)).toBeInTheDocument();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
5
|
+
import { Loader2, AlertCircle, Check } from 'lucide-react';
|
|
6
|
+
import type { CategoryConfig } from '../../../types/opencodeConfig';
|
|
7
|
+
import { CategoriesList } from './CategoriesList';
|
|
8
|
+
import { CategoryConfigForm } from './CategoryConfigForm';
|
|
9
|
+
|
|
10
|
+
interface OpencodeConfigResponse {
|
|
11
|
+
agents: Record<string, unknown>;
|
|
12
|
+
categories: Record<string, CategoryConfig>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface CategoriesManagerProps {
|
|
16
|
+
onSaveSuccess?: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function CategoriesManager({ onSaveSuccess }: CategoriesManagerProps) {
|
|
20
|
+
const queryClient = useQueryClient();
|
|
21
|
+
const [editingCategory, setEditingCategory] = React.useState<string | null>(null);
|
|
22
|
+
const [editingConfig, setEditingConfig] = React.useState<CategoryConfig | undefined>(undefined);
|
|
23
|
+
const [toast, setToast] = React.useState<{
|
|
24
|
+
type: 'success' | 'error';
|
|
25
|
+
message: string;
|
|
26
|
+
} | null>(null);
|
|
27
|
+
|
|
28
|
+
const {
|
|
29
|
+
data: config,
|
|
30
|
+
isLoading,
|
|
31
|
+
isError,
|
|
32
|
+
error,
|
|
33
|
+
} = useQuery<OpencodeConfigResponse>({
|
|
34
|
+
queryKey: ['opencode-config'],
|
|
35
|
+
queryFn: async () => {
|
|
36
|
+
const res = await fetch('/api/opencode-config');
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
throw new Error('Failed to fetch configuration');
|
|
39
|
+
}
|
|
40
|
+
return res.json();
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
React.useEffect(() => {
|
|
45
|
+
if (toast) {
|
|
46
|
+
const timer = setTimeout(() => setToast(null), 3000);
|
|
47
|
+
return () => clearTimeout(timer);
|
|
48
|
+
}
|
|
49
|
+
}, [toast]);
|
|
50
|
+
|
|
51
|
+
const saveMutation = useMutation({
|
|
52
|
+
mutationFn: async (categories: Record<string, CategoryConfig>) => {
|
|
53
|
+
const res = await fetch('/api/opencode-config', {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'Content-Type': 'application/json' },
|
|
56
|
+
body: JSON.stringify({ categories }),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
const errorData = await res.json().catch(() => ({}));
|
|
61
|
+
throw new Error(errorData.error || 'Failed to save categories');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return res.json();
|
|
65
|
+
},
|
|
66
|
+
onSuccess: () => {
|
|
67
|
+
queryClient.invalidateQueries({ queryKey: ['opencode-config'] });
|
|
68
|
+
setToast({ type: 'success', message: 'Categories saved successfully' });
|
|
69
|
+
setEditingCategory(null);
|
|
70
|
+
setEditingConfig(undefined);
|
|
71
|
+
onSaveSuccess?.();
|
|
72
|
+
},
|
|
73
|
+
onError: (err: Error) => {
|
|
74
|
+
setToast({ type: 'error', message: err.message });
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const handleEditCategory = (categoryKey: string, categoryConfig: CategoryConfig) => {
|
|
79
|
+
setEditingCategory(categoryKey);
|
|
80
|
+
setEditingConfig(categoryConfig);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const handleCancelEdit = () => {
|
|
84
|
+
setEditingCategory(null);
|
|
85
|
+
setEditingConfig(undefined);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const handleSaveCategory = (categoryConfig: CategoryConfig) => {
|
|
89
|
+
if (!editingCategory) return;
|
|
90
|
+
|
|
91
|
+
const currentCategories = config?.categories || {};
|
|
92
|
+
const updatedCategories = {
|
|
93
|
+
...currentCategories,
|
|
94
|
+
[editingCategory]: categoryConfig,
|
|
95
|
+
};
|
|
96
|
+
saveMutation.mutate(updatedCategories);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const handleDeleteCategory = (categoryKey: string) => {
|
|
100
|
+
const currentCategories = config?.categories || {};
|
|
101
|
+
const { [categoryKey]: _removed, ...updatedCategories } = currentCategories;
|
|
102
|
+
saveMutation.mutate(updatedCategories);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
if (isLoading) {
|
|
106
|
+
return (
|
|
107
|
+
<div className="flex items-center justify-center py-12">
|
|
108
|
+
<Loader2 className="h-6 w-6 animate-spin text-zinc-400" />
|
|
109
|
+
<span className="ml-2 text-sm text-zinc-500 dark:text-zinc-400">
|
|
110
|
+
Loading categories...
|
|
111
|
+
</span>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (isError) {
|
|
117
|
+
return (
|
|
118
|
+
<div className="rounded-lg border border-red-200 bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/20">
|
|
119
|
+
<div className="flex items-center gap-2">
|
|
120
|
+
<AlertCircle className="h-5 w-5 text-red-600 dark:text-red-400" />
|
|
121
|
+
<div>
|
|
122
|
+
<p className="text-sm font-medium text-red-800 dark:text-red-300">
|
|
123
|
+
Failed to load categories
|
|
124
|
+
</p>
|
|
125
|
+
<p className="text-xs text-red-600 dark:text-red-400">
|
|
126
|
+
{error instanceof Error ? error.message : 'An unknown error occurred'}
|
|
127
|
+
</p>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const categories = config?.categories || {};
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div className="space-y-4">
|
|
138
|
+
{toast && (
|
|
139
|
+
<div
|
|
140
|
+
role="alert"
|
|
141
|
+
className={`flex items-center gap-2 rounded-lg px-4 py-3 text-sm ${
|
|
142
|
+
toast.type === 'success'
|
|
143
|
+
? 'bg-emerald-50 text-emerald-800 dark:bg-emerald-900/20 dark:text-emerald-300'
|
|
144
|
+
: 'bg-red-50 text-red-800 dark:bg-red-900/20 dark:text-red-300'
|
|
145
|
+
}`}
|
|
146
|
+
>
|
|
147
|
+
{toast.type === 'success' ? (
|
|
148
|
+
<Check className="h-4 w-4 shrink-0" aria-hidden="true" />
|
|
149
|
+
) : (
|
|
150
|
+
<AlertCircle className="h-4 w-4 shrink-0" aria-hidden="true" />
|
|
151
|
+
)}
|
|
152
|
+
<span>{toast.message}</span>
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
{editingCategory ? (
|
|
157
|
+
<CategoryConfigForm
|
|
158
|
+
categoryName={editingCategory}
|
|
159
|
+
initialConfig={editingConfig}
|
|
160
|
+
onSave={handleSaveCategory}
|
|
161
|
+
onCancel={handleCancelEdit}
|
|
162
|
+
/>
|
|
163
|
+
) : (
|
|
164
|
+
<CategoriesList
|
|
165
|
+
categories={categories}
|
|
166
|
+
onEdit={handleEditCategory}
|
|
167
|
+
onDelete={handleDeleteCategory}
|
|
168
|
+
/>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export default CategoriesManager;
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { useForm, Controller } from 'react-hook-form';
|
|
5
|
+
import { Check, AlertCircle, Loader2, AlertTriangle } from 'lucide-react';
|
|
6
|
+
import { useQuery } from '@tanstack/react-query';
|
|
7
|
+
import { AgentModelSelector } from '../AgentModelSelector';
|
|
8
|
+
import { CategoryConfig } from '../../../types/opencodeConfig';
|
|
9
|
+
|
|
10
|
+
interface ModelsResponse {
|
|
11
|
+
models: string[];
|
|
12
|
+
source: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface CategoryConfigFormData {
|
|
16
|
+
model: string;
|
|
17
|
+
variant: string;
|
|
18
|
+
temperature: number;
|
|
19
|
+
top_p: number;
|
|
20
|
+
prompt_append: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface CategoryConfigFormProps {
|
|
24
|
+
categoryName: string;
|
|
25
|
+
initialConfig?: CategoryConfig;
|
|
26
|
+
onSave: (data: CategoryConfig) => void;
|
|
27
|
+
onCancel: () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function CategoryConfigForm({
|
|
31
|
+
categoryName,
|
|
32
|
+
initialConfig,
|
|
33
|
+
onSave,
|
|
34
|
+
onCancel,
|
|
35
|
+
}: CategoryConfigFormProps) {
|
|
36
|
+
const [toast, setToast] = React.useState<{
|
|
37
|
+
type: 'success' | 'error';
|
|
38
|
+
message: string;
|
|
39
|
+
} | null>(null);
|
|
40
|
+
|
|
41
|
+
const {
|
|
42
|
+
control,
|
|
43
|
+
handleSubmit,
|
|
44
|
+
watch,
|
|
45
|
+
formState: { isSubmitting },
|
|
46
|
+
} = useForm<CategoryConfigFormData>({
|
|
47
|
+
defaultValues: {
|
|
48
|
+
model: initialConfig?.model || '',
|
|
49
|
+
variant: initialConfig?.variant || '',
|
|
50
|
+
temperature: initialConfig?.temperature ?? 0.7,
|
|
51
|
+
top_p: initialConfig?.top_p ?? 1,
|
|
52
|
+
prompt_append: initialConfig?.prompt_append || '',
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const { data: modelsData } = useQuery<ModelsResponse>({
|
|
57
|
+
queryKey: ['opencode-models'],
|
|
58
|
+
queryFn: async () => {
|
|
59
|
+
const res = await fetch('/api/opencode-models');
|
|
60
|
+
if (!res.ok) throw new Error('Failed to fetch models');
|
|
61
|
+
return res.json();
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const availableModels = React.useMemo(
|
|
66
|
+
() => new Set(modelsData?.models ?? []),
|
|
67
|
+
[modelsData]
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const watchedModel = watch('model');
|
|
71
|
+
const isModelInvalid = watchedModel && availableModels.size > 0 && !availableModels.has(watchedModel);
|
|
72
|
+
const isModelMissing = !watchedModel;
|
|
73
|
+
|
|
74
|
+
React.useEffect(() => {
|
|
75
|
+
if (toast) {
|
|
76
|
+
const timer = setTimeout(() => setToast(null), 3000);
|
|
77
|
+
return () => clearTimeout(timer);
|
|
78
|
+
}
|
|
79
|
+
}, [toast]);
|
|
80
|
+
|
|
81
|
+
const onSubmit = (data: CategoryConfigFormData) => {
|
|
82
|
+
const temperature = Math.max(0, Math.min(2, data.temperature));
|
|
83
|
+
const top_p = Math.max(0, Math.min(1, data.top_p));
|
|
84
|
+
|
|
85
|
+
const config: CategoryConfig = {
|
|
86
|
+
model: data.model || undefined,
|
|
87
|
+
variant: data.variant || undefined,
|
|
88
|
+
temperature,
|
|
89
|
+
top_p,
|
|
90
|
+
prompt_append: data.prompt_append || undefined,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
onSave(config);
|
|
94
|
+
setToast({ type: 'success', message: 'Configuration saved successfully' });
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6" aria-label="Category configuration form">
|
|
99
|
+
{toast && (
|
|
100
|
+
<div
|
|
101
|
+
role="alert"
|
|
102
|
+
className={`flex items-center gap-2 rounded-lg px-4 py-3 text-sm ${
|
|
103
|
+
toast.type === 'success'
|
|
104
|
+
? 'bg-emerald-50 text-emerald-800 dark:bg-emerald-900/20 dark:text-emerald-300'
|
|
105
|
+
: 'bg-red-50 text-red-800 dark:bg-red-900/20 dark:text-red-300'
|
|
106
|
+
}`}
|
|
107
|
+
>
|
|
108
|
+
{toast.type === 'success' ? (
|
|
109
|
+
<Check className="h-4 w-4 shrink-0" aria-hidden="true" />
|
|
110
|
+
) : (
|
|
111
|
+
<AlertCircle className="h-4 w-4 shrink-0" aria-hidden="true" />
|
|
112
|
+
)}
|
|
113
|
+
<span>{toast.message}</span>
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
<div className="space-y-2">
|
|
118
|
+
<label htmlFor="category-name" className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
119
|
+
Category
|
|
120
|
+
</label>
|
|
121
|
+
<input
|
|
122
|
+
id="category-name"
|
|
123
|
+
type="text"
|
|
124
|
+
value={categoryName}
|
|
125
|
+
disabled
|
|
126
|
+
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"
|
|
127
|
+
/>
|
|
128
|
+
<p className="text-xs text-zinc-500 dark:text-zinc-400">
|
|
129
|
+
The category identifier for this configuration.
|
|
130
|
+
</p>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{isModelMissing && (
|
|
134
|
+
<div className="flex items-start gap-2 rounded-lg border border-zinc-200 bg-zinc-50 px-4 py-3 dark:border-zinc-800 dark:bg-zinc-900/20">
|
|
135
|
+
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-zinc-500 dark:text-zinc-400" />
|
|
136
|
+
<p className="text-sm text-zinc-700 dark:text-zinc-300">
|
|
137
|
+
<span className="font-medium">System default fallback</span> — this category has no specific model assigned. It will fall back to the global default agent model.
|
|
138
|
+
</p>
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
{isModelInvalid && (
|
|
143
|
+
<div className="flex items-start gap-2 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 dark:border-amber-800 dark:bg-amber-900/20">
|
|
144
|
+
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-500 dark:text-amber-400" />
|
|
145
|
+
<p className="text-sm text-amber-700 dark:text-amber-300">
|
|
146
|
+
<span className="font-medium">Model unavailable</span> — <code className="rounded bg-amber-100 px-1 py-0.5 text-xs dark:bg-amber-800">{watchedModel}</code> is missing from current providers. Please check your provider settings.
|
|
147
|
+
</p>
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
|
|
151
|
+
<div className="space-y-2">
|
|
152
|
+
<label htmlFor="model-selector" className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
153
|
+
Model
|
|
154
|
+
</label>
|
|
155
|
+
<Controller
|
|
156
|
+
name="model"
|
|
157
|
+
control={control}
|
|
158
|
+
render={({ field }) => (
|
|
159
|
+
<div id="model-selector">
|
|
160
|
+
<AgentModelSelector
|
|
161
|
+
value={field.value}
|
|
162
|
+
onValueChange={field.onChange}
|
|
163
|
+
placeholder="Select a model..."
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
/>
|
|
168
|
+
<p className="text-xs text-zinc-500 dark:text-zinc-400">
|
|
169
|
+
The AI model identifier for this category.
|
|
170
|
+
</p>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div className="space-y-2">
|
|
174
|
+
<label htmlFor="variant-selector" className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
175
|
+
Variant
|
|
176
|
+
</label>
|
|
177
|
+
<Controller
|
|
178
|
+
name="variant"
|
|
179
|
+
control={control}
|
|
180
|
+
render={({ field }) => (
|
|
181
|
+
<select
|
|
182
|
+
id="variant-selector"
|
|
183
|
+
value={field.value}
|
|
184
|
+
onChange={(e) => field.onChange(e.target.value)}
|
|
185
|
+
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"
|
|
186
|
+
>
|
|
187
|
+
<option value="">Not set</option>
|
|
188
|
+
<option value="max">max</option>
|
|
189
|
+
<option value="high">high</option>
|
|
190
|
+
<option value="medium">medium</option>
|
|
191
|
+
<option value="low">low</option>
|
|
192
|
+
<option value="xhigh">xhigh</option>
|
|
193
|
+
</select>
|
|
194
|
+
)}
|
|
195
|
+
/>
|
|
196
|
+
<p className="text-xs text-zinc-500 dark:text-zinc-400">
|
|
197
|
+
Model reasoning variant. Higher values mean more thinking.
|
|
198
|
+
</p>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<div className="space-y-3">
|
|
202
|
+
<div className="flex items-center justify-between">
|
|
203
|
+
<label htmlFor="temperature-slider" className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
204
|
+
Temperature
|
|
205
|
+
</label>
|
|
206
|
+
<Controller
|
|
207
|
+
name="temperature"
|
|
208
|
+
control={control}
|
|
209
|
+
rules={{
|
|
210
|
+
min: { value: 0, message: 'Minimum is 0' },
|
|
211
|
+
max: { value: 2, message: 'Maximum is 2' },
|
|
212
|
+
}}
|
|
213
|
+
render={({ field }) => (
|
|
214
|
+
<span className="rounded-md bg-zinc-100 px-2 py-0.5 text-xs font-medium text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
|
|
215
|
+
{field.value.toFixed(1)}
|
|
216
|
+
</span>
|
|
217
|
+
)}
|
|
218
|
+
/>
|
|
219
|
+
</div>
|
|
220
|
+
<Controller
|
|
221
|
+
name="temperature"
|
|
222
|
+
control={control}
|
|
223
|
+
rules={{
|
|
224
|
+
min: { value: 0, message: 'Minimum is 0' },
|
|
225
|
+
max: { value: 2, message: 'Maximum is 2' },
|
|
226
|
+
}}
|
|
227
|
+
render={({ field, fieldState }) => (
|
|
228
|
+
<>
|
|
229
|
+
<div className="flex items-center gap-3">
|
|
230
|
+
<input
|
|
231
|
+
id="temperature-slider"
|
|
232
|
+
type="range"
|
|
233
|
+
min={0}
|
|
234
|
+
max={2}
|
|
235
|
+
step={0.1}
|
|
236
|
+
value={field.value}
|
|
237
|
+
onChange={(e) => field.onChange(parseFloat(e.target.value))}
|
|
238
|
+
className="flex-1 h-2 cursor-pointer appearance-none rounded-lg bg-zinc-200 accent-blue-600 dark:bg-zinc-700"
|
|
239
|
+
aria-label="Temperature slider"
|
|
240
|
+
/>
|
|
241
|
+
<input
|
|
242
|
+
type="number"
|
|
243
|
+
min={0}
|
|
244
|
+
max={2}
|
|
245
|
+
step={0.1}
|
|
246
|
+
value={field.value}
|
|
247
|
+
onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
|
|
248
|
+
className="w-16 rounded-lg border border-zinc-200 bg-white px-2 py-1 text-center text-sm dark:border-zinc-800 dark:bg-zinc-950"
|
|
249
|
+
aria-label="Temperature value"
|
|
250
|
+
/>
|
|
251
|
+
</div>
|
|
252
|
+
{fieldState.error && (
|
|
253
|
+
<p className="text-xs text-red-600 dark:text-red-400" role="alert">
|
|
254
|
+
{fieldState.error.message}
|
|
255
|
+
</p>
|
|
256
|
+
)}
|
|
257
|
+
</>
|
|
258
|
+
)}
|
|
259
|
+
/>
|
|
260
|
+
<div className="flex justify-between text-xs text-zinc-500 dark:text-zinc-400">
|
|
261
|
+
<span>Precise (0)</span>
|
|
262
|
+
<span>Balanced (1)</span>
|
|
263
|
+
<span>Creative (2)</span>
|
|
264
|
+
</div>
|
|
265
|
+
<p className="text-xs text-zinc-500 dark:text-zinc-400">
|
|
266
|
+
Controls randomness: lower values make responses more deterministic.
|
|
267
|
+
</p>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<div className="space-y-3">
|
|
271
|
+
<div className="flex items-center justify-between">
|
|
272
|
+
<label htmlFor="top-p-slider" className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
273
|
+
Top P
|
|
274
|
+
</label>
|
|
275
|
+
<Controller
|
|
276
|
+
name="top_p"
|
|
277
|
+
control={control}
|
|
278
|
+
rules={{
|
|
279
|
+
min: { value: 0, message: 'Minimum is 0' },
|
|
280
|
+
max: { value: 1, message: 'Maximum is 1' },
|
|
281
|
+
}}
|
|
282
|
+
render={({ field }) => (
|
|
283
|
+
<span className="rounded-md bg-zinc-100 px-2 py-0.5 text-xs font-medium text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
|
|
284
|
+
{field.value.toFixed(2)}
|
|
285
|
+
</span>
|
|
286
|
+
)}
|
|
287
|
+
/>
|
|
288
|
+
</div>
|
|
289
|
+
<Controller
|
|
290
|
+
name="top_p"
|
|
291
|
+
control={control}
|
|
292
|
+
rules={{
|
|
293
|
+
min: { value: 0, message: 'Minimum is 0' },
|
|
294
|
+
max: { value: 1, message: 'Maximum is 1' },
|
|
295
|
+
}}
|
|
296
|
+
render={({ field, fieldState }) => (
|
|
297
|
+
<>
|
|
298
|
+
<div className="flex items-center gap-3">
|
|
299
|
+
<input
|
|
300
|
+
id="top-p-slider"
|
|
301
|
+
type="range"
|
|
302
|
+
min={0}
|
|
303
|
+
max={1}
|
|
304
|
+
step={0.05}
|
|
305
|
+
value={field.value}
|
|
306
|
+
onChange={(e) => field.onChange(parseFloat(e.target.value))}
|
|
307
|
+
className="flex-1 h-2 cursor-pointer appearance-none rounded-lg bg-zinc-200 accent-blue-600 dark:bg-zinc-700"
|
|
308
|
+
aria-label="Top P slider"
|
|
309
|
+
/>
|
|
310
|
+
<input
|
|
311
|
+
type="number"
|
|
312
|
+
min={0}
|
|
313
|
+
max={1}
|
|
314
|
+
step={0.05}
|
|
315
|
+
value={field.value}
|
|
316
|
+
onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
|
|
317
|
+
className="w-16 rounded-lg border border-zinc-200 bg-white px-2 py-1 text-center text-sm dark:border-zinc-800 dark:bg-zinc-950"
|
|
318
|
+
aria-label="Top P value"
|
|
319
|
+
/>
|
|
320
|
+
</div>
|
|
321
|
+
{fieldState.error && (
|
|
322
|
+
<p className="text-xs text-red-600 dark:text-red-400" role="alert">
|
|
323
|
+
{fieldState.error.message}
|
|
324
|
+
</p>
|
|
325
|
+
)}
|
|
326
|
+
</>
|
|
327
|
+
)}
|
|
328
|
+
/>
|
|
329
|
+
<div className="flex justify-between text-xs text-zinc-500 dark:text-zinc-400">
|
|
330
|
+
<span>Diverse (0)</span>
|
|
331
|
+
<span>Default (1)</span>
|
|
332
|
+
</div>
|
|
333
|
+
<p className="text-xs text-zinc-500 dark:text-zinc-400">
|
|
334
|
+
Controls nucleus sampling: lower values sample from more likely tokens.
|
|
335
|
+
</p>
|
|
336
|
+
</div>
|
|
337
|
+
|
|
338
|
+
<div className="space-y-2">
|
|
339
|
+
<label htmlFor="prompt-append" className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
340
|
+
Prompt Append
|
|
341
|
+
</label>
|
|
342
|
+
<Controller
|
|
343
|
+
name="prompt_append"
|
|
344
|
+
control={control}
|
|
345
|
+
render={({ field }) => (
|
|
346
|
+
<textarea
|
|
347
|
+
id="prompt-append"
|
|
348
|
+
value={field.value}
|
|
349
|
+
onChange={(e) => field.onChange(e.target.value)}
|
|
350
|
+
rows={4}
|
|
351
|
+
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"
|
|
352
|
+
placeholder="Additional system instructions to append..."
|
|
353
|
+
/>
|
|
354
|
+
)}
|
|
355
|
+
/>
|
|
356
|
+
<p className="text-xs text-zinc-500 dark:text-zinc-400">
|
|
357
|
+
Additional instructions appended to the system prompt.
|
|
358
|
+
</p>
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
<div className="flex items-center justify-end gap-3 pt-2">
|
|
362
|
+
<button
|
|
363
|
+
type="button"
|
|
364
|
+
onClick={onCancel}
|
|
365
|
+
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"
|
|
366
|
+
>
|
|
367
|
+
Cancel
|
|
368
|
+
</button>
|
|
369
|
+
<button
|
|
370
|
+
type="submit"
|
|
371
|
+
disabled={isSubmitting}
|
|
372
|
+
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"
|
|
373
|
+
>
|
|
374
|
+
{isSubmitting && (
|
|
375
|
+
<Loader2 className="h-4 w-4 animate-spin" aria-hidden="true" />
|
|
376
|
+
)}
|
|
377
|
+
Save Changes
|
|
378
|
+
</button>
|
|
379
|
+
</div>
|
|
380
|
+
</form>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export default CategoryConfigForm;
|