Skip to content

Commit 6a90bdf

Browse files
committed
fix (tasks): recurring tasks qna
1 parent 8a6b4f3 commit 6a90bdf

13 files changed

Lines changed: 546 additions & 335 deletions

File tree

src/client/app/integrations/page.js

Lines changed: 268 additions & 299 deletions
Large diffs are not rendered by default.

src/client/app/layout.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { PostHogProvider } from "@components/PostHogProvider"
1313
*/
1414
export const metadata = {
1515
title: "Sentient", // Title of the application, displayed in browser tab or window title
16-
description: "Your autopilot for productivity" // Description of the application, used for SEO purposes
16+
description: "Your personal AI that actually gets work done" // Description of the application, used for SEO purposes
1717
}
1818

1919
/**

src/client/app/tasks/page.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,20 @@ function TasksPageContent() {
276276
[fetchTasks]
277277
)
278278

279+
const handleAnswerClarifications = async (taskId, answers) => {
280+
await handleAction(
281+
() =>
282+
fetch("/api/tasks/answer-clarifications", {
283+
method: "POST",
284+
headers: { "Content-Type": "application/json" },
285+
body: JSON.stringify({ taskId, answers })
286+
}),
287+
"Answers submitted successfully. The task will now resume."
288+
)
289+
// After submitting, close the panel to show the updated task list
290+
handleCloseRightPanel()
291+
}
292+
279293
const handleAddTask = (newTask) => {
280294
// Optimistically add the new task to the state
281295
setAllTasks((prevTasks) => [...prevTasks, newTask])
@@ -524,6 +538,9 @@ Description: ${event.description || "No description."}`
524538
integrations={integrations}
525539
onClose={handleCloseRightPanel}
526540
onSave={handleUpdateTask}
541+
onAnswerClarifications={
542+
handleAnswerClarifications
543+
}
527544
onDelete={(taskId) =>
528545
handleAction(
529546
() =>

src/client/components/Sidebar.js

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,113 @@ import {
2121
IconHeadphones,
2222
IconBrandDiscord,
2323
IconPlayerPlay,
24-
IconSlideshow
24+
IconSlideshow,
25+
IconSparkles,
26+
IconCheck
2527
} from "@tabler/icons-react"
2628
import { cn } from "@utils/cn"
2729
import { usePlan } from "@hooks/usePlan"
2830
import { motion, AnimatePresence } from "framer-motion"
2931
import useClickOutside from "@hooks/useClickOutside"
3032

33+
const proPlanFeatures = [
34+
{ name: "Text Chat", limit: "100 messages per day" },
35+
{ name: "Voice Chat", limit: "10 minutes per day" },
36+
{ name: "One-Time Tasks", limit: "20 async tasks per day" },
37+
{ name: "Recurring Tasks", limit: "10 active recurring workflows" },
38+
{ name: "Triggered Tasks", limit: "10 triggered workflows" },
39+
{
40+
name: "Parallel Agents",
41+
limit: "5 complex tasks per day with 50 sub agents"
42+
},
43+
{ name: "File Uploads", limit: "20 files per day" },
44+
{ name: "Memories", limit: "Unlimited memories" },
45+
{
46+
name: "Other Integrations",
47+
limit: "Notion, GitHub, Slack, Discord, Trello"
48+
}
49+
]
50+
51+
const UpgradeToProModal = ({ isOpen, onClose }) => {
52+
if (!isOpen) return null
53+
54+
const handleUpgrade = () => {
55+
const dashboardUrl = process.env.NEXT_PUBLIC_LANDING_PAGE_URL
56+
if (dashboardUrl) {
57+
window.open(`${dashboardUrl}/dashboard`, "_blank")
58+
}
59+
onClose()
60+
}
61+
62+
return (
63+
<AnimatePresence>
64+
{isOpen && (
65+
<motion.div
66+
initial={{ opacity: 0 }}
67+
animate={{ opacity: 1 }}
68+
exit={{ opacity: 0 }}
69+
className="fixed inset-0 bg-black/70 backdrop-blur-md z-[100] flex items-center justify-center p-4"
70+
onClick={onClose}
71+
>
72+
<motion.div
73+
initial={{ scale: 0.95, y: 20 }}
74+
animate={{ scale: 1, y: 0 }}
75+
exit={{ scale: 0.95, y: -20 }}
76+
transition={{ duration: 0.2, ease: "easeInOut" }}
77+
onClick={(e) => e.stopPropagation()}
78+
className="relative bg-neutral-900/90 backdrop-blur-xl p-6 rounded-2xl shadow-2xl w-full max-w-md border border-neutral-700 flex flex-col"
79+
>
80+
<header className="text-center mb-6">
81+
<h2 className="text-2xl font-bold text-white flex items-center justify-center gap-2">
82+
<IconSparkles className="text-brand-orange" />
83+
Upgrade to Pro
84+
</h2>
85+
<p className="text-neutral-400 mt-2">
86+
For professionals who want to conquer their day.
87+
</p>
88+
</header>
89+
<main className="space-y-3">
90+
{proPlanFeatures.map((feature) => (
91+
<div
92+
key={feature.name}
93+
className="flex items-start gap-3"
94+
>
95+
<IconCheck
96+
size={20}
97+
className="text-green-400 flex-shrink-0 mt-0.5"
98+
/>
99+
<div>
100+
<p className="text-white font-medium">
101+
{feature.name}
102+
</p>
103+
<p className="text-neutral-400 text-sm">
104+
{feature.limit}
105+
</p>
106+
</div>
107+
</div>
108+
))}
109+
</main>
110+
<footer className="mt-8 flex flex-col gap-2">
111+
<button
112+
onClick={handleUpgrade}
113+
className="w-full py-3 px-5 rounded-lg bg-brand-orange hover:bg-brand-orange/90 text-brand-black font-semibold transition-colors"
114+
>
115+
Upgrade to Pro - $9/month
116+
</button>
117+
<button
118+
onClick={onClose}
119+
className="w-full py-2 px-5 rounded-lg hover:bg-neutral-800 text-sm font-medium text-neutral-400"
120+
>
121+
Not now
122+
</button>
123+
</footer>
124+
</motion.div>
125+
</motion.div>
126+
)}
127+
</AnimatePresence>
128+
)
129+
}
130+
31131
const UserProfileSection = ({ isCollapsed, user }) => {
32132
const [isUserMenuOpen, setUserMenuOpen] = useState(false)
33133
const userMenuRef = useRef(null)
@@ -233,6 +333,7 @@ const SidebarContent = ({
233333
const pathname = usePathname()
234334
const [isHelpMenuOpen, setHelpMenuOpen] = useState(false)
235335
const [isVideoModalOpen, setVideoModalOpen] = useState(false)
336+
const [isUpgradeModalOpen, setUpgradeModalOpen] = useState(false)
236337
const router = useRouter()
237338

238339
// CHANGED: Use the environment variable for the namespace
@@ -269,6 +370,10 @@ const SidebarContent = ({
269370

270371
return (
271372
<div className="flex flex-col h-full w-full overflow-y-auto custom-scrollbar">
373+
<UpgradeToProModal
374+
isOpen={isUpgradeModalOpen}
375+
onClose={() => setUpgradeModalOpen(false)}
376+
/>
272377
<AnimatePresence>
273378
{isVideoModalOpen && (
274379
<HelpVideoModal onClose={() => setVideoModalOpen(false)} />
@@ -341,9 +446,8 @@ const SidebarContent = ({
341446
</button>
342447

343448
{!isPro && (
344-
<a
345-
href={dashboardUrl}
346-
rel="noopener noreferrer"
449+
<button
450+
onClick={() => setUpgradeModalOpen(true)}
347451
className={cn(
348452
"w-full bg-neutral-800/40 border border-neutral-700/80 rounded-lg p-2.5 text-left mb-2 hover:bg-neutral-800/80 transition-colors",
349453
isCollapsed && "flex justify-center"
@@ -371,7 +475,7 @@ const SidebarContent = ({
371475
)}
372476
</AnimatePresence>
373477
</div>
374-
</a>
478+
</button>
375479
)}
376480

377481
<nav className="flex flex-col gap-1 flex-grow overflow-hidden">

src/client/components/tasks/RecurringTaskDetails.js

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,110 @@
11
"use client"
22

3-
import React from "react"
3+
import React, { useState } from "react"
44
import { format, parseISO } from "date-fns"
55
import { taskStatusColors, priorityMap } from "./constants"
66
import { cn } from "@utils/cn"
77
import CollapsibleSection from "./CollapsibleSection"
88
import ExecutionUpdate from "./ExecutionUpdate"
99
import ReactMarkdown from "react-markdown"
10+
import toast from "react-hot-toast"
11+
import { IconLoader } from "@tabler/icons-react"
12+
13+
// This component is copied from TaskDetailsContent.js to handle clarification questions
14+
// within recurring task runs. It's modified to use the run's status.
15+
const QnaSection = ({ questions, task, onAnswerClarifications, runStatus }) => {
16+
const [answers, setAnswers] = useState({})
17+
const [isSubmitting, setIsSubmitting] = useState(false)
18+
// Use the status of the specific run to determine if input is needed.
19+
const isInputMode = runStatus === "clarification_pending"
20+
21+
const handleAnswerChange = (questionId, text) => {
22+
setAnswers((prev) => ({ ...prev, [questionId]: text }))
23+
}
24+
25+
const handleSubmit = async () => {
26+
const unansweredQuestions = questions.filter(
27+
(q) => !answers[q.question_id]?.trim()
28+
)
29+
if (unansweredQuestions.length > 0) {
30+
toast.error("Please answer all questions before submitting.")
31+
return
32+
}
33+
34+
setIsSubmitting(true)
35+
const answersPayload = Object.entries(answers).map(
36+
([question_id, answer_text]) => ({
37+
question_id,
38+
answer_text
39+
})
40+
)
41+
await onAnswerClarifications(task.task_id, answersPayload)
42+
setIsSubmitting(false)
43+
}
44+
45+
return (
46+
<div>
47+
<h4 className="font-semibold text-neutral-300 mb-2">
48+
Clarifying Questions
49+
</h4>
50+
<div
51+
className={cn(
52+
"space-y-4 p-4 rounded-lg border",
53+
isInputMode
54+
? "bg-yellow-500/10 border-yellow-500/20"
55+
: "bg-neutral-800/20 border-neutral-700/50"
56+
)}
57+
>
58+
{questions.map((q, index) => (
59+
<div key={q.question_id || index}>
60+
<label className="block text-sm font-medium text-neutral-300 mb-2">
61+
{q.text}
62+
</label>
63+
{isInputMode ? (
64+
<textarea
65+
value={answers[q.question_id] || ""}
66+
onChange={(e) =>
67+
handleAnswerChange(
68+
q.question_id,
69+
e.target.value
70+
)
71+
}
72+
rows={2}
73+
className="w-full p-2 bg-neutral-800 border border-neutral-700 rounded-md text-sm text-white transition-colors focus:border-yellow-400 focus:ring-0"
74+
placeholder="Your answer..."
75+
/>
76+
) : (
77+
<p className="text-sm text-neutral-100 p-2 bg-neutral-900/50 rounded-md whitespace-pre-wrap">
78+
{q.answer || (
79+
<span className="italic text-neutral-500">
80+
No answer provided.
81+
</span>
82+
)}
83+
</p>
84+
)}
85+
</div>
86+
))}
87+
{isInputMode && (
88+
<div className="flex justify-end">
89+
<button
90+
onClick={handleSubmit}
91+
disabled={isSubmitting}
92+
className="px-4 py-2 text-sm font-semibold bg-yellow-400 text-black rounded-md hover:bg-yellow-300 disabled:opacity-50 flex items-center gap-2"
93+
>
94+
{isSubmitting && (
95+
<IconLoader
96+
size={16}
97+
className="animate-spin"
98+
/>
99+
)}
100+
{isSubmitting ? "Submitting..." : "Submit Answers"}
101+
</button>
102+
</div>
103+
)}
104+
</div>
105+
</div>
106+
)
107+
}
10108

11109
const RecurringTaskDetails = ({ task, onAnswerClarifications }) => {
12110
if (!task) return null
@@ -156,6 +254,20 @@ const RecurringTaskDetails = ({ task, onAnswerClarifications }) => {
156254
defaultOpen={index === 0}
157255
>
158256
<div className="bg-neutral-800/50 p-4 rounded-lg border border-neutral-700/50 space-y-4 mt-2">
257+
{run.clarifying_questions &&
258+
run.clarifying_questions
259+
.length > 0 && (
260+
<QnaSection
261+
questions={
262+
run.clarifying_questions
263+
}
264+
task={task}
265+
onAnswerClarifications={
266+
onAnswerClarifications
267+
}
268+
runStatus={run.status}
269+
/>
270+
)}
159271
{run.plan &&
160272
run.plan.length > 0 && (
161273
<div>

src/client/components/tasks/TaskDetailsContent.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,8 @@ const TaskDetailsContent = ({
358358
handleStepChange,
359359
allTools,
360360
integrations,
361-
onSendChatMessage
361+
onSendChatMessage,
362+
onAnswerClarifications
362363
}) => {
363364
if (!task) {
364365
return null

src/client/components/tasks/TaskDetailsPanel.js

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ const TaskDetailsPanel = ({
3232
onRerun,
3333
onArchiveTask,
3434
className,
35-
onSendChatMessage
35+
onSendChatMessage,
36+
onAnswerClarifications
3637
}) => {
3738
const [isEditing, setIsEditing] = useState(false)
3839
const [editableTask, setEditableTask] = useState(task)
@@ -211,26 +212,19 @@ const TaskDetailsPanel = ({
211212
handleStepChange={handleStepChange}
212213
allTools={allTools}
213214
integrations={integrations}
214-
onSendChatMessage={onSendChatMessage}
215215
/>
216216
) : scheduleType === "recurring" ? (
217217
<RecurringTaskDetails
218218
task={task}
219219
onAnswerClarifications={onAnswerClarifications}
220220
/>
221+
) : scheduleType === "triggered" ? (
222+
<TriggeredTaskDetails task={task} />
221223
) : (
222224
<TaskDetailsContent
223225
task={task}
224-
isEditing={isEditing}
225-
editableTask={editableTask}
226-
handleFieldChange={handleFieldChange}
227-
handleScheduleChange={handleScheduleChange}
228-
handleAddStep={handleAddStep}
229-
handleRemoveStep={handleRemoveStep}
230-
handleStepChange={handleStepChange}
231-
allTools={allTools}
232-
integrations={integrations}
233226
onSendChatMessage={onSendChatMessage}
227+
onAnswerClarifications={onAnswerClarifications}
234228
/>
235229
)}
236230
</main>

src/client/utils/taskUtils.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,13 @@ export function getDisplayName(task) {
6666
* @returns {Date|null} The next run date object or null.
6767
*/
6868
export function calculateNextRun(schedule, createdAt, runs) {
69-
// FIX: Check that schedule.time is a string before trying to split it.
70-
// This handles cases where a recurring task is created without a specific time.
7169
if (
7270
!schedule ||
7371
schedule.type !== "recurring" ||
74-
typeof schedule.time !== "string"
72+
// FIX: Check that schedule.time is a string before trying to split it.
73+
// This handles cases where a recurring task is created without a specific time.
74+
typeof schedule.time !== "string" ||
75+
!schedule.time.includes(":")
7576
) {
7677
return null
7778
}

0 commit comments

Comments
 (0)