Skip to content

Commit 80c2552

Browse files
authored
@ assistants beta (mckaywrigley#1432)
* ui for assistant @ * add assistant to messages * add assistant id to message * improve source styling * more style modifiation * round image * minor schema check * add validation to tools * context length * @ ready for beta * fix image bug
1 parent 73f7d7b commit 80c2552

33 files changed

+682
-250
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ next-env.d.ts
3939
seed.sql
4040
.VSCodeCounter
4141
tool-schemas
42+
prompts
4243

4344
sw.js
4445
sw.js.map

app/api/chat/tools/route.ts

+30-26
Original file line numberDiff line numberDiff line change
@@ -29,36 +29,40 @@ export async function POST(request: Request) {
2929
let schemaDetails = []
3030

3131
for (const selectedTool of selectedTools) {
32-
const convertedSchema = await openapiToFunctions(
33-
JSON.parse(selectedTool.schema as string)
34-
)
35-
const tools = convertedSchema.functions || []
36-
allTools = allTools.concat(tools)
37-
38-
const routeMap = convertedSchema.routes.reduce(
39-
(map: Record<string, string>, route) => {
40-
map[route.path.replace(/{(\w+)}/g, ":$1")] = route.operationId
41-
return map
42-
},
43-
{}
44-
)
45-
46-
allRouteMaps = { ...allRouteMaps, ...routeMap }
47-
48-
schemaDetails.push({
49-
title: convertedSchema.info.title,
50-
description: convertedSchema.info.description,
51-
url: convertedSchema.info.server,
52-
headers: selectedTool.custom_headers,
53-
routeMap,
54-
request_in_body: selectedTool.request_in_body
55-
})
32+
try {
33+
const convertedSchema = await openapiToFunctions(
34+
JSON.parse(selectedTool.schema as string)
35+
)
36+
const tools = convertedSchema.functions || []
37+
allTools = allTools.concat(tools)
38+
39+
const routeMap = convertedSchema.routes.reduce(
40+
(map: Record<string, string>, route) => {
41+
map[route.path.replace(/{(\w+)}/g, ":$1")] = route.operationId
42+
return map
43+
},
44+
{}
45+
)
46+
47+
allRouteMaps = { ...allRouteMaps, ...routeMap }
48+
49+
schemaDetails.push({
50+
title: convertedSchema.info.title,
51+
description: convertedSchema.info.description,
52+
url: convertedSchema.info.server,
53+
headers: selectedTool.custom_headers,
54+
routeMap,
55+
requestInBody: convertedSchema.routes[0].requestInBody
56+
})
57+
} catch (error: any) {
58+
console.error("Error converting schema", error)
59+
}
5660
}
5761

5862
const firstResponse = await openai.chat.completions.create({
5963
model: chatSettings.model as ChatCompletionCreateParamsBase["model"],
6064
messages,
61-
tools: allTools
65+
tools: allTools.length > 0 ? allTools : undefined
6266
})
6367

6468
const message = firstResponse.choices[0].message
@@ -104,7 +108,7 @@ export async function POST(request: Request) {
104108
}
105109

106110
// Determine if the request should be in the body or as a query
107-
const isRequestInBody = schemaDetail.request_in_body // Moved this line up to the loop
111+
const isRequestInBody = schemaDetail.requestInBody
108112
let data = {}
109113

110114
if (isRequestInBody) {

components/chat/assistant-picker.tsx

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { ChatbotUIContext } from "@/context/context"
2+
import { Tables } from "@/supabase/types"
3+
import { IconRobotFace } from "@tabler/icons-react"
4+
import Image from "next/image"
5+
import { FC, useContext, useEffect, useRef } from "react"
6+
import { usePromptAndCommand } from "./chat-hooks/use-prompt-and-command"
7+
8+
interface AssistantPickerProps {}
9+
10+
export const AssistantPicker: FC<AssistantPickerProps> = ({}) => {
11+
const {
12+
assistants,
13+
assistantImages,
14+
focusAssistant,
15+
atCommand,
16+
isAssistantPickerOpen,
17+
setIsAssistantPickerOpen
18+
} = useContext(ChatbotUIContext)
19+
20+
const { handleSelectAssistant } = usePromptAndCommand()
21+
22+
const itemsRef = useRef<(HTMLDivElement | null)[]>([])
23+
24+
useEffect(() => {
25+
if (focusAssistant && itemsRef.current[0]) {
26+
itemsRef.current[0].focus()
27+
}
28+
}, [focusAssistant])
29+
30+
const filteredAssistants = assistants.filter(assistant =>
31+
assistant.name.toLowerCase().includes(atCommand.toLowerCase())
32+
)
33+
34+
const handleOpenChange = (isOpen: boolean) => {
35+
setIsAssistantPickerOpen(isOpen)
36+
}
37+
38+
const callSelectAssistant = (assistant: Tables<"assistants">) => {
39+
handleSelectAssistant(assistant)
40+
handleOpenChange(false)
41+
}
42+
43+
const getKeyDownHandler =
44+
(index: number) => (e: React.KeyboardEvent<HTMLDivElement>) => {
45+
if (e.key === "Backspace") {
46+
e.preventDefault()
47+
handleOpenChange(false)
48+
} else if (e.key === "Enter") {
49+
e.preventDefault()
50+
callSelectAssistant(filteredAssistants[index])
51+
} else if (
52+
(e.key === "Tab" || e.key === "ArrowDown") &&
53+
!e.shiftKey &&
54+
index === filteredAssistants.length - 1
55+
) {
56+
e.preventDefault()
57+
itemsRef.current[0]?.focus()
58+
} else if (e.key === "ArrowUp" && !e.shiftKey && index === 0) {
59+
// go to last element if arrow up is pressed on first element
60+
e.preventDefault()
61+
itemsRef.current[itemsRef.current.length - 1]?.focus()
62+
} else if (e.key === "ArrowUp") {
63+
e.preventDefault()
64+
const prevIndex =
65+
index - 1 >= 0 ? index - 1 : itemsRef.current.length - 1
66+
itemsRef.current[prevIndex]?.focus()
67+
} else if (e.key === "ArrowDown") {
68+
e.preventDefault()
69+
const nextIndex = index + 1 < itemsRef.current.length ? index + 1 : 0
70+
itemsRef.current[nextIndex]?.focus()
71+
}
72+
}
73+
74+
return (
75+
<>
76+
{isAssistantPickerOpen && (
77+
<div className="bg-background flex flex-col space-y-1 rounded-xl border-2 p-2 text-sm">
78+
{filteredAssistants.length === 0 ? (
79+
<div className="text-md flex h-14 cursor-pointer items-center justify-center italic hover:opacity-50">
80+
No matching assistants.
81+
</div>
82+
) : (
83+
<>
84+
{filteredAssistants.map((item, index) => (
85+
<div
86+
key={item.id}
87+
ref={ref => {
88+
itemsRef.current[index] = ref
89+
}}
90+
tabIndex={0}
91+
className="hover:bg-accent focus:bg-accent flex cursor-pointer items-center rounded p-2 focus:outline-none"
92+
onClick={() =>
93+
callSelectAssistant(item as Tables<"assistants">)
94+
}
95+
onKeyDown={getKeyDownHandler(index)}
96+
>
97+
{item.image_path ? (
98+
<Image
99+
src={
100+
assistantImages.find(
101+
image => image.path === item.image_path
102+
)?.url || ""
103+
}
104+
alt={item.name}
105+
width={32}
106+
height={32}
107+
className="rounded"
108+
/>
109+
) : (
110+
<IconRobotFace size={32} />
111+
)}
112+
113+
<div className="ml-3 flex flex-col">
114+
<div className="font-bold">{item.name}</div>
115+
116+
<div className="truncate text-sm opacity-80">
117+
{item.description || "No description."}
118+
</div>
119+
</div>
120+
</div>
121+
))}
122+
</>
123+
)}
124+
</div>
125+
)}
126+
</>
127+
)
128+
}

components/chat/chat-command-input.tsx

+9-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ChatbotUIContext } from "@/context/context"
22
import { FC, useContext } from "react"
3+
import { AssistantPicker } from "./assistant-picker"
34
import { usePromptAndCommand } from "./chat-hooks/use-prompt-and-command"
45
import { FilePicker } from "./file-picker"
56
import { PromptPicker } from "./prompt-picker"
@@ -12,9 +13,9 @@ export const ChatCommandInput: FC<ChatCommandInputProps> = ({}) => {
1213
newMessageFiles,
1314
chatFiles,
1415
slashCommand,
15-
isAtPickerOpen,
16-
setIsAtPickerOpen,
17-
atCommand,
16+
isFilePickerOpen,
17+
setIsFilePickerOpen,
18+
hashtagCommand,
1819
focusPrompt,
1920
focusFile
2021
} = useContext(ChatbotUIContext)
@@ -27,9 +28,9 @@ export const ChatCommandInput: FC<ChatCommandInputProps> = ({}) => {
2728
<PromptPicker />
2829

2930
<FilePicker
30-
isOpen={isAtPickerOpen}
31-
searchQuery={atCommand}
32-
onOpenChange={setIsAtPickerOpen}
31+
isOpen={isFilePickerOpen}
32+
searchQuery={hashtagCommand}
33+
onOpenChange={setIsFilePickerOpen}
3334
selectedFileIds={[...newMessageFiles, ...chatFiles].map(
3435
file => file.id
3536
)}
@@ -40,6 +41,8 @@ export const ChatCommandInput: FC<ChatCommandInputProps> = ({}) => {
4041
/>
4142

4243
<ToolPicker />
44+
45+
<AssistantPicker />
4346
</>
4447
)
4548
}

components/chat/chat-files-display.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ export const ChatFilesDisplay: FC<ChatFilesDisplayProps> = ({}) => {
226226
</>
227227
) : (
228228
combinedMessageFiles.length > 0 && (
229-
<div className="mb-4 flex w-full items-center justify-center space-x-2">
229+
<div className="flex w-full items-center justify-center space-x-2">
230230
<Button
231231
className="flex h-[32px] w-[140px] space-x-2"
232232
onClick={() => setShowFilesDisplay(true)}

components/chat/chat-helpers/index.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,13 @@ export const createTempMessages = (
8585
chatSettings: ChatSettings,
8686
b64Images: string[],
8787
isRegeneration: boolean,
88-
setChatMessages: React.Dispatch<React.SetStateAction<ChatMessage[]>>
88+
setChatMessages: React.Dispatch<React.SetStateAction<ChatMessage[]>>,
89+
selectedAssistant: Tables<"assistants"> | null
8990
) => {
9091
let tempUserChatMessage: ChatMessage = {
9192
message: {
9293
chat_id: "",
94+
assistant_id: null,
9395
content: messageContent,
9496
created_at: "",
9597
id: uuidv4(),
@@ -106,6 +108,7 @@ export const createTempMessages = (
106108
let tempAssistantChatMessage: ChatMessage = {
107109
message: {
108110
chat_id: "",
111+
assistant_id: selectedAssistant?.id || null,
109112
content: "",
110113
created_at: "",
111114
id: uuidv4(),
@@ -398,10 +401,12 @@ export const handleCreateMessages = async (
398401
setChatFileItems: React.Dispatch<
399402
React.SetStateAction<Tables<"file_items">[]>
400403
>,
401-
setChatImages: React.Dispatch<React.SetStateAction<MessageImage[]>>
404+
setChatImages: React.Dispatch<React.SetStateAction<MessageImage[]>>,
405+
selectedAssistant: Tables<"assistants"> | null
402406
) => {
403407
const finalUserMessage: TablesInsert<"messages"> = {
404408
chat_id: currentChat.id,
409+
assistant_id: null,
405410
user_id: profile.user_id,
406411
content: messageContent,
407412
model: modelData.modelId,
@@ -412,6 +417,7 @@ export const handleCreateMessages = async (
412417

413418
const finalAssistantMessage: TablesInsert<"messages"> = {
414419
chat_id: currentChat.id,
420+
assistant_id: selectedAssistant?.id || null,
415421
user_id: profile.user_id,
416422
content: generatedText,
417423
model: modelData.modelId,

components/chat/chat-hooks/use-chat-handler.tsx

+11-9
Original file line numberDiff line numberDiff line change
@@ -55,23 +55,23 @@ export const useChatHandler = () => {
5555
useRetrieval,
5656
sourceCount,
5757
setIsPromptPickerOpen,
58-
setIsAtPickerOpen,
58+
setIsFilePickerOpen,
5959
selectedTools,
6060
selectedPreset,
6161
setChatSettings,
6262
models,
6363
isPromptPickerOpen,
64-
isAtPickerOpen,
64+
isFilePickerOpen,
6565
isToolPickerOpen
6666
} = useContext(ChatbotUIContext)
6767

6868
const chatInputRef = useRef<HTMLTextAreaElement>(null)
6969

7070
useEffect(() => {
71-
if (!isPromptPickerOpen || !isAtPickerOpen || !isToolPickerOpen) {
71+
if (!isPromptPickerOpen || !isFilePickerOpen || !isToolPickerOpen) {
7272
chatInputRef.current?.focus()
7373
}
74-
}, [isPromptPickerOpen, isAtPickerOpen, isToolPickerOpen])
74+
}, [isPromptPickerOpen, isFilePickerOpen, isToolPickerOpen])
7575

7676
const handleNewChat = async () => {
7777
if (!selectedWorkspace) return
@@ -90,7 +90,7 @@ export const useChatHandler = () => {
9090
setNewMessageImages([])
9191
setShowFilesDisplay(false)
9292
setIsPromptPickerOpen(false)
93-
setIsAtPickerOpen(false)
93+
setIsFilePickerOpen(false)
9494

9595
setSelectedTools([])
9696
setToolInUse("none")
@@ -164,7 +164,7 @@ export const useChatHandler = () => {
164164
setUserInput("")
165165
setIsGenerating(true)
166166
setIsPromptPickerOpen(false)
167-
setIsAtPickerOpen(false)
167+
setIsFilePickerOpen(false)
168168
setNewMessageImages([])
169169

170170
const newAbortController = new AbortController()
@@ -220,7 +220,8 @@ export const useChatHandler = () => {
220220
chatSettings!,
221221
b64Images,
222222
isRegeneration,
223-
setChatMessages
223+
setChatMessages,
224+
selectedAssistant
224225
)
225226

226227
let payload: ChatPayload = {
@@ -237,7 +238,7 @@ export const useChatHandler = () => {
237238
let generatedText = ""
238239

239240
if (selectedTools.length > 0) {
240-
setToolInUse(selectedTools.length > 1 ? "Tools" : selectedTools[0].name)
241+
setToolInUse("Tools")
241242

242243
const formattedMessages = await buildFinalMessages(
243244
payload,
@@ -340,7 +341,8 @@ export const useChatHandler = () => {
340341
retrievedFileItems,
341342
setChatMessages,
342343
setChatFileItems,
343-
setChatImages
344+
setChatImages,
345+
selectedAssistant
344346
)
345347

346348
setIsGenerating(false)

0 commit comments

Comments
 (0)