From 42adf476ec792437d00c6b94c4e7be0676171d6d Mon Sep 17 00:00:00 2001 From: Nick Hale <4175918+njhale@users.noreply.github.com> Date: Wed, 18 Dec 2024 20:27:32 -0500 Subject: [PATCH 01/18] chore: hide atlassian oauth config from admins Hide the Atlassian OAuth integration from admins until we have tools that use it. Signed-off-by: Nick Hale <4175918+njhale@users.noreply.github.com> --- ui/admin/app/lib/model/oauthApps/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/admin/app/lib/model/oauthApps/index.ts b/ui/admin/app/lib/model/oauthApps/index.ts index 72573fb7..bd4c8589 100644 --- a/ui/admin/app/lib/model/oauthApps/index.ts +++ b/ui/admin/app/lib/model/oauthApps/index.ts @@ -2,7 +2,6 @@ import { OAuthAppSpec, OAuthProvider, } from "~/lib/model/oauthApps/oauth-helpers"; -import { AtlassianOAuthApp } from "~/lib/model/oauthApps/providers/atlassian"; import { GitHubOAuthApp } from "~/lib/model/oauthApps/providers/github"; import { GoogleOAuthApp } from "~/lib/model/oauthApps/providers/google"; import { Microsoft365OAuthApp } from "~/lib/model/oauthApps/providers/microsoft365"; @@ -11,7 +10,6 @@ import { SlackOAuthApp } from "~/lib/model/oauthApps/providers/slack"; import { EntityMeta } from "~/lib/model/primitives"; export const OAuthAppSpecMap = { - [OAuthProvider.Atlassian]: AtlassianOAuthApp, [OAuthProvider.GitHub]: GitHubOAuthApp, [OAuthProvider.Google]: GoogleOAuthApp, [OAuthProvider.Microsoft365]: Microsoft365OAuthApp, From ee2ac7ec32f066fdd4dd472ea8cdee460ebbbf22 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Wed, 18 Dec 2024 21:10:41 -0500 Subject: [PATCH 02/18] fix: update workflow tools more consistently The useEffect updating on the watch is not effectively updating the tools for workflows. This change switches back using the form. Signed-off-by: Donnie Adams --- ui/admin/app/components/tools/BasicToolForm.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/admin/app/components/tools/BasicToolForm.tsx b/ui/admin/app/components/tools/BasicToolForm.tsx index cc91fded..ae7bc258 100644 --- a/ui/admin/app/components/tools/BasicToolForm.tsx +++ b/ui/admin/app/components/tools/BasicToolForm.tsx @@ -34,7 +34,7 @@ export function BasicToolForm({ resolver: zodResolver(formSchema), defaultValues: { tools: defaultValues?.tools || [] }, }); - const { watch, getValues, reset } = form; + const { getValues, reset } = form; useEffect(() => { const unchanged = compareArrays( @@ -50,14 +50,14 @@ export function BasicToolForm({ const toolArr = useFieldArray({ control: form.control, name: "tools" }); useEffect(() => { - return watch((values) => { + return form.watch((values) => { const { data, success } = formSchema.safeParse(values); if (!success) return; onChange?.({ tools: data.tools.map((t) => t.value) }); }).unsubscribe; - }, [watch, onChange]); + }, [form, onChange]); const removeTools = (toolsToRemove: string[]) => { const indexes = toolsToRemove From 31ceab62d455bd798a069be034ea8effb1b41ac4 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Wed, 18 Dec 2024 23:39:29 -0700 Subject: [PATCH 03/18] chore: add file upload and download --- pkg/api/authz/authz.go | 16 +++-- pkg/api/authz/thread.go | 55 ++++++++++++++++ pkg/api/handlers/assistants.go | 14 +---- pkg/api/handlers/files.go | 3 +- pkg/api/request.go | 11 +++- pkg/api/router/router.go | 6 +- pkg/services/config.go | 19 +++--- ui/user/src/app.html | 3 - ui/user/src/lib/components/Editors.svelte | 5 +- .../src/lib/components/editor/Controls.svelte | 23 ++++++- .../src/lib/components/navbar/Files.svelte | 59 +++++++++++++++-- .../components/navbar/KnowledgeFile.svelte | 2 +- ui/user/src/lib/components/navbar/Logo.svelte | 2 +- .../src/lib/components/navbar/Tables.svelte | 2 +- .../src/lib/components/navbar/Tasks.svelte | 2 +- ui/user/src/lib/components/tasks/Files.svelte | 44 ++++++++++++- ui/user/src/lib/icons/AssistantIcon.svelte | 9 ++- ui/user/src/lib/image.ts | 3 + ui/user/src/lib/services/chat/http.ts | 63 +++++++++---------- ui/user/src/lib/services/chat/operations.ts | 60 +++++++++++++++++- .../src/lib/services/editor/index.svelte.ts | 20 ++++++ ui/user/src/lib/stores/assistants.ts | 4 +- ui/user/src/lib/stores/loadedassistants.ts | 2 +- ui/user/src/routes/+page.svelte | 7 ++- 24 files changed, 345 insertions(+), 89 deletions(-) create mode 100644 ui/user/src/lib/image.ts diff --git a/pkg/api/authz/authz.go b/pkg/api/authz/authz.go index 62e2e1b6..ab957506 100644 --- a/pkg/api/authz/authz.go +++ b/pkg/api/authz/authz.go @@ -6,6 +6,7 @@ import ( "slices" "k8s.io/apiserver/pkg/authentication/user" + kclient "sigs.k8s.io/controller-runtime/pkg/client" ) const ( @@ -52,16 +53,19 @@ var staticRules = map[string][]string{ "POST /api/llm-proxy/", "POST /api/prompt", "GET /api/models", + "GET /api/version", }, } type Authorizer struct { - rules []rule + rules []rule + storage kclient.Client } -func NewAuthorizer() *Authorizer { +func NewAuthorizer(storage kclient.Client) *Authorizer { return &Authorizer{ - rules: defaultRules(), + rules: defaultRules(), + storage: storage, } } @@ -75,7 +79,11 @@ func (a *Authorizer) Authorize(req *http.Request, user user.Info) bool { } } - return authorizeThread(req, user) + if authorizeThread(req, user) { + return true + } + + return a.authorizeThreadFileDownload(req, user) } type rule struct { diff --git a/pkg/api/authz/thread.go b/pkg/api/authz/thread.go index 3ef0278f..35091589 100644 --- a/pkg/api/authz/thread.go +++ b/pkg/api/authz/thread.go @@ -5,6 +5,9 @@ import ( "strings" "github.com/gptscript-ai/gptscript/pkg/types" + "github.com/obot-platform/nah/pkg/router" + v1 "github.com/obot-platform/obot/pkg/storage/apis/otto.otto8.ai/v1" + "github.com/obot-platform/obot/pkg/system" "k8s.io/apiserver/pkg/authentication/user" ) @@ -23,3 +26,55 @@ func authorizeThread(req *http.Request, user user.Info) bool { return false } + +func (a *Authorizer) authorizeThreadFileDownload(req *http.Request, user user.Info) bool { + if req.Method != http.MethodGet { + return false + } + + if !strings.HasPrefix(req.URL.Path, "/api/threads/") { + return false + } + + parts := strings.Split(req.URL.Path, "/") + if len(parts) < 6 { + return false + } + if parts[0] != "" || + parts[1] != "api" || + parts[2] != "threads" || + parts[4] != "file" { + return false + } + + var ( + id = parts[3] + thread v1.Thread + ) + if err := a.storage.Get(req.Context(), router.Key(system.DefaultNamespace, id), &thread); err != nil { + return false + } + + if thread.Spec.UserUID == user.GetUID() { + return true + } + + if thread.Spec.WorkflowName == "" { + return false + } + + var workflow v1.Workflow + if err := a.storage.Get(req.Context(), router.Key(thread.Namespace, thread.Spec.WorkflowName), &workflow); err != nil { + return false + } + + if workflow.Spec.ThreadName == "" { + return false + } + + if err := a.storage.Get(req.Context(), router.Key(system.DefaultNamespace, workflow.Spec.ThreadName), &thread); err != nil { + return false + } + + return thread.Spec.UserUID == user.GetUID() +} diff --git a/pkg/api/handlers/assistants.go b/pkg/api/handlers/assistants.go index af513f8c..04ab61f1 100644 --- a/pkg/api/handlers/assistants.go +++ b/pkg/api/handlers/assistants.go @@ -281,17 +281,13 @@ func (a *AssistantHandler) GetFile(req api.Context) error { } func (a *AssistantHandler) UploadFile(req api.Context) error { - var ( - id = req.PathValue("id") - ) - - thread, err := getUserThread(req, id) + thread, err := getThreadForScope(req) if err != nil { return err } if thread.Status.WorkspaceID == "" { - return types.NewErrHttp(http.StatusTooEarly, fmt.Sprintf("no workspace found for assistant %s", id)) + return types.NewErrNotFound("no workspace found") } _, err = uploadFileToWorkspace(req.Context(), req, a.gptScript, thread.Status.WorkspaceID, "files/") @@ -299,11 +295,7 @@ func (a *AssistantHandler) UploadFile(req api.Context) error { } func (a *AssistantHandler) DeleteFile(req api.Context) error { - var ( - id = req.PathValue("id") - ) - - thread, err := getUserThread(req, id) + thread, err := getThreadForScope(req) if err != nil { return err } diff --git a/pkg/api/handlers/files.go b/pkg/api/handlers/files.go index e64a4735..be4b902c 100644 --- a/pkg/api/handlers/files.go +++ b/pkg/api/handlers/files.go @@ -3,7 +3,6 @@ package handlers import ( "context" "fmt" - "io" "net/http" "path/filepath" "strings" @@ -186,7 +185,7 @@ func uploadFileToWorkspace(ctx context.Context, req api.Context, gClient *gptscr return 0, fmt.Errorf("file path parameter is required") } - contents, err := io.ReadAll(req.Request.Body) + contents, err := req.Body() if err != nil { return 0, fmt.Errorf("failed to read request body: %w", err) } diff --git a/pkg/api/request.go b/pkg/api/request.go index c26dc5e5..a186a69e 100644 --- a/pkg/api/request.go +++ b/pkg/api/request.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "errors" "io" "net/http" "slices" @@ -95,8 +96,14 @@ func (r *Context) Read(obj any) error { return json.Unmarshal(data, obj) } -func (r *Context) Body() ([]byte, error) { - return io.ReadAll(io.LimitReader(r.Request.Body, 1<<20)) +func (r *Context) Body() (_ []byte, err error) { + defer func() { + if maxErr := (*http.MaxBytesError)(nil); errors.As(err, &maxErr) { + err = types.NewErrHttp(http.StatusRequestEntityTooLarge, "request body too large") + } + _, _ = io.Copy(io.Discard, r.Request.Body) + }() + return io.ReadAll(http.MaxBytesReader(r.ResponseWriter, r.Request.Body, 1<<20)) } func (r *Context) WriteCreated(obj any) error { diff --git a/pkg/api/router/router.go b/pkg/api/router/router.go index 58ede7a8..609c8d24 100644 --- a/pkg/api/router/router.go +++ b/pkg/api/router/router.go @@ -59,8 +59,8 @@ func Router(services *services.Services) (http.Handler, error) { // Assistant files mux.HandleFunc("GET /api/assistants/{assistant_id}/files", assistants.Files) mux.HandleFunc("GET /api/assistants/{assistant_id}/file/{file...}", assistants.GetFile) - mux.HandleFunc("POST /api/assistants/{id}/files/{file...}", assistants.UploadFile) - mux.HandleFunc("DELETE /api/assistants/{id}/files/{file...}", assistants.DeleteFile) + mux.HandleFunc("POST /api/assistants/{assistant_id}/file/{file...}", assistants.UploadFile) + mux.HandleFunc("DELETE /api/assistants/{assistant_id}/files/{file...}", assistants.DeleteFile) // Assistant knowledge files mux.HandleFunc("GET /api/assistants/{id}/knowledge", assistants.Knowledge) mux.HandleFunc("POST /api/assistants/{id}/knowledge/{file}", assistants.UploadKnowledge) @@ -86,6 +86,8 @@ func Router(services *services.Services) (http.Handler, error) { mux.HandleFunc("DELETE /api/assistants/{assistant_id}/tasks/{id}/runs/{run_id}", tasks.DeleteRun) mux.HandleFunc("GET /api/assistants/{assistant_id}/tasks/{task_id}/runs/{run_id}/files", assistants.Files) mux.HandleFunc("GET /api/assistants/{assistant_id}/tasks/{task_id}/runs/{run_id}/file/{file...}", assistants.GetFile) + mux.HandleFunc("POST /api/assistants/{assistant_id}/tasks/{task_id}/runs/{run_id}/file/{file...}", assistants.UploadFile) + mux.HandleFunc("DELETE /api/assistants/{assistant_id}/tasks/{task_id}/runs/{run_id}/files/{file...}", assistants.DeleteFile) mux.HandleFunc("GET /api/assistants/{assistant_id}/tasks/{id}/events", tasks.Events) mux.HandleFunc("POST /api/assistants/{assistant_id}/tasks/{id}/events", tasks.Abort) mux.HandleFunc("GET /api/assistants/{assistant_id}/tasks/{id}/runs/{run_id}/events", tasks.Events) diff --git a/pkg/services/config.go b/pkg/services/config.go index ecc86332..7f4554b9 100644 --- a/pkg/services/config.go +++ b/pkg/services/config.go @@ -265,15 +265,16 @@ func New(ctx context.Context, config Config) (*Services, error) { // For now, always auto-migrate the gateway database return &Services{ - WorkspaceProviderType: config.WorkspaceProviderType, - ServerURL: config.Hostname, - DevUIPort: devPort, - ToolRegistryURL: config.ToolRegistry, - Events: events, - StorageClient: storageClient, - Router: r, - GPTClient: c, - APIServer: server.NewServer(storageClient, c, authn.NewAuthenticator(authenticators), authz.NewAuthorizer(), proxyServer, config.Hostname), + WorkspaceProviderType: config.WorkspaceProviderType, + ServerURL: config.Hostname, + DevUIPort: devPort, + ToolRegistryURL: config.ToolRegistry, + Events: events, + StorageClient: storageClient, + Router: r, + GPTClient: c, + APIServer: server.NewServer(storageClient, c, authn.NewAuthenticator(authenticators), + authz.NewAuthorizer(storageClient), proxyServer, config.Hostname), TokenServer: tokenServer, Invoker: invoker, AIHelper: aihelper.New(c, config.HelperModel), diff --git a/ui/user/src/app.html b/ui/user/src/app.html index 94694a31..0122169b 100644 --- a/ui/user/src/app.html +++ b/ui/user/src/app.html @@ -2,9 +2,6 @@ - %sveltekit.head% diff --git a/ui/user/src/lib/components/Editors.svelte b/ui/user/src/lib/components/Editors.svelte index 1a37defb..215ea459 100644 --- a/ui/user/src/lib/components/Editors.svelte +++ b/ui/user/src/lib/components/Editors.svelte @@ -9,6 +9,7 @@ import { currentAssistant } from '$lib/stores'; import Image from '$lib/components/editor/Image.svelte'; import { CheckSquare, Table as TableIcon, Image as ImageIcon } from 'lucide-svelte'; + import { isImage } from '$lib/image'; function onFileChanged(name: string, contents: string) { for (const item of EditorService.items) { @@ -45,7 +46,7 @@ {:else if item.task} - {:else if item.name.toLowerCase().endsWith('.png')} + {:else if isImage(item.name)} {:else} @@ -86,7 +87,7 @@ file.name = task.name || file.name; }} /> - {:else if file.name.toLowerCase().endsWith('.png')} + {:else if isImage(file.name)} {:else} diff --git a/ui/user/src/lib/components/editor/Controls.svelte b/ui/user/src/lib/components/editor/Controls.svelte index 330d82d2..1760b9b9 100644 --- a/ui/user/src/lib/components/editor/Controls.svelte +++ b/ui/user/src/lib/components/editor/Controls.svelte @@ -1,6 +1,7 @@ {#if show} @@ -33,6 +38,22 @@ {/if} + {#if downloadable} + + {/if} + + + {/each} {/if} + + (fileToDelete = undefined)} +/> diff --git a/ui/user/src/lib/icons/AssistantIcon.svelte b/ui/user/src/lib/icons/AssistantIcon.svelte index 0f32e3c2..cd14b225 100644 --- a/ui/user/src/lib/icons/AssistantIcon.svelte +++ b/ui/user/src/lib/icons/AssistantIcon.svelte @@ -33,9 +33,14 @@ {#if icon(assistant)} - assistant icon + assistant icon {:else} -
+
{assistant?.name ? assistant.name[0].toUpperCase() : '?'}
{/if} diff --git a/ui/user/src/lib/image.ts b/ui/user/src/lib/image.ts new file mode 100644 index 00000000..fc9fcfac --- /dev/null +++ b/ui/user/src/lib/image.ts @@ -0,0 +1,3 @@ +export function isImage(filename: string): boolean { + return /\.(jpe?g|png|gif|bmp|webp)$/i.test(filename); +} diff --git a/ui/user/src/lib/services/chat/http.ts b/ui/user/src/lib/services/chat/http.ts index c5784e3c..fb0b7fbe 100644 --- a/ui/user/src/lib/services/chat/http.ts +++ b/ui/user/src/lib/services/chat/http.ts @@ -35,27 +35,7 @@ export async function doDelete(path: string): Promise { } export async function doPut(path: string, input?: string | object | Blob): Promise { - let headers: Record | undefined; - if (input instanceof Blob) { - headers = { - 'Content-Type': 'application/octet-stream' - }; - } else if (typeof input === 'object') { - input = JSON.stringify(input); - headers = { - 'Content-Type': 'application/json' - }; - } else if (input) { - headers = { - 'Content-Type': 'text/plain' - }; - } - const resp = await fetch(baseURL + path, { - method: 'PUT', - headers: headers, - body: input - }); - return handleResponse(resp, path); + return await doWithBody('PUT', path, input); } async function handleResponse(resp: Response, path: string): Promise { @@ -71,20 +51,37 @@ async function handleResponse(resp: Response, path: string): Promise { return resp.text(); } -export async function doPost(path: string, input: string | object | Blob): Promise { - let contentType = 'text/plain'; +export async function doWithBody( + method: string, + path: string, + input?: string | object | Blob +): Promise { + let headers: Record | undefined; if (input instanceof Blob) { - contentType = 'application/octet-stream'; + headers = { 'Content-Type': 'application/octet-stream' }; } else if (typeof input === 'object') { input = JSON.stringify(input); - contentType = 'application/json'; + headers = { 'Content-Type': 'application/json' }; + } else if (input) { + headers = { 'Content-Type': 'text/plain' }; } - const resp = await fetch(baseURL + path, { - method: 'POST', - headers: { - 'Content-Type': contentType - }, - body: input - }); - return handleResponse(resp, path); + try { + const resp = await fetch(baseURL + path, { + method: method, + headers: headers, + body: input + }); + return handleResponse(resp, path); + } catch (e) { + if (e instanceof Error) { + errors.append(e); + } else { + errors.append(new Error(`${e}`)); + } + throw e; + } +} + +export async function doPost(path: string, input: string | object | Blob): Promise { + return await doWithBody('POST', path, input); } diff --git a/ui/user/src/lib/services/chat/operations.ts b/ui/user/src/lib/services/chat/operations.ts index 84c5e25d..3a2cc389 100644 --- a/ui/user/src/lib/services/chat/operations.ts +++ b/ui/user/src/lib/services/chat/operations.ts @@ -42,10 +42,68 @@ export async function deleteKnowledgeFile(assistant: string, filename: string) { return doDelete(`/assistants/${assistant}/knowledge/${filename}`); } -export async function deleteFile(assistant: string, filename: string) { +export async function deleteFile( + assistant: string, + filename: string, + opts?: { taskID?: string; runID?: string } +) { + if (opts?.taskID && opts?.runID) { + return doDelete( + `/assistants/${assistant}/tasks/${opts.taskID}/runs/${opts.runID}/files/${filename}` + ); + } return doDelete(`/assistants/${assistant}/files/${filename}`); } +export async function download( + assistant: string, + filename: string, + opts?: { + taskID?: string; + runID?: string; + } +) { + let url = `/assistants/${assistant}/file/${filename}`; + if (opts?.taskID && opts?.runID) { + url = `/assistants/${assistant}/tasks/${opts.taskID}/runs/${opts.runID}/file/${filename}`; + } + url = baseURL + url; + + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); +} + +export async function saveFile( + assistant: string, + file: File, + opts?: { taskID?: string; runID?: string } +) { + if (opts?.taskID && opts?.runID) { + return (await doPost( + `/assistants/${assistant}/tasks/${opts.taskID}/runs/${opts.runID}/file/${file.name}`, + file + )) as Files; + } + return (await doPost(`/assistants/${assistant}/file/${file.name}`, file)) as Files; +} + +export async function saveContents( + assistant: string, + filename: string, + contents: string, + opts?: { taskID?: string; runID?: string } +) { + if (opts?.taskID && opts?.runID) { + return (await doPost( + `/assistants/${assistant}/tasks/${opts.taskID}/runs/${opts.runID}/file/${filename}`, + contents + )) as Files; + } + return (await doPost(`/assistants/${assistant}/file/${filename}`, contents)) as Files; +} + export async function getFile( assistant: string, filename: string, diff --git a/ui/user/src/lib/services/editor/index.svelte.ts b/ui/user/src/lib/services/editor/index.svelte.ts index 13006638..0844fdb0 100644 --- a/ui/user/src/lib/services/editor/index.svelte.ts +++ b/ui/user/src/lib/services/editor/index.svelte.ts @@ -9,6 +9,7 @@ const maxSize = writable(false); const editor: Editor = { remove, load, + download, select, items, maxSize, @@ -24,6 +25,14 @@ export interface Editor { runID?: string; } ) => Promise; + download: ( + assistant: string, + id: string, + opts?: { + taskID?: string; + runID?: string; + } + ) => Promise; remove: (name: string) => void; select: (name: string) => void; items: EditorItem[]; @@ -105,6 +114,17 @@ async function loadTask(assistant: string, taskID: string) { } } +async function download(assistant: string, id: string, opts?: { taskID?: string; runID?: string }) { + const item = items.find((item) => item.id === id); + if (item && item.modified && item.buffer) { + await ChatService.saveContents(assistant, item.id, item.buffer, opts); + item.contents = item.buffer; + item.modified = false; + item.blob = undefined; + } + await ChatService.download(assistant, id, opts); +} + async function loadFile( assistant: string, file: string, diff --git a/ui/user/src/lib/stores/assistants.ts b/ui/user/src/lib/stores/assistants.ts index f0e1a7ef..90ad7f45 100644 --- a/ui/user/src/lib/stores/assistants.ts +++ b/ui/user/src/lib/stores/assistants.ts @@ -1,14 +1,14 @@ import { listAssistants } from '$lib/services/chat/operations'; import { type Assistant } from '$lib/services/chat/types'; -import { writable } from 'svelte/store'; import loadedAssistants from './loadedassistants'; +import { writable } from 'svelte/store'; const store = writable([]); if (typeof window !== 'undefined') { listAssistants().then((assistants) => { store.set(assistants.items); - loadedAssistants.set(true) + loadedAssistants.set(true); }); } diff --git a/ui/user/src/lib/stores/loadedassistants.ts b/ui/user/src/lib/stores/loadedassistants.ts index 131b8b3c..2a685478 100644 --- a/ui/user/src/lib/stores/loadedassistants.ts +++ b/ui/user/src/lib/stores/loadedassistants.ts @@ -2,4 +2,4 @@ import { writable } from 'svelte/store'; const store = writable(false); -export default store; \ No newline at end of file +export default store; diff --git a/ui/user/src/routes/+page.svelte b/ui/user/src/routes/+page.svelte index 40635e3f..ec406f50 100644 --- a/ui/user/src/routes/+page.svelte +++ b/ui/user/src/routes/+page.svelte @@ -23,7 +23,7 @@ if (id) { goto(`/${id}`); } else if ($loadedAssistants) { - window.location.href = '/admin/' + window.location.href = '/admin/'; } }); @@ -46,7 +46,10 @@ - + {#if $darkMode} GitHub {:else} From 6cf7b775b4ed62278cc397a56ba9bb67f917bb3e Mon Sep 17 00:00:00 2001 From: Thorsten Klein Date: Thu, 19 Dec 2024 14:34:29 +0100 Subject: [PATCH 04/18] change: call llm-proxy on localhost instead of external domain (#976) --- pkg/invoke/invoker.go | 6 ++++-- pkg/services/config.go | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/invoke/invoker.go b/pkg/invoke/invoker.go index 4f79efcb..35bbe8d8 100644 --- a/pkg/invoke/invoker.go +++ b/pkg/invoke/invoker.go @@ -41,9 +41,10 @@ type Invoker struct { tokenService *jwt.TokenService events *events.Emitter serverURL string + serverPort int } -func NewInvoker(c kclient.WithWatch, gptClient *gptscript.GPTScript, gatewayClient *client.Client, serverURL string, tokenService *jwt.TokenService, events *events.Emitter) *Invoker { +func NewInvoker(c kclient.WithWatch, gptClient *gptscript.GPTScript, gatewayClient *client.Client, serverURL string, serverPort int, tokenService *jwt.TokenService, events *events.Emitter) *Invoker { return &Invoker{ uncached: c, gptClient: gptClient, @@ -51,6 +52,7 @@ func NewInvoker(c kclient.WithWatch, gptClient *gptscript.GPTScript, gatewayClie tokenService: tokenService, events: events, serverURL: serverURL, + serverPort: serverPort, } } @@ -495,7 +497,7 @@ func (i *Invoker) Resume(ctx context.Context, c kclient.WithWatch, thread *v1.Th options := gptscript.Options{ GlobalOptions: gptscript.GlobalOptions{ Env: append(run.Spec.Env, - fmt.Sprintf("GPTSCRIPT_MODEL_PROVIDER_PROXY_URL=%s/api/llm-proxy", i.serverURL), + fmt.Sprintf("GPTSCRIPT_MODEL_PROVIDER_PROXY_URL=http://localhost:%d/api/llm-proxy", i.serverPort), "GPTSCRIPT_MODEL_PROVIDER_PROXY_TOKEN="+token, "GPTSCRIPT_MODEL_PROVIDER_TOKEN="+token, "OBOT_SERVER_URL="+i.serverURL, diff --git a/pkg/services/config.go b/pkg/services/config.go index 7f4554b9..ebcdd4b6 100644 --- a/pkg/services/config.go +++ b/pkg/services/config.go @@ -217,7 +217,7 @@ func New(ctx context.Context, config Config) (*Services, error) { tokenServer = &jwt.TokenService{} events = events.NewEmitter(storageClient) gatewayClient = client.New(gatewayDB, config.AuthAdminEmails) - invoker = invoke.NewInvoker(storageClient, c, client.New(gatewayDB, config.AuthAdminEmails), config.Hostname, tokenServer, events) + invoker = invoke.NewInvoker(storageClient, c, client.New(gatewayDB, config.AuthAdminEmails), config.Hostname, config.HTTPListenPort, tokenServer, events) modelProviderDispatcher = dispatcher.New(invoker, storageClient, c) proxyServer *proxy.Proxy From c97aad598e2b8ecf9029469d7a74ac2fb6569e92 Mon Sep 17 00:00:00 2001 From: Thorsten Klein Date: Thu, 19 Dec 2024 15:42:41 +0100 Subject: [PATCH 05/18] feat: add Anthropic on Bedrock model provider (#939) --- .../ModelProviderConfigure.tsx | 4 +- .../model-providers/ModelProviderForm.tsx | 1 + .../components/model-providers/constants.ts | 17 ++++++- ui/admin/app/routes/_auth.model-providers.tsx | 1 + .../public/assets/anthropic_bedrock_icon.svg | 51 +++++++++++++++++++ 5 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 ui/admin/public/assets/anthropic_bedrock_icon.svg diff --git a/ui/admin/app/components/model-providers/ModelProviderConfigure.tsx b/ui/admin/app/components/model-providers/ModelProviderConfigure.tsx index 9567fd29..3050e4d0 100644 --- a/ui/admin/app/components/model-providers/ModelProviderConfigure.tsx +++ b/ui/admin/app/components/model-providers/ModelProviderConfigure.tsx @@ -153,7 +153,9 @@ export function ModelProviderConfigureContent({ - {modelProvider.id === CommonModelProviderIds.ANTHROPIC && ( + {(modelProvider.id === CommonModelProviderIds.ANTHROPIC || + modelProvider.id == + CommonModelProviderIds.ANTHROPIC_BEDROCK) && ( Note: Anthropic does not have an embeddings model and{" "} { "OBOT_OLLAMA_MODEL_PROVIDER", "OBOT_VOYAGE_MODEL_PROVIDER", "OBOT_GROQ_MODEL_PROVIDER", + "OBOT_ANTHROPIC_BEDROCK_MODEL_PROVIDER", ]; return fieldsToStrip diff --git a/ui/admin/app/components/model-providers/constants.ts b/ui/admin/app/components/model-providers/constants.ts index 4fa68f4d..00bc002c 100644 --- a/ui/admin/app/components/model-providers/constants.ts +++ b/ui/admin/app/components/model-providers/constants.ts @@ -5,6 +5,7 @@ export const CommonModelProviderIds = { ANTHROPIC: "anthropic-model-provider", OPENAI: "openai-model-provider", AZURE_OPENAI: "azure-openai-model-provider", + ANTHROPIC_BEDROCK: "anthropic-bedrock-model-provider", }; export const ModelProviderLinks = { @@ -15,6 +16,8 @@ export const ModelProviderLinks = { "https://azure.microsoft.com/en-us/explore/", [CommonModelProviderIds.ANTHROPIC]: "https://www.anthropic.com", [CommonModelProviderIds.OPENAI]: "https://openai.com/", + [CommonModelProviderIds.ANTHROPIC_BEDROCK]: + "https://aws.amazon.com/bedrock/claude/", }; export const ModelProviderConfigurationLinks = { @@ -41,7 +44,7 @@ export const ModelProviderRequiredTooltips: { }, [CommonModelProviderIds.AZURE_OPENAI]: { Endpoint: - "Endpoint for the Azure OpenAI service (eg. https://..api.cognitive.microsoft.com/)", + "Endpoint for the Azure OpenAI service (e.g. https://..api.cognitive.microsoft.com/)", "Client Id": "Unique identifier for the application when using Azure Active Directory. Can typically be found in App Registrations > [application].", "Client Secret": @@ -53,6 +56,12 @@ export const ModelProviderRequiredTooltips: { "Resource Group": "Container that holds related Azure resources. Can typically be found in Azure Portal > Resource Groups > [OpenAI Resource Group] > Overview", }, + [CommonModelProviderIds.ANTHROPIC_BEDROCK]: { + "Access Key ID": "AWS Access Key ID", + "Secret Access Key": "AWS Secret Access Key", + "Session Token": "AWS Session Token", + Region: "AWS Region - make sure that the models you want to use are available in this region: https://docs.aws.amazon.com/bedrock/latest/userguide/models-regions.html", + }, }; export const ModelProviderSensitiveFields: Record = @@ -79,4 +88,10 @@ export const ModelProviderSensitiveFields: Record = // Groq OBOT_GROQ_MODEL_PROVIDER_API_KEY: true, + + // Anthropic Bedrock + OBOT_ANTHROPIC_BEDROCK_MODEL_PROVIDER_ACCESS_KEY_ID: true, + OBOT_ANTHROPIC_BEDROCK_MODEL_PROVIDER_SECRET_ACCESS_KEY: true, + OBOT_ANTHROPIC_BEDROCK_MODEL_PROVIDER_SESSION_TOKEN: true, + OBOT_ANTHROPIC_BEDROCK_MODEL_PROVIDER_REGION: false, }; diff --git a/ui/admin/app/routes/_auth.model-providers.tsx b/ui/admin/app/routes/_auth.model-providers.tsx index 29979000..3116afa8 100644 --- a/ui/admin/app/routes/_auth.model-providers.tsx +++ b/ui/admin/app/routes/_auth.model-providers.tsx @@ -29,6 +29,7 @@ const sortModelProviders = (modelProviders: ModelProvider[]) => { CommonModelProviderIds.OPENAI, CommonModelProviderIds.AZURE_OPENAI, CommonModelProviderIds.ANTHROPIC, + CommonModelProviderIds.ANTHROPIC_BEDROCK, CommonModelProviderIds.OLLAMA, CommonModelProviderIds.VOYAGE, CommonModelProviderIds.GROQ, diff --git a/ui/admin/public/assets/anthropic_bedrock_icon.svg b/ui/admin/public/assets/anthropic_bedrock_icon.svg new file mode 100644 index 00000000..db1faad6 --- /dev/null +++ b/ui/admin/public/assets/anthropic_bedrock_icon.svg @@ -0,0 +1,51 @@ + + + + + + + + + + From e394134af8ab47369c328d5d86a0522a00f4b045 Mon Sep 17 00:00:00 2001 From: Ivy Date: Thu, 19 Dec 2024 11:46:00 -0500 Subject: [PATCH 06/18] fix: click edit button opens workflow (#977) --- .../app/routes/_auth.workflows._index.tsx | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/ui/admin/app/routes/_auth.workflows._index.tsx b/ui/admin/app/routes/_auth.workflows._index.tsx index 5e0f8811..b305f765 100644 --- a/ui/admin/app/routes/_auth.workflows._index.tsx +++ b/ui/admin/app/routes/_auth.workflows._index.tsx @@ -66,6 +66,14 @@ export default function Workflows() { ); }, [getThreads.data, getWorkflows.data]); + const navigateToWorkflow = (workflow: Workflow) => { + navigate( + $path("/workflows/:workflow", { + workflow: workflow.id, + }) + ); + }; + return (
@@ -83,13 +91,7 @@ export default function Workflows() { disableClickPropagation={(cell) => cell.id.includes("action") } - onRowClick={(row) => { - navigate( - $path("/workflows/:workflow", { - workflow: row.id, - }) - ); - }} + onRowClick={navigateToWorkflow} />
@@ -143,7 +145,13 @@ export default function Workflows() { - From f37c7bb59eb4b9fbd6b838acbf821d6474f25a6e Mon Sep 17 00:00:00 2001 From: Taylor Price Date: Thu, 19 Dec 2024 10:33:18 -0700 Subject: [PATCH 07/18] chore: add support for salesforce oauth app (#960) Signed-off-by: Taylor Price --- apiclient/types/oauthapp.go | 3 + pkg/gateway/server/oauth_apps.go | 25 ++++++ pkg/gateway/types/oauth_apps.go | 32 +++++++ .../openapi/generated/openapi_generated.go | 7 ++ .../oauth-apps/OAuthAppTypeIcon.tsx | 2 + ui/admin/app/lib/model/oauthApps/index.ts | 4 + .../app/lib/model/oauthApps/oauth-helpers.ts | 1 + .../model/oauthApps/providers/salesforce.ts | 81 ++++++++++++++++++ ui/admin/public/assets/salesforce_logo.png | Bin 0 -> 12858 bytes 9 files changed, 155 insertions(+) create mode 100644 ui/admin/app/lib/model/oauthApps/providers/salesforce.ts create mode 100644 ui/admin/public/assets/salesforce_logo.png diff --git a/apiclient/types/oauthapp.go b/apiclient/types/oauthapp.go index 190757e5..95e24018 100644 --- a/apiclient/types/oauthapp.go +++ b/apiclient/types/oauthapp.go @@ -8,6 +8,7 @@ const ( OAuthAppTypeHubSpot OAuthAppType = "hubspot" OAuthAppTypeGitHub OAuthAppType = "github" OAuthAppTypeGoogle OAuthAppType = "google" + OAuthAppTypeSalesforce OAuthAppType = "salesforce" OAuthAppTypeCustom OAuthAppType = "custom" ) @@ -36,6 +37,8 @@ type OAuthAppManifest struct { Integration string `json:"integration,omitempty"` // Global indicates if the OAuth app is globally applied to all agents. Global *bool `json:"global,omitempty"` + // This field is only used by Salesforce + InstanceURL string `json:"instanceURL,omitempty"` } type OAuthAppList List[OAuthApp] diff --git a/pkg/gateway/server/oauth_apps.go b/pkg/gateway/server/oauth_apps.go index c826aacd..10df9c8f 100644 --- a/pkg/gateway/server/oauth_apps.go +++ b/pkg/gateway/server/oauth_apps.go @@ -11,6 +11,7 @@ import ( "io" "net/http" "net/url" + "strconv" "strings" "time" @@ -445,6 +446,30 @@ func (s *Server) callbackOAuthApp(apiContext api.Context) error { CreatedAt: time.Now(), RefreshToken: googleTokenResp.RefreshToken, } + case types2.OAuthAppTypeSalesforce: + salesforceTokenResp := new(types.SalesforceOAuthTokenResponse) + if err := json.NewDecoder(resp.Body).Decode(salesforceTokenResp); err != nil { + return fmt.Errorf("failed to parse token response: %w", err) + } + issuedAt, err := strconv.ParseInt(salesforceTokenResp.IssuedAt, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse token response: %w", err) + } + createdAt := time.Unix(issuedAt/1000, (issuedAt%1000)*1000000) + + tokenResp = &types.OAuthTokenResponse{ + State: state, + TokenType: salesforceTokenResp.TokenType, + Scope: salesforceTokenResp.Scope, + AccessToken: salesforceTokenResp.AccessToken, + ExpiresIn: 7200, // Relies on Salesforce admin not overriding the default 2 hours + Ok: true, // Assuming true if no error is present + CreatedAt: createdAt, + RefreshToken: salesforceTokenResp.RefreshToken, + Extras: map[string]string{ + "GPTSCRIPT_SALESFORCE_URL": salesforceTokenResp.InstanceURL, + }, + } default: if err := json.NewDecoder(resp.Body).Decode(tokenResp); err != nil { return fmt.Errorf("failed to parse token response: %w", err) diff --git a/pkg/gateway/types/oauth_apps.go b/pkg/gateway/types/oauth_apps.go index ac4586aa..90cda837 100644 --- a/pkg/gateway/types/oauth_apps.go +++ b/pkg/gateway/types/oauth_apps.go @@ -68,6 +68,25 @@ func ValidateAndSetDefaultsOAuthAppManifest(r *types.OAuthAppManifest, create bo case types.OAuthAppTypeGitHub: r.AuthURL = GitHubAuthorizeURL r.TokenURL = GitHubTokenURL + case types.OAuthAppTypeSalesforce: + salesforceAuthorizeFragment := "/services/oauth2/authorize" + salesforceTokenFragment := "/services/oauth2/token" + instanceURL, err := url.Parse(r.InstanceURL) + if err != nil { + errs = append(errs, err) + } + if instanceURL.Scheme != "" { + instanceURL.Scheme = "https" + } + + r.AuthURL, err = url.JoinPath(instanceURL.String(), salesforceAuthorizeFragment) + if err != nil { + errs = append(errs, err) + } + r.TokenURL, err = url.JoinPath(instanceURL.String(), salesforceTokenFragment) + if err != nil { + errs = append(errs, err) + } } if r.AuthURL == "" { @@ -160,6 +179,7 @@ type OAuthTokenResponse struct { Ok bool `json:"ok"` Error string `json:"error"` CreatedAt time.Time + Extras map[string]string `json:"extras" gorm:"serializer:json"` } type GoogleOAuthTokenResponse struct { @@ -170,6 +190,18 @@ type GoogleOAuthTokenResponse struct { TokenType string `json:"token_type"` } +type SalesforceOAuthTokenResponse struct { + AccessToken string `json:"access_token"` + Signature string `json:"signature"` + Scope string `json:"scope"` + IDToken string `json:"id_token"` + InstanceURL string `json:"instance_url"` + ID string `json:"id"` + RefreshToken string `json:"refresh_token"` + TokenType string `json:"token_type"` + IssuedAt string `json:"issued_at"` +} + type SlackOAuthTokenResponse struct { Ok bool `json:"ok"` Error string `json:"error"` diff --git a/pkg/storage/openapi/generated/openapi_generated.go b/pkg/storage/openapi/generated/openapi_generated.go index a508291b..5ddd01a8 100644 --- a/pkg/storage/openapi/generated/openapi_generated.go +++ b/pkg/storage/openapi/generated/openapi_generated.go @@ -2216,6 +2216,13 @@ func schema_obot_platform_obot_apiclient_types_OAuthAppManifest(ref common.Refer Format: "", }, }, + "instanceURL": { + SchemaProps: spec.SchemaProps{ + Description: "This field is only used by Salesforce", + Type: []string{"string"}, + Format: "", + }, + }, }, Required: []string{"Metadata", "type", "clientID"}, }, diff --git a/ui/admin/app/components/oauth-apps/OAuthAppTypeIcon.tsx b/ui/admin/app/components/oauth-apps/OAuthAppTypeIcon.tsx index 330edd3d..c69710cd 100644 --- a/ui/admin/app/components/oauth-apps/OAuthAppTypeIcon.tsx +++ b/ui/admin/app/components/oauth-apps/OAuthAppTypeIcon.tsx @@ -5,6 +5,7 @@ import { FaGithub, FaGoogle, FaMicrosoft, + FaSalesforce, FaSlack, } from "react-icons/fa"; @@ -15,6 +16,7 @@ const IconMap = { [OAuthProvider.Atlassian]: FaAtlassian, [OAuthProvider.GitHub]: FaGithub, [OAuthProvider.Slack]: FaSlack, + [OAuthProvider.Salesforce]: FaSalesforce, [OAuthProvider.Google]: FaGoogle, [OAuthProvider.Microsoft365]: FaMicrosoft, [OAuthProvider.Notion]: NotionLogoIcon, diff --git a/ui/admin/app/lib/model/oauthApps/index.ts b/ui/admin/app/lib/model/oauthApps/index.ts index bd4c8589..f0957f15 100644 --- a/ui/admin/app/lib/model/oauthApps/index.ts +++ b/ui/admin/app/lib/model/oauthApps/index.ts @@ -6,6 +6,7 @@ import { GitHubOAuthApp } from "~/lib/model/oauthApps/providers/github"; import { GoogleOAuthApp } from "~/lib/model/oauthApps/providers/google"; import { Microsoft365OAuthApp } from "~/lib/model/oauthApps/providers/microsoft365"; import { NotionOAuthApp } from "~/lib/model/oauthApps/providers/notion"; +import { SalesforceOAuthApp } from "~/lib/model/oauthApps/providers/salesforce"; import { SlackOAuthApp } from "~/lib/model/oauthApps/providers/slack"; import { EntityMeta } from "~/lib/model/primitives"; @@ -14,6 +15,7 @@ export const OAuthAppSpecMap = { [OAuthProvider.Google]: GoogleOAuthApp, [OAuthProvider.Microsoft365]: Microsoft365OAuthApp, [OAuthProvider.Slack]: SlackOAuthApp, + [OAuthProvider.Salesforce]: SalesforceOAuthApp, [OAuthProvider.Notion]: NotionOAuthApp, // Custom OAuth apps are intentionally omitted from the map. // They are handled separately @@ -45,6 +47,8 @@ export type OAuthAppParams = { optionalScope?: string; // This field is required, it correlates to the integration name in the gptscript oauth cred tool integration: string; + // This field is only needed for Salesforce OAuth apps + instanceURL?: string; }; export type OAuthAppBase = OAuthAppParams & { diff --git a/ui/admin/app/lib/model/oauthApps/oauth-helpers.ts b/ui/admin/app/lib/model/oauthApps/oauth-helpers.ts index 2fb86f48..236588c7 100644 --- a/ui/admin/app/lib/model/oauthApps/oauth-helpers.ts +++ b/ui/admin/app/lib/model/oauthApps/oauth-helpers.ts @@ -8,6 +8,7 @@ export const OAuthProvider = { Google: "google", Microsoft365: "microsoft365", Slack: "slack", + Salesforce: "salesforce", Notion: "notion", Custom: "custom", } as const; diff --git a/ui/admin/app/lib/model/oauthApps/providers/salesforce.ts b/ui/admin/app/lib/model/oauthApps/providers/salesforce.ts new file mode 100644 index 00000000..10dd03e5 --- /dev/null +++ b/ui/admin/app/lib/model/oauthApps/providers/salesforce.ts @@ -0,0 +1,81 @@ +import { z } from "zod"; + +import { + OAuthAppSpec, + OAuthFormStep, + getOAuthLinks, +} from "~/lib/model/oauthApps/oauth-helpers"; +import { assetUrl } from "~/lib/utils"; + +const schema = z.object({ + clientID: z.string().min(1, "Client ID is required"), + clientSecret: z.string().min(1, "Client Secret is required"), + instanceURL: z.string().min(1, "Instance URL is required"), +}); + +const steps: OAuthFormStep[] = [ + { + type: "markdown", + text: + "### Step 1: Create a new Salesforce External Client App\n" + + "- Log in to your Salesforce portal.\n" + + "- Go to Setup, and then search for 'External Client App Manager.'\n" + + "- Select 'New External Client App' from the top right.\n" + + "- Enter `Obot` as the External Client App Name.\n" + + "- Fill in a Contact Email for your Salesforce Administrator.\n" + + "- Set 'Distribution State' to 'Local' from the dropdown menu.\n" + + "- (Optionally) Fill in the other fields.\n" + + "- Click the `Create` button.\n", + }, + { + type: "copy", + text: getOAuthLinks("salesforce").redirectURL, + }, + { + type: "markdown", + text: + "### Step 2: Configure OAuth Settings\n" + + "- Expand the 'Api (Enable OAuth Settings)' section, and check the box to Enable OAuth.\n" + + "- Enter your callback url\n" + + "- Select the 'api' and 'refresh_token' OAuth Scopes from the list.\n" + + "- Uncheck 'Require Proof Key for Code Exchange.'\n" + + "- Check 'Enable Refresh Token Rotation.\n" + + "- Click the 'Create' button.\n", + }, + { + type: "markdown", + text: + "### Step 3: Configure App Policies\n" + + "- Under the Policies tab, click 'Edit'.\n" + + "- Inside the 'App Authorization' box" + + " - Change 'Refresh Token Policy' to 'Immediately expire refresh token.'\n" + + " - (Optionally) Change 'IP Relaxation' to 'Relax IP Restrictions.'\n" + + "- Click 'Save.'\n", + }, + { + type: "markdown", + text: + "### Step 4: Register your OAuth App credentials with Obot\n" + + "- Navigate to the `Settings` tab in the sidebar.\n" + + "- Enter the `Consumer Key and Secret` from the `Settings -> OAuth Settings` section into the fields below.\n" + + "- Enter your Salesforce instance URL into the field below.\n", + }, + { type: "input", input: "clientID", label: "Consumer Key" }, + { + type: "input", + input: "clientSecret", + label: "Consumer Secret", + inputType: "password", + }, + { type: "input", input: "instanceURL", label: "Instance URL" }, +]; + +export const SalesforceOAuthApp = { + schema, + alias: "salesforce", + type: "salesforce", + displayName: "Salesforce", + logo: assetUrl("/assets/salesforce_logo.png"), + steps: steps, + noGatewayIntegration: true, +} satisfies OAuthAppSpec; diff --git a/ui/admin/public/assets/salesforce_logo.png b/ui/admin/public/assets/salesforce_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..01ab6b94c19d75f600cba7486b8f425f5a1339f0 GIT binary patch literal 12858 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 Date: Thu, 19 Dec 2024 11:16:48 -0500 Subject: [PATCH 08/18] Revert "chore: hide atlassian oauth config from admins" This reverts commit 42adf476ec792437d00c6b94c4e7be0676171d6d. --- ui/admin/app/lib/model/oauthApps/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/admin/app/lib/model/oauthApps/index.ts b/ui/admin/app/lib/model/oauthApps/index.ts index f0957f15..e283cf3c 100644 --- a/ui/admin/app/lib/model/oauthApps/index.ts +++ b/ui/admin/app/lib/model/oauthApps/index.ts @@ -2,6 +2,7 @@ import { OAuthAppSpec, OAuthProvider, } from "~/lib/model/oauthApps/oauth-helpers"; +import { AtlassianOAuthApp } from "~/lib/model/oauthApps/providers/atlassian"; import { GitHubOAuthApp } from "~/lib/model/oauthApps/providers/github"; import { GoogleOAuthApp } from "~/lib/model/oauthApps/providers/google"; import { Microsoft365OAuthApp } from "~/lib/model/oauthApps/providers/microsoft365"; @@ -11,6 +12,7 @@ import { SlackOAuthApp } from "~/lib/model/oauthApps/providers/slack"; import { EntityMeta } from "~/lib/model/primitives"; export const OAuthAppSpecMap = { + [OAuthProvider.Atlassian]: AtlassianOAuthApp, [OAuthProvider.GitHub]: GitHubOAuthApp, [OAuthProvider.Google]: GoogleOAuthApp, [OAuthProvider.Microsoft365]: Microsoft365OAuthApp, From a053c1f4e153478b0d0db48809bc46192b7a7ad8 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Thu, 19 Dec 2024 13:28:24 -0500 Subject: [PATCH 09/18] fix: update auth provider environment variables Signed-off-by: Donnie Adams --- .../05-configuration/03-auth-providers.md | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/docs/05-configuration/03-auth-providers.md b/docs/docs/05-configuration/03-auth-providers.md index fc827371..699c9259 100644 --- a/docs/docs/05-configuration/03-auth-providers.md +++ b/docs/docs/05-configuration/03-auth-providers.md @@ -8,14 +8,14 @@ The following environment variables are required for all authentication provider Setting the Client ID and Client Secret will mean that the authentication provider is enabled. The remaining configuration will be validated on startup. -- `OBOT_AUTH_CLIENT_ID`: The client ID of the authentication provider. -- `OBOT_AUTH_CLIENT_SECRET`: The client secret of the authentication provider. -- `OBOT_AUTH_COOKIE_SECRET`: The secret used to encrypt the authentication cookie. Must be of size 16, 24, or 32 bytes. -- `OBOT_AUTH_ADMIN_EMAILS`: A comma-separated list of the email addresses of the admin users. +- `OBOT_SERVER_AUTH_CLIENT_ID`: The client ID of the authentication provider. +- `OBOT_SERVER_AUTH_CLIENT_SECRET`: The client secret of the authentication provider. +- `OBOT_SERVER_AUTH_COOKIE_SECRET`: The secret used to encrypt the authentication cookie. Must be of size 16, 24, or 32 bytes. +- `OBOT_SERVER_AUTH_ADMIN_EMAILS`: A comma-separated list of the email addresses of the admin users. The following environment variables are optional for all authentication providers: -- `OBOT_AUTH_EMAIL_DOMAINS`: A comma-separated list of email domains allowed for authentication. Ignored if not set. -- `OBOT_AUTH_CONFIG_TYPE`: The type of the authentication provider. For example, `google` or `github`. Defaults to `google`. +- `OBOT_SERVER_AUTH_EMAIL_DOMAINS`: A comma-separated list of email domains allowed for authentication. Ignored if not set. +- `OBOT_SERVER_AUTH_CONFIG_TYPE`: The type of the authentication provider. For example, `google` or `github`. Defaults to `google`. ## Google @@ -25,8 +25,8 @@ Google is the default authentication provider. There are currently no additional GitHub authentication has the following optional configuration: -- `OBOT_AUTH_GITHUB_ORG`: The name of the organization allowed for authentication. Ignored if not set. -- `OBOT_AUTH_GITHUB_TEAM`: The name of the team allowed for authentication. Ignored if not set. -- `OBOT_AUTH_GITHUB_REPO`: Restrict logins to collaborators of this repository formatted as `orgname/repo`. Ignored if not set. -- `OBOT_AUTH_GITHUB_TOKEN`: The token to use when verifying repository collaborators (must have push access to the repository). -- `OBOT_AUTH_GITHUB_ALLOW_USERS`: A comma-separated list of users allowed to log in even if they don't belong to the organization or team. \ No newline at end of file +- `OBOT_SERVER_AUTH_GITHUB_ORG`: The name of the organization allowed for authentication. Ignored if not set. +- `OBOT_SERVER_AUTH_GITHUB_TEAM`: The name of the team allowed for authentication. Ignored if not set. +- `OBOT_SERVER_AUTH_GITHUB_REPO`: Restrict logins to collaborators of this repository formatted as `orgname/repo`. Ignored if not set. +- `OBOT_SERVER_AUTH_GITHUB_TOKEN`: The token to use when verifying repository collaborators (must have push access to the repository). +- `OBOT_SERVER_AUTH_GITHUB_ALLOW_USERS`: A comma-separated list of users allowed to log in even if they don't belong to the organization or team. \ No newline at end of file From aa30bc74869284ebc0326f5e962ad8c7ff4dd3fd Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Thu, 19 Dec 2024 13:04:31 -0700 Subject: [PATCH 10/18] chore: add input params to run in tasks --- .../{Modal.svelte => Confirm.svelte} | 35 ++++++-- .../src/lib/components/editor/Controls.svelte | 2 +- .../src/lib/components/navbar/Files.svelte | 4 +- .../components/navbar/KnowledgeFiles.svelte | 4 +- .../src/lib/components/navbar/Tasks.svelte | 4 +- ui/user/src/lib/components/tasks/Files.svelte | 4 +- ui/user/src/lib/components/tasks/Input.svelte | 17 ++-- ui/user/src/lib/components/tasks/Runs.svelte | 85 ++++++++++++++----- ui/user/src/lib/components/tasks/Step.svelte | 4 +- ui/user/src/lib/components/tasks/Task.svelte | 4 +- 10 files changed, 113 insertions(+), 50 deletions(-) rename ui/user/src/lib/components/{Modal.svelte => Confirm.svelte} (56%) diff --git a/ui/user/src/lib/components/Modal.svelte b/ui/user/src/lib/components/Confirm.svelte similarity index 56% rename from ui/user/src/lib/components/Modal.svelte rename to ui/user/src/lib/components/Confirm.svelte index 114df18e..60531c00 100644 --- a/ui/user/src/lib/components/Modal.svelte +++ b/ui/user/src/lib/components/Confirm.svelte @@ -9,22 +9,39 @@ } let { show = false, msg = 'OK?', onsuccess, oncancel }: Props = $props(); + + let div: HTMLDivElement | undefined = $state(); + + $effect(() => { + if (show && div) { + console.log('focused'); + div.focus(); + } + });
{ + console.log('onkeydown', e.key); + if (e.key === 'Escape') { + oncancel(); + } + e.stopPropagation(); + }} > -
-
+ diff --git a/ui/user/src/lib/components/editor/Controls.svelte b/ui/user/src/lib/components/editor/Controls.svelte index 1760b9b9..2d7b5167 100644 --- a/ui/user/src/lib/components/editor/Controls.svelte +++ b/ui/user/src/lib/components/editor/Controls.svelte @@ -13,7 +13,7 @@ let show = $derived(navBar || EditorService.items.length <= 1); let downloadable = $derived.by(() => { const selected = EditorService.items.find((item) => item.selected); - return selected && !selected.table; + return selected && !selected.table && !selected.task; }); diff --git a/ui/user/src/lib/components/navbar/Files.svelte b/ui/user/src/lib/components/navbar/Files.svelte index c8291340..c9150e9f 100644 --- a/ui/user/src/lib/components/navbar/Files.svelte +++ b/ui/user/src/lib/components/navbar/Files.svelte @@ -2,7 +2,7 @@ import { FileText, Trash, Upload } from '$lib/icons'; import { files, currentAssistant } from '$lib/stores'; import { ChatService, EditorService, type Files } from '$lib/services'; - import Modal from '$lib/components/Modal.svelte'; + import Confirm from '$lib/components/Confirm.svelte'; import Menu from '$lib/components/navbar/Menu.svelte'; import { Download, Image } from 'lucide-svelte'; import { isImage } from '$lib/image'; @@ -112,7 +112,7 @@ {/snippet} - - - {/if} - { if (task?.onDemand?.params) { @@ -94,10 +95,10 @@ >
{/each} @@ -108,7 +109,7 @@ To Subject {/if}
diff --git a/ui/user/src/lib/components/tasks/Runs.svelte b/ui/user/src/lib/components/tasks/Runs.svelte index 61c6685d..49d162d2 100644 --- a/ui/user/src/lib/components/tasks/Runs.svelte +++ b/ui/user/src/lib/components/tasks/Runs.svelte @@ -1,11 +1,12 @@ -{#snippet runButton()} +{#snippet runButton(opts?: { input?: string; text?: string })} {/snippet} @@ -102,7 +128,7 @@ {#if runs.length > 0}

Runs

- +
@@ -115,19 +141,19 @@ {#each runs as run} select(run.id)}>
Start
{formatTime(run.created)} {formatInput(run)} {#if run.startTime && run.endTime} @@ -141,13 +167,10 @@ {/if} -
+
{/if} - +

Input Parameters

+ +
+ {@render runButton({ + input: taskInput, + text: 'Run' + })} +
+ + + + { @@ -181,6 +226,6 @@ diff --git a/ui/user/src/lib/components/tasks/Step.svelte b/ui/user/src/lib/components/tasks/Step.svelte index 991b9d75..ada6e350 100644 --- a/ui/user/src/lib/components/tasks/Step.svelte +++ b/ui/user/src/lib/components/tasks/Step.svelte @@ -6,7 +6,7 @@ import { LoaderCircle, OctagonX, Play, RefreshCcw, Save, Undo } from 'lucide-svelte'; import { tick } from 'svelte'; import { autoHeight } from '$lib/actions/textarea'; - import Modal from '$lib/components/Modal.svelte'; + import Confirm from '$lib/components/Confirm.svelte'; import { currentAssistant } from '$lib/stores'; interface Props { @@ -255,7 +255,7 @@ {/key} {/if} -id: {id}
- Date: Thu, 19 Dec 2024 15:39:07 -0500 Subject: [PATCH 11/18] fix: minor ui/ux bugs (#982) * fix: seeing icons/chip through sticky when scrolling up * fix: hitting enter goes through even though chat button disabled * nit: text center model provider * chore: otto -> obot --- ui/admin/app/components/chat/Chatbar.tsx | 11 ++++++----- .../components/model-providers/ModelProviderLists.tsx | 2 +- ui/admin/app/lib/model/oauthApps/providers/github.ts | 2 +- ui/admin/app/lib/model/oauthApps/providers/google.ts | 4 ++-- ui/admin/app/lib/model/oauthApps/providers/notion.ts | 4 ++-- ui/admin/app/lib/model/oauthApps/providers/slack.ts | 4 ++-- ui/admin/app/routes/_auth.model-providers.tsx | 4 ++-- 7 files changed, 16 insertions(+), 15 deletions(-) diff --git a/ui/admin/app/components/chat/Chatbar.tsx b/ui/admin/app/components/chat/Chatbar.tsx index b7a18120..10a7746c 100644 --- a/ui/admin/app/components/chat/Chatbar.tsx +++ b/ui/admin/app/components/chat/Chatbar.tsx @@ -21,9 +21,14 @@ export function Chatbar({ className }: ChatbarProps) { useChat(); const { configured: modelProviderConfigured } = useModelProviders(); + const disabled = + (!input && !isRunning) || isInvoking || !modelProviderConfigured; + const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); + if (disabled) return; + if (isRunning) { abortRunningThread(); } @@ -65,11 +70,7 @@ export function Chatbar({ className }: ChatbarProps) { className="m-2" color="primary" type="submit" - disabled={ - (!input && !isRunning) || - isInvoking || - !modelProviderConfigured - } + disabled={disabled} > {isInvoking ? ( diff --git a/ui/admin/app/components/model-providers/ModelProviderLists.tsx b/ui/admin/app/components/model-providers/ModelProviderLists.tsx index 715abd69..bc66881a 100644 --- a/ui/admin/app/components/model-providers/ModelProviderLists.tsx +++ b/ui/admin/app/components/model-providers/ModelProviderLists.tsx @@ -42,7 +42,7 @@ export function ModelProviderList({ size="lg" /> -
+
{modelProvider.name}
diff --git a/ui/admin/app/lib/model/oauthApps/providers/github.ts b/ui/admin/app/lib/model/oauthApps/providers/github.ts index 8dca1755..fd10dad5 100644 --- a/ui/admin/app/lib/model/oauthApps/providers/github.ts +++ b/ui/admin/app/lib/model/oauthApps/providers/github.ts @@ -39,7 +39,7 @@ const steps: OAuthFormStep[] = [ type: "markdown", text: "5. Click `Register application` to create the OAuth app. It will now take you to the OAuth app's settings page.\n" + - "### Step 2: Register GitHub OAuth in Otto\n" + + "### Step 2: Register GitHub OAuth in Obot\n" + "1. Locate the `Client ID` on the OAuth app's settings page and copy the `Client ID` into the input below\n", }, { type: "input", input: "clientID", label: "Client ID" }, diff --git a/ui/admin/app/lib/model/oauthApps/providers/google.ts b/ui/admin/app/lib/model/oauthApps/providers/google.ts index 766d61e2..8b58e9b5 100644 --- a/ui/admin/app/lib/model/oauthApps/providers/google.ts +++ b/ui/admin/app/lib/model/oauthApps/providers/google.ts @@ -169,8 +169,8 @@ const steps: OAuthFormStep[] = [ { type: "markdown", text: - "### Step 5: Register your OAuth App in Otto\n" + - "With the credentials you just created, register your OAuth App in Otto by entering the `Client ID` and `Client Secret` into the fields below and clicking on the `Submit` button.", + "### Step 5: Register your OAuth App in Obot\n" + + "With the credentials you just created, register your OAuth App in Obot by entering the `Client ID` and `Client Secret` into the fields below and clicking on the `Submit` button.", }, { type: "input", input: "clientID", label: "Client ID" }, { diff --git a/ui/admin/app/lib/model/oauthApps/providers/notion.ts b/ui/admin/app/lib/model/oauthApps/providers/notion.ts index 08453dc0..0b8b47e5 100644 --- a/ui/admin/app/lib/model/oauthApps/providers/notion.ts +++ b/ui/admin/app/lib/model/oauthApps/providers/notion.ts @@ -50,7 +50,7 @@ const steps: OAuthFormStep>[] = [ text: "- Add a **Name** for your integration.\n" + "- From the **Type** dropdown menu, select **Public**.\n" + - " - It's important to select **Public** for Otto to properly connect via OAuth.\n" + + " - It's important to select **Public** for Obot to properly connect via OAuth.\n" + "- Enter the fields pertaining to your **company name**, **website**, **privacy policy**, and **terms of use**.\n" + "- Copy the url below and paste it into the **Redirect URI** field.\n", }, @@ -77,7 +77,7 @@ const steps: OAuthFormStep>[] = [ { type: "markdown", text: - "### Step 3: Register your integration with Otto\n" + + "### Step 3: Register your integration with Obot\n" + "- Navigate to the **Configuration** tab from the top of the integration's settings page.\n" + "- Copy the **Client ID** and **Client Secret** and paste them into the respective fields below.\n", }, diff --git a/ui/admin/app/lib/model/oauthApps/providers/slack.ts b/ui/admin/app/lib/model/oauthApps/providers/slack.ts index e126f405..3f4e20d7 100644 --- a/ui/admin/app/lib/model/oauthApps/providers/slack.ts +++ b/ui/admin/app/lib/model/oauthApps/providers/slack.ts @@ -106,7 +106,7 @@ const steps: OAuthFormStep[] = [ { type: "markdown", text: - "### Step 5: Register OAuth App in Otto\n" + + "### Step 5: Register OAuth App in Obot\n" + "Click the `Basic Information` section in the side nav, locate the `Client ID` and `Client Secret` fields, copy/paste them into the form below, and click `Submit`.\n", }, { type: "input", input: "clientID", label: "Client ID" }, @@ -131,6 +131,6 @@ export const SlackOAuthApp = { steps, disableConfiguration, disabledReason: disableConfiguration - ? "Slack requires that redirect URLs start with `https`. Since this application is running on `http`, you will need to redeploy Otto using `https` in order to configure a custom Slack OAuth app." + ? "Slack requires that redirect URLs start with `https`. Since this application is running on `http`, you will need to redeploy Obot using `https` in order to configure a custom Slack OAuth app." : undefined, } satisfies OAuthAppSpec; diff --git a/ui/admin/app/routes/_auth.model-providers.tsx b/ui/admin/app/routes/_auth.model-providers.tsx index 3116afa8..f7755bee 100644 --- a/ui/admin/app/routes/_auth.model-providers.tsx +++ b/ui/admin/app/routes/_auth.model-providers.tsx @@ -58,8 +58,8 @@ export default function ModelProviders() { const sortedModelProviders = sortModelProviders(modelProviders); return (
-
-
+
+
Model Providers From 930250a6133453eda47cec49247dc0d2229c2af7 Mon Sep 17 00:00:00 2001 From: Ivy Date: Thu, 19 Dec 2024 15:39:20 -0500 Subject: [PATCH 12/18] fix: revalidate getModels after confirming modelsBackPopulated (#990) --- .../components/model-providers/ModelProviderConfigure.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/admin/app/components/model-providers/ModelProviderConfigure.tsx b/ui/admin/app/components/model-providers/ModelProviderConfigure.tsx index 3050e4d0..a586ce5d 100644 --- a/ui/admin/app/components/model-providers/ModelProviderConfigure.tsx +++ b/ui/admin/app/components/model-providers/ModelProviderConfigure.tsx @@ -1,8 +1,9 @@ import { useEffect, useState } from "react"; -import useSWR from "swr"; +import useSWR, { mutate } from "swr"; import { ModelProvider } from "~/lib/model/modelProviders"; import { NotFoundError } from "~/lib/service/api/apiErrors"; +import { ModelApiService } from "~/lib/service/api/modelApiService"; import { ModelProviderApiService } from "~/lib/service/api/modelProviderApiService"; import { ModelProviderForm } from "~/components/model-providers/ModelProviderForm"; @@ -55,6 +56,8 @@ export function ModelProviderConfigure({ if (data?.modelsBackPopulated) { setShowDefaultModelAliasForm(true); setLoadingModelProviderId(""); + // revalidate models to get back populated models + mutate(ModelApiService.getModels.key()); } }, [getLoadingModelProviderModels, loadingModelProviderId]); From aa3405d01c24c0bb6f6e83b29a80e8d5766f0853 Mon Sep 17 00:00:00 2001 From: Ryan Hopper-Lowe <46546486+ryanhopperlowe@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:58:39 -0600 Subject: [PATCH 13/18] feat: implement new linter rules in preparation for react compiler (#961) - All code logic should be identical - did some swapping out of refs for state vars to improve adherence to the rules of react --- ui/admin/.eslintrc.cjs | 3 +- ui/admin/app/components/ui/multi-select.tsx | 21 ++++++-- ui/admin/app/components/ui/scroll-area.tsx | 12 ++++- ui/admin/app/components/ui/sidebar.tsx | 7 +-- ui/admin/app/components/ui/textarea.tsx | 38 +++++++------- ui/admin/app/hooks/useLogEffect.ts | 8 --- ui/admin/package.json | 1 + ui/admin/pnpm-lock.yaml | 58 +++++++++++++++++++++ 8 files changed, 109 insertions(+), 39 deletions(-) delete mode 100644 ui/admin/app/hooks/useLogEffect.ts diff --git a/ui/admin/.eslintrc.cjs b/ui/admin/.eslintrc.cjs index 286a2ab0..d8df50b8 100644 --- a/ui/admin/.eslintrc.cjs +++ b/ui/admin/.eslintrc.cjs @@ -28,7 +28,7 @@ module.exports = { // React { files: ["**/*.{js,jsx,ts,tsx}"], - plugins: ["react", "jsx-a11y"], + plugins: ["react", "jsx-a11y", "react-compiler"], extends: [ "plugin:react/recommended", "plugin:react/jsx-runtime", @@ -50,6 +50,7 @@ module.exports = { }, rules: { "react/prop-types": "off", + "react-compiler/react-compiler": "error", "no-restricted-imports": [ "error", { diff --git a/ui/admin/app/components/ui/multi-select.tsx b/ui/admin/app/components/ui/multi-select.tsx index ea812909..095d01cc 100644 --- a/ui/admin/app/components/ui/multi-select.tsx +++ b/ui/admin/app/components/ui/multi-select.tsx @@ -181,6 +181,9 @@ const CommandEmpty = forwardRef< CommandEmpty.displayName = "CommandEmpty"; +/** + * @deprecated This component is super bulky and has some weird bugs. We need to create a new one that behaves similarly to ComboBox + */ const MultiSelect = React.forwardRef( ( { @@ -334,8 +337,13 @@ const MultiSelect = React.forwardRef( }; void exec(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [debouncedSearchTerm, groupBy, open, triggerSearchOnFocus]); + }, [ + debouncedSearchTerm, + groupBy, + open, + triggerSearchOnFocus, + onSearchSync, + ]); useEffect(() => { /** async search */ @@ -360,8 +368,13 @@ const MultiSelect = React.forwardRef( }; void exec(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [debouncedSearchTerm, groupBy, open, triggerSearchOnFocus]); + }, [ + debouncedSearchTerm, + groupBy, + open, + triggerSearchOnFocus, + onSearch, + ]); const CreatableItem = () => { if (!creatable) return undefined; diff --git a/ui/admin/app/components/ui/scroll-area.tsx b/ui/admin/app/components/ui/scroll-area.tsx index f193edb2..b7bc5efb 100644 --- a/ui/admin/app/components/ui/scroll-area.tsx +++ b/ui/admin/app/components/ui/scroll-area.tsx @@ -26,6 +26,9 @@ const ScrollArea = React.forwardRef< ...rootProps } = props; + const [viewportEl, setViewportEl] = React.useState( + null + ); const viewportRef = React.useRef(null); const [shouldStickToBottom, setShouldStickToBottom] = React.useState( enableScrollStick === "bottom" @@ -49,6 +52,11 @@ const ScrollArea = React.forwardRef< } }, [enableScrollStick, shouldStickToBottom, children]); + const initRef = React.useCallback((node: HTMLDivElement | null) => { + setViewportEl(node); + viewportRef.current = node; + }, []); + return ( setShouldStickToBottom(isScrolledToBottom(e.currentTarget)) } @@ -66,7 +74,7 @@ const ScrollArea = React.forwardRef< {enableScrollTo === "bottom" && ( setShouldStickToBottom(true)} - scrollContainerEl={viewportRef.current} + scrollContainerEl={viewportEl} disabled={shouldStickToBottom} /> )} diff --git a/ui/admin/app/components/ui/sidebar.tsx b/ui/admin/app/components/ui/sidebar.tsx index 0c9aace0..3d0c5717 100644 --- a/ui/admin/app/components/ui/sidebar.tsx +++ b/ui/admin/app/components/ui/sidebar.tsx @@ -17,8 +17,8 @@ import { } from "~/components/ui/tooltip"; import { useIsMobile } from "~/hooks/use-mobile"; -const SIDEBAR_COOKIE_NAME = "sidebar:state"; -const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +const _SIDEBAR_COOKIE_NAME = "sidebar:state"; +const _SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; const SIDEBAR_WIDTH = "13rem"; const SIDEBAR_WIDTH_MOBILE = "18rem"; const SIDEBAR_WIDTH_ICON = "3rem"; @@ -81,9 +81,6 @@ const SidebarProvider = React.forwardRef< } _setOpen(value); - - // This sets the cookie to keep the sidebar state. - document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; }, [setOpenProp, open] ); diff --git a/ui/admin/app/components/ui/textarea.tsx b/ui/admin/app/components/ui/textarea.tsx index 6b35c544..b99fb045 100644 --- a/ui/admin/app/components/ui/textarea.tsx +++ b/ui/admin/app/components/ui/textarea.tsx @@ -128,14 +128,14 @@ const useAutosizeTextArea = ({ setInit(false); } - node.style.height = `${ - Math.min(Math.max(node.scrollHeight, minHeight), maxHeight) + - offsetBorder - }px`; + const newHeight = Math.min( + Math.max(node.scrollHeight, minHeight + offsetBorder), + maxHeight + offsetBorder + ); + + node.style.height = `${newHeight}px`; }, - // disable exhaustive deps because we don't want to rerun this after init is set to false - // eslint-disable-next-line react-hooks/exhaustive-deps - [maxHeight, minHeight] + [maxHeight, minHeight, setInit, init] ); const initResizer = React.useCallback( @@ -145,6 +145,7 @@ const useAutosizeTextArea = ({ node.oninput = () => resize(node); node.onresize = () => resize(node); node.onchange = () => resize(node); + resize(node); }, [resize] @@ -153,15 +154,14 @@ const useAutosizeTextArea = ({ React.useEffect(() => { if (textAreaRef) { initResizer(textAreaRef); - resize(textAreaRef); } - }, [resize, initResizer, textAreaRef]); + }, [initResizer, textAreaRef]); return { initResizer }; }; export type AutosizeTextAreaRef = { - textArea: HTMLTextAreaElement; + textArea: HTMLTextAreaElement | null; maxHeight: number; minHeight: number; }; @@ -185,28 +185,27 @@ const AutosizeTextarea = React.forwardRef< }: AutosizeTextAreaProps, ref: React.Ref ) => { - const textAreaRef = React.useRef(null); + const [textAreaEl, setTextAreaEl] = + React.useState(null); useImperativeHandle(ref, () => ({ - textArea: textAreaRef.current as HTMLTextAreaElement, - focus: textAreaRef?.current?.focus, + textArea: textAreaEl, + focus: textAreaEl?.focus, maxHeight, minHeight, })); const { initResizer } = useAutosizeTextArea({ - textAreaRef: textAreaRef.current, + textAreaRef: textAreaEl, maxHeight, minHeight, }); const initRef = React.useCallback( (node: HTMLTextAreaElement | null) => { - textAreaRef.current = node; - - if (!node) return; + setTextAreaEl(node); - initResizer(node); + if (node) initResizer(node); }, [initResizer] ); @@ -214,6 +213,7 @@ const AutosizeTextarea = React.forwardRef< return (