diff --git a/apps/web/src/actions/datasets/generateDataset.ts b/apps/web/src/actions/datasets/generateDataset.ts
index 06d926aebb..38da40b61d 100644
--- a/apps/web/src/actions/datasets/generateDataset.ts
+++ b/apps/web/src/actions/datasets/generateDataset.ts
@@ -1,84 +1,34 @@
'use server'
import { z } from 'zod'
-import { BadRequestError } from '@latitude-data/constants/errors'
-import { env } from '@latitude-data/env'
-import { createSdk } from '$/app/(private)/_lib/createSdk'
-import { getCurrentUserOrRedirect } from '$/services/auth/getCurrentUser'
import { authProcedure } from '$/actions/procedures'
+import { generateDatasetWithCopilot } from '@latitude-data/core/services/datasets/generateWithCopilot'
import { createDatasetFromJson } from '@latitude-data/core/services/datasets/createFromJson'
-import {
- ChainStepResponse,
- CLOUD_MESSAGES,
- LogSources,
-} from '@latitude-data/core/constants'
export const generateDatasetAction = authProcedure
.inputSchema(
z.object({
parameters: z.string(),
- description: z.string(),
+ description: z.string().optional(),
rowCount: z.number(),
name: z.string(),
}),
)
- .action(async ({ parsedInput }) => {
- if (!env.LATITUDE_CLOUD) {
- throw new BadRequestError(CLOUD_MESSAGES.generateDatasets)
- }
- if (!env.COPILOT_PROJECT_ID) {
- throw new BadRequestError('COPILOT_PROJECT_ID is not set')
- }
- if (!env.COPILOT_PROMPT_DATASET_GENERATOR_PATH) {
- throw new BadRequestError(
- 'COPILOT_PROMPT_DATASET_GENERATOR_PATH is not set',
- )
- }
- if (!env.COPILOT_WORKSPACE_API_KEY) {
- throw new BadRequestError('COPILOT_WORKSPACE_API_KEY is not set')
- }
+ .action(async ({ parsedInput, ctx }) => {
+ const { parameters, description, rowCount, name } = parsedInput
- const { user, workspace } = await getCurrentUserOrRedirect()
- const sdk = await createSdk({
- workspace,
- apiKey: env.COPILOT_WORKSPACE_API_KEY,
- projectId: env.COPILOT_PROJECT_ID,
- __internal: { source: LogSources.Playground },
+ const generatedDatasetContent = await generateDatasetWithCopilot({
+ parameters,
+ description,
+ rowCount,
}).then((r) => r.unwrap())
- const sdkResponse = await sdk.prompts.run<{}>(
- env.COPILOT_PROMPT_DATASET_GENERATOR_PATH,
- {
- stream: false,
- parameters: {
- row_count: parsedInput.rowCount,
- parameters: parsedInput.parameters,
- user_message: parsedInput.description,
- },
- },
- )
- const sdkResult = sdkResponse
-
- if (!sdkResult) {
- throw new BadRequestError(
- 'Something went wrong generating the Dataset preview',
- )
- }
-
- const response = sdkResult.response as ChainStepResponse<'object'>
- const name = parsedInput.name
- const result = await createDatasetFromJson({
- author: user,
- workspace,
+ return await createDatasetFromJson({
+ author: ctx.user,
+ workspace: ctx.workspace,
data: {
name,
- rows: response.object.rows,
+ rows: JSON.stringify(generatedDatasetContent.rows),
},
- })
-
- if (result.error) {
- throw result.error
- }
-
- return result.value
+ }).then((r) => r.unwrap())
})
diff --git a/apps/web/src/actions/datasets/generateOnboardingDataset.ts b/apps/web/src/actions/datasets/generateOnboardingDataset.ts
new file mode 100644
index 0000000000..2a5ece9d6d
--- /dev/null
+++ b/apps/web/src/actions/datasets/generateOnboardingDataset.ts
@@ -0,0 +1,50 @@
+'use server'
+
+import { z } from 'zod'
+import { authProcedure } from '$/actions/procedures'
+import { generateDatasetWithCopilot } from '@latitude-data/core/services/datasets/generateWithCopilot'
+import {
+ SAMPLE_PROMPT,
+ SAMPLE_PROMPT_DATASET,
+} from '$/app/(onboarding)/onboarding-dataset/paste-your-prompt/constants'
+import { createDatasetFromJson } from '@latitude-data/core/services/datasets/createFromJson'
+
+export const generateOnboardingDatasetAction = authProcedure
+ .inputSchema(
+ z.object({
+ parameters: z.string(),
+ prompt: z.string().optional(),
+ rowCount: z.number(),
+ name: z.string(),
+ }),
+ )
+ .action(async ({ parsedInput, ctx }) => {
+ const { parameters, prompt, rowCount, name } = parsedInput
+
+ if (prompt === SAMPLE_PROMPT) {
+ // Caching the sample prompt dataset to avoid generating it every time
+ return await createDatasetFromJson({
+ author: ctx.user,
+ workspace: ctx.workspace,
+ data: {
+ name,
+ rows: JSON.stringify(SAMPLE_PROMPT_DATASET),
+ },
+ }).then((r) => r.unwrap())
+ }
+
+ const generatedDatasetContent = await generateDatasetWithCopilot({
+ parameters,
+ prompt,
+ rowCount,
+ }).then((r) => r.unwrap())
+
+ return await createDatasetFromJson({
+ author: ctx.user,
+ workspace: ctx.workspace,
+ data: {
+ name,
+ rows: JSON.stringify(generatedDatasetContent.rows),
+ },
+ }).then((r) => r.unwrap())
+ })
diff --git a/apps/web/src/actions/sdk/generateDatasetPreviewAction.ts b/apps/web/src/actions/sdk/generateDatasetPreviewAction.ts
index 1da781fed8..1f1e5736a1 100644
--- a/apps/web/src/actions/sdk/generateDatasetPreviewAction.ts
+++ b/apps/web/src/actions/sdk/generateDatasetPreviewAction.ts
@@ -1,11 +1,8 @@
'use server'
-import { BadRequestError } from '@latitude-data/constants/errors'
-import { env } from '@latitude-data/env'
-import { createSdk } from '$/app/(private)/_lib/createSdk'
import { generatePreviewRowsFromJson } from '@latitude-data/core/services/datasetRows/generatePreviewRowsFromJson'
import { authProcedure } from '$/actions/procedures'
import { z } from 'zod'
-import { CLOUD_MESSAGES, LogSources } from '@latitude-data/core/constants'
+import { generateDatasetWithCopilot } from '@latitude-data/core/services/datasets/generateWithCopilot'
export const generateDatasetPreviewAction = authProcedure
.inputSchema(
@@ -14,62 +11,21 @@ export const generateDatasetPreviewAction = authProcedure
parameters: z.string(),
}),
)
- .action(async ({ parsedInput, ctx }) => {
- if (!env.LATITUDE_CLOUD) {
- throw new BadRequestError(CLOUD_MESSAGES.generateDatasets)
- }
-
- if (!env.COPILOT_PROJECT_ID) {
- throw new BadRequestError('COPILOT_PROJECT_ID is not set')
- }
- if (!env.COPILOT_PROMPT_DATASET_GENERATOR_PATH) {
- throw new BadRequestError(
- 'COPILOT_PROMPT_DATASET_GENERATOR_PATH is not set',
- )
- }
- if (!env.COPILOT_WORKSPACE_API_KEY) {
- throw new BadRequestError('COPILOT_WORKSPACE_API_KEY is not set')
- }
- const sdk = await createSdk({
- workspace: ctx.workspace,
- apiKey: env.COPILOT_WORKSPACE_API_KEY,
- projectId: env.COPILOT_PROJECT_ID,
- __internal: { source: LogSources.Playground },
+ .action(async ({ parsedInput }) => {
+ const generatedDatasetContent = await generateDatasetWithCopilot({
+ parameters: parsedInput.parameters,
+ description: parsedInput.description,
+ rowCount: 10,
}).then((r) => r.unwrap())
- const result = await sdk.prompts.run<{
- rows: string
- explanation: string
- }>(env.COPILOT_PROMPT_DATASET_GENERATOR_PATH, {
- stream: false,
- parameters: {
- row_count: 10,
- parameters: parsedInput.parameters,
- user_message: parsedInput.description,
- },
- })
- if (!result) {
- throw new BadRequestError(
- 'Something went wrong generating the CSV preview',
- )
- }
-
- const response = result.response
-
- if (response.streamType !== 'object') {
- throw new BadRequestError(
- 'Generated AI response for CSV preview is not valid',
- )
- }
const parseResult = generatePreviewRowsFromJson({
- rows: response.object.rows,
+ rows: JSON.stringify(generatedDatasetContent.rows),
})
const { headers, rows } = parseResult.unwrap()
- const explanation = response.object.explanation as string
return {
headers,
rows,
- explanation,
+ explanation: generatedDatasetContent.explanation,
}
})
diff --git a/apps/web/src/actions/user/setupAction.ts b/apps/web/src/actions/user/setupAction.ts
index 58afdd325d..7a8703f8ac 100644
--- a/apps/web/src/actions/user/setupAction.ts
+++ b/apps/web/src/actions/user/setupAction.ts
@@ -10,6 +10,11 @@ import { unsafelyFindUserByEmail } from '@latitude-data/core/data-access/users'
import { errorHandlingProcedure } from '../procedures'
import { frontendRedirect } from '$/lib/frontendRedirect'
import { UserTitle } from '@latitude-data/constants/users'
+import { isFeatureEnabledByName } from '@latitude-data/core/services/workspaceFeatures/isFeatureEnabledByName'
+import { Result } from '@latitude-data/core/lib/Result'
+import { env } from '@latitude-data/env'
+import { markWorkspaceOnboardingComplete } from '@latitude-data/core/services/workspaceOnboarding/update'
+import { getWorkspaceOnboarding } from '@latitude-data/core/services/workspaceOnboarding/get'
export const setupAction = errorHandlingProcedure
.inputSchema(
@@ -55,6 +60,34 @@ export const setupAction = errorHandlingProcedure
// If there is no returnTo or its NOT a clone action url, redirect to the setup form
if (!parsedInput.returnTo || !isCloneActionUrl(parsedInput.returnTo)) {
+ const isDatasetOnboardingEnabledResult = await isFeatureEnabledByName(
+ workspace.id,
+ 'datasetOnboarding',
+ )
+
+ if (!Result.isOk(isDatasetOnboardingEnabledResult)) {
+ return frontendRedirect(ROUTES.dashboard.root)
+ }
+ const isDatasetOnboardingEnabled =
+ isDatasetOnboardingEnabledResult.unwrap()
+
+ const isCloud = !!env.LATITUDE_CLOUD
+
+ if (isDatasetOnboardingEnabled) {
+ if (isCloud) {
+ return frontendRedirect(ROUTES.onboarding.dataset.pasteYourPrompt)
+ }
+ // If user is self-hosted and they're in the new dataset onboarding, we complete the onboarding and redirect to the dashboard as they cannot generate the dataset with copilot
+ const onboarding = await getWorkspaceOnboarding({
+ workspace,
+ }).then((r) => r.unwrap())
+
+ await markWorkspaceOnboardingComplete({
+ onboarding,
+ }).then((r) => r.unwrap())
+
+ return frontendRedirect(ROUTES.dashboard.root)
+ }
return frontendRedirect(ROUTES.auth.setup.form)
}
diff --git a/apps/web/src/actions/workspaceOnboarding/complete.ts b/apps/web/src/actions/workspaceOnboarding/complete.ts
index ab28bb87ef..75f8955ee9 100644
--- a/apps/web/src/actions/workspaceOnboarding/complete.ts
+++ b/apps/web/src/actions/workspaceOnboarding/complete.ts
@@ -6,6 +6,8 @@ import { authProcedure } from '../procedures'
import { frontendRedirect } from '$/lib/frontendRedirect'
import { ROUTES } from '$/services/routes'
import { z } from 'zod'
+import { isFeatureEnabledByName } from '@latitude-data/core/services/workspaceFeatures/isFeatureEnabledByName'
+import { Result } from '@latitude-data/core/lib/Result'
/**
* Mark onboarding as complete
@@ -40,6 +42,24 @@ export const completeOnboardingAction = authProcedure
)
}
+ const isDatasetOnboardingEnabledResult = await isFeatureEnabledByName(
+ ctx.workspace.id,
+ 'datasetOnboarding',
+ )
+
+ if (!Result.isOk(isDatasetOnboardingEnabledResult)) {
+ return frontendRedirect(ROUTES.dashboard.root)
+ }
+ const isDatasetOnboardingEnabled = isDatasetOnboardingEnabledResult.unwrap()
+
+ if (isDatasetOnboardingEnabled) {
+ return frontendRedirect(
+ ROUTES.projects
+ .detail({ id: projectId })
+ .commits.detail({ uuid: commitUuid }).runs.root,
+ )
+ }
+
// Default redirect to the project's home page
return frontendRedirect(
ROUTES.projects
diff --git a/apps/web/src/app/(onboarding)/onboarding-agents/start/lib/OnboardingStep.tsx b/apps/web/src/app/(onboarding)/_lib/OnboardingStep.tsx
similarity index 74%
rename from apps/web/src/app/(onboarding)/onboarding-agents/start/lib/OnboardingStep.tsx
rename to apps/web/src/app/(onboarding)/_lib/OnboardingStep.tsx
index cb9c874a1c..a42586ece5 100644
--- a/apps/web/src/app/(onboarding)/onboarding-agents/start/lib/OnboardingStep.tsx
+++ b/apps/web/src/app/(onboarding)/_lib/OnboardingStep.tsx
@@ -25,3 +25,15 @@ export const OnboardingStep = {
Body: OnboardingStepBody,
Header: OnboardingStepHeader,
}
+
+export const DatasetOnboardingStepRoot = ({
+ children,
+}: {
+ children: ReactNode
+}) => {
+ return (
+
({
+ key: AppLocalStorage.datasetOnboardingState,
+ defaultValue: defaultDatasetOnboardingState,
+ })
+
+ const setInitialValue = useCallback(
+ (newValue: BlockRootNode) => {
+ setState((prev) => ({
+ ...prev,
+ initialValue: newValue,
+ }))
+ },
+ [setState],
+ )
+
+ const setDocumentParameters = useCallback(
+ (newValue: string[]) => {
+ setState((prev) => ({
+ ...prev,
+ documentParameters: newValue,
+ }))
+ },
+ [setState],
+ )
+
+ const setLatestDatasetName = useCallback(
+ (newValue: string) => {
+ setState((prev) => ({
+ ...prev,
+ latestDatasetName: newValue,
+ }))
+ },
+ [setState],
+ )
+
+ return {
+ initialValue: state.initialValue,
+ setInitialValue,
+ documentParameters: state.documentParameters,
+ setDocumentParameters,
+ latestDatasetName: state.latestDatasetName,
+ setLatestDatasetName,
+ }
+}
diff --git a/apps/web/src/app/(onboarding)/onboarding-dataset/generate-dataset/_components/GenerateDataset/index.tsx b/apps/web/src/app/(onboarding)/onboarding-dataset/generate-dataset/_components/GenerateDataset/index.tsx
new file mode 100644
index 0000000000..b67f4f7a3f
--- /dev/null
+++ b/apps/web/src/app/(onboarding)/onboarding-dataset/generate-dataset/_components/GenerateDataset/index.tsx
@@ -0,0 +1,78 @@
+'use client'
+
+import { Badge } from '@latitude-data/web-ui/atoms/Badge'
+import { Text } from '@latitude-data/web-ui/atoms/Text'
+import { Button } from '@latitude-data/web-ui/atoms/Button'
+import { Suspense, useCallback } from 'react'
+import { BlocksEditorPlaceholder } from '$/components/BlocksEditor'
+import { OnboardingEditor } from '../../../_components/OnboardingEditor'
+import { TableSkeleton } from '@latitude-data/web-ui/molecules/TableSkeleton'
+import { useDatasetOnboarding } from '$/app/(onboarding)/onboarding-dataset/datasetOnboarding'
+import { ROUTES } from '$/services/routes'
+import { useNavigate } from '$/hooks/useNavigate'
+
+export function GenerateDatasetBody() {
+ const { initialValue, documentParameters } = useDatasetOnboarding()
+ const router = useNavigate()
+ const moveNextStep = useCallback(() => {
+ router.push(ROUTES.onboarding.dataset.runExperiment)
+ }, [router])
+
+ return (
+
+
+
+
+
+
+ Step 2 of 3
+
+ Use a dataset
+
+
+
+ Next, we need to use some data to
+
+ populate your prompt.
+
+
+ Later, you can upload your own dataset or
+
+ integrate our SDK to use production data.
+
+
+ For now, we'll generate some synthetic
+
+ data based on your prompt.
+
+
+
+
+ Generate Dataset
+
+
+
+ )
+}
diff --git a/apps/web/src/app/(onboarding)/onboarding-dataset/generate-dataset/page.tsx b/apps/web/src/app/(onboarding)/onboarding-dataset/generate-dataset/page.tsx
new file mode 100644
index 0000000000..80661fee5a
--- /dev/null
+++ b/apps/web/src/app/(onboarding)/onboarding-dataset/generate-dataset/page.tsx
@@ -0,0 +1,31 @@
+import { DatasetOnboardingStepRoot } from '../../_lib/OnboardingStep'
+import { GenerateDatasetBody } from './_components/GenerateDataset'
+import OnboardingHeader from '../_components/OnboardingHeader'
+import { getCurrentUserOrRedirect } from '$/services/auth/getCurrentUser'
+import buildMetatags from '$/app/_lib/buildMetatags'
+import { PageTrackingWrapper } from '$/components/PageTrackingWrapper'
+
+export async function generateMetadata() {
+ // TODO(onboarding): change this to prompt engineering onboarding title once we activate the onboarding
+ return buildMetatags({
+ title: 'Dataset Onboarding - Generate Dataset',
+ })
+}
+
+export default async function GenerateDatasetPage() {
+ const { user, workspace } = await getCurrentUserOrRedirect()
+
+ return (
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/web/src/app/(onboarding)/onboarding-dataset/layout.tsx b/apps/web/src/app/(onboarding)/onboarding-dataset/layout.tsx
new file mode 100644
index 0000000000..abcbf93331
--- /dev/null
+++ b/apps/web/src/app/(onboarding)/onboarding-dataset/layout.tsx
@@ -0,0 +1,82 @@
+'use server'
+
+import { PageTrackingWrapper } from '$/components/PageTrackingWrapper'
+import { getCurrentUserOrRedirect } from '$/services/auth/getCurrentUser'
+import { isFeatureEnabledByName } from '@latitude-data/core/services/workspaceFeatures/isFeatureEnabledByName'
+import { Result } from '@latitude-data/core/lib/Result'
+import buildMetatags from '$/app/_lib/buildMetatags'
+import { getOnboardingResources } from '$/data-access/workspaceOnboarding'
+import { redirect } from 'next/navigation'
+import { ROUTES } from '$/services/routes'
+import { ProjectProvider } from '$/app/providers/ProjectProvider'
+import { CommitProvider } from '$/app/providers/CommitProvider'
+import { ONBOARDING_DOCUMENT_PATH } from '@latitude-data/core/constants'
+import { DocumentValueProvider } from '$/hooks/useDocumentValueContext'
+import { DevModeProvider } from '$/hooks/useDevMode'
+import { MetadataProvider } from '$/components/MetadataProvider'
+import { DocumentVersionProvider } from '$/app/providers/DocumentProvider'
+import { ReactNode } from 'react'
+
+export async function generateMetadata() {
+ // TODO(onboarding): change this to prompt engineering onboarding title once we activate the onboarding
+ return buildMetatags({
+ title: 'Dataset Onboarding',
+ })
+}
+
+export default async function OnboardingDatasetLayout({
+ children,
+}: {
+ children: ReactNode
+}) {
+ const { user, workspace } = await getCurrentUserOrRedirect()
+ const { project, commit, documents } = await getOnboardingResources()
+ if (project === null || commit === null) {
+ return redirect(ROUTES.auth.login)
+ }
+ const document = documents.find((d) => d.path === ONBOARDING_DOCUMENT_PATH)
+ if (!document) {
+ return redirect(ROUTES.auth.login)
+ }
+
+ // TODO(onboarding): remove this once we activate the onboarding
+ const isDatasetOnboardingEnabledResult = await isFeatureEnabledByName(
+ workspace.id,
+ 'datasetOnboarding',
+ )
+
+ if (!Result.isOk(isDatasetOnboardingEnabledResult)) {
+ return redirect(ROUTES.dashboard.root)
+ }
+ const isDatasetOnboardingEnabled = isDatasetOnboardingEnabledResult.unwrap()
+
+ if (!isDatasetOnboardingEnabled) {
+ return redirect(ROUTES.dashboard.root)
+ }
+
+ // TODO(onboarding): change dataset onboarding to prompt engineering onboarding once we activate the onboarding
+ return (
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/web/src/app/(onboarding)/onboarding-dataset/paste-your-prompt/_components/PasteYourPrompt/index.tsx b/apps/web/src/app/(onboarding)/onboarding-dataset/paste-your-prompt/_components/PasteYourPrompt/index.tsx
new file mode 100644
index 0000000000..fbcf2416c9
--- /dev/null
+++ b/apps/web/src/app/(onboarding)/onboarding-dataset/paste-your-prompt/_components/PasteYourPrompt/index.tsx
@@ -0,0 +1,146 @@
+'use client'
+
+import { Badge } from '@latitude-data/web-ui/atoms/Badge'
+import { Button } from '@latitude-data/web-ui/atoms/Button'
+import { Text } from '@latitude-data/web-ui/atoms/Text'
+import { Suspense, useCallback, useState } from 'react'
+import { BlocksEditorPlaceholder } from '$/components/BlocksEditor'
+import { useDocumentValue } from '$/hooks/useDocumentValueContext'
+import useDatasets from '$/stores/datasets'
+import { OnboardingEditor } from '../../../_components/OnboardingEditor'
+import { scan } from 'promptl-ai'
+import { fromAstToBlocks } from '$/components/BlocksEditor/Editor/state/promptlToLexical/fromAstToBlocks'
+import { useDatasetOnboarding } from '$/app/(onboarding)/onboarding-dataset/datasetOnboarding'
+import { ROUTES } from '$/services/routes'
+import { useNavigate } from '$/hooks/useNavigate'
+import { SAMPLE_PROMPT } from '../../constants'
+
+export function PasteYourPromptBody() {
+ const { value, updateDocumentContent } = useDocumentValue()
+ const { data: datasets, runGenerateOnboardingAction } = useDatasets()
+ const [editorKey, setEditorKey] = useState(0)
+ const {
+ initialValue,
+ setInitialValue,
+ setDocumentParameters,
+ setLatestDatasetName,
+ } = useDatasetOnboarding()
+ const router = useNavigate()
+
+ const onNext = useCallback(async () => {
+ // We need max speed here, so we don't want to use the useMetadata hook to get the metadata.ast or parameters
+ const metadata = await scan({ prompt: value })
+ setInitialValue(fromAstToBlocks({ ast: metadata.ast, prompt: value }))
+ // If the user doesnt add any parameters, we default to 'message'
+ const documentParameters =
+ Array.from(metadata.parameters).length > 0
+ ? Array.from(metadata.parameters)
+ : ['message']
+ setDocumentParameters(documentParameters)
+ const parameters = documentParameters.join(', ') ?? 'message'
+
+ const latestDatasetName = datasets?.[datasets.length - 1]
+ ? `Dataset Onboarding ${datasets.length}`
+ : 'Dataset Onboarding'
+ setLatestDatasetName(latestDatasetName)
+
+ runGenerateOnboardingAction({
+ parameters,
+ prompt: value,
+ rowCount: 10,
+ name: latestDatasetName,
+ })
+ router.push(ROUTES.onboarding.dataset.generateDataset)
+ }, [
+ runGenerateOnboardingAction,
+ setDocumentParameters,
+ setInitialValue,
+ value,
+ router,
+ datasets,
+ setLatestDatasetName,
+ ])
+
+ const onUseSamplePrompt = useCallback(async () => {
+ // We need max speed here, so we don't want to use the useMetadata hook to get the metadata.ast or parameters
+ const metadata = await scan({ prompt: SAMPLE_PROMPT })
+ const newInitialValue = fromAstToBlocks({
+ ast: metadata.ast,
+ prompt: SAMPLE_PROMPT,
+ })
+ setInitialValue(newInitialValue)
+ updateDocumentContent(SAMPLE_PROMPT)
+ setDocumentParameters(Array.from(metadata.parameters ?? []))
+ // Force re-render of OnboardingEditor by changing key
+ setEditorKey((prev) => prev + 1)
+ }, [setInitialValue, setDocumentParameters, updateDocumentContent])
+
+ return (
+
+
+
+
}>
+
+
+
+ Use sample prompt
+
+
+
+
+
+
+
+
+
+ Step 1 of 3
+
+ Paste Your Prompt
+
+
+
+ With Latitude, it's easy to test your
+
+ prompts at scale.
+
+
+ Paste one of your existing prompts here.
+
+
+ Make sure you replace any dynamic parts with{' '}
+
+ {{ input }}
+ {' '}
+ variables.
+
+
+
+
+ Next
+
+
+
+ )
+}
diff --git a/apps/web/src/app/(onboarding)/onboarding-dataset/paste-your-prompt/constants.ts b/apps/web/src/app/(onboarding)/onboarding-dataset/paste-your-prompt/constants.ts
new file mode 100644
index 0000000000..dcb6f1d72e
--- /dev/null
+++ b/apps/web/src/app/(onboarding)/onboarding-dataset/paste-your-prompt/constants.ts
@@ -0,0 +1,98 @@
+import { nanoidHashAlgorithm } from '@latitude-data/core/services/datasets/utils'
+import { Column } from '@latitude-data/core/schema/models/datasets'
+import { DATASET_COLUMN_ROLES } from '@latitude-data/core/constants'
+
+export type SamplePromptDocumentParameterKeys = 'score' | 'message'
+
+export type SamplePromptParameters = Record<
+ SamplePromptDocumentParameterKeys,
+ string | number
+>
+
+const SAMPLE_PROMPT = `
+---
+provider: OpenAI
+model: gpt-4.1-mini
+---
+
+This is a response from an NPS survey:
+
+Score: {{score}}
+Message: {{message}}
+
+Analyze the sentiment based on both the score and message. Prioritize identifying the primary concern in the feedback,
+focusing on the core issue mentioned by the user. Categorize the sentiment into one of the following categories:
+
+- Product Features and Functionality
+- User Interface (UI) and User Experience (UX)
+- Performance and Reliability
+- Customer Support and Service
+- Onboarding and Learning Curve
+- Pricing and Value Perception
+- Integrations and Compatibility
+- Scalability and Customization
+- Feature Requests and Product Roadmap
+- Competitor Comparison
+- General Feedback (Neutral/Non-specific)
+
+Return only one of the categories.
+`
+
+const SAMPLE_PROMPT_DATASET_COLUMNS: Column[] = [
+ {
+ identifier: nanoidHashAlgorithm({ columnName: 'score' }),
+ name: 'score',
+ role: DATASET_COLUMN_ROLES.parameter,
+ },
+ {
+ identifier: nanoidHashAlgorithm({ columnName: 'message' }),
+ name: 'message',
+ role: DATASET_COLUMN_ROLES.parameter,
+ },
+]
+
+const SAMPLE_PROMPT_DATASET: SamplePromptParameters[] = [
+ {
+ score: 5,
+ message: 'The experience is neutral, with no standout problems.',
+ },
+ {
+ score: 7,
+ message:
+ 'Overall, pretty satisfied, though some competition offers more features.',
+ },
+ {
+ score: 2,
+ message: 'Really hard to use; the learning curve is tough.',
+ },
+ {
+ score: 10,
+ message: 'Great service and seamless integration with other tools.',
+ },
+ {
+ score: 4,
+ message: 'The product is great, but the pricing is a bit high.',
+ },
+ {
+ score: 6,
+ message: 'The product is good, but the pricing is a bit high.',
+ },
+ {
+ score: 8,
+ message: 'Performance is generally good, though it can be slow at times.',
+ },
+ {
+ score: 6,
+ message: 'Navigation is okay, but onboarding materials are lacking.',
+ },
+ {
+ score: 3,
+ message: 'The app crashes frequently, making it unreliable.',
+ },
+ {
+ score: 5,
+ message: 'The experience is neutral, with no standout problems.',
+ },
+]
+
+export { SAMPLE_PROMPT_DATASET_COLUMNS, SAMPLE_PROMPT_DATASET, SAMPLE_PROMPT }
diff --git a/apps/web/src/app/(onboarding)/onboarding-dataset/paste-your-prompt/page.tsx b/apps/web/src/app/(onboarding)/onboarding-dataset/paste-your-prompt/page.tsx
new file mode 100644
index 0000000000..3f906364a1
--- /dev/null
+++ b/apps/web/src/app/(onboarding)/onboarding-dataset/paste-your-prompt/page.tsx
@@ -0,0 +1,31 @@
+import { DatasetOnboardingStepRoot } from '../../_lib/OnboardingStep'
+import { PasteYourPromptBody } from './_components/PasteYourPrompt'
+import OnboardingHeader from '../_components/OnboardingHeader'
+import { getCurrentUserOrRedirect } from '$/services/auth/getCurrentUser'
+import buildMetatags from '$/app/_lib/buildMetatags'
+import { PageTrackingWrapper } from '$/components/PageTrackingWrapper'
+
+export async function generateMetadata() {
+ // TODO(onboarding): change this to prompt engineering onboarding title once we activate the onboarding
+ return buildMetatags({
+ title: 'Dataset Onboarding - Paste Your Prompt',
+ })
+}
+
+export default async function PasteYourPromptPage() {
+ const { user, workspace } = await getCurrentUserOrRedirect()
+
+ return (
+
+
+
+ )
+}
diff --git a/apps/web/src/app/(onboarding)/onboarding-dataset/run-experiment/_components/RunExperiment/index.tsx b/apps/web/src/app/(onboarding)/onboarding-dataset/run-experiment/_components/RunExperiment/index.tsx
new file mode 100644
index 0000000000..b3d652d95b
--- /dev/null
+++ b/apps/web/src/app/(onboarding)/onboarding-dataset/run-experiment/_components/RunExperiment/index.tsx
@@ -0,0 +1,151 @@
+'use client'
+
+import { useCurrentProject } from '$/app/providers/ProjectProvider'
+import { useCurrentCommit } from '$/app/providers/CommitProvider'
+import { useCurrentDocument } from '$/app/providers/DocumentProvider'
+import { useCallback, useMemo } from 'react'
+import { Badge } from '@latitude-data/web-ui/atoms/Badge'
+import { Text } from '@latitude-data/web-ui/atoms/Text'
+import { Button } from '@latitude-data/web-ui/atoms/Button'
+import { Suspense } from 'react'
+import { BlocksEditorPlaceholder } from '$/components/BlocksEditor'
+import { OnboardingEditor } from '../../../_components/OnboardingEditor'
+import SimpleDatasetTable from '../../../_components/SimpleDatasetTable'
+import { useExperiments } from '$/stores/experiments'
+import useDatasets from '$/stores/datasets'
+import { envClient } from '$/envClient'
+import { useDatasetOnboarding } from '$/app/(onboarding)/onboarding-dataset/datasetOnboarding'
+import useWorkspaceOnboarding from '$/stores/workspaceOnboarding'
+
+const EXPERIMENT_VARIANT = [
+ {
+ name: 'Onboarding Experiment',
+ provider: envClient.NEXT_PUBLIC_DEFAULT_PROVIDER_NAME,
+ model: 'gpt-4o-mini',
+ temperature: 1,
+ },
+]
+
+export default function RunExperimentBody() {
+ const { executeCompleteOnboarding } = useWorkspaceOnboarding()
+ const { project } = useCurrentProject()
+ const { commit } = useCurrentCommit()
+ const { document } = useCurrentDocument()
+ const { data: datasets } = useDatasets()
+ const { initialValue, documentParameters, latestDatasetName } =
+ useDatasetOnboarding()
+
+ // Get the latest dataset, as the user might have gone back and forth between steps, creating multiple datasets
+ const latestDataset = datasets.find((ds) => ds.name === latestDatasetName)
+
+ const parametersMap = useMemo(() => {
+ return latestDataset && documentParameters.length
+ ? Object.fromEntries(
+ latestDataset.columns
+ .map((col, index) =>
+ documentParameters.includes(col.name) ? [col.name, index] : null,
+ )
+ .filter(Boolean) as [string, number][],
+ )
+ : {}
+ }, [latestDataset, documentParameters])
+
+ const { create, isCreating } = useExperiments(
+ {
+ projectId: project.id,
+ documentUuid: document.documentUuid,
+ },
+ {
+ onCreate: async () => {
+ executeCompleteOnboarding({
+ projectId: project.id,
+ commitUuid: commit.uuid,
+ documentUuid: document.documentUuid,
+ })
+ },
+ },
+ )
+
+ const onCompleteOnboarding = useCallback(() => {
+ create({
+ projectId: project.id,
+ commitUuid: commit.uuid,
+ documentUuid: document.documentUuid,
+ variants: EXPERIMENT_VARIANT,
+ datasetId: latestDataset?.id,
+ parametersMap,
+ datasetLabels: {},
+ fromRow: 1,
+ evaluationUuids: [],
+ })
+ }, [
+ create,
+ latestDataset?.id,
+ project.id,
+ commit.uuid,
+ document.documentUuid,
+ parametersMap,
+ ])
+
+ return (
+
+
+
+
+
+
+ Step 3 of 3
+
+ Run experiment
+
+
+
+ Finally, let's run an experiment to see how
+
+ your prompt does.
+
+
+ After running it, you'll be able to review
+
+ and annotate each run.
+
+
+ We automatically aggregate the issues
+
+ detected and generate specific LLM
+
+ evaluators for each one.
+
+
+
+
+ Run Experiment
+
+
+
+ )
+}
diff --git a/apps/web/src/app/(onboarding)/onboarding-dataset/run-experiment/page.tsx b/apps/web/src/app/(onboarding)/onboarding-dataset/run-experiment/page.tsx
new file mode 100644
index 0000000000..76c3b06738
--- /dev/null
+++ b/apps/web/src/app/(onboarding)/onboarding-dataset/run-experiment/page.tsx
@@ -0,0 +1,31 @@
+import { DatasetOnboardingStepRoot } from '../../_lib/OnboardingStep'
+import RunExperimentBody from './_components/RunExperiment'
+import OnboardingHeader from '../_components/OnboardingHeader'
+import { getCurrentUserOrRedirect } from '$/services/auth/getCurrentUser'
+import buildMetatags from '$/app/_lib/buildMetatags'
+import { PageTrackingWrapper } from '$/components/PageTrackingWrapper'
+
+export async function generateMetadata() {
+ // TODO(onboarding): change this to prompt engineering onboarding title once we activate the onboarding
+ return buildMetatags({
+ title: 'Dataset Onboarding - Run Experiment',
+ })
+}
+
+export default async function RunExperimentPage() {
+ const { user, workspace } = await getCurrentUserOrRedirect()
+
+ return (
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/web/src/app/(private)/datasets/_components/RootHeader/GenerateDatasetModal/GenerateDatasetModal/index.tsx b/apps/web/src/app/(private)/datasets/_components/RootHeader/GenerateDatasetModal/GenerateDatasetModal/index.tsx
index 62942c2216..631b946822 100644
--- a/apps/web/src/app/(private)/datasets/_components/RootHeader/GenerateDatasetModal/GenerateDatasetModal/index.tsx
+++ b/apps/web/src/app/(private)/datasets/_components/RootHeader/GenerateDatasetModal/GenerateDatasetModal/index.tsx
@@ -1,13 +1,11 @@
import { FormEvent } from 'react'
import { useToast } from '@latitude-data/web-ui/atoms/Toast'
-import { generateDatasetAction } from '$/actions/datasets/generateDataset'
import { useNavigate } from '$/hooks/useNavigate'
import { ROUTES } from '$/services/routes'
import useDatasets from '$/stores/datasets'
import { GenerateDatasetModalComponent } from './GenerateDatasetModalComponent'
import { useDatasetPreviewModal } from './useDatasetPreviewModal'
-import useLatitudeAction from '$/hooks/useLatitudeAction'
export function GenerateDatasetModal({
open,
@@ -24,20 +22,14 @@ export function GenerateDatasetModal({
}) {
const navigate = useNavigate()
const { toast } = useToast()
- const { data: datasets, mutate } = useDatasets()
const {
- execute: runGenerateAction,
- isPending: generateIsLoading,
- error: generateError,
- } = useLatitudeAction(generateDatasetAction, {
- onError: (error) => {
- toast({
- title: 'Failed to generate dataset',
- description: error.message,
- variant: 'destructive',
- })
- },
- })
+ data: datasets,
+ mutate,
+ runGenerateAction,
+ generateIsLoading,
+ generateError,
+ } = useDatasets()
+
const modalData = useDatasetPreviewModal({
defaultParameters,
generateErrorMessage: generateError?.message,
diff --git a/apps/web/src/app/(private)/layout.tsx b/apps/web/src/app/(private)/layout.tsx
index d3f417257d..7567b98f43 100644
--- a/apps/web/src/app/(private)/layout.tsx
+++ b/apps/web/src/app/(private)/layout.tsx
@@ -17,6 +17,8 @@ import { redirect } from 'next/navigation'
import { CSPostHogProvider, IdentifyUser } from '../providers'
import { PaywallModalProvider } from './providers/PaywallModalProvider'
+import { isFeatureEnabledByName } from '@latitude-data/core/services/workspaceFeatures/isFeatureEnabledByName'
+import { Result } from '@latitude-data/core/lib/Result'
export const metadata = buildMetatags({
title: 'Home',
@@ -31,7 +33,20 @@ export default async function PrivateLayout({
const { workspace, user, subscriptionPlan } = await getCurrentUserOrRedirect()
const completed = await isOnboardingCompleted()
+
+ const datasetOnboardingEnabledResult = await isFeatureEnabledByName(
+ workspace.id,
+ 'datasetOnboarding',
+ )
+ if (!Result.isOk(datasetOnboardingEnabledResult)) {
+ redirect(ROUTES.dashboard.root)
+ }
+ const isDatasetOnboardingEnabled = datasetOnboardingEnabledResult.unwrap()
+
if (!completed) {
+ if (isDatasetOnboardingEnabled) {
+ redirect(ROUTES.onboarding.dataset.pasteYourPrompt)
+ }
redirect(ROUTES.onboarding.agents.selectAgent)
}
diff --git a/apps/web/src/components/BlocksEditor/Editor/index.tsx b/apps/web/src/components/BlocksEditor/Editor/index.tsx
index ec621a16b5..7c7928a663 100644
--- a/apps/web/src/components/BlocksEditor/Editor/index.tsx
+++ b/apps/web/src/components/BlocksEditor/Editor/index.tsx
@@ -98,6 +98,7 @@ export function BlocksEditor({
onToggleDevEditor,
readOnlyMessage,
autoFocus = false,
+ greyTheme = false,
}: BlocksEditorProps) {
const readOnly = Boolean(readOnlyMessage)
const [floatingAnchorElem, setFloatingAnchorElem] =
@@ -137,6 +138,7 @@ export function BlocksEditor({
@@ -192,14 +194,18 @@ export function BlocksEditor({
-
-
+ {onRequestPromptMetadata && onToggleDevEditor && prompts && (
+
+ )}
+ {onRequestPromptMetadata && (
+
+ )}
diff --git a/apps/web/src/components/BlocksEditor/Editor/plugins/ReferenceEditPlugin.tsx b/apps/web/src/components/BlocksEditor/Editor/plugins/ReferenceEditPlugin.tsx
index b9081a6d48..957300195a 100644
--- a/apps/web/src/components/BlocksEditor/Editor/plugins/ReferenceEditPlugin.tsx
+++ b/apps/web/src/components/BlocksEditor/Editor/plugins/ReferenceEditPlugin.tsx
@@ -13,6 +13,8 @@ export function ReferenceEditPlugin({
const [editor] = useLexicalComposerContext()
useEffect(() => {
+ if (!onRequestPromptMetadata) return
+
const abortController = new AbortController()
const handleReferencePathUpdate = async (event: Event) => {
diff --git a/apps/web/src/components/BlocksEditor/Editor/plugins/ReferencesPlugin.tsx b/apps/web/src/components/BlocksEditor/Editor/plugins/ReferencesPlugin.tsx
index 9220a2ca91..d0fb24a21c 100644
--- a/apps/web/src/components/BlocksEditor/Editor/plugins/ReferencesPlugin.tsx
+++ b/apps/web/src/components/BlocksEditor/Editor/plugins/ReferencesPlugin.tsx
@@ -49,19 +49,19 @@ export function EventListeners({
onToggleDevEditor: BlocksEditorProps['onToggleDevEditor']
}) {
useEffect(() => {
+ if (!onToggleDevEditor) return
+
const abortController = new AbortController()
- document.addEventListener(
- CUSTOM_EVENTS.GO_TO_DEV_EDITOR,
- onToggleDevEditor,
- { signal: abortController.signal },
- )
+ const handleEvent = () => {
+ onToggleDevEditor()
+ }
+ document.addEventListener(CUSTOM_EVENTS.GO_TO_DEV_EDITOR, handleEvent, {
+ signal: abortController.signal,
+ })
return () => {
abortController.abort()
- document.removeEventListener(
- CUSTOM_EVENTS.GO_TO_DEV_EDITOR,
- onToggleDevEditor,
- )
+ document.removeEventListener(CUSTOM_EVENTS.GO_TO_DEV_EDITOR, handleEvent)
}
}, [onToggleDevEditor])
@@ -144,6 +144,8 @@ export function ReferencesPlugin({
closeMenu: () => void,
_matchingString: string,
) => {
+ if (!onRequestPromptMetadata) return
+
let referenceNode: ReferenceNode
editor.update(() => {
diff --git a/apps/web/src/components/BlocksEditor/Editor/state/promptlToLexical/fromAstToBlocks.ts b/apps/web/src/components/BlocksEditor/Editor/state/promptlToLexical/fromAstToBlocks.ts
index 80f7817370..ee5143040b 100644
--- a/apps/web/src/components/BlocksEditor/Editor/state/promptlToLexical/fromAstToBlocks.ts
+++ b/apps/web/src/components/BlocksEditor/Editor/state/promptlToLexical/fromAstToBlocks.ts
@@ -874,4 +874,13 @@ export function fromAstToBlocks({
} satisfies BlockRootNode
}
+export const EMPTY_ROOT_BLOCK: BlockRootNode = {
+ type: BLOCK_EDITOR_TYPE.ROOT,
+ children: [createEmptyParagraph({ content: '' })],
+ version: 1,
+ direction: 'ltr',
+ indent: 0,
+ format: '',
+}
+
export type { BlockRootNode }
diff --git a/apps/web/src/components/BlocksEditor/types.ts b/apps/web/src/components/BlocksEditor/types.ts
index b5b30d782a..7281e665a7 100644
--- a/apps/web/src/components/BlocksEditor/types.ts
+++ b/apps/web/src/components/BlocksEditor/types.ts
@@ -21,14 +21,15 @@ export type BlocksEditorProps = {
currentDocument: { path: string }
initialValue: BlockRootNode
placeholder: string
- onToggleDevEditor: () => void
+ onToggleDevEditor?: () => void
onError: (error: Error) => void
prompts: Record
- onRequestPromptMetadata: (
+ onRequestPromptMetadata?: (
prompt: IncludedPrompt,
) => Promise
onChange: updateContentFn
className?: string
readOnlyMessage?: string
autoFocus?: boolean
+ greyTheme?: boolean
}
diff --git a/apps/web/src/components/MetadataProvider.tsx b/apps/web/src/components/MetadataProvider.tsx
index 3cfff67500..a827203d36 100644
--- a/apps/web/src/components/MetadataProvider.tsx
+++ b/apps/web/src/components/MetadataProvider.tsx
@@ -23,7 +23,7 @@ type MetadataProviderProps = {
}
export function MetadataProvider({ children }: MetadataProviderProps) {
- const { setMetadata, setWorker } = useMetadataStore()
+ const { setMetadata, setWorker, reset } = useMetadataStore()
useEffect(() => {
if (typeof window === 'undefined') return // Only on client code
@@ -56,8 +56,9 @@ export function MetadataProvider({ children }: MetadataProviderProps) {
currentWorker.terminate()
setWorker(null)
}
+ reset()
}
- }, [setMetadata, setWorker])
+ }, [setMetadata, setWorker, reset])
return <>{children}>
}
diff --git a/apps/web/src/data-access/workspaceOnboarding.ts b/apps/web/src/data-access/workspaceOnboarding.ts
index 875543ff50..d3b3d704d5 100644
--- a/apps/web/src/data-access/workspaceOnboarding.ts
+++ b/apps/web/src/data-access/workspaceOnboarding.ts
@@ -62,6 +62,9 @@ export async function getOnboardingResources() {
return { workspace, documents, project, commit }
}
+/**
+ * Get the onboarding dataset (if it exists)
+ */
export async function getOnboardingDataset() {
const { workspace } = await getCurrentUserOrRedirect()
if (!workspace?.id) {
@@ -74,6 +77,9 @@ export async function getOnboardingDataset() {
return datasetResult.unwrap()
}
+/**
+ * Get the necessary onboarding steps to complete the onboarding
+ */
export async function getNecessaryOnboardingSteps() {
const { workspace } = await getCurrentUserOrRedirect()
if (!workspace?.id) {
diff --git a/apps/web/src/services/routes.ts b/apps/web/src/services/routes.ts
index d444834eb0..e181e38e78 100644
--- a/apps/web/src/services/routes.ts
+++ b/apps/web/src/services/routes.ts
@@ -46,6 +46,11 @@ export const ROUTES = {
start: '/onboarding-agents/start',
},
promptEngineering: '/onboarding-prompt-engineering',
+ dataset: {
+ pasteYourPrompt: '/onboarding-dataset/paste-your-prompt',
+ generateDataset: '/onboarding-dataset/generate-dataset',
+ runExperiment: '/onboarding-dataset/run-experiment',
+ },
},
backoffice: {
root: BACKOFFICE_ROOT,
diff --git a/apps/web/src/stores/datasetRows/index.ts b/apps/web/src/stores/datasetRows/index.ts
index 163b168bb9..fdd0a286cf 100644
--- a/apps/web/src/stores/datasetRows/index.ts
+++ b/apps/web/src/stores/datasetRows/index.ts
@@ -149,7 +149,7 @@ export default function useDatasetRows(
if (!createdRow || !dataset) return
const row = serializeRow({ row: createdRow })
- mutate([row, ...data])
+ mutate([...data, row])
},
},
)
diff --git a/apps/web/src/stores/datasets.ts b/apps/web/src/stores/datasets.ts
index 87b11880dd..e5bdb6da8f 100644
--- a/apps/web/src/stores/datasets.ts
+++ b/apps/web/src/stores/datasets.ts
@@ -9,6 +9,8 @@ import useSWR, { SWRConfiguration } from 'swr'
import { compactObject } from '@latitude-data/core/lib/compactObject'
import { useCallback, useState } from 'react'
import { Dataset } from '@latitude-data/core/schema/models/types/Dataset'
+import { generateDatasetAction } from '$/actions/datasets/generateDataset'
+import { generateOnboardingDatasetAction } from '$/actions/datasets/generateOnboardingDataset'
const EMPTY_ARRAY: Dataset[] = []
@@ -137,6 +139,40 @@ export default function useDatasets(
},
})
+ const {
+ execute: runGenerateAction,
+ isPending: generateIsLoading,
+ error: generateError,
+ } = useLatitudeAction(generateDatasetAction, {
+ onError: (error) => {
+ toast({
+ title: 'Failed to generate dataset',
+ description: error.message,
+ variant: 'destructive',
+ })
+ },
+ onSuccess: ({ data: dataset }) => {
+ mutate([...data, dataset])
+ },
+ })
+
+ const {
+ execute: runGenerateOnboardingAction,
+ isPending: generateOnboardingIsLoading,
+ error: generateOnboardingError,
+ } = useLatitudeAction(generateOnboardingDatasetAction, {
+ onError: (error) => {
+ toast({
+ title: 'Failed to generate onboarding dataset',
+ description: error.message,
+ variant: 'destructive',
+ })
+ },
+ onSuccess: ({ data: dataset }) => {
+ mutate([...data, dataset])
+ },
+ })
+
return {
data,
mutate,
@@ -147,6 +183,12 @@ export default function useDatasets(
isDestroying,
updateColumn,
isUpdatingColumn,
+ runGenerateAction,
+ generateIsLoading,
+ generateError,
+ runGenerateOnboardingAction,
+ generateOnboardingIsLoading,
+ generateOnboardingError,
...rest,
}
}
diff --git a/apps/web/src/stores/workspaceOnboarding.ts b/apps/web/src/stores/workspaceOnboarding.ts
index 189f6abf90..0b7b761b96 100644
--- a/apps/web/src/stores/workspaceOnboarding.ts
+++ b/apps/web/src/stores/workspaceOnboarding.ts
@@ -25,13 +25,27 @@ export default function useWorkspaceOnboarding() {
const { execute: executeCompleteOnboarding } = useLatitudeAction(
completeOnboardingAction,
+ {
+ onSuccess: () => {
+ // No-op
+ },
+ },
)
const { execute: executeCreatePromptEngineeringResources } =
- useLatitudeAction(createPromptEngineeringResourcesAction)
+ useLatitudeAction(createPromptEngineeringResourcesAction, {
+ onSuccess: () => {
+ // No-op
+ },
+ })
const { execute: createDefaultAgentOnboardingProject } = useLatitudeAction(
createDefaultAgentOnboardingProjectAction,
+ {
+ onSuccess: () => {
+ // No-op
+ },
+ },
)
const {
diff --git a/packages/constants/src/onboardingSteps.ts b/packages/constants/src/onboardingSteps.ts
index a8a19f3ada..71e7b090be 100644
--- a/packages/constants/src/onboardingSteps.ts
+++ b/packages/constants/src/onboardingSteps.ts
@@ -19,3 +19,21 @@ export const ONBOARDING_STEPS = {
order: 4,
},
} as const
+
+export enum DatasetOnboardingStepKey {
+ PasteYourPrompt = 'pasteYourPrompt',
+ GenerateDataset = 'generateDataset',
+ RunExperiment = 'runExperiment',
+}
+
+export const DATASET_ONBOARDING_STEPS = {
+ [DatasetOnboardingStepKey.PasteYourPrompt]: {
+ order: 1,
+ },
+ [DatasetOnboardingStepKey.GenerateDataset]: {
+ order: 2,
+ },
+ [DatasetOnboardingStepKey.RunExperiment]: {
+ order: 3,
+ },
+} as const
diff --git a/packages/core/src/services/datasets/generateWithCopilot.test.ts b/packages/core/src/services/datasets/generateWithCopilot.test.ts
new file mode 100644
index 0000000000..5c7ee7a9a6
--- /dev/null
+++ b/packages/core/src/services/datasets/generateWithCopilot.test.ts
@@ -0,0 +1,173 @@
+import { describe, it, expect, beforeEach, vi, type MockInstance } from 'vitest'
+import * as env from '@latitude-data/env'
+import { BadRequestError } from '@latitude-data/constants/errors'
+import { Result } from '../../lib/Result'
+import { UnprocessableEntityError } from '../../lib/errors'
+import { CLOUD_MESSAGES } from '../../constants'
+import * as copilotGet from '../copilot/get'
+import * as copilotRun from '../copilot/run'
+import { generateDatasetWithCopilot } from './generateWithCopilot'
+
+let mockGetCopilot: MockInstance
+let mockRunCopilot: MockInstance
+let envSpy: MockInstance
+
+describe('generateDatasetWithCopilot', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ vi.resetAllMocks()
+
+ // Setup env spy with default values
+ envSpy = vi.spyOn(env, 'env', 'get').mockReturnValue({
+ ...env.env,
+ LATITUDE_CLOUD: true,
+ COPILOT_PROJECT_ID: 'project-id',
+ COPILOT_PROMPT_DATASET_GENERATOR_PATH: '/copilot/datasets/generator',
+ COPILOT_WORKSPACE_API_KEY: 'workspace-api-key',
+ } as any)
+
+ // Setup default mocks for copilot functions
+ mockGetCopilot = vi.spyOn(copilotGet, 'getCopilot').mockResolvedValue(
+ Result.ok({
+ workspace: {} as any,
+ commit: {} as any,
+ document: {} as any,
+ }),
+ )
+
+ mockRunCopilot = vi.spyOn(copilotRun, 'runCopilot').mockResolvedValue(
+ Result.ok({
+ rows: [
+ { name: 'Alice', age: 30 },
+ { name: 'Bob', age: 25 },
+ ],
+ explanation: 'Generated sample dataset',
+ }),
+ )
+ })
+
+ it('returns rows and explanation from copilot', async () => {
+ const result = await generateDatasetWithCopilot({
+ parameters: 'name,age',
+ description: 'People dataset',
+ prompt: 'Generate sample people',
+ rowCount: 2,
+ })
+
+ expect(Result.isOk(result)).toBe(true)
+ const data = result.value!
+ expect(data.rows).toEqual([
+ { name: 'Alice', age: 30 },
+ { name: 'Bob', age: 25 },
+ ])
+ expect(data.explanation).toBe('Generated sample dataset')
+ expect(data.rows).toHaveLength(2)
+ })
+
+ it('returns error when LATITUDE_CLOUD is not enabled', async () => {
+ envSpy.mockReturnValue({
+ ...env.env,
+ LATITUDE_CLOUD: false,
+ COPILOT_PROJECT_ID: 'project-id',
+ COPILOT_PROMPT_DATASET_GENERATOR_PATH: '/copilot/datasets/generator',
+ COPILOT_WORKSPACE_API_KEY: 'workspace-api-key',
+ } as any)
+
+ const res = await generateDatasetWithCopilot({
+ parameters: 'foo',
+ rowCount: 1,
+ })
+
+ expect(Result.isOk(res)).toBe(false)
+ expect(res.error).toBeInstanceOf(BadRequestError)
+ expect(res.error?.message).toBe(CLOUD_MESSAGES.generateDatasets)
+ })
+
+ it('returns error when COPILOT_PROJECT_ID is not set', async () => {
+ envSpy.mockReturnValue({
+ ...env.env,
+ LATITUDE_CLOUD: true,
+ COPILOT_PROJECT_ID: '',
+ COPILOT_PROMPT_DATASET_GENERATOR_PATH: '/copilot/datasets/generator',
+ COPILOT_WORKSPACE_API_KEY: 'workspace-api-key',
+ } as any)
+
+ const res = await generateDatasetWithCopilot({
+ parameters: 'foo',
+ rowCount: 1,
+ })
+
+ expect(Result.isOk(res)).toBe(false)
+ expect(res.error).toBeInstanceOf(BadRequestError)
+ expect(res.error?.message).toBe('COPILOT_PROJECT_ID is not set')
+ })
+
+ it('returns error when COPILOT_PROMPT_DATASET_GENERATOR_PATH is not set', async () => {
+ envSpy.mockReturnValue({
+ ...env.env,
+ LATITUDE_CLOUD: true,
+ COPILOT_PROJECT_ID: 'project-id',
+ COPILOT_PROMPT_DATASET_GENERATOR_PATH: '',
+ COPILOT_WORKSPACE_API_KEY: 'workspace-api-key',
+ } as any)
+
+ const res = await generateDatasetWithCopilot({
+ parameters: 'foo',
+ rowCount: 1,
+ })
+
+ expect(Result.isOk(res)).toBe(false)
+ expect(res.error).toBeInstanceOf(BadRequestError)
+ expect(res.error?.message).toBe(
+ 'COPILOT_PROMPT_DATASET_GENERATOR_PATH is not set',
+ )
+ })
+
+ it('returns error when COPILOT_WORKSPACE_API_KEY is not set', async () => {
+ envSpy.mockReturnValue({
+ ...env.env,
+ LATITUDE_CLOUD: true,
+ COPILOT_PROJECT_ID: 'project-id',
+ COPILOT_PROMPT_DATASET_GENERATOR_PATH: '/copilot/datasets/generator',
+ COPILOT_WORKSPACE_API_KEY: '',
+ } as any)
+
+ const res = await generateDatasetWithCopilot({
+ parameters: 'foo',
+ rowCount: 1,
+ })
+
+ expect(Result.isOk(res)).toBe(false)
+ expect(res.error).toBeInstanceOf(BadRequestError)
+ expect(res.error?.message).toBe('COPILOT_WORKSPACE_API_KEY is not set')
+ })
+
+ it('returns error when getCopilot fails', async () => {
+ mockGetCopilot.mockResolvedValue(
+ Result.error(new Error('Copilot not found')),
+ )
+
+ const res = await generateDatasetWithCopilot({
+ parameters: 'foo',
+ rowCount: 1,
+ })
+
+ expect(Result.isOk(res)).toBe(false)
+ expect(res.error?.message).toBe('Copilot not found')
+ })
+
+ it('returns error when runCopilot fails', async () => {
+ mockRunCopilot.mockResolvedValue(
+ Result.error(new UnprocessableEntityError('Copilot execution failed')),
+ )
+
+ const res = await generateDatasetWithCopilot({
+ parameters: 'foo',
+ rowCount: 1,
+ })
+
+ expect(Result.isOk(res)).toBe(false)
+ expect(res.error).toBeInstanceOf(UnprocessableEntityError)
+ expect(res.error?.message).toBe('Copilot execution failed')
+ })
+})
diff --git a/packages/core/src/services/datasets/generateWithCopilot.ts b/packages/core/src/services/datasets/generateWithCopilot.ts
new file mode 100644
index 0000000000..37f78b0056
--- /dev/null
+++ b/packages/core/src/services/datasets/generateWithCopilot.ts
@@ -0,0 +1,69 @@
+import { env } from '@latitude-data/env'
+import { BadRequestError } from '@latitude-data/constants/errors'
+import { Result } from '../../lib/Result'
+import { database } from '../../client'
+import { getCopilot } from '../copilot/get'
+import { runCopilot } from '../copilot/run'
+import { z } from 'zod'
+import { CLOUD_MESSAGES } from '../../constants'
+
+const generatedDatasetResponseSchema = z.object({
+ rows: z.array(z.record(z.string(), z.unknown())),
+ explanation: z.string(),
+})
+
+export async function generateDatasetWithCopilot(
+ {
+ parameters,
+ description,
+ prompt,
+ rowCount,
+ }: {
+ parameters: string
+ description?: string
+ prompt?: string
+ rowCount: number
+ },
+ db = database,
+) {
+ if (!env.LATITUDE_CLOUD) {
+ return Result.error(new BadRequestError(CLOUD_MESSAGES.generateDatasets))
+ }
+
+ if (!env.COPILOT_PROJECT_ID) {
+ return Result.error(new BadRequestError('COPILOT_PROJECT_ID is not set'))
+ }
+ if (!env.COPILOT_PROMPT_DATASET_GENERATOR_PATH) {
+ return Result.error(
+ new BadRequestError('COPILOT_PROMPT_DATASET_GENERATOR_PATH is not set'),
+ )
+ }
+ if (!env.COPILOT_WORKSPACE_API_KEY) {
+ return Result.error(
+ new BadRequestError('COPILOT_WORKSPACE_API_KEY is not set'),
+ )
+ }
+
+ const copilotResult = await getCopilot(
+ {
+ path: env.COPILOT_PROMPT_DATASET_GENERATOR_PATH,
+ },
+ db,
+ )
+
+ if (!Result.isOk(copilotResult)) {
+ return copilotResult
+ }
+
+ const copilot = copilotResult.unwrap()
+ return await runCopilot({
+ copilot: copilot,
+ parameters: {
+ row_count: rowCount,
+ parameters: parameters,
+ user_message: description,
+ prompt: prompt,
+ },
+ schema: generatedDatasetResponseSchema,
+ })
+}
diff --git a/packages/core/src/services/onboardingResources/createDatasetOnboarding.ts b/packages/core/src/services/onboardingResources/createDatasetOnboarding.ts
new file mode 100644
index 0000000000..cc9e304cef
--- /dev/null
+++ b/packages/core/src/services/onboardingResources/createDatasetOnboarding.ts
@@ -0,0 +1,36 @@
+import { type User } from '../../schema/models/types/User'
+import { type Workspace } from '../../schema/models/types/Workspace'
+import Transaction from '../../lib/Transaction'
+import { createOnboardingProject } from '../projects/createOnboardingProject'
+import { Result } from '../../lib/Result'
+
+export async function createDatasetOnboarding(
+ {
+ workspace,
+ user,
+ }: {
+ workspace: Workspace
+ user: User
+ },
+ transaction = new Transaction(),
+) {
+ return transaction.call(async () => {
+ const projectResult = await createOnboardingProject(
+ {
+ workspace,
+ user,
+ },
+ transaction,
+ )
+ if (projectResult.error) {
+ return projectResult
+ }
+ const { project, documents, commit } = projectResult.unwrap()
+
+ return Result.ok({
+ project,
+ documents,
+ commit,
+ })
+ })
+}
diff --git a/packages/core/src/services/projects/createOnboardingProject.ts b/packages/core/src/services/projects/createOnboardingProject.ts
index bd82d3e393..09d5940623 100644
--- a/packages/core/src/services/projects/createOnboardingProject.ts
+++ b/packages/core/src/services/projects/createOnboardingProject.ts
@@ -4,10 +4,9 @@ import { Result } from '../../lib/Result'
import Transaction from '../../lib/Transaction'
import { createProject } from './create'
import { createNewDocument } from '../documents/create'
-import { mergeCommit } from '../commits/merge'
/**
- * Creates an onboarding project with a single document containing a template for product descriptions
+ * Creates an onboarding project with a single empty document to let the user fill in
*/
export async function createOnboardingProject(
{
@@ -29,39 +28,25 @@ export async function createOnboardingProject(
transaction,
).then((r) => r.unwrap())
- // Create a document with the specified content
+ // Create an empty document to fill in by the user
const documentResult = await createNewDocument(
{
workspace,
user,
commit,
path: 'onboarding',
- content: `---
-provider: Latitude
-model: gpt-4o-mini
----
-
-Write a compelling product description for {{product_name}} with the following features:
-{{features}}
-
-The description should be appropriate for {{target_audience}} and highlight the main benefits.
-Tone: {{tone}}
-Length: {{word_count}} words`,
},
transaction,
)
- if (documentResult.error) {
+ if (!Result.isOk(documentResult)) {
return documentResult
}
- // Merge the commit to finalize the document
- const mergedCommit = await mergeCommit(commit, transaction).then((r) =>
- r.unwrap(),
- )
+ const document = documentResult.unwrap()
return Result.ok({
project,
- documents: [documentResult.unwrap()],
- commit: mergedCommit,
+ documents: [document],
+ commit,
})
}
diff --git a/packages/core/src/services/users/setupService.ts b/packages/core/src/services/users/setupService.ts
index ab5bf62c66..9e8e89c64b 100644
--- a/packages/core/src/services/users/setupService.ts
+++ b/packages/core/src/services/users/setupService.ts
@@ -11,6 +11,8 @@ import { createProviderApiKey } from '../providerApiKeys'
import { createWorkspace } from '../workspaces'
import { createUser } from './createUser'
import { UserTitle } from '@latitude-data/constants/users'
+import { createDatasetOnboarding } from '../onboardingResources/createDatasetOnboarding'
+import { isFeatureEnabledByName } from '../workspaceFeatures/isFeatureEnabledByName'
const DEFAULT_MODEL = 'gpt-4o-mini'
@@ -36,7 +38,7 @@ export default async function setupService(
},
transaction = new Transaction(),
): PromisedResult<{ user: User; workspace: Workspace }> {
- return transaction.call(async () => {
+ return transaction.call(async (trx) => {
const user = await createUser(
{ email, name, confirmedAt: new Date(), title },
transaction,
@@ -75,6 +77,25 @@ export default async function setupService(
r.unwrap(),
)
+ const isDatasetOnboardingEnabledResult = await isFeatureEnabledByName(
+ workspace.id,
+ 'datasetOnboarding',
+ trx,
+ )
+
+ if (!Result.isOk(isDatasetOnboardingEnabledResult)) {
+ return Result.error(
+ new Error('Failed checking dataset onboarding feature'),
+ )
+ }
+ const isDatasetOnboardingEnabled = isDatasetOnboardingEnabledResult.unwrap()
+
+ if (isDatasetOnboardingEnabled) {
+ await createDatasetOnboarding({ workspace, user }, transaction).then(
+ (r) => r.unwrap(),
+ )
+ }
+
publisher.publishLater({
type: 'userCreated',
data: {
diff --git a/packages/web-ui/src/lib/hooks/useLocalStorage.ts b/packages/web-ui/src/lib/hooks/useLocalStorage.ts
index 7ea24c3be3..62af44fc2d 100644
--- a/packages/web-ui/src/lib/hooks/useLocalStorage.ts
+++ b/packages/web-ui/src/lib/hooks/useLocalStorage.ts
@@ -20,6 +20,7 @@ export enum AppLocalStorage {
chatDebugMode = 'chatDebugMode',
latteThread = 'latteThread',
latteSidebarWidth = 'latteSidebarWidth',
+ datasetOnboardingState = 'datasetOnboardingState',
}
export const isLocalStorageAvailable = (() => {