fastapi-fullstack 0.1.7__py3-none-any.whl → 0.1.15__py3-none-any.whl
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.
- {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/METADATA +9 -2
- {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/RECORD +71 -55
- fastapi_gen/__init__.py +6 -1
- fastapi_gen/cli.py +9 -0
- fastapi_gen/config.py +154 -2
- fastapi_gen/generator.py +34 -14
- fastapi_gen/prompts.py +172 -31
- fastapi_gen/template/VARIABLES.md +33 -4
- fastapi_gen/template/cookiecutter.json +10 -0
- fastapi_gen/template/hooks/post_gen_project.py +87 -2
- fastapi_gen/template/{{cookiecutter.project_slug}}/.env.prod.example +9 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.gitlab-ci.yml +178 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/CLAUDE.md +3 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/README.md +334 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.env.example +32 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/env.py +10 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/admin.py +1 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/__init__.py +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/crewai_assistant.py +563 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/deepagents_assistant.py +526 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langchain_assistant.py +4 -3
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langgraph_assistant.py +371 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/agent.py +1472 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/oauth.py +3 -7
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/cleanup.py +2 -2
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/seed.py +7 -2
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/config.py +44 -7
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/__init__.py +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/base.py +42 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/conversation.py +262 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/item.py +76 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/session.py +118 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/user.py +158 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/webhook.py +185 -3
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/main.py +29 -2
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/base.py +6 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/session.py +4 -4
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/conversation.py +9 -9
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/session.py +6 -6
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/webhook.py +7 -7
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/__init__.py +1 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/arq_app.py +165 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/__init__.py +10 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/pyproject.toml +40 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_metrics.py +53 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_agents.py +2 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.dev.yml +6 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.prod.yml +100 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.yml +39 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.env.example +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-container.tsx +28 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/index.ts +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-item.tsx +22 -4
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-list.tsx +23 -3
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/tool-approval-dialog.tsx +138 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-chat.ts +242 -18
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-local-chat.ts +242 -17
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/constants.ts +1 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/chat.ts +57 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/configmap.yaml +63 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/deployment.yaml +242 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/ingress.yaml +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/kustomization.yaml +28 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/namespace.yaml +12 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/secret.yaml +59 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/service.yaml +23 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/nginx.conf +225 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/ssl/.gitkeep +18 -0
- {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/WHEEL +0 -0
- {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/entry_points.txt +0 -0
- {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/licenses/LICENSE +0 -0
|
@@ -47,6 +47,12 @@ services:
|
|
|
47
47
|
{%- endif %}
|
|
48
48
|
{%- endif %}
|
|
49
49
|
restart: unless-stopped
|
|
50
|
+
{%- if cookiecutter.enable_prometheus %}
|
|
51
|
+
labels:
|
|
52
|
+
- "prometheus.scrape=true"
|
|
53
|
+
- "prometheus.port={{ cookiecutter.backend_port }}"
|
|
54
|
+
- "prometheus.path=/metrics"
|
|
55
|
+
{%- endif %}
|
|
50
56
|
|
|
51
57
|
{%- if cookiecutter.use_postgresql %}
|
|
52
58
|
|
|
@@ -222,6 +228,39 @@ services:
|
|
|
222
228
|
restart: unless-stopped
|
|
223
229
|
{%- endif %}
|
|
224
230
|
|
|
231
|
+
{%- if cookiecutter.use_arq %}
|
|
232
|
+
|
|
233
|
+
arq_worker:
|
|
234
|
+
build:
|
|
235
|
+
context: ./backend
|
|
236
|
+
dockerfile: Dockerfile
|
|
237
|
+
container_name: {{ cookiecutter.project_slug }}_arq_worker
|
|
238
|
+
volumes:
|
|
239
|
+
- ./backend/app:/app/app:ro
|
|
240
|
+
command: arq app.worker.arq_app.WorkerSettings
|
|
241
|
+
env_file:
|
|
242
|
+
- ./backend/.env
|
|
243
|
+
environment:
|
|
244
|
+
- DEBUG=true
|
|
245
|
+
{%- if cookiecutter.use_postgresql %}
|
|
246
|
+
- POSTGRES_HOST=db
|
|
247
|
+
{%- endif %}
|
|
248
|
+
- REDIS_HOST=redis
|
|
249
|
+
- ARQ_REDIS_HOST=redis
|
|
250
|
+
- ARQ_REDIS_PORT=6379
|
|
251
|
+
- ARQ_REDIS_DB=2
|
|
252
|
+
networks:
|
|
253
|
+
- backend
|
|
254
|
+
depends_on:
|
|
255
|
+
redis:
|
|
256
|
+
condition: service_healthy
|
|
257
|
+
{%- if cookiecutter.use_postgresql %}
|
|
258
|
+
db:
|
|
259
|
+
condition: service_healthy
|
|
260
|
+
{%- endif %}
|
|
261
|
+
restart: unless-stopped
|
|
262
|
+
{%- endif %}
|
|
263
|
+
|
|
225
264
|
networks:
|
|
226
265
|
backend:
|
|
227
266
|
driver: bridge
|
|
@@ -3,6 +3,11 @@ BACKEND_URL=http://localhost:{{ cookiecutter.backend_port }}
|
|
|
3
3
|
|
|
4
4
|
# WebSocket URL for real-time features
|
|
5
5
|
BACKEND_WS_URL=ws://localhost:{{ cookiecutter.backend_port }}
|
|
6
|
+
{%- if cookiecutter.enable_oauth %}
|
|
7
|
+
|
|
8
|
+
# Public API URL for OAuth redirects (exposed to browser)
|
|
9
|
+
NEXT_PUBLIC_API_URL=http://localhost:{{ cookiecutter.backend_port }}
|
|
10
|
+
{%- endif %}
|
|
6
11
|
{%- if cookiecutter.enable_logfire %}
|
|
7
12
|
|
|
8
13
|
# Logfire/OpenTelemetry (server-side instrumentation)
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-container.tsx
CHANGED
|
@@ -4,8 +4,10 @@ import { useEffect, useRef, useCallback } from "react";
|
|
|
4
4
|
import { useChat, useLocalChat } from "@/hooks";
|
|
5
5
|
import { MessageList } from "./message-list";
|
|
6
6
|
import { ChatInput } from "./chat-input";
|
|
7
|
+
import { ToolApprovalDialog } from "./tool-approval-dialog";
|
|
7
8
|
import { Button } from "@/components/ui";
|
|
8
9
|
import { Wifi, WifiOff, RotateCcw, Bot } from "lucide-react";
|
|
10
|
+
import type { PendingApproval, Decision } from "@/types";
|
|
9
11
|
{%- if cookiecutter.enable_conversation_persistence and cookiecutter.use_database %}
|
|
10
12
|
import { useConversationStore, useChatStore, useAuthStore } from "@/stores";
|
|
11
13
|
import { useConversations } from "@/hooks";
|
|
@@ -46,6 +48,8 @@ function AuthenticatedChatContainer() {
|
|
|
46
48
|
disconnect,
|
|
47
49
|
sendMessage,
|
|
48
50
|
clearMessages,
|
|
51
|
+
pendingApproval,
|
|
52
|
+
sendResumeDecisions,
|
|
49
53
|
} = useChat({
|
|
50
54
|
conversationId: currentConversationId,
|
|
51
55
|
onConversationCreated: handleConversationCreated,
|
|
@@ -118,6 +122,8 @@ function AuthenticatedChatContainer() {
|
|
|
118
122
|
sendMessage={sendMessage}
|
|
119
123
|
clearMessages={clearMessages}
|
|
120
124
|
messagesEndRef={messagesEndRef}
|
|
125
|
+
pendingApproval={pendingApproval}
|
|
126
|
+
onResumeDecisions={sendResumeDecisions}
|
|
121
127
|
/>
|
|
122
128
|
);
|
|
123
129
|
}
|
|
@@ -132,6 +138,8 @@ function LocalChatContainer() {
|
|
|
132
138
|
disconnect,
|
|
133
139
|
sendMessage,
|
|
134
140
|
clearMessages,
|
|
141
|
+
pendingApproval,
|
|
142
|
+
sendResumeDecisions,
|
|
135
143
|
} = useLocalChat();
|
|
136
144
|
|
|
137
145
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
@@ -153,6 +161,8 @@ function LocalChatContainer() {
|
|
|
153
161
|
sendMessage={sendMessage}
|
|
154
162
|
clearMessages={clearMessages}
|
|
155
163
|
messagesEndRef={messagesEndRef}
|
|
164
|
+
pendingApproval={pendingApproval}
|
|
165
|
+
onResumeDecisions={sendResumeDecisions}
|
|
156
166
|
/>
|
|
157
167
|
);
|
|
158
168
|
}
|
|
@@ -170,6 +180,9 @@ interface ChatUIProps {
|
|
|
170
180
|
sendMessage: (content: string) => void;
|
|
171
181
|
clearMessages: () => void;
|
|
172
182
|
messagesEndRef: React.RefObject<HTMLDivElement | null>;
|
|
183
|
+
// Human-in-the-Loop support
|
|
184
|
+
pendingApproval?: PendingApproval | null;
|
|
185
|
+
onResumeDecisions?: (decisions: Decision[]) => void;
|
|
173
186
|
}
|
|
174
187
|
|
|
175
188
|
function ChatUI({
|
|
@@ -179,6 +192,8 @@ function ChatUI({
|
|
|
179
192
|
sendMessage,
|
|
180
193
|
clearMessages,
|
|
181
194
|
messagesEndRef,
|
|
195
|
+
pendingApproval,
|
|
196
|
+
onResumeDecisions,
|
|
182
197
|
}: ChatUIProps) {
|
|
183
198
|
return (
|
|
184
199
|
<div className="flex flex-col h-full max-w-4xl mx-auto w-full">
|
|
@@ -199,11 +214,23 @@ function ChatUI({
|
|
|
199
214
|
<div ref={messagesEndRef} />
|
|
200
215
|
</div>
|
|
201
216
|
|
|
217
|
+
{/* Human-in-the-Loop: Tool Approval Dialog */}
|
|
218
|
+
{pendingApproval && onResumeDecisions && (
|
|
219
|
+
<div className="px-2 pb-2 sm:px-4 sm:pb-2">
|
|
220
|
+
<ToolApprovalDialog
|
|
221
|
+
actionRequests={pendingApproval.actionRequests}
|
|
222
|
+
reviewConfigs={pendingApproval.reviewConfigs}
|
|
223
|
+
onDecisions={onResumeDecisions}
|
|
224
|
+
disabled={!isConnected}
|
|
225
|
+
/>
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
228
|
+
|
|
202
229
|
<div className="px-2 pb-2 sm:px-4 sm:pb-4">
|
|
203
230
|
<div className="rounded-xl border bg-card shadow-sm p-3 sm:p-4">
|
|
204
231
|
<ChatInput
|
|
205
232
|
onSend={sendMessage}
|
|
206
|
-
disabled={!isConnected || isProcessing}
|
|
233
|
+
disabled={!isConnected || isProcessing || !!pendingApproval}
|
|
207
234
|
isProcessing={isProcessing}
|
|
208
235
|
/>
|
|
209
236
|
<div className="flex items-center justify-between mt-3 pt-3 border-t">
|
|
@@ -2,6 +2,7 @@ export { ChatContainer } from "./chat-container";
|
|
|
2
2
|
export { MessageList } from "./message-list";
|
|
3
3
|
export { MessageItem } from "./message-item";
|
|
4
4
|
export { ToolCallCard } from "./tool-call-card";
|
|
5
|
+
export { ToolApprovalDialog } from "./tool-approval-dialog";
|
|
5
6
|
export { ChatInput } from "./chat-input";
|
|
6
7
|
export { LocalConversationSidebar, ChatSidebarToggle } from "./local-conversation-sidebar";
|
|
7
8
|
export { CopyButton } from "./copy-button";
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-item.tsx
CHANGED
|
@@ -9,22 +9,40 @@ import { User, Bot } from "lucide-react";
|
|
|
9
9
|
|
|
10
10
|
interface MessageItemProps {
|
|
11
11
|
message: ChatMessage;
|
|
12
|
+
groupPosition?: "first" | "middle" | "last" | "single";
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
export function MessageItem({ message }: MessageItemProps) {
|
|
15
|
+
export function MessageItem({ message, groupPosition }: MessageItemProps) {
|
|
15
16
|
const isUser = message.role === "user";
|
|
17
|
+
const isGrouped = groupPosition && groupPosition !== "single";
|
|
16
18
|
|
|
17
19
|
return (
|
|
18
20
|
<div
|
|
19
21
|
className={cn(
|
|
20
|
-
"group flex gap-2 sm:gap-4
|
|
22
|
+
"group flex gap-2 sm:gap-4 relative overflow-visible",
|
|
23
|
+
isGrouped ? "py-2 sm:py-3" : "py-3 sm:py-4",
|
|
21
24
|
isUser && "flex-row-reverse"
|
|
22
25
|
)}
|
|
23
26
|
>
|
|
27
|
+
{/* Timeline connector line for grouped messages */}
|
|
28
|
+
{isGrouped && !isUser && (
|
|
29
|
+
<div
|
|
30
|
+
className="absolute left-[15px] sm:left-[17px] w-0.5 bg-orange-500/40"
|
|
31
|
+
style={
|
|
32
|
+
groupPosition === "first"
|
|
33
|
+
? { top: "24px", bottom: "0" }
|
|
34
|
+
: groupPosition === "last"
|
|
35
|
+
? { top: "0", height: "24px" }
|
|
36
|
+
: { top: "0", bottom: "0" }
|
|
37
|
+
}
|
|
38
|
+
/>
|
|
39
|
+
)}
|
|
40
|
+
|
|
24
41
|
<div
|
|
25
42
|
className={cn(
|
|
26
|
-
"flex-shrink-0 w-8 h-8 sm:w-9 sm:h-9 rounded-full flex items-center justify-center",
|
|
27
|
-
isUser ? "bg-primary text-primary-foreground" : "bg-orange-500/10 text-orange-500"
|
|
43
|
+
"flex-shrink-0 w-8 h-8 sm:w-9 sm:h-9 rounded-full flex items-center justify-center z-10",
|
|
44
|
+
isUser ? "bg-primary text-primary-foreground" : "bg-orange-500/10 text-orange-500",
|
|
45
|
+
isGrouped && !isUser && "ring-2 ring-background"
|
|
28
46
|
)}
|
|
29
47
|
>
|
|
30
48
|
{isUser ? <User className="h-4 w-4" /> : <Bot className="h-4 w-4 sm:h-5 sm:w-5" />}
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-list.tsx
CHANGED
|
@@ -8,10 +8,30 @@ interface MessageListProps {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function MessageList({ messages }: MessageListProps) {
|
|
11
|
+
// Calculate group positions for timeline connector
|
|
12
|
+
const getGroupPosition = (
|
|
13
|
+
message: ChatMessage,
|
|
14
|
+
index: number
|
|
15
|
+
): "first" | "middle" | "last" | "single" | undefined => {
|
|
16
|
+
if (!message.groupId) return undefined;
|
|
17
|
+
|
|
18
|
+
const groupMessages = messages.filter((m) => m.groupId === message.groupId);
|
|
19
|
+
if (groupMessages.length <= 1) return "single";
|
|
20
|
+
|
|
21
|
+
const groupIndex = groupMessages.findIndex((m) => m.id === message.id);
|
|
22
|
+
if (groupIndex === 0) return "first";
|
|
23
|
+
if (groupIndex === groupMessages.length - 1) return "last";
|
|
24
|
+
return "middle";
|
|
25
|
+
};
|
|
26
|
+
|
|
11
27
|
return (
|
|
12
|
-
<div className="space-y-
|
|
13
|
-
{messages.map((message) => (
|
|
14
|
-
<MessageItem
|
|
28
|
+
<div className="space-y-0">
|
|
29
|
+
{messages.map((message, index) => (
|
|
30
|
+
<MessageItem
|
|
31
|
+
key={message.id}
|
|
32
|
+
message={message}
|
|
33
|
+
groupPosition={getGroupPosition(message, index)}
|
|
34
|
+
/>
|
|
15
35
|
))}
|
|
16
36
|
</div>
|
|
17
37
|
);
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Button } from "@/components/ui";
|
|
5
|
+
import type { ActionRequest, ReviewConfig, Decision } from "@/types";
|
|
6
|
+
import { Wrench, AlertTriangle } from "lucide-react";
|
|
7
|
+
import { cn } from "@/lib/utils";
|
|
8
|
+
|
|
9
|
+
interface ToolApprovalDialogProps {
|
|
10
|
+
actionRequests: ActionRequest[];
|
|
11
|
+
reviewConfigs: ReviewConfig[];
|
|
12
|
+
onDecisions: (decisions: Decision[]) => void;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ToolApprovalDialog({
|
|
17
|
+
actionRequests,
|
|
18
|
+
reviewConfigs,
|
|
19
|
+
onDecisions,
|
|
20
|
+
disabled = false,
|
|
21
|
+
}: ToolApprovalDialogProps) {
|
|
22
|
+
// Store edited args for each action
|
|
23
|
+
const [editedArgs, setEditedArgs] = useState<Record<string, string>>(() =>
|
|
24
|
+
Object.fromEntries(
|
|
25
|
+
actionRequests.map((a) => [a.id, JSON.stringify(a.args, null, 2)])
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
const [hasChanges, setHasChanges] = useState(false);
|
|
29
|
+
|
|
30
|
+
const handleArgsChange = (id: string, text: string) => {
|
|
31
|
+
setEditedArgs((prev) => ({ ...prev, [id]: text }));
|
|
32
|
+
setHasChanges(true);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const handleCancel = () => {
|
|
36
|
+
// Reset to original args
|
|
37
|
+
setEditedArgs(
|
|
38
|
+
Object.fromEntries(
|
|
39
|
+
actionRequests.map((a) => [a.id, JSON.stringify(a.args, null, 2)])
|
|
40
|
+
)
|
|
41
|
+
);
|
|
42
|
+
setHasChanges(false);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleSave = () => {
|
|
46
|
+
// Validate all JSON
|
|
47
|
+
for (const id of Object.keys(editedArgs)) {
|
|
48
|
+
try {
|
|
49
|
+
JSON.parse(editedArgs[id]);
|
|
50
|
+
} catch {
|
|
51
|
+
return; // Invalid JSON, don't save
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
setHasChanges(false);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleSubmit = () => {
|
|
58
|
+
const decisions: Decision[] = actionRequests.map((a) => {
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(editedArgs[a.id]);
|
|
61
|
+
const original = JSON.stringify(a.args);
|
|
62
|
+
const edited = JSON.stringify(parsed);
|
|
63
|
+
|
|
64
|
+
if (original !== edited) {
|
|
65
|
+
return {
|
|
66
|
+
type: "edit" as const,
|
|
67
|
+
editedAction: { id: a.id, tool_name: a.tool_name, args: parsed },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return { type: "approve" as const };
|
|
71
|
+
} catch {
|
|
72
|
+
return { type: "reject" as const };
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
onDecisions(decisions);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className="rounded-lg border border-yellow-500/50 bg-yellow-50/5 p-3 space-y-3">
|
|
80
|
+
<div className="flex items-center gap-2 text-sm text-yellow-600">
|
|
81
|
+
<AlertTriangle className="h-4 w-4" />
|
|
82
|
+
<span className="font-medium">Tool approval required</span>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{actionRequests.map((action) => (
|
|
86
|
+
<div key={action.id} className="space-y-1.5">
|
|
87
|
+
<div className="flex items-center gap-2">
|
|
88
|
+
<Wrench className="h-3.5 w-3.5 text-muted-foreground" />
|
|
89
|
+
<code className="text-xs font-semibold">{action.tool_name}</code>
|
|
90
|
+
</div>
|
|
91
|
+
<textarea
|
|
92
|
+
className={cn(
|
|
93
|
+
"w-full p-2 text-xs font-mono bg-background border rounded resize-none",
|
|
94
|
+
"min-h-[80px] max-h-[200px]"
|
|
95
|
+
)}
|
|
96
|
+
value={editedArgs[action.id]}
|
|
97
|
+
onChange={(e) => handleArgsChange(action.id, e.target.value)}
|
|
98
|
+
disabled={disabled}
|
|
99
|
+
rows={Math.min(10, (editedArgs[action.id]?.split("\n").length || 3) + 1)}
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
))}
|
|
103
|
+
|
|
104
|
+
<div className="flex justify-end gap-2 pt-1 border-t">
|
|
105
|
+
{hasChanges && (
|
|
106
|
+
<>
|
|
107
|
+
<Button
|
|
108
|
+
size="sm"
|
|
109
|
+
variant="ghost"
|
|
110
|
+
className="h-7 text-xs"
|
|
111
|
+
onClick={handleCancel}
|
|
112
|
+
disabled={disabled}
|
|
113
|
+
>
|
|
114
|
+
Cancel
|
|
115
|
+
</Button>
|
|
116
|
+
<Button
|
|
117
|
+
size="sm"
|
|
118
|
+
variant="outline"
|
|
119
|
+
className="h-7 text-xs"
|
|
120
|
+
onClick={handleSave}
|
|
121
|
+
disabled={disabled}
|
|
122
|
+
>
|
|
123
|
+
Save
|
|
124
|
+
</Button>
|
|
125
|
+
</>
|
|
126
|
+
)}
|
|
127
|
+
<Button
|
|
128
|
+
size="sm"
|
|
129
|
+
className="h-7 text-xs"
|
|
130
|
+
onClick={handleSubmit}
|
|
131
|
+
disabled={disabled}
|
|
132
|
+
>
|
|
133
|
+
Submit ({actionRequests.length})
|
|
134
|
+
</Button>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|