Skip to content

Commit

Permalink
Merge pull request #3946 from signalco-io/next
Browse files Browse the repository at this point in the history
Next
  • Loading branch information
AleksandarDev authored Nov 26, 2023
2 parents 1742520 + 5c500d5 commit 751194c
Show file tree
Hide file tree
Showing 10 changed files with 319 additions and 6 deletions.
1 change: 1 addition & 0 deletions web/apps/doprocess/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ CLERK_SECRET_KEY=secret
DOPROCESS_DATABASE_HOST=secret
DOPROCESS_DATABASE_USERNAME=secret
DOPROCESS_DATABASE_PASSWORD=secret
OPENAI_API_KEY=secret
2 changes: 2 additions & 0 deletions web/apps/doprocess/app/api/dtos/dtos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export type ProcessRunDto = Omit<ProcessRun, 'publicId' | 'id' | 'processId'> &

export type ProcessTaskDefinitionDto = Omit<TaskDefinition, 'publicId' | 'id' | 'processId'> & { id: string, processId: string };

export type ProcessTaskDefinitionsSuggestionsDto = { suggestions: string[] };

export type ProcessRunTaskDto = Omit<Task, 'publicId' | 'id' | 'processId' | 'taskDefinitionId'> & { id: string, processId: string, taskDefinitionId: string };

export type DocumentDto = Omit<Document, 'publicId' | 'id'> & { id: string };
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { OpenAI } from 'openai';
import { ProcessTaskDefinitionsSuggestionsDto } from '../../../../dtos/dtos';
import { getProcess, getProcessIdByPublicId, getTaskDefinitions } from '../../../../../../src/lib/repo/processesRepository';
import { ensureUserId } from '../../../../../../src/lib/auth/apiAuth';
import { requiredParamString } from '../../../../../../src/lib/api/apiParam';

const openai = new OpenAI({
apiKey: process.env['OPENAI_API_KEY']
});

export async function GET(_request: Request, { params }: { params: { id: string } }) {
const processPublicId = requiredParamString(params.id);

const { userId } = ensureUserId();

const processId = await getProcessIdByPublicId(processPublicId);
if (processId == null)
return new Response(null, { status: 404 });

const process = await getProcess(userId, processId);
if (process == null)
return new Response(null, { status: 404 });
const taskDefinitions = await getTaskDefinitions(userId, processId);

const assistantId = 'asst_rgjTqqv25NSyZVWVaySzKnI8';
const result = await openai.beta.threads.createAndRun({
assistant_id: assistantId,
thread: {
messages: [
{ role: 'user', content: `Process \"${process.name}\":\n${taskDefinitions.map(t => '- ' + t.text).join('\n')}\n\n` }
]
}
});

let run = await openai.beta.threads.runs.retrieve(result.thread_id, result.id);
console.debug('OpenAI run:', run);

// Wait for the run to complete
const maxDuration = 20000;
const delay = 500;
let retries = 0;
while ((run.status === 'in_progress' || run.status === 'queued') && retries++ < maxDuration / delay) {
await new Promise(resolve => setTimeout(resolve, delay));
run = await openai.beta.threads.runs.retrieve(result.thread_id, run.id);
console.debug('OpenAI run in-progress/queued:', run.id, run.status);
}

console.debug('OpenAI run completed:', run.id, run.status);

// Retrieve thread messages
const messages = await openai.beta.threads.messages.list(result.thread_id);
console.debug('OpenAI run messages:', run.id, result.thread_id, JSON.stringify(messages.data));

// Construct suggestions from the messages
const suggestions = messages.data
.filter(i => i.role === 'assistant')
.flatMap(m => m.content
.map(i => i.type === 'text' ? i.text.value : null)
.filter(Boolean)
.flatMap(text => text.split('\n').filter(i => i.startsWith('- ')).map(i => i.substring(2))));

return Response.json({
suggestions
} satisfies ProcessTaskDefinitionsSuggestionsDto);
}
15 changes: 14 additions & 1 deletion web/apps/doprocess/components/processes/tasks/TaskList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useMemo } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { List } from '@signalco/ui-primitives/List';
import { NoDataPlaceholder } from '@signalco/ui/NoDataPlaceholder';
import { Loadable } from '@signalco/ui/Loadable';
Expand All @@ -15,6 +15,7 @@ import { useProcessTaskDefinitionUpdate } from '../../../src/hooks/useProcessTas
import { useProcessTaskDefinitions } from '../../../src/hooks/useProcessTaskDefinitions';
import { useProcessTaskDefinitionCreate } from '../../../src/hooks/useProcessTaskDefinitionCreate';
import { useProcessRunTasks } from '../../../src/hooks/useProcessRunTasks';
import { TaskListSuggestions } from './TaskListSuggestions';
import { TaskListItem } from './TaskListItem';

type TaskListProps = {
Expand All @@ -31,6 +32,15 @@ export function TaskList({ processId, runId, editable }: TaskListProps) {
const { data: taskDefinitions, isLoading: isLoadingTaskDefinitions, error: errorTaskDefinitions } = useProcessTaskDefinitions(processId);
const { data: tasks, isLoading: isLoadingTasks, error: errorTasks } = useProcessRunTasks(processId, runId);

const [showSuggestions, setShowSuggestions] = useState(false);
useEffect(() => {
if ((taskDefinitions?.length ?? 0) > 0) {
setTimeout(() => {
setShowSuggestions(true);
}, 500);
}
}, [taskDefinitions]);

const taskListItems = useMemo(() => {
return orderBy(taskDefinitions?.map(td => ({
taskDefinition: td,
Expand Down Expand Up @@ -116,6 +126,9 @@ export function TaskList({ processId, runId, editable }: TaskListProps) {
</List>
</SortableContext>
</DndContext>
{showSuggestions && (
<TaskListSuggestions processId={processId} />
)}
</Loadable>
);
}
113 changes: 113 additions & 0 deletions web/apps/doprocess/components/processes/tasks/TaskListSuggestions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
'use client';
import { useEffect, useState } from 'react';
import { Typography } from '@signalco/ui-primitives/Typography';
import { Stack } from '@signalco/ui-primitives/Stack';
import { List } from '@signalco/ui-primitives/List';
import { cx } from '@signalco/ui-primitives/cx';
import { Button } from '@signalco/ui-primitives/Button';
import { ListItem } from '../../shared/ListItem';
import { fetchGetProcessTaskDefinitionsSuggestions, useProcessTaskDefinitionsSuggestions } from '../../../src/hooks/useProcessTaskDefinitionsSuggestions';
import { useProcessTaskDefinitionCreate } from '../../../src/hooks/useProcessTaskDefinitionCreate';
import { ProcessTaskDefinitionsSuggestionsDto } from '../../../app/api/dtos/dtos';

export function TaskListSuggestions({ processId }: { processId: string; }) {
const [suggestions, setSuggestions] = useState<ProcessTaskDefinitionsSuggestionsDto | null | undefined>();
const [suggestionsLoading, setSuggestionsLoading] = useState(false);
const [suggestionsError, setSuggestionsError] = useState<unknown>();

const aiThinkingText = 'AI is thinking...';
const aiSuggestionsText = 'AI suggestions';

const [aiText, setAiText] = useState<string | null>(null);
const handleRequestSuggestions = async () => {
if (suggestionsLoading) return;
setSuggestionsLoading(true);
setAiText(aiThinkingText);

try {
setSuggestions(await fetchGetProcessTaskDefinitionsSuggestions(processId));

// Reset AI text (if it hasn't changed)
setAiText(aiSuggestionsText);
setTimeout(() => {
setAiText((current) => current === aiSuggestionsText ? null : current);
}, 2000);
} catch(err) {
setSuggestionsError(err);
} finally {
setSuggestionsLoading(false);
}
};

const createTaskDefinition = useProcessTaskDefinitionCreate();
const handleSuggestionSelected = async (suggestion: string) => {
await createTaskDefinition.mutateAsync({
processId,
text: suggestion,
});

// Remove suggestions from list
setSuggestions((curr) => curr ? ({
...curr,
suggestions: curr.suggestions.filter((s) => s !== suggestion),
}) : null);
}

if (suggestionsError) {
console.error(suggestionsError);
return null;
}

return (
<Stack spacing={1}>
<div className="self-end animate-in fade-in">
<Button
onClick={handleRequestSuggestions}
className={cx(
'w-[52px] gap-2 rounded-full transition-all duration-500 justify-start overflow-hidden whitespace-nowrap',
aiText === aiThinkingText && 'w-44',
aiText === aiSuggestionsText && 'w-40'
)}
startDecorator={(
<div className="h-5 w-5">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 320 320"
width={20}
height={20}
className={cx(
'fill-secondary-foreground',
suggestionsLoading && 'animate-pulse'
)}>
<path d="M297.06 130.97a79.712 79.712 0 0 0-6.85-65.48c-17.46-30.4-52.56-46.04-86.84-38.68A79.747 79.747 0 0 0 143.24 0C108.2-.08 77.11 22.48 66.33 55.82a79.754 79.754 0 0 0-53.31 38.67c-17.59 30.32-13.58 68.54 9.92 94.54a79.712 79.712 0 0 0 6.85 65.48c17.46 30.4 52.56 46.04 86.84 38.68a79.687 79.687 0 0 0 60.13 26.8c35.06.09 66.16-22.49 76.94-55.86a79.754 79.754 0 0 0 53.31-38.67c17.57-30.32 13.55-68.51-9.94-94.51zM176.78 299.08a59.77 59.77 0 0 1-38.39-13.88c.49-.26 1.34-.73 1.89-1.07l63.72-36.8a10.36 10.36 0 0 0 5.24-9.07v-89.83l26.93 15.55c.29.14.48.42.52.74v74.39c-.04 33.08-26.83 59.9-59.91 59.97zM47.94 244.05a59.71 59.71 0 0 1-7.15-40.18c.47.28 1.3.79 1.89 1.13l63.72 36.8c3.23 1.89 7.23 1.89 10.47 0l77.79-44.92v31.1c.02.32-.13.63-.38.83L129.87 266c-28.69 16.52-65.33 6.7-81.92-21.95zM31.17 104.96c7-12.16 18.05-21.46 31.21-26.29 0 .55-.03 1.52-.03 2.2v73.61c-.02 3.74 1.98 7.21 5.23 9.06l77.79 44.91L118.44 224c-.27.18-.61.21-.91.08l-64.42-37.22c-28.63-16.58-38.45-53.21-21.95-81.89zm221.26 51.49-77.79-44.92 26.93-15.54c.27-.18.61-.21.91-.08l64.42 37.19c28.68 16.57 38.51 53.26 21.94 81.94a59.94 59.94 0 0 1-31.2 26.28v-75.81c.03-3.74-1.96-7.2-5.2-9.06zm26.8-40.34c-.47-.29-1.3-.79-1.89-1.13l-63.72-36.8a10.375 10.375 0 0 0-10.47 0l-77.79 44.92V92c-.02-.32.13-.63.38-.83l64.41-37.16c28.69-16.55 65.37-6.7 81.91 22a59.95 59.95 0 0 1 7.15 40.1zm-168.51 55.43-26.94-15.55a.943.943 0 0 1-.52-.74V80.86c.02-33.12 26.89-59.96 60.01-59.94 14.01 0 27.57 4.92 38.34 13.88-.49.26-1.33.73-1.89 1.07L116 72.67a10.344 10.344 0 0 0-5.24 9.06l-.04 89.79zM125.35 140 160 119.99l34.65 20V180L160 200l-34.65-20z" />
</svg>
</div>
)}>
{aiText}
</Button>
</div>
{Boolean(suggestions?.suggestions.length) && (
<List className="divide-y rounded-lg border animate-in slide-in-from-right-4 slide-in-from-top-4">
{suggestions?.suggestions.map((suggestion) => (
<ListItem
key={suggestion}
label={suggestion}
nodeId={suggestion}
onSelected={() => handleSuggestionSelected(suggestion)}
className="w-full gap-2 px-3 text-base"
startDecorator={(
<div className="h-[18px] w-[18px] text-center">
<Typography
level="body3"
secondary
className={cx(
'[line-height:1.6em]'
)}>AI*</Typography>
</div>
)} />
))}
</List>
)}
</Stack>
);
}
5 changes: 3 additions & 2 deletions web/apps/doprocess/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@
"@signalco/lexorder": "workspace:*",
"@signalco/tailwindcss-config-signalco": "workspace:*",
"@signalco/ui": "workspace:*",
"@signalco/ui-primitives": "workspace:*",
"@signalco/ui-notifications": "workspace:*",
"@signalco/ui-icons": "workspace:*",
"@signalco/ui-notifications": "workspace:*",
"@signalco/ui-primitives": "workspace:*",
"@tanstack/react-query": "5.8.7",
"@tanstack/react-query-devtools": "5.8.7",
"@vercel/analytics": "1.1.1",
Expand All @@ -49,6 +49,7 @@
"next": "14.0.3",
"next-secure-headers": "2.2.0",
"next-themes": "0.2.1",
"openai": "4.20.0",
"react": "18.2.0",
"react-cool-inview": "3.0.1",
"react-dom": "18.2.0",
Expand Down
1 change: 1 addition & 0 deletions web/apps/doprocess/src/hooks/useProcessTaskDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function useProcessTaskDefinition(processId?: string, taskDefinitionId?:
queryFn: async () => {
if (!processId || !taskDefinitionId)
throw new Error('Process Id and Task Definition Id is required');
// TODO: Try to retrieve from local query cache first
return await fetchGetProcessTaskDefinition(processId, taskDefinitionId);
},
enabled: processId != null && taskDefinitionId != null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { UseQueryResult, useQuery } from '@tanstack/react-query';
import { ProcessTaskDefinitionsSuggestionsDto } from '../../app/api/dtos/dtos';
import { processKey } from './useProcess';

export function processTaskDefinitionsSuggestionsKey(processId?: string, nonce?: string | number) {
return [...processKey(processId), 'taskDefinitions', 'suggestions', nonce].filter(Boolean);
}

export async function fetchGetProcessTaskDefinitionsSuggestions(processId: string) {
const response = await fetch(`/api/processes/${processId}/task-definitions/suggestions`);
if (response.status === 404)
return null;
return await response.json() as ProcessTaskDefinitionsSuggestionsDto | undefined;
}

export function useProcessTaskDefinitionsSuggestions(processId?: string, nonce?: string | number): UseQueryResult<ProcessTaskDefinitionsSuggestionsDto | null | undefined, Error> {
return useQuery({
queryKey: processTaskDefinitionsSuggestionsKey(processId, nonce),
queryFn: async () => {
if (!processId)
throw new Error('Process ID is required');
return await fetchGetProcessTaskDefinitionsSuggestions(processId);
},
enabled: processId != null,
staleTime: Infinity,
});
}
3 changes: 2 additions & 1 deletion web/apps/doprocess/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"DOPROCESS_DATABASE_USERNAME",
"DOPROCESS_DATABASE_PASSWORD",
"CLERK_SECRET_KEY",
"NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY"
"NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY",
"OPENAI_API_KEY"
]
}
}
Expand Down
Loading

7 comments on commit 751194c

@vercel
Copy link

@vercel vercel bot commented on 751194c Nov 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 751194c Nov 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

signalco-ui-docs – ./web/apps/ui-docs

signalco-ui-docs.vercel.app
signalco-ui-docs-git-main-signalco.vercel.app
signalco-ui-docs-signalco.vercel.app
ui.signalco.io

@vercel
Copy link

@vercel vercel bot commented on 751194c Nov 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

signalco-blog – ./web/apps/blog

signalco-blog-signalco.vercel.app
signalco-blog-git-main-signalco.vercel.app
signalco-blog.vercel.app
blog.signalco.io

@vercel
Copy link

@vercel vercel bot commented on 751194c Nov 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

signalco-slco – ./web/apps/slco

signalco-slco-git-main-signalco.vercel.app
signalco-slco.vercel.app
signalco-slco-signalco.vercel.app
slco.signalco.io
slco.io

@vercel
Copy link

@vercel vercel bot commented on 751194c Nov 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 751194c Nov 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 751194c Nov 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

signalco-app – ./web/apps/app

signalco-app-signalco.vercel.app
signalco-app.vercel.app
signalco-app-git-main-signalco.vercel.app
app.signalco.io

Please sign in to comment.