Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion prisma
Submodule prisma updated 1 files
+24 −0 schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,57 @@ import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"
import Remark from "@/components/ux/remark"
import useWorkflowId from "@/lib/context/workflow-id"
import { Role, WorkflowIdComponentProps } from "@/lib/types/graph"
import { getSuggestions } from "@/lib/utils"
import { cn, getSuggestions } from "@/lib/utils"
import { Send } from "lucide-react"
import React, { useEffect, useMemo, useRef } from "react"
import React, { useCallback, useEffect, useMemo, useRef } from "react"

export default function QueryForm({ workflowData }: WorkflowIdComponentProps) {
const { queryWorkflowForm, handleSubmit, messages, handleSuggestion } =
useWorkflowId()
const {
queryWorkflowForm,
handleSubmit,
messages,
handleSuggestion,
streamedMessage,
isMessageLoading,
} = useWorkflowId()
const {
registerField,
isLoading,
formValues: {
bodyParams: { userQuery },
},
} = queryWorkflowForm

const isValid = userQuery.trim().length > 0
const isValid = userQuery.trim().length > 0 && !isMessageLoading
const divRef = useRef<HTMLDivElement | null>(null)

useEffect(() => {
divRef.current?.scrollTo({
top: divRef.current?.scrollHeight,
behavior: "smooth",
})
}, [isLoading])
}, [isMessageLoading])

const suggestions = useMemo(
() => getSuggestions(workflowData),
// eslint-disable-next-line react-hooks/exhaustive-deps
[messages, workflowData],
)

const getMessage = useCallback(
(idx: number) => {
if (
messages[idx].role === Role.AI &&
idx === messages.length - 1 &&
streamedMessage
) {
return streamedMessage
}

return messages[idx].content || "### Thinking..."
},
[messages, streamedMessage],
)

useEffect(() => {
window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" })
}, [])
Expand All @@ -49,19 +69,16 @@ export default function QueryForm({ workflowData }: WorkflowIdComponentProps) {
>
{messages.map((message, index) => (
<div key={index} className="mb-4 text-background">
{message.role === Role.AI ? (
<div className="max-w-4/5 w-4/5 justify-self-start rounded-lg bg-foreground p-3">
<Remark
markdown={
message.content || "### Thinking..."
}
/>
</div>
) : (
<div className="max-w-4/5 w-4/5 justify-self-end rounded-lg bg-foreground p-3">
{message.content}
</div>
)}
<div
className={cn(
"max-w-4/5 w-4/5 overflow-x-auto rounded-lg bg-foreground p-3",
message.role === Role.AI
? "justify-self-start"
: "justify-self-end",
)}
>
<Remark markdown={getMessage(index)} />
</div>
</div>
))}
</ScrollArea>
Expand All @@ -73,10 +90,10 @@ export default function QueryForm({ workflowData }: WorkflowIdComponentProps) {
>
<ScrollArea className="flex w-full pb-2 *:*:!flex *:*:gap-2">
<ScrollBar className="h-2" orientation="horizontal" />
{!isLoading &&
{!isMessageLoading &&
suggestions.map((suggestion, index) => (
<Button
disabled={isLoading}
disabled={isMessageLoading}
key={index}
onClick={() => handleSuggestion(suggestion)}
variant="outline"
Expand Down Expand Up @@ -106,7 +123,7 @@ export default function QueryForm({ workflowData }: WorkflowIdComponentProps) {
"flex-grow px-3 py-2 text-sm border rounded-md w-full",
placeholder:
"How are commands in src/utils/commands.ts used?",
disabled: isLoading,
disabled: isMessageLoading,
}}
/>
<Button disabled={!isValid} type="submit">
Expand Down
22 changes: 21 additions & 1 deletion src/lib/api/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type MakeAPIRequestArgs<
bodyParams: BodyT
queryParams: QueryT
customHeaders?: Headers
onlyResponse?: boolean
}

export type MakeAPIRequestRet<
Expand All @@ -32,13 +33,15 @@ export type MakeAPIRequestRet<
statusCode: StatusCode
responseData: ResponseJSON<ResponseT, ParamsT, BodyT, QueryT>
errorData: undefined
response?: Response
}
| {
hasResponse: false
hasError: true
statusCode: StatusCode
responseData: undefined
errorData: Error
response?: Response
}

export async function makeAPIRequest<
Expand All @@ -59,6 +62,7 @@ export async function makeAPIRequest<
queryParams,
bodyParams,
customHeaders = new Headers(),
onlyResponse,
} = args

let resolvedUrl = requestUrl
Expand Down Expand Up @@ -86,7 +90,7 @@ export async function makeAPIRequest<
// if (!sessionToken) {
const cookies = document.cookie.split(";")
const authCookie = cookies.find((c) =>
c.trim().startsWith("x-engaze-auth-local="),
c.trim().startsWith(LOCAL_AUTH_SESSION),
)
if (authCookie) {
sessionToken = authCookie.split("=")[1].trim()
Expand Down Expand Up @@ -135,6 +139,22 @@ export async function makeAPIRequest<
}
}

if (onlyResponse) {
return {
responseData: {} as ResponseJSON<
ResponseT,
ParamsT,
BodyT,
QueryT
>,
hasResponse: true,
hasError: false,
statusCode: statusCode,
errorData: undefined,
response: fetchResponse,
}
}

const responseJson = (await fetchResponse.json()) as ResponseJSON<
ResponseT,
ParamsT,
Expand Down
105 changes: 73 additions & 32 deletions src/lib/context/workflow-id.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client"

import { makeAPIRequest } from "@/lib/api/helpers"
import { QUERY_RESPONSE_ID } from "@/lib/constants/workflow"
import {
GetWorkflowParams,
Expand All @@ -12,25 +13,28 @@ import { useAPIRequest } from "@/lib/hooks/useAPI"
import { useRequestForm } from "@/lib/hooks/useRequestForm"
import { Message, Role } from "@/lib/types/graph"
import { useParams } from "next/navigation"
import React, { createContext, useCallback, useRef } from "react"
import React, { createContext, useCallback, useMemo, useState } from "react"

function useWorkflowIdData() {
const { projectId, workflowId } = useParams()
const messages = useRef<Message[]>([
const [messages, setMessages] = useState<Message[]>([
{
role: Role.AI,
content:
"Hello! How can I assist you today? If you have any questions or need information about your project, feel free to ask!",
},
])
const [streamedMessage, setStreamedMessage] = useState("")

function setMessages(newMessages: Message[]) {
messages.current = newMessages
}
const isMessageLoading = useMemo(
() => !!streamedMessage || !messages[messages.length - 1].content,
[streamedMessage, messages],
)

const appendMessage = useCallback((message: Message) => {
setMessages([...messages.current, message])
setMessages((prev) => [...prev, message])
}, [])

const [response, setResponse] =
React.useState<QueryWorkflowResponse | null>(null)

Expand Down Expand Up @@ -66,16 +70,8 @@ function useWorkflowIdData() {
queryParams: {},
},
responseHandlers: {
onSuccess: (data) => {
onSuccess: () => {
resetForm()
setResponse(data)
const aiMessageIndex = messages.current.length - 1
const updatedMessages = [...messages.current]
updatedMessages[aiMessageIndex] = {
role: Role.AI,
content: data.queryData.chatResponse,
}
setMessages(updatedMessages)
},
},
})
Expand All @@ -85,30 +81,73 @@ function useWorkflowIdData() {
formValues: {
bodyParams: { userQuery },
},
submitForm,
} = queryWorkflowForm

function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
e?.preventDefault()
const query = userQuery.trim()
if (!query) return

const userMessage: Message = { role: Role.USER, content: query }
const fetchStream = async (query: string) => {
const userQuery = query.trim()
if (isMessageLoading || !query) return
const userMessage: Message = { role: Role.USER, content: userQuery }
appendMessage(userMessage)

appendMessage({ role: Role.AI, content: "" })
void submitForm()
}

function handleSuggestion(suggestion: string) {
const query = suggestion.trim()
if (!query) return
setStreamedMessage("")
const { response } = await makeAPIRequest({
requestMethod: "POST",
requestUrl: "/orgs/me/projects/:projectId/workflows/:workflowId",
bodyParams: {
userQuery,
},
urlParams: {
projectId: String(projectId),
workflowId: String(workflowId),
},
queryParams: {},
onlyResponse: true,
customHeaders: new Headers({
"Content-Type": "text/plain",
}),
})

if (response) {
if (response.status !== 200) {
setMessages((prev) => {
const updatedMessages = [...prev]
updatedMessages[updatedMessages.length - 1].content =
"I'm sorry, I couldn't find the information you requested. Please try again."
return updatedMessages
})
}
if (!response.body) return
const reader = response.body.getReader()
const decoder = new TextDecoder("utf-8")
let accumulated = ""
while (true) {
await new Promise((resolve) => setTimeout(resolve, 100))
const { done, value } = await reader.read()
if (done) break

const chunk = decoder.decode(value, { stream: true })
accumulated += chunk
setStreamedMessage((prev) => prev + chunk)
}
setMessages((prev) => {
const updatedMessages = [...prev]
updatedMessages[updatedMessages.length - 1].content =
accumulated
return updatedMessages
})
setStreamedMessage("")
}
}

const userMessage: Message = { role: Role.USER, content: query }
appendMessage(userMessage)
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
e?.preventDefault()
void fetchStream(userQuery)
}

appendMessage({ role: Role.AI, content: "" })
void submitForm({ bodyParams: { userQuery: query } })
function handleSuggestion(suggestion: string) {
void fetchStream(suggestion)
}

return {
Expand All @@ -119,7 +158,9 @@ function useWorkflowIdData() {
setResponse,
handleSubmit,
handleSuggestion,
messages: messages.current,
messages,
streamedMessage,
isMessageLoading,
}
}
const WorkflowIdContext = createContext<
Expand Down
2 changes: 1 addition & 1 deletion src/lib/hooks/useAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export function useAPIRequest<
setResponseState({
isLoading: false,
...fetchResponse,
})
} as UseAPIRequestRet<ResponseT, ParamsT, BodyT, QueryT>)
} catch (error) {
setResponseState({
isLoading: false,
Expand Down
1 change: 0 additions & 1 deletion src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ export function getSuggestions(
workflowData: GetWorkflowResponse["workflowData"],
n: number = 5,
) {
console.log({ workflowData })
const symbols = workflowData.symbolNodes
.flat()
.map((node) => node?.properties?.symbolIdentifier)
Expand Down
Loading