Skip to content

Commit f1f9f49

Browse files
committed
Add new tasks suggestions flow to lib and UI
UI can use some extra work, but it's mostly OK for now
1 parent d1cfa7c commit f1f9f49

File tree

12 files changed

+479
-7
lines changed

12 files changed

+479
-7
lines changed

app/components/catalyst/listbox.tsx

+218
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import * as Headless from '@headlessui/react';
2+
import clsx from 'clsx';
3+
import { Fragment } from 'react';
4+
5+
export function Listbox<T>({
6+
className,
7+
optionsClassName,
8+
placeholder,
9+
autoFocus,
10+
'aria-label': ariaLabel,
11+
children: options,
12+
...props
13+
}: {
14+
className?: string;
15+
optionsClassName?: string;
16+
placeholder?: React.ReactNode;
17+
autoFocus?: boolean;
18+
'aria-label'?: string;
19+
children?: React.ReactNode;
20+
} & Omit<Headless.ListboxProps<typeof Fragment, T>, 'as' | 'multiple'>) {
21+
return (
22+
<Headless.Listbox {...props} multiple={false}>
23+
<Headless.ListboxButton
24+
autoFocus={autoFocus}
25+
data-slot="control"
26+
aria-label={ariaLabel}
27+
className={clsx([
28+
className,
29+
// Basic layout
30+
'group relative block w-full',
31+
// Background color + shadow applied to inset pseudo element, so shadow blends with border in light mode
32+
'before:absolute before:inset-px before:rounded-[calc(theme(borderRadius.lg)-1px)] before:bg-white before:shadow',
33+
// Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
34+
'dark:before:hidden',
35+
// Hide default focus styles
36+
'focus:outline-none',
37+
// Focus ring
38+
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-inset after:ring-transparent after:data-[focus]:ring-2 after:data-[focus]:ring-blue-500',
39+
// Disabled state
40+
'data-[disabled]:opacity-50 before:data-[disabled]:bg-zinc-950/5 before:data-[disabled]:shadow-none',
41+
])}
42+
>
43+
<Headless.ListboxSelectedOption
44+
as="span"
45+
options={options}
46+
placeholder={
47+
placeholder && (
48+
<span className="block truncate text-zinc-500">
49+
{placeholder}
50+
</span>
51+
)
52+
}
53+
className={clsx([
54+
// Basic layout
55+
'relative block w-full appearance-none rounded-lg py-[calc(theme(spacing[2.5])-1px)] sm:py-[calc(theme(spacing[1.5])-1px)]',
56+
// Set minimum height for when no value is selected
57+
'min-h-11 sm:min-h-9',
58+
// Horizontal padding
59+
'pl-[calc(theme(spacing[3.5])-1px)] pr-[calc(theme(spacing.7)-1px)] sm:pl-[calc(theme(spacing.3)-1px)]',
60+
// Typography
61+
'text-left text-base/6 text-zinc-950 placeholder:text-zinc-500 sm:text-sm/6 dark:text-white forced-colors:text-[CanvasText]',
62+
// Border
63+
'border border-zinc-950/10 group-data-[active]:border-zinc-950/20 group-data-[hover]:border-zinc-950/20 dark:border-white/10 dark:group-data-[active]:border-white/20 dark:group-data-[hover]:border-white/20',
64+
// Background color
65+
'bg-transparent dark:bg-white/5',
66+
// Invalid state
67+
'group-data-[invalid]:border-red-500 group-data-[invalid]:group-data-[hover]:border-red-500 group-data-[invalid]:dark:border-red-600 group-data-[invalid]:data-[hover]:dark:border-red-600',
68+
// Disabled state
69+
'group-data-[disabled]:border-zinc-950/20 group-data-[disabled]:opacity-100 group-data-[disabled]:dark:border-white/15 group-data-[disabled]:dark:bg-white/[2.5%] dark:data-[hover]:group-data-[disabled]:border-white/15',
70+
])}
71+
/>
72+
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
73+
<svg
74+
className="size-5 stroke-zinc-500 group-data-[disabled]:stroke-zinc-600 sm:size-4 dark:stroke-zinc-400 forced-colors:stroke-[CanvasText]"
75+
viewBox="0 0 16 16"
76+
aria-hidden="true"
77+
fill="none"
78+
>
79+
<path
80+
d="M5.75 10.75L8 13L10.25 10.75"
81+
strokeWidth={1.5}
82+
strokeLinecap="round"
83+
strokeLinejoin="round"
84+
/>
85+
<path
86+
d="M10.25 5.25L8 3L5.75 5.25"
87+
strokeWidth={1.5}
88+
strokeLinecap="round"
89+
strokeLinejoin="round"
90+
/>
91+
</svg>
92+
</span>
93+
</Headless.ListboxButton>
94+
<Headless.ListboxOptions
95+
transition
96+
anchor="selection start"
97+
className={clsx(
98+
optionsClassName,
99+
// Anchor positioning
100+
'[--anchor-offset:-1.625rem] [--anchor-padding:theme(spacing.4)] sm:[--anchor-offset:-1.375rem]',
101+
// Base styles
102+
'isolate w-max min-w-[calc(var(--button-width)+1.75rem)] select-none scroll-py-1 rounded-xl p-1',
103+
// Invisible border that is only visible in `forced-colors` mode for accessibility purposes
104+
'outline outline-1 outline-transparent focus:outline-none',
105+
// Handle scrolling when menu won't fit in viewport
106+
'overflow-y-scroll overscroll-contain',
107+
// Popover background
108+
'bg-white/75 backdrop-blur-xl dark:bg-zinc-800/75',
109+
// Shadows
110+
'shadow-lg ring-1 ring-zinc-950/10 dark:ring-inset dark:ring-white/10',
111+
// Transitions
112+
'transition-opacity duration-100 ease-in data-[transition]:pointer-events-none data-[closed]:data-[leave]:opacity-0'
113+
)}
114+
>
115+
{options}
116+
</Headless.ListboxOptions>
117+
</Headless.Listbox>
118+
);
119+
}
120+
121+
export function ListboxOption<T>({
122+
children,
123+
className,
124+
...props
125+
}: { className?: string; children?: React.ReactNode } & Omit<
126+
Headless.ListboxOptionProps<'div', T>,
127+
'as' | 'className'
128+
>) {
129+
const sharedClasses = clsx(
130+
// Base
131+
'flex min-w-0 items-center',
132+
// Icons
133+
'[&>[data-slot=icon]]:size-5 [&>[data-slot=icon]]:shrink-0 sm:[&>[data-slot=icon]]:size-4',
134+
'[&>[data-slot=icon]]:text-zinc-500 [&>[data-slot=icon]]:group-data-[focus]/option:text-white [&>[data-slot=icon]]:dark:text-zinc-400',
135+
'forced-colors:[&>[data-slot=icon]]:text-[CanvasText] forced-colors:[&>[data-slot=icon]]:group-data-[focus]/option:text-[Canvas]',
136+
// Avatars
137+
'[&>[data-slot=avatar]]:-mx-0.5 [&>[data-slot=avatar]]:size-6 sm:[&>[data-slot=avatar]]:size-5'
138+
);
139+
140+
return (
141+
<Headless.ListboxOption as={Fragment} {...props}>
142+
{({ selectedOption }) => {
143+
if (selectedOption) {
144+
return (
145+
<div className={clsx(className, sharedClasses)}>{children}</div>
146+
);
147+
}
148+
149+
return (
150+
<div
151+
className={clsx(
152+
// Basic layout
153+
'group/option grid cursor-default grid-cols-[theme(spacing.5),1fr] items-baseline gap-x-2 rounded-lg py-2.5 pl-2 pr-3.5 sm:grid-cols-[theme(spacing.4),1fr] sm:py-1.5 sm:pl-1.5 sm:pr-3',
154+
// Typography
155+
'text-base/6 text-zinc-950 sm:text-sm/6 dark:text-white forced-colors:text-[CanvasText]',
156+
// Focus
157+
'outline-none data-[focus]:bg-blue-500 data-[focus]:text-white',
158+
// Forced colors mode
159+
'forced-color-adjust-none forced-colors:data-[focus]:bg-[Highlight] forced-colors:data-[focus]:text-[HighlightText]',
160+
// Disabled
161+
'data-[disabled]:opacity-50'
162+
)}
163+
>
164+
<svg
165+
className="relative hidden size-5 self-center stroke-current group-data-[selected]/option:inline sm:size-4"
166+
viewBox="0 0 16 16"
167+
fill="none"
168+
aria-hidden="true"
169+
>
170+
<path
171+
d="M4 8.5l3 3L12 4"
172+
strokeWidth={1.5}
173+
strokeLinecap="round"
174+
strokeLinejoin="round"
175+
/>
176+
</svg>
177+
<span className={clsx(className, sharedClasses, 'col-start-2')}>
178+
{children}
179+
</span>
180+
</div>
181+
);
182+
}}
183+
</Headless.ListboxOption>
184+
);
185+
}
186+
187+
export function ListboxLabel({
188+
className,
189+
...props
190+
}: React.ComponentPropsWithoutRef<'span'>) {
191+
return (
192+
<span
193+
{...props}
194+
className={clsx(
195+
className,
196+
'ml-2.5 truncate first:ml-0 sm:ml-2 sm:first:ml-0'
197+
)}
198+
/>
199+
);
200+
}
201+
202+
export function ListboxDescription({
203+
className,
204+
children,
205+
...props
206+
}: React.ComponentPropsWithoutRef<'span'>) {
207+
return (
208+
<span
209+
{...props}
210+
className={clsx(
211+
className,
212+
'flex flex-1 overflow-hidden text-zinc-500 before:w-2 before:min-w-0 before:shrink group-data-[focus]/option:text-white dark:text-zinc-400'
213+
)}
214+
>
215+
<span className="flex-1 truncate">{children}</span>
216+
</span>
217+
);
218+
}

app/components/pipeline/GeneratePipeline.tsx

+62-7
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { Field, FieldGroup, Label } from '~/components/catalyst/fieldset';
1313
import { Textarea } from '~/components/catalyst/textarea';
1414
import { Spinner } from '~/components/Spinner';
1515
import { useLitlytics } from '~/store/WithLitLytics';
16+
import { Listbox, ListboxOption } from '../catalyst/listbox';
1617
import { RefinePipeline } from './RefinePipeline';
1718

1819
const tabClass = clsx(
@@ -26,6 +27,7 @@ const tabClass = clsx(
2627
export default function GeneratePipeline() {
2728
const { litlytics, pipeline, setPipeline, pipelineStatus } = useLitlytics();
2829
const [selectedTab, setSelectedTab] = useState(0);
30+
const [selectedTask, setSelectedTask] = useState<string>('');
2931
const [isOpen, setIsOpen] = useState(false);
3032
const [loading, setLoading] = useState(false);
3133
const [error, setError] = useState<Error>();
@@ -46,6 +48,23 @@ export default function GeneratePipeline() {
4648
}
4749
};
4850

51+
const suggestTasks = async () => {
52+
try {
53+
setLoading(true);
54+
setError(undefined);
55+
56+
// generate plan from LLM
57+
const newPipeline = await litlytics.suggestTasks();
58+
setPipeline(newPipeline);
59+
setSelectedTask(newPipeline.pipelineTasks?.at(0) ?? '');
60+
setLoading(false);
61+
setSelectedTab(1);
62+
} catch (err) {
63+
setError(err as Error);
64+
setLoading(false);
65+
}
66+
};
67+
4968
const closeDialog = () => {
5069
if (
5170
loading ||
@@ -108,16 +127,52 @@ export default function GeneratePipeline() {
108127
/>
109128
</Field>
110129
</FieldGroup>
111-
<div className="flex justify-end">
112-
<Button onClick={runPlan} disabled={loading} className="mt-2">
113-
{loading && (
114-
<div className="flex items-center">
115-
<Spinner className="h-5 w-5" />
116-
</div>
117-
)}
130+
<div className="flex justify-between items-center mt-2">
131+
{!pipeline.pipelineTasks?.length && (
132+
<Button onClick={suggestTasks} plain disabled={loading}>
133+
Suggest tasks using test docs
134+
</Button>
135+
)}
136+
{loading && (
137+
<div className="flex items-center">
138+
<Spinner className="h-5 w-5" />
139+
</div>
140+
)}
141+
<Button onClick={runPlan} disabled={loading}>
118142
Plan
119143
</Button>
120144
</div>
145+
{pipeline.pipelineTasks?.length && (
146+
<div className="mt-4">
147+
<Field>
148+
<Label>Suggested tasks</Label>
149+
<Listbox
150+
className="!mt-1"
151+
optionsClassName="z-50"
152+
value={selectedTask}
153+
onChange={(e) => setSelectedTask(e)}
154+
>
155+
{pipeline.pipelineTasks.map((task) => (
156+
<ListboxOption key={task} value={task}>
157+
{task}
158+
</ListboxOption>
159+
))}
160+
</Listbox>
161+
</Field>
162+
163+
<Button
164+
onClick={() =>
165+
setPipeline({
166+
...pipeline,
167+
pipelineDescription: selectedTask,
168+
})
169+
}
170+
disabled={loading}
171+
>
172+
Use suggested task
173+
</Button>
174+
</div>
175+
)}
121176
{error && (
122177
<div className="flex items-center justify-between bg-red-400 dark:bg-red-700 rounded-xl py-1 px-2 my-2">
123178
Error planning: {error.message}

bun.lockb

28 Bytes
Binary file not shown.

packages/litlytics/doc/Document.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ export interface Doc {
66
content: string;
77
test?: boolean;
88
processingResults: StepResult[];
9+
summary?: string;
910
}

packages/litlytics/litlytics.ts

+30
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
type PipelineStatus,
2323
} from './pipeline/Pipeline';
2424
import { refinePipeline } from './pipeline/refine';
25+
import { suggestTasks } from './pipeline/suggestTasks';
2526
import { generateCodeExplain } from './step/explain';
2627
import { generateStep, type GenerateStepArgs } from './step/generate';
2728
import { refineStep } from './step/refine';
@@ -251,6 +252,35 @@ export class LitLytics {
251252
});
252253
};
253254

255+
suggestTasks = async () => {
256+
if (!this.pipeline.source?.docs?.length) {
257+
return this.pipeline;
258+
}
259+
260+
const res = await suggestTasks({
261+
litlytics: this,
262+
pipeline: this.pipeline,
263+
});
264+
265+
const newDocs = this.pipeline.source.docs.map((d) => {
266+
// try to find updated doc
267+
const upDoc = res.docs.find((newD) => newD.id === d.id);
268+
// if exists - return it
269+
if (upDoc) {
270+
return upDoc;
271+
}
272+
// otherwise - use original
273+
return d;
274+
});
275+
// update docs
276+
this.setDocs(newDocs);
277+
278+
// update pipeline tasks
279+
return this.setPipeline({
280+
pipelineTasks: res.tasks,
281+
});
282+
};
283+
254284
refinePipeline = async ({ refineRequest }: { refineRequest: string }) => {
255285
const plan = await refinePipeline({
256286
litlytics: this,

packages/litlytics/llm/llm.ts

+2
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ export async function executeOnLLM({
8080
// set key
8181
const modelObj = getModel({ provider, model, key });
8282
const { text, usage } = await generateText({
83+
// default to 4k max tokens output which is what most models support
84+
maxTokens: 4096,
8385
...modelArgs,
8486
model: modelObj,
8587
messages,

packages/litlytics/pipeline/Pipeline.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface Pipeline {
66
name: string;
77
pipelineDescription?: string;
88
pipelinePlan?: string;
9+
pipelineTasks?: string[];
910
source: SourceStep;
1011
resultDocs?: Doc[];
1112
results?: Result[];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const docToDescriptionPrompt = `You are an expert at analyzing documents.
2+
3+
Your task is to analyze given document and describe it succinctly.
4+
The information about the document will be used to suggest data science tasks that can be done with it, so make sure to include all relevant information in your response.
5+
Do not include list of tasks into your response.
6+
7+
Think the request through step-by-step inside <thinking> tags, and then provide your final response inside <output> tags.`;

0 commit comments

Comments
 (0)