From f9921569a9549086834fb269b91fd6649ed08c33 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Thu, 21 Aug 2025 08:56:54 -0230 Subject: [PATCH 1/3] Nudge feedback widget, move request to bedrock (#38074) * Nudge feedback widget, move request to bedrock * Fix the response object * Remove an import to deleted file. --------- Co-authored-by: Ivan Vasilov --- .../FeedbackDropdown/FeedbackDropdown.tsx | 10 +- apps/studio/pages/api/ai/feedback/classify.ts | 96 +++++++++++++++---- packages/ai-commands/edge.ts | 1 - .../ai-commands/src/sql/classify-feedback.ts | 72 -------------- 4 files changed, 83 insertions(+), 96 deletions(-) delete mode 100644 packages/ai-commands/src/sql/classify-feedback.ts diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackDropdown.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackDropdown.tsx index 5f02871ce143c..3602fb585b9f4 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackDropdown.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackDropdown.tsx @@ -44,18 +44,20 @@ const FeedbackDropdown = ({ className }: { className?: string }) => {
What would you like to share?
- -
diff --git a/apps/studio/pages/api/ai/feedback/classify.ts b/apps/studio/pages/api/ai/feedback/classify.ts index 3a064fe7de1bb..c27a76c3d4d2d 100644 --- a/apps/studio/pages/api/ai/feedback/classify.ts +++ b/apps/studio/pages/api/ai/feedback/classify.ts @@ -1,17 +1,10 @@ -import { ContextLengthError } from 'ai-commands' -import { classifyFeedback } from 'ai-commands/edge' -import apiWrapper from 'lib/api/apiWrapper' +import { generateObject } from 'ai' import { NextApiRequest, NextApiResponse } from 'next' - -const openAiKey = process.env.OPENAI_API_KEY +import { z } from 'zod' +import { getModel } from 'lib/ai/model' +import apiWrapper from 'lib/api/apiWrapper' async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!openAiKey) { - return res.status(500).json({ - error: 'No OPENAI_API_KEY set. Create this environment variable to use AI features.', - }) - } - const { method } = req switch (method) { @@ -28,18 +21,83 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) { body: { prompt }, } = req + if (!prompt) { + return res.status(400).json({ + error: 'Prompt is required', + }) + } + try { - const result = await classifyFeedback(prompt) - res.status(200).json({ feedback_category: result }) - return + const { model, error: modelError } = await getModel() + + if (modelError) { + return res.status(500).json({ error: modelError.message }) + } + + const { object } = await generateObject({ + model, + schema: z.object({ + feedback_category: z.enum(['support', 'feedback', 'unknown']), + }), + temperature: 0, + prompt: ` + Classify the following feedback as ONE of: support, feedback, unknown. + - support: bug reports, help requests, or issues + - feedback: feature requests or suggestions + - unknown: unclear or unrelated + + If you can't determine support or feedback, always output "unknown". + + Only output a JSON object in this format: { "feedback_category": "support|feedback|unknown" } + + Examples: + Feedback: "Whenever I try to invite a team member, the invite email doesn't get sent." + Response: { "feedback_category": "support" } + + Feedback: "I have reached the storage limit for my project and my plan. I cannot understand how I can expand the storage space in my project." + Response: { "feedback_category": "support" } + + Feedback: "Please delete the project x in my account" + Response: { "feedback_category": "support" } + + Feedback: "My billing page is broken" + Response: { "feedback_category": "support" } + + Feedback: "I accidentally deleted my database—can it be recovered?" + Response: { "feedback_category": "support" } + + Feedback: "My login tokens are expiring too quickly, even though I didn't change any settings." + Response: { "feedback_category": "support" } + + Feedback: "Can you add more integrations?" + Response: { "feedback_category": "feedback" } + + Feedback: "I'm getting charged for a project I thought I deleted. Can you help me stop billing?" + Response: { "feedback_category": "support" } + + Feedback: "Could you support OAuth login for more providers like Apple or LinkedIn?" + Response: { "feedback_category": "feedback" } + + Feedback: "It's unclear in the docs how to set up row-level security with multiple roles." + Response: { "feedback_category": "feedback" } + + Feedback: "I am trying to pause my Pro project" + Response: { "feedback_category": "feedback" } + + Feedback: "${prompt}" + Response: + `, + }) + + return res.json({ feedback_category: object.feedback_category }) } catch (error) { if (error instanceof Error) { - console.error(`AI feedback classification failed: ${error.message}`) + console.error(`Classifying this feedback failed`) - if (error instanceof ContextLengthError) { + // Check for context length error + if (error.message.includes('context_length') || error.message.includes('too long')) { return res.status(400).json({ - error: - 'Your feedback prompt is too large for Supabase AI to ingest. Try splitting it into smaller prompts.', + error: 'This prompt is too large to ingest', }) } } else { @@ -47,7 +105,7 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) { } return res.status(500).json({ - error: 'There was an unknown error classifying the feedback. Please try again.', + error: 'There was an unknown error generating the feedback category.', }) } } diff --git a/packages/ai-commands/edge.ts b/packages/ai-commands/edge.ts index 95023e6bb1d53..729d55b5128a8 100644 --- a/packages/ai-commands/edge.ts +++ b/packages/ai-commands/edge.ts @@ -1,5 +1,4 @@ export * from './src/errors' export * from './src/docs' -export * from './src/sql/classify-feedback' export * from './src/sql/cron' diff --git a/packages/ai-commands/src/sql/classify-feedback.ts b/packages/ai-commands/src/sql/classify-feedback.ts deleted file mode 100644 index 68ecd25553314..0000000000000 --- a/packages/ai-commands/src/sql/classify-feedback.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { generateObject } from 'ai' -import { openai as openaiProvider } from '@ai-sdk/openai' -import { z } from 'zod' -import { ContextLengthError } from '../errors' - -// Given a prompt from the feedback widget, classify it as support, feedback, or unknown. -export async function classifyFeedback(prompt: string) { - try { - const { object } = await generateObject({ - model: openaiProvider('gpt-4o'), - temperature: 0, - - schema: z.object({ - feedback_category: z.enum(['support', 'feedback', 'unknown']), - }), - prompt: ` - Classify the following feedback as ONE of: support, feedback, unknown. - - support: bug reports, help requests, or issues - - feedback: feature requests or suggestions - - unknown: unclear or unrelated - - If you can't determine support or feedback, always output "unknown". - - Only output a JSON object in this format: { "feedback_category": "support|feedback|unknown" } - - Examples: - Feedback: "Whenever I try to invite a team member, the invite email doesn't get sent." - Response: { "feedback_category": "support" } - - Feedback: "I have reached the storage limit for my project and my plan. I cannot understand how I can expand the storage space in my project." - Response: { "feedback_category": "support" } - - Feedback: "Please delete the project x in my account" - Response: { "feedback_category": "support" } - - Feedback: "My billing page is broken" - Response: { "feedback_category": "support" } - - Feedback: "I accidentally deleted my database—can it be recovered?" - Response: { "feedback_category": "support" } - - Feedback: "My login tokens are expiring too quickly, even though I didn't change any settings." - Response: { "feedback_category": "support" } - - Feedback: "Can you add more integrations?" - Response: { "feedback_category": "feedback" } - - Feedback: "I'm getting charged for a project I thought I deleted. Can you help me stop billing?" - Response: { "feedback_category": "support" } - - Feedback: "Could you support OAuth login for more providers like Apple or LinkedIn?" - Response: { "feedback_category": "feedback" } - - Feedback: "It's unclear in the docs how to set up row-level security with multiple roles." - Response: { "feedback_category": "feedback" } - - Feedback: "I am trying to pause my Pro project" - Response: { "feedback_category": "feedback" } - - Feedback: "${prompt}" - Response: - `, - }) - - return object.feedback_category - } catch (error) { - if (error instanceof Error && 'code' in error && error.code === 'context_length_exceeded') { - throw new ContextLengthError() - } - throw error - } -} From 0fadb115607dd6f6f18550ac86ed7272fc0c6ef9 Mon Sep 17 00:00:00 2001 From: "kemal.earth" <606977+kemaldotearth@users.noreply.github.com> Date: Thu, 21 Aug 2025 13:40:02 +0100 Subject: [PATCH 2/3] feat(studio): account area ui updates (#38006) * feat: quick prototype of account area Update to the layout of the account area. Small prototype yolo. * fix: prettier warnings * feat: fix back button to bottom of sidebar * feat: handle back button on desktop and mobile * chore: run prettier --- .../layouts/AccountLayout/AccountLayout.tsx | 97 ++++--- .../layouts/AccountLayout/WithSidebar.tsx | 271 +++++++----------- .../components/layouts/DefaultLayout.tsx | 28 +- .../LayoutHeader/LayoutHeader.tsx | 19 +- apps/studio/pages/account/audit.tsx | 16 +- apps/studio/pages/account/me.tsx | 5 +- apps/studio/pages/account/security.tsx | 5 +- apps/studio/pages/account/tokens.tsx | 5 +- 8 files changed, 211 insertions(+), 235 deletions(-) diff --git a/apps/studio/components/layouts/AccountLayout/AccountLayout.tsx b/apps/studio/components/layouts/AccountLayout/AccountLayout.tsx index cc6105011284e..cf7095261417a 100644 --- a/apps/studio/components/layouts/AccountLayout/AccountLayout.tsx +++ b/apps/studio/components/layouts/AccountLayout/AccountLayout.tsx @@ -1,6 +1,4 @@ -import { ArrowLeft } from 'lucide-react' import Head from 'next/head' -import Link from 'next/link' import { useRouter } from 'next/router' import { PropsWithChildren, useEffect } from 'react' @@ -9,13 +7,8 @@ import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { withAuth } from 'hooks/misc/withAuth' import { IS_PLATFORM } from 'lib/constants' import { useAppStateSnapshot } from 'state/app-state' -import { cn, NavMenu, NavMenuItem } from 'ui' -import { - MAX_WIDTH_CLASSES, - PADDING_CLASSES, - ScaffoldContainerLegacy, - ScaffoldTitle, -} from '../Scaffold' +import { cn } from 'ui' +import { WithSidebar } from './WithSidebar' export interface AccountLayoutProps { title: string @@ -37,19 +30,6 @@ const AccountLayout = ({ children, title }: PropsWithChildren { @@ -64,33 +44,52 @@ const AccountLayout = ({ children, title }: PropsWithChildren{title ? `${title} | Supabase` : 'Supabase'} -
- - - - Back to dashboard - - Account settings - -
- - {accountLinks.map((item, i) => ( - - {item.label} - - ))} - -
- {children} +
+ + {children} +
) diff --git a/apps/studio/components/layouts/AccountLayout/WithSidebar.tsx b/apps/studio/components/layouts/AccountLayout/WithSidebar.tsx index 2ea219a9d509c..b70d439f9f5e2 100644 --- a/apps/studio/components/layouts/AccountLayout/WithSidebar.tsx +++ b/apps/studio/components/layouts/AccountLayout/WithSidebar.tsx @@ -1,11 +1,9 @@ -import { isUndefined } from 'lodash' -import { ArrowUpRight, LogOut } from 'lucide-react' +import { ArrowLeft } from 'lucide-react' import Link from 'next/link' -import { PropsWithChildren, ReactNode, useState } from 'react' -import { Badge, cn, Menu } from 'ui' +import { PropsWithChildren, ReactNode } from 'react' +import { cn, Menu } from 'ui' import MobileSheetNav from 'ui-patterns/MobileSheetNav/MobileSheetNav' -import { LayoutHeader } from '../ProjectLayout/LayoutHeader' -import type { SidebarLink, SidebarSection } from './AccountLayout.types' +import type { SidebarSection } from './AccountLayout.types' import { useAppStateSnapshot } from 'state/app-state' interface WithSidebarProps { @@ -17,9 +15,10 @@ interface WithSidebarProps { subitemsParentKey?: number hideSidebar?: boolean customSidebarContent?: ReactNode + backToDashboardURL?: string } -const WithSidebar = ({ +export const WithSidebar = ({ title, header, breadcrumbs = [], @@ -29,6 +28,7 @@ const WithSidebar = ({ subitemsParentKey, hideSidebar = false, customSidebarContent, + backToDashboardURL, }: PropsWithChildren) => { const noContent = !sections && !customSidebarContent const { mobileMenuOpen, setMobileMenuOpen } = useAppStateSnapshot() @@ -43,11 +43,16 @@ const WithSidebar = ({ subitems={subitems} subitemsParentKey={subitemsParentKey} customSidebarContent={customSidebarContent} - className="hidden md:block" + backToDashboardURL={backToDashboardURL} + className="hidden md:flex" /> )}
-
{children}
+
+
+ {children} +
+
) } -export default WithSidebar export const SidebarContent = ({ title, @@ -71,6 +76,7 @@ export const SidebarContent = ({ subitems, subitemsParentKey, customSidebarContent, + backToDashboardURL, className, }: PropsWithChildren> & { className?: string }) => { return ( @@ -78,43 +84,72 @@ export const SidebarContent = ({
- {title && ( -
-
-

- {title} -

+
+ {backToDashboardURL && ( +
+
+ + + Back to dashboard + +
+
+ )} + {header && header} +
+
+ + {customSidebarContent} + {sections.map((section, idx) => ( +
+ {Boolean(section.heading) ? ( + + ) : ( +
+
+ {section.links.map((link, i: number) => { + const isActive = link.isActive && !subitems + return ( + + +
+
+ {link.label} +
+
+ +
+ ) + })} +
+
+ )} + {idx !== sections.length - 1 && ( +
+ )} +
+ ))} +
- )} - {header && header} -
- - {customSidebarContent} - {sections.map((section) => { - return Boolean(section.heading) ? ( - - ) : ( -
- -
- ) - })} -
@@ -128,131 +163,33 @@ interface SectionWithHeadersProps { } const SectionWithHeaders = ({ section, subitems, subitemsParentKey }: SectionWithHeadersProps) => ( -
- {section.heading && } - {section.versionLabel && ( -
- {section.versionLabel} +
+
+ {section.heading && ( + + {section.heading} +
+ } + /> + )} +
+ {section.links.map((link, i: number) => { + const isActive = link.isActive && !subitems + return ( + + +
+
+ {link.label} +
+
+ +
+ ) + })}
- )} - { - - } +
) -interface SidebarItemProps { - links: SidebarLink[] - subitems?: any[] - subitemsParentKey?: number -} - -const SidebarItem = ({ links, subitems, subitemsParentKey }: SidebarItemProps) => { - return ( -
    - {links.map((link, i: number) => { - // disable active state for link with subitems - const isActive = link.isActive && !subitems - - let render: any = ( - - ) - - if (subitems && link.subitemsKey === subitemsParentKey) { - const subItemsRender = subitems.map((y: any, i: number) => ( - - )) - render = [render, ...subItemsRender] - } - - return render - })} -
- ) -} - -interface SidebarLinkProps extends SidebarLink { - id: string - isSubitem?: boolean -} - -const SidebarLinkItem = ({ - id, - label, - href, - isActive, - isSubitem, - isExternal, - onClick, - icon, -}: SidebarLinkProps) => { - if (isUndefined(href)) { - let icon - if (isExternal) { - icon = - } - - if (label === 'Log out') { - icon = - } - - return ( - {})} - icon={icon} - > - {isSubitem ?

{label}

: label} -
- ) - } - - return ( - - - {isExternal && ( - - - - )} -
- - {isSubitem ?

{label}

: label} -
- {icon} -
-
- - ) -} diff --git a/apps/studio/components/layouts/DefaultLayout.tsx b/apps/studio/components/layouts/DefaultLayout.tsx index c9b8a70542865..9361bd4dce92f 100644 --- a/apps/studio/components/layouts/DefaultLayout.tsx +++ b/apps/studio/components/layouts/DefaultLayout.tsx @@ -1,11 +1,14 @@ import { useRouter } from 'next/router' import { PropsWithChildren } from 'react' +import { LOCAL_STORAGE_KEYS } from 'common' import { useParams } from 'common' import { AppBannerWrapper } from 'components/interfaces/App' import { AppBannerContextProvider } from 'components/interfaces/App/AppBannerWrapperContext' import { Sidebar } from 'components/interfaces/Sidebar' import { useCheckLatestDeploy } from 'hooks/use-check-latest-deploy' +import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' +import { useAppStateSnapshot } from 'state/app-state' import { SidebarProvider } from 'ui' import { LayoutHeader } from './ProjectLayout/LayoutHeader' import MobileNavigationBar from './ProjectLayout/NavigationBar/MobileNavigationBar' @@ -28,8 +31,21 @@ export interface DefaultLayoutProps { const DefaultLayout = ({ children, headerTitle }: PropsWithChildren) => { const { ref } = useParams() const router = useRouter() + const appSnap = useAppStateSnapshot() const showProductMenu = !!ref && router.pathname !== '/project/[ref]' + const [lastVisitedOrganization] = useLocalStorageQuery( + LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, + '' + ) + + const backToDashboardURL = + appSnap.lastRouteBeforeVisitingAccountPage.length > 0 + ? appSnap.lastRouteBeforeVisitingAccountPage + : !!lastVisitedOrganization + ? `/org/${lastVisitedOrganization}` + : '/organizations' + useCheckLatestDeploy() return ( @@ -41,12 +57,18 @@ const DefaultLayout = ({ children, headerTitle }: PropsWithChildren
- +
{/* Main Content Area */}
- {/* Sidebar */} - + {/* Sidebar - Only show for project pages, not account pages */} + {!router.pathname.startsWith('/account') && } {/* Main Content */}
{children}
diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx index b0d99ecc117df..fe558c231face 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx @@ -1,4 +1,5 @@ import { AnimatePresence, motion } from 'framer-motion' +import { ChevronLeft } from 'lucide-react' import Link from 'next/link' import { ReactNode, useMemo, useState } from 'react' @@ -21,6 +22,7 @@ import { useHotKey } from 'hooks/ui/useHotKey' import { IS_PLATFORM } from 'lib/constants' import { useAppStateSnapshot } from 'state/app-state' import { Badge, cn } from 'ui' +import { useRouter } from 'next/router' import { BreadcrumbsView } from './BreadcrumbsView' import { FeedbackDropdown } from './FeedbackDropdown' import { HelpPopover } from './HelpPopover' @@ -52,6 +54,7 @@ interface LayoutHeaderProps { breadcrumbs?: any[] headerTitle?: string showProductMenu?: boolean + backToDashboardURL?: string } const LayoutHeader = ({ @@ -59,13 +62,17 @@ const LayoutHeader = ({ breadcrumbs = [], headerTitle, showProductMenu, + backToDashboardURL, }: LayoutHeaderProps) => { const { ref: projectRef, slug } = useParams() + const router = useRouter() const { data: selectedProject } = useSelectedProjectQuery() const { data: selectedOrganization } = useSelectedOrganizationQuery() const { setMobileMenuOpen } = useAppStateSnapshot() const gitlessBranching = useIsBranching2Enabled() + const isAccountPage = router.pathname.startsWith('/account') + const [showEditorPanel, setShowEditorPanel] = useState(false) useHotKey( () => { @@ -95,7 +102,17 @@ const LayoutHeader = ({ return ( <>
- {showProductMenu && ( + {backToDashboardURL && isAccountPage && ( +
+ + + +
+ )} + {(showProductMenu || isAccountPage) && (