Skip to content

Commit 49006ed

Browse files
committed
Add procurement agent ui
1 parent 19a2568 commit 49006ed

36 files changed

Lines changed: 3517 additions & 65 deletions
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
3+
import Database from 'better-sqlite3';
4+
5+
import { ScheduleData, ProcurementItem, TaskDataTables } from '@/lib/types';
6+
7+
export async function GET(
8+
_request: NextRequest,
9+
{ params }: { params: Promise<{ taskId: string }> }
10+
): Promise<NextResponse<TaskDataTables | { error: string }>> {
11+
try {
12+
const { taskId } = await params;
13+
14+
const dbPath = process.env.NEXT_PUBLIC_SQLITE_DB_PATH;
15+
16+
if (!dbPath) {
17+
return NextResponse.json(
18+
{ error: 'NEXT_PUBLIC_SQLITE_DB_PATH environment variable not set' },
19+
{ status: 500 }
20+
);
21+
}
22+
23+
const db = new Database(dbPath, { readonly: true });
24+
25+
try {
26+
const scheduleQuery = db.prepare(`
27+
SELECT workflow_id, project_name, project_start_date, project_end_date,
28+
schedule_json, created_at, updated_at
29+
FROM master_construction_schedule
30+
WHERE workflow_id = ?
31+
`);
32+
const schedule = scheduleQuery.get(taskId) as ScheduleData | undefined;
33+
34+
const procurementQuery = db.prepare(`
35+
SELECT workflow_id, item, status, eta, date_arrived,
36+
purchase_order_id, created_at, updated_at
37+
FROM procurement_items
38+
WHERE workflow_id = ?
39+
ORDER BY created_at DESC
40+
`);
41+
const procurementItems = procurementQuery.all(
42+
taskId
43+
) as ProcurementItem[];
44+
45+
return NextResponse.json({
46+
schedule: schedule || null,
47+
procurement_items: procurementItems,
48+
});
49+
} finally {
50+
db.close();
51+
}
52+
} catch (error) {
53+
console.error('Error querying SQLite database:', error);
54+
return NextResponse.json(
55+
{ error: error instanceof Error ? error.message : 'Unknown error' },
56+
{ status: 500 }
57+
);
58+
}
59+
}

agentex-ui/app/globals.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@
5252
--ring: #19202f;
5353
--secondary-foreground: #ffffff;
5454
--secondary: #45464f;
55+
--toastify-icon-color-success: var(--color-green-700);
56+
--toastify-color-progress-success: var(--color-green-700);
57+
--toastify-icon-color-error: var(--color-red-700);
58+
--toastify-color-progress-error: var(--color-red-700);
59+
--toastify-icon-color-warning: var(--color-yellow-700);
60+
--toastify-color-progress-warning: var(--color-yellow-700);
61+
--toastify-icon-color-info: var(--color-blue-700);
62+
--toastify-color-progress-info: var(--color-blue-700);
63+
--toastify-icon-color-default: var(--color-gray-700);
64+
--toastify-color-progress-default: var(--color-gray-700);
5565
}
5666

5767
.dark {

agentex-ui/app/layout.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Geist, Geist_Mono } from 'next/font/google';
22

33
import { QueryProvider, ThemeProvider } from '@/components/providers';
4+
import { ArtifactPanelProvider } from '@/contexts/artifact-panel-context';
45

56
import type { Metadata } from 'next';
67
import '@/app/globals.css';
@@ -37,7 +38,7 @@ export default function RootLayout({
3738
enableSystem={false}
3839
disableTransitionOnChange
3940
>
40-
{children}
41+
<ArtifactPanelProvider>{children}</ArtifactPanelProvider>
4142
</ThemeProvider>
4243
</QueryProvider>
4344
</body>

agentex-ui/components/agentex-ui-root.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import { useCallback, useEffect, useState } from 'react';
44

55
import { ToastContainer } from 'react-toastify';
66

7+
import { ArtifactPanel } from '@/components/artifacts/artifact-panel';
78
import { PrimaryContent } from '@/components/primary-content/primary-content';
89
import { useAgentexClient } from '@/components/providers';
910
import { TaskSidebar } from '@/components/task-sidebar/task-sidebar';
1011
import { TracesSidebar } from '@/components/traces-sidebar/traces-sidebar';
12+
import { useArtifactPanel } from '@/contexts/artifact-panel-context';
1113
import { useAgents } from '@/hooks/use-agents';
1214
import { useLocalStorageState } from '@/hooks/use-local-storage-state';
1315
import {
@@ -20,6 +22,7 @@ export function AgentexUIRoot() {
2022
const [isTracesSidebarOpen, setIsTracesSidebarOpen] = useState(false);
2123
const { agentexClient } = useAgentexClient();
2224
const { data: agents = [], isLoading } = useAgents(agentexClient);
25+
const { isOpen: isArtifactPanelOpen, closeArtifact } = useArtifactPanel();
2326
const [localAgentName, setLocalAgentName] = useLocalStorageState<
2427
string | undefined
2528
>('lastSelectedAgent', undefined);
@@ -41,6 +44,21 @@ export function AgentexUIRoot() {
4144
// eslint-disable-next-line react-hooks/exhaustive-deps
4245
}, [isLoading]);
4346

47+
// Close artifact when task or agent changes
48+
useEffect(() => {
49+
if (isArtifactPanelOpen) {
50+
closeArtifact();
51+
}
52+
// eslint-disable-next-line react-hooks/exhaustive-deps
53+
}, [taskID, agentName]);
54+
55+
// Close traces sidebar when artifact opens
56+
useEffect(() => {
57+
if (isArtifactPanelOpen && isTracesSidebarOpen) {
58+
setIsTracesSidebarOpen(false);
59+
}
60+
}, [isArtifactPanelOpen, isTracesSidebarOpen]);
61+
4462
const handleSelectTask = useCallback(
4563
(taskId: string | null) => {
4664
updateParams({
@@ -82,8 +100,10 @@ export function AgentexUIRoot() {
82100
toggleTracesSidebar={() =>
83101
setIsTracesSidebarOpen(!isTracesSidebarOpen)
84102
}
103+
isArtifactPanelOpen={isArtifactPanelOpen}
85104
/>
86105
<TracesSidebar isOpen={isTracesSidebarOpen} />
106+
<ArtifactPanel />
87107
</div>
88108
<ToastContainer />
89109
</>
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
5+
import { motion } from 'framer-motion';
6+
import { useForm } from 'react-hook-form';
7+
8+
import { useAgentexClient } from '@/components/providers';
9+
import { Button } from '@/components/ui/button';
10+
import {
11+
Form,
12+
FormControl,
13+
FormField,
14+
FormItem,
15+
FormLabel,
16+
FormMessage,
17+
} from '@/components/ui/form';
18+
import { ShimmeringText } from '@/components/ui/shimmering-text';
19+
import { Textarea } from '@/components/ui/textarea';
20+
import { useSendMessage } from '@/hooks/use-task-messages';
21+
22+
type HumanResponseFormValues = {
23+
response: string;
24+
};
25+
26+
type HumanResponseFormProps = {
27+
message: string;
28+
taskId: string;
29+
agentName: string;
30+
userResponse?: string | null;
31+
isResponded?: boolean;
32+
};
33+
34+
export function HumanResponseForm({
35+
message,
36+
taskId,
37+
agentName,
38+
userResponse = null,
39+
isResponded = false,
40+
}: HumanResponseFormProps) {
41+
const { agentexClient } = useAgentexClient();
42+
const sendMessageMutation = useSendMessage({ agentexClient });
43+
44+
// Track local submission state for optimistic UI
45+
const [localSubmittedResponse, setLocalSubmittedResponse] = useState<
46+
string | null
47+
>(null);
48+
49+
// Determine the effective state (use local state if submitted, otherwise use props)
50+
const effectiveResponse = localSubmittedResponse || userResponse;
51+
const effectiveIsResponded = localSubmittedResponse !== null || isResponded;
52+
53+
const form = useForm<HumanResponseFormValues>({
54+
defaultValues: {
55+
response: effectiveResponse || '',
56+
},
57+
});
58+
59+
// Update form when response comes in from props or local state changes
60+
useEffect(() => {
61+
if (effectiveResponse) {
62+
form.setValue('response', effectiveResponse);
63+
}
64+
}, [effectiveResponse, form]);
65+
66+
const handleSubmit = async (values: HumanResponseFormValues) => {
67+
try {
68+
// Optimistically set the local submitted response
69+
setLocalSubmittedResponse(values.response);
70+
71+
await sendMessageMutation.mutateAsync({
72+
taskId,
73+
agentName,
74+
content: {
75+
type: 'text',
76+
author: 'user',
77+
format: 'plain',
78+
attachments: [],
79+
content: values.response,
80+
},
81+
});
82+
83+
// Don't reset the form - keep the submitted value showing
84+
} catch (error) {
85+
console.error('Failed to send human response:', error);
86+
// On error, clear the optimistic state
87+
setLocalSubmittedResponse(null);
88+
}
89+
};
90+
91+
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
92+
// Submit on Command+Enter (Mac) or Ctrl+Enter (Windows/Linux)
93+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
94+
e.preventDefault();
95+
form.handleSubmit(handleSubmit)();
96+
}
97+
};
98+
99+
return (
100+
<motion.div
101+
initial={{ opacity: 0, y: -10 }}
102+
animate={{ opacity: 1, y: 0 }}
103+
transition={{ duration: 0.3, ease: 'easeOut' }}
104+
className="border-border bg-card my-4 w-full rounded-lg border p-6 shadow-lg"
105+
>
106+
<div className="space-y-4">
107+
<div className="flex items-center gap-2">
108+
<ShimmeringText
109+
text={
110+
effectiveIsResponded
111+
? 'Human response received'
112+
: 'Waiting for human response'
113+
}
114+
enabled={!effectiveIsResponded && !sendMessageMutation.isPending}
115+
/>
116+
</div>
117+
118+
{message && (
119+
<div className="text-muted-foreground bg-muted/50 rounded-md p-3 text-sm">
120+
{message}
121+
</div>
122+
)}
123+
124+
<Form {...form}>
125+
<form
126+
onSubmit={form.handleSubmit(handleSubmit)}
127+
className="space-y-4"
128+
>
129+
<FormField
130+
control={form.control}
131+
name="response"
132+
rules={
133+
!effectiveIsResponded
134+
? { required: 'Response is required' }
135+
: {}
136+
}
137+
render={({ field }) => (
138+
<FormItem>
139+
<FormLabel>Your Response</FormLabel>
140+
<FormControl>
141+
<Textarea
142+
{...field}
143+
placeholder="Enter your response"
144+
rows={3}
145+
className="resize-none"
146+
disabled={
147+
sendMessageMutation.isPending || effectiveIsResponded
148+
}
149+
readOnly={effectiveIsResponded}
150+
onKeyDown={handleKeyDown}
151+
/>
152+
</FormControl>
153+
<FormMessage />
154+
</FormItem>
155+
)}
156+
/>
157+
158+
{!effectiveIsResponded && (
159+
<Button
160+
type="submit"
161+
disabled={sendMessageMutation.isPending}
162+
className="w-full"
163+
>
164+
{sendMessageMutation.isPending ? 'Sending...' : 'Send Response'}
165+
</Button>
166+
)}
167+
</form>
168+
</Form>
169+
</div>
170+
</motion.div>
171+
);
172+
}

0 commit comments

Comments
 (0)