@hed-hog/operations 0.0.300 → 0.0.301
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/dist/operations.controller.d.ts +713 -31
- package/dist/operations.controller.d.ts.map +1 -1
- package/dist/operations.controller.js +157 -0
- package/dist/operations.controller.js.map +1 -1
- package/dist/operations.module.d.ts.map +1 -1
- package/dist/operations.module.js +5 -1
- package/dist/operations.module.js.map +1 -1
- package/dist/operations.proposal.subscriber.d.ts +11 -0
- package/dist/operations.proposal.subscriber.d.ts.map +1 -0
- package/dist/operations.proposal.subscriber.js +80 -0
- package/dist/operations.proposal.subscriber.js.map +1 -0
- package/dist/operations.proposal.subscriber.spec.d.ts +2 -0
- package/dist/operations.proposal.subscriber.spec.d.ts.map +1 -0
- package/dist/operations.proposal.subscriber.spec.js +88 -0
- package/dist/operations.proposal.subscriber.spec.js.map +1 -0
- package/dist/operations.service.d.ts +490 -46
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +2442 -119
- package/dist/operations.service.js.map +1 -1
- package/dist/operations.service.spec.d.ts +2 -0
- package/dist/operations.service.spec.d.ts.map +1 -0
- package/dist/operations.service.spec.js +159 -0
- package/dist/operations.service.spec.js.map +1 -0
- package/hedhog/data/menu.yaml +34 -0
- package/hedhog/data/role_route.yaml +39 -0
- package/hedhog/data/route.yaml +130 -0
- package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +8 -6
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +1163 -327
- package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +256 -0
- package/hedhog/frontend/app/_components/contract-content-editor.tsx.ejs +258 -0
- package/hedhog/frontend/app/_components/contract-creation-wizard.tsx.ejs +631 -0
- package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +353 -27
- package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +1926 -87
- package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +526 -0
- package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +247 -0
- package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +3520 -0
- package/hedhog/frontend/app/_components/department-select-with-create.tsx.ejs +370 -0
- package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +826 -0
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +1251 -364
- package/hedhog/frontend/app/_components/section-card.tsx.ejs +48 -13
- package/hedhog/frontend/app/_lib/api.ts.ejs +2 -5
- package/hedhog/frontend/app/_lib/types.ts.ejs +76 -33
- package/hedhog/frontend/app/_lib/utils/format.ts.ejs +85 -8
- package/hedhog/frontend/app/approvals/page.tsx.ejs +90 -54
- package/hedhog/frontend/app/collaborators/[id]/edit/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/collaborators/[id]/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/collaborators/page.tsx.ejs +597 -140
- package/hedhog/frontend/app/contracts/[id]/edit/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/contracts/page.tsx.ejs +941 -262
- package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +384 -0
- package/hedhog/frontend/app/departments/page.tsx.ejs +442 -0
- package/hedhog/frontend/app/page.tsx.ejs +36 -12
- package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/projects/new/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/projects/page.tsx.ejs +264 -102
- package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +50 -28
- package/hedhog/frontend/app/time-off/page.tsx.ejs +57 -31
- package/hedhog/frontend/app/timesheets/page.tsx.ejs +85 -42
- package/hedhog/frontend/messages/en.json +473 -12
- package/hedhog/frontend/messages/pt.json +528 -66
- package/hedhog/table/operations_collaborator.yaml +20 -0
- package/hedhog/table/operations_contract.yaml +22 -1
- package/hedhog/table/operations_contract_document.yaml +33 -16
- package/hedhog/table/operations_contract_template.yaml +58 -0
- package/hedhog/table/operations_department.yaml +24 -0
- package/package.json +6 -4
- package/src/operations.controller.ts +122 -0
- package/src/operations.module.ts +6 -2
- package/src/operations.proposal.subscriber.spec.ts +121 -0
- package/src/operations.proposal.subscriber.ts +86 -0
- package/src/operations.service.spec.ts +210 -0
- package/src/operations.service.ts +3934 -212
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import {
|
|
5
|
+
Command,
|
|
6
|
+
CommandEmpty,
|
|
7
|
+
CommandGroup,
|
|
8
|
+
CommandInput,
|
|
9
|
+
CommandItem,
|
|
10
|
+
CommandList,
|
|
11
|
+
} from '@/components/ui/command';
|
|
12
|
+
import {
|
|
13
|
+
Popover,
|
|
14
|
+
PopoverContent,
|
|
15
|
+
PopoverTrigger,
|
|
16
|
+
} from '@/components/ui/popover';
|
|
17
|
+
import {
|
|
18
|
+
Sheet,
|
|
19
|
+
SheetContent,
|
|
20
|
+
SheetDescription,
|
|
21
|
+
SheetHeader,
|
|
22
|
+
SheetTitle,
|
|
23
|
+
} from '@/components/ui/sheet';
|
|
24
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
25
|
+
import { Check, ChevronsUpDown, Plus, X } from 'lucide-react';
|
|
26
|
+
import { useEffect, useRef, useState } from 'react';
|
|
27
|
+
import type { OperationsCollaborator, OperationsCollaboratorDetails } from '../_lib/types';
|
|
28
|
+
import { fetchOperations } from '../_lib/api';
|
|
29
|
+
import { CollaboratorFormScreen } from './collaborator-form-screen';
|
|
30
|
+
|
|
31
|
+
type CollaboratorSelectWithCreateProps = {
|
|
32
|
+
label: string;
|
|
33
|
+
value?: number | null;
|
|
34
|
+
initialSelectedLabel?: string;
|
|
35
|
+
placeholder: string;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
onChange: (option: OperationsCollaborator | null) => void;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function CollaboratorSelectWithCreate({
|
|
41
|
+
label,
|
|
42
|
+
value,
|
|
43
|
+
initialSelectedLabel = '',
|
|
44
|
+
placeholder,
|
|
45
|
+
disabled = false,
|
|
46
|
+
onChange,
|
|
47
|
+
}: CollaboratorSelectWithCreateProps) {
|
|
48
|
+
const { request, currentLocaleCode } = useApp();
|
|
49
|
+
const [open, setOpen] = useState(false);
|
|
50
|
+
const [search, setSearch] = useState('');
|
|
51
|
+
const [createOpen, setCreateOpen] = useState(false);
|
|
52
|
+
const [selectedLabel, setSelectedLabel] = useState(initialSelectedLabel);
|
|
53
|
+
const parentScrollContainerRef = useRef<HTMLElement | null>(null);
|
|
54
|
+
const parentScrollTopRef = useRef(0);
|
|
55
|
+
|
|
56
|
+
const { data: collaborators = [] } = useQuery<OperationsCollaborator[]>({
|
|
57
|
+
queryKey: ['operations-inline-collaborators', currentLocaleCode],
|
|
58
|
+
queryFn: () =>
|
|
59
|
+
fetchOperations<OperationsCollaborator[]>(request, '/operations/collaborators'),
|
|
60
|
+
placeholderData: (old) => old ?? [],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
setSelectedLabel(initialSelectedLabel);
|
|
65
|
+
}, [initialSelectedLabel]);
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (value == null) {
|
|
69
|
+
setSelectedLabel('');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const option = collaborators.find((item) => item.id === value);
|
|
74
|
+
if (option) {
|
|
75
|
+
setSelectedLabel(option.displayName);
|
|
76
|
+
}
|
|
77
|
+
}, [collaborators, value]);
|
|
78
|
+
|
|
79
|
+
const filteredOptions = collaborators.filter((item) => {
|
|
80
|
+
const haystack = [
|
|
81
|
+
item.displayName,
|
|
82
|
+
item.code,
|
|
83
|
+
item.title,
|
|
84
|
+
item.department,
|
|
85
|
+
]
|
|
86
|
+
.filter(Boolean)
|
|
87
|
+
.join(' ')
|
|
88
|
+
.toLowerCase();
|
|
89
|
+
return haystack.includes(search.trim().toLowerCase());
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const captureParentScrollPosition = (trigger: HTMLElement) => {
|
|
93
|
+
const parentSheetContent = trigger.closest(
|
|
94
|
+
'[data-radix-dialog-content]'
|
|
95
|
+
) as HTMLElement | null;
|
|
96
|
+
|
|
97
|
+
if (!parentSheetContent) {
|
|
98
|
+
parentScrollContainerRef.current = null;
|
|
99
|
+
parentScrollTopRef.current = 0;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
parentScrollContainerRef.current = parentSheetContent;
|
|
104
|
+
parentScrollTopRef.current = parentSheetContent.scrollTop;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const restoreParentScrollPosition = () => {
|
|
108
|
+
const container =
|
|
109
|
+
parentScrollContainerRef.current &&
|
|
110
|
+
document.body.contains(parentScrollContainerRef.current)
|
|
111
|
+
? parentScrollContainerRef.current
|
|
112
|
+
: null;
|
|
113
|
+
|
|
114
|
+
if (!container) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const restore = () => {
|
|
119
|
+
container.scrollTop = parentScrollTopRef.current;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
requestAnimationFrame(restore);
|
|
123
|
+
setTimeout(restore, 0);
|
|
124
|
+
setTimeout(restore, 120);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div className="space-y-1.5">
|
|
129
|
+
<label className="text-xs font-medium text-muted-foreground">
|
|
130
|
+
{label}
|
|
131
|
+
</label>
|
|
132
|
+
|
|
133
|
+
<div className="flex gap-2">
|
|
134
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
135
|
+
<PopoverTrigger asChild>
|
|
136
|
+
<Button
|
|
137
|
+
type="button"
|
|
138
|
+
variant="outline"
|
|
139
|
+
role="combobox"
|
|
140
|
+
disabled={disabled}
|
|
141
|
+
className="h-9 flex-1 justify-between overflow-hidden"
|
|
142
|
+
>
|
|
143
|
+
<span className="truncate text-left">
|
|
144
|
+
{selectedLabel || placeholder}
|
|
145
|
+
</span>
|
|
146
|
+
<ChevronsUpDown className="size-4 shrink-0 opacity-50" />
|
|
147
|
+
</Button>
|
|
148
|
+
</PopoverTrigger>
|
|
149
|
+
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
|
|
150
|
+
<Command shouldFilter={false}>
|
|
151
|
+
<CommandInput
|
|
152
|
+
value={search}
|
|
153
|
+
onValueChange={setSearch}
|
|
154
|
+
placeholder="Buscar colaborador..."
|
|
155
|
+
/>
|
|
156
|
+
<CommandList>
|
|
157
|
+
<CommandEmpty>Nenhum colaborador encontrado.</CommandEmpty>
|
|
158
|
+
<CommandGroup>
|
|
159
|
+
{filteredOptions.map((option) => (
|
|
160
|
+
<CommandItem
|
|
161
|
+
key={option.id}
|
|
162
|
+
value={String(option.id)}
|
|
163
|
+
onSelect={() => {
|
|
164
|
+
onChange(option);
|
|
165
|
+
setSelectedLabel(option.displayName);
|
|
166
|
+
setOpen(false);
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
<Check
|
|
170
|
+
className={`mr-2 size-4 ${
|
|
171
|
+
value === option.id ? 'opacity-100' : 'opacity-0'
|
|
172
|
+
}`}
|
|
173
|
+
/>
|
|
174
|
+
<div className="min-w-0">
|
|
175
|
+
<div className="truncate">{option.displayName}</div>
|
|
176
|
+
<div className="truncate text-xs text-muted-foreground">
|
|
177
|
+
{[option.code, option.title, option.department]
|
|
178
|
+
.filter(Boolean)
|
|
179
|
+
.join(' • ')}
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</CommandItem>
|
|
183
|
+
))}
|
|
184
|
+
</CommandGroup>
|
|
185
|
+
</CommandList>
|
|
186
|
+
</Command>
|
|
187
|
+
</PopoverContent>
|
|
188
|
+
</Popover>
|
|
189
|
+
|
|
190
|
+
{value ? (
|
|
191
|
+
<Button
|
|
192
|
+
type="button"
|
|
193
|
+
variant="outline"
|
|
194
|
+
size="icon"
|
|
195
|
+
className="h-9 w-9 shrink-0 cursor-pointer"
|
|
196
|
+
onClick={() => {
|
|
197
|
+
onChange(null);
|
|
198
|
+
setSelectedLabel('');
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
<X className="size-4" />
|
|
202
|
+
</Button>
|
|
203
|
+
) : null}
|
|
204
|
+
|
|
205
|
+
<Button
|
|
206
|
+
type="button"
|
|
207
|
+
variant="outline"
|
|
208
|
+
size="icon"
|
|
209
|
+
className="h-9 w-9 shrink-0 cursor-pointer"
|
|
210
|
+
disabled={disabled}
|
|
211
|
+
onClick={(event) => {
|
|
212
|
+
captureParentScrollPosition(event.currentTarget);
|
|
213
|
+
setCreateOpen(true);
|
|
214
|
+
}}
|
|
215
|
+
aria-label={`Criar ${label.toLowerCase()}`}
|
|
216
|
+
>
|
|
217
|
+
<Plus className="size-4" />
|
|
218
|
+
</Button>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<Sheet
|
|
222
|
+
open={createOpen}
|
|
223
|
+
onOpenChange={(nextOpen) => {
|
|
224
|
+
setCreateOpen(nextOpen);
|
|
225
|
+
if (!nextOpen) {
|
|
226
|
+
restoreParentScrollPosition();
|
|
227
|
+
}
|
|
228
|
+
}}
|
|
229
|
+
>
|
|
230
|
+
<SheetContent className="w-full overflow-y-auto sm:max-w-3xl">
|
|
231
|
+
<SheetHeader>
|
|
232
|
+
<SheetTitle>Novo colaborador</SheetTitle>
|
|
233
|
+
<SheetDescription>
|
|
234
|
+
Crie o colaborador sem sair do cadastro do contrato.
|
|
235
|
+
</SheetDescription>
|
|
236
|
+
</SheetHeader>
|
|
237
|
+
<CollaboratorFormScreen
|
|
238
|
+
onCancel={() => setCreateOpen(false)}
|
|
239
|
+
onSaved={async (collaborator: OperationsCollaboratorDetails) => {
|
|
240
|
+
onChange({
|
|
241
|
+
id: collaborator.id,
|
|
242
|
+
code: collaborator.code,
|
|
243
|
+
displayName: collaborator.displayName,
|
|
244
|
+
title: collaborator.title,
|
|
245
|
+
department: collaborator.department,
|
|
246
|
+
status: collaborator.status,
|
|
247
|
+
});
|
|
248
|
+
setSelectedLabel(collaborator.displayName);
|
|
249
|
+
setCreateOpen(false);
|
|
250
|
+
}}
|
|
251
|
+
/>
|
|
252
|
+
</SheetContent>
|
|
253
|
+
</Sheet>
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { RichTextEditor } from '@/components/rich-text-editor';
|
|
4
|
+
import { Button } from '@/components/ui/button';
|
|
5
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
6
|
+
import { Textarea } from '@/components/ui/textarea';
|
|
7
|
+
import { useApp } from '@hed-hog/next-app-provider';
|
|
8
|
+
import { LoaderCircle, Sparkles } from 'lucide-react';
|
|
9
|
+
import { useTranslations } from 'next-intl';
|
|
10
|
+
import { useMemo, useState } from 'react';
|
|
11
|
+
import { mutateOperations } from '../_lib/api';
|
|
12
|
+
import { SectionCard } from './section-card';
|
|
13
|
+
|
|
14
|
+
type ContractContentEditorProps = {
|
|
15
|
+
value: string;
|
|
16
|
+
onChange: (value: string) => void;
|
|
17
|
+
editorTitle: string;
|
|
18
|
+
editorDescription: string;
|
|
19
|
+
previewTitle: string;
|
|
20
|
+
previewDescription: string;
|
|
21
|
+
previewFallbackHtml?: string;
|
|
22
|
+
promptContext?: Record<string, string | number | boolean | null | undefined>;
|
|
23
|
+
compact?: boolean;
|
|
24
|
+
descriptionMode?: 'inline' | 'tooltip';
|
|
25
|
+
layout?: 'split' | 'tabs';
|
|
26
|
+
showPreview?: boolean;
|
|
27
|
+
chrome?: 'card' | 'plain';
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function ContractContentEditor({
|
|
31
|
+
value,
|
|
32
|
+
onChange,
|
|
33
|
+
editorTitle,
|
|
34
|
+
editorDescription,
|
|
35
|
+
previewTitle,
|
|
36
|
+
previewDescription,
|
|
37
|
+
previewFallbackHtml,
|
|
38
|
+
promptContext,
|
|
39
|
+
compact = false,
|
|
40
|
+
descriptionMode = 'inline',
|
|
41
|
+
layout = 'split',
|
|
42
|
+
showPreview = true,
|
|
43
|
+
chrome = 'card',
|
|
44
|
+
}: ContractContentEditorProps) {
|
|
45
|
+
const t = useTranslations('operations.ContractContentEditor');
|
|
46
|
+
const { request, showToastHandler, getSettingValue } = useApp();
|
|
47
|
+
const [prompt, setPrompt] = useState('');
|
|
48
|
+
const [isGenerating, setIsGenerating] = useState(false);
|
|
49
|
+
|
|
50
|
+
const hasOpenAi = Boolean(getSettingValue('ai-openai-api-key-enabled'));
|
|
51
|
+
const hasGemini = Boolean(getSettingValue('ai-gemini-api-key-enabled'));
|
|
52
|
+
const provider = hasOpenAi ? 'openai' : hasGemini ? 'gemini' : null;
|
|
53
|
+
const providerLabel = provider === 'openai' ? 'OpenAI' : 'Gemini';
|
|
54
|
+
|
|
55
|
+
const promptContextSummary = useMemo(
|
|
56
|
+
() =>
|
|
57
|
+
Object.entries(promptContext ?? {})
|
|
58
|
+
.filter(
|
|
59
|
+
([, rawValue]) =>
|
|
60
|
+
rawValue !== null &&
|
|
61
|
+
rawValue !== undefined &&
|
|
62
|
+
String(rawValue).trim()
|
|
63
|
+
)
|
|
64
|
+
.map(([key, rawValue]) => `${key}: ${String(rawValue)}`)
|
|
65
|
+
.join('\n'),
|
|
66
|
+
[promptContext]
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const fillSuggestedPrompt = () => {
|
|
70
|
+
setPrompt(
|
|
71
|
+
t('suggestedPrompt', {
|
|
72
|
+
subject: String(
|
|
73
|
+
promptContext?.name ||
|
|
74
|
+
promptContext?.project_name ||
|
|
75
|
+
promptContext?.client_name ||
|
|
76
|
+
'contrato'
|
|
77
|
+
),
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const generateWithAi = async () => {
|
|
83
|
+
if (!provider) {
|
|
84
|
+
showToastHandler?.('error', t('messages.missingConfiguration'));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const trimmedPrompt = prompt.trim();
|
|
89
|
+
if (!trimmedPrompt) {
|
|
90
|
+
showToastHandler?.('error', t('messages.missingPrompt'));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
setIsGenerating(true);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const response = await mutateOperations<{ content?: string }>(
|
|
98
|
+
request,
|
|
99
|
+
'/ai/chat',
|
|
100
|
+
'POST',
|
|
101
|
+
{
|
|
102
|
+
provider,
|
|
103
|
+
message: trimmedPrompt,
|
|
104
|
+
systemPrompt: [
|
|
105
|
+
'You are drafting professional business contract content for a back-office operations system.',
|
|
106
|
+
'Return ONLY clean HTML suitable for a rich text editor preview.',
|
|
107
|
+
'Do not include Markdown fences, explanations, or commentary.',
|
|
108
|
+
'When useful, keep placeholders like {{client_name}}, {{project_name}}, {{project_code}}, {{start_date}}, {{end_date}}, {{budget_amount}}, and {{monthly_hour_cap}} in the generated content.',
|
|
109
|
+
'Prefer short sections, headings, bullet points, and concise legal-style language.',
|
|
110
|
+
value?.trim()
|
|
111
|
+
? `Current HTML content to improve or rewrite:\n${value}`
|
|
112
|
+
: 'There is no current contract content yet.',
|
|
113
|
+
promptContextSummary
|
|
114
|
+
? `Context:\n${promptContextSummary}`
|
|
115
|
+
: 'No extra context was provided.',
|
|
116
|
+
].join('\n\n'),
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const generatedHtml = String(response?.content ?? '').trim();
|
|
121
|
+
if (!generatedHtml) {
|
|
122
|
+
throw new Error('Empty AI response');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
onChange(generatedHtml);
|
|
126
|
+
showToastHandler?.(
|
|
127
|
+
'success',
|
|
128
|
+
t('messages.generateSuccess', { provider: providerLabel })
|
|
129
|
+
);
|
|
130
|
+
} catch {
|
|
131
|
+
showToastHandler?.('error', t('messages.generateError'));
|
|
132
|
+
} finally {
|
|
133
|
+
setIsGenerating(false);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const renderSection = (
|
|
138
|
+
title: string,
|
|
139
|
+
description: string,
|
|
140
|
+
content: React.ReactNode
|
|
141
|
+
) => {
|
|
142
|
+
if (chrome === 'plain') {
|
|
143
|
+
return (
|
|
144
|
+
<div className="space-y-3">
|
|
145
|
+
<div className="space-y-1">
|
|
146
|
+
<h3 className="text-lg font-semibold">{title}</h3>
|
|
147
|
+
<p className="text-sm text-muted-foreground">{description}</p>
|
|
148
|
+
</div>
|
|
149
|
+
{content}
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<SectionCard
|
|
156
|
+
title={title}
|
|
157
|
+
description={description}
|
|
158
|
+
compact={compact}
|
|
159
|
+
descriptionMode={descriptionMode}
|
|
160
|
+
>
|
|
161
|
+
{content}
|
|
162
|
+
</SectionCard>
|
|
163
|
+
);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const editorSection = renderSection(
|
|
167
|
+
editorTitle,
|
|
168
|
+
editorDescription,
|
|
169
|
+
<RichTextEditor value={value} onChange={onChange} />
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const previewSection = renderSection(
|
|
173
|
+
previewTitle,
|
|
174
|
+
previewDescription,
|
|
175
|
+
<div
|
|
176
|
+
className="prose prose-sm max-w-none rounded-lg border p-4"
|
|
177
|
+
dangerouslySetInnerHTML={{
|
|
178
|
+
__html: value || previewFallbackHtml || `<p>${t('emptyPreview')}</p>`,
|
|
179
|
+
}}
|
|
180
|
+
/>
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<div className="space-y-4">
|
|
185
|
+
{provider ? (
|
|
186
|
+
<div className="rounded-lg border border-dashed px-3 py-3">
|
|
187
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
188
|
+
<div className="space-y-1">
|
|
189
|
+
<div className="flex items-center gap-2 text-sm font-medium">
|
|
190
|
+
<Sparkles className="size-4 text-primary" />
|
|
191
|
+
<span>{t('assistantTitle')}</span>
|
|
192
|
+
</div>
|
|
193
|
+
<p className="text-xs text-muted-foreground">
|
|
194
|
+
{t('assistantDescription', { provider: providerLabel })}
|
|
195
|
+
</p>
|
|
196
|
+
</div>
|
|
197
|
+
<Button
|
|
198
|
+
type="button"
|
|
199
|
+
variant="outline"
|
|
200
|
+
size="sm"
|
|
201
|
+
className="cursor-pointer"
|
|
202
|
+
onClick={fillSuggestedPrompt}
|
|
203
|
+
>
|
|
204
|
+
{t('actions.useSuggestion')}
|
|
205
|
+
</Button>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<div className="mt-3 space-y-2">
|
|
209
|
+
<Textarea
|
|
210
|
+
rows={compact ? 2 : 3}
|
|
211
|
+
value={prompt}
|
|
212
|
+
placeholder={t('promptPlaceholder')}
|
|
213
|
+
onChange={(event) => setPrompt(event.target.value)}
|
|
214
|
+
/>
|
|
215
|
+
<div className="flex flex-wrap gap-2">
|
|
216
|
+
<Button
|
|
217
|
+
type="button"
|
|
218
|
+
size="sm"
|
|
219
|
+
className="cursor-pointer"
|
|
220
|
+
disabled={isGenerating}
|
|
221
|
+
onClick={() => void generateWithAi()}
|
|
222
|
+
>
|
|
223
|
+
{isGenerating ? (
|
|
224
|
+
<LoaderCircle className="size-4 animate-spin" />
|
|
225
|
+
) : (
|
|
226
|
+
<Sparkles className="size-4" />
|
|
227
|
+
)}
|
|
228
|
+
{t('actions.generate')}
|
|
229
|
+
</Button>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
) : null}
|
|
234
|
+
|
|
235
|
+
{!showPreview ? (
|
|
236
|
+
editorSection
|
|
237
|
+
) : layout === 'tabs' ? (
|
|
238
|
+
<Tabs defaultValue="editor" className="w-full">
|
|
239
|
+
<TabsList className="grid w-full grid-cols-2">
|
|
240
|
+
<TabsTrigger value="editor">{editorTitle}</TabsTrigger>
|
|
241
|
+
<TabsTrigger value="preview">{previewTitle}</TabsTrigger>
|
|
242
|
+
</TabsList>
|
|
243
|
+
<TabsContent value="editor" className="mt-4">
|
|
244
|
+
{editorSection}
|
|
245
|
+
</TabsContent>
|
|
246
|
+
<TabsContent value="preview" className="mt-4">
|
|
247
|
+
{previewSection}
|
|
248
|
+
</TabsContent>
|
|
249
|
+
</Tabs>
|
|
250
|
+
) : (
|
|
251
|
+
<div className="grid gap-4 xl:grid-cols-2">
|
|
252
|
+
{editorSection}
|
|
253
|
+
{previewSection}
|
|
254
|
+
</div>
|
|
255
|
+
)}
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
}
|