mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-12 19:03:42 -05:00
add document search
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
"use server";
|
||||
|
||||
import { embed, generateText } from "ai";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { embeddingsModel, llmModel } from "@formbricks/lib/ai";
|
||||
import { findNearestDocuments, getDocumentsByTypeAndReferenceId } from "@formbricks/lib/document/service";
|
||||
import { getQuestionResponseReferenceId } from "@formbricks/lib/document/utils";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
|
||||
const ZSearchDocuments = z.object({
|
||||
environmentId: ZId,
|
||||
searchTerm: z.string(),
|
||||
});
|
||||
|
||||
export const searchDocumentsAction = authenticatedActionClient
|
||||
.schema(ZSearchDocuments)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
rules: ["environment", "read"],
|
||||
});
|
||||
|
||||
const { text } = await generateText({
|
||||
model: llmModel,
|
||||
prompt: `Generate 4 example survey feedback responses from users in one line, separated by comma for the following topic: ${parsedInput.searchTerm}`,
|
||||
});
|
||||
|
||||
const { embedding } = await embed({
|
||||
model: embeddingsModel,
|
||||
value: text,
|
||||
});
|
||||
|
||||
const documents = findNearestDocuments(parsedInput.environmentId, embedding, 10);
|
||||
|
||||
return documents;
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { TDocument } from "@formbricks/types/documents";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Card } from "@formbricks/ui/Card";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { searchDocumentsAction } from "../actions";
|
||||
|
||||
interface DocumentSearchProps {
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
export const DocumentSearch = ({ environmentId }: DocumentSearchProps) => {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [documents, setDocuments] = useState<TDocument[]>([]);
|
||||
|
||||
const searchDocuments = async () => {
|
||||
const documents = await searchDocumentsAction({ environmentId, searchTerm });
|
||||
if (documents?.data) {
|
||||
setDocuments(documents.data);
|
||||
} else {
|
||||
console.error(documents);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
searchDocuments();
|
||||
}}
|
||||
className="flex w-full space-x-2">
|
||||
<Input
|
||||
placeholder="What are users thinking the performance of my app?"
|
||||
value={searchTerm}
|
||||
onChange={(v) => setSearchTerm(v.target.value)}
|
||||
/>
|
||||
<Button className="h-10">Search</Button>
|
||||
</form>
|
||||
<div className="flex-col space-y-4">
|
||||
{documents.map((document) => (
|
||||
<div className="overflow-hidden rounded-lg bg-white shadow">
|
||||
<div className="whitespace-pre-wrap px-4 py-5 sm:p-6">
|
||||
<p>{document.text}</p>
|
||||
<hr className="my-4 text-slate-300" />
|
||||
<p className="text-xs">Survey Response</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { LoadingSpinner } from "@formbricks/ui/LoadingSpinner";
|
||||
|
||||
const Loading = () => {
|
||||
return <LoadingSpinner />;
|
||||
};
|
||||
|
||||
export default Loading;
|
||||
@@ -0,0 +1,45 @@
|
||||
import { DocumentSearch } from "@/app/(app)/environments/[environmentId]/documents/components/DocumentSearch";
|
||||
import { Metadata } from "next";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { TTemplateRole } from "@formbricks/types/templates";
|
||||
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
|
||||
import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Your Surveys",
|
||||
};
|
||||
|
||||
interface SurveyTemplateProps {
|
||||
params: {
|
||||
environmentId: string;
|
||||
};
|
||||
searchParams: {
|
||||
role?: TTemplateRole;
|
||||
};
|
||||
}
|
||||
|
||||
const Page = async ({ params, searchParams }: SurveyTemplateProps) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session) {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
|
||||
const user = await getUser(session.user.id);
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle="Documents" />
|
||||
<DocumentSearch environmentId={params.environmentId} />
|
||||
</PageContentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ZDocumentCreateInput,
|
||||
ZDocumentType,
|
||||
} from "@formbricks/types/documents";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
import { DatabaseError } from "@formbricks/types/errors";
|
||||
import { cache } from "../cache";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
@@ -72,9 +73,9 @@ export const getDocumentsByTypeAndReferenceId = reactCache(
|
||||
text,
|
||||
"referenceId",
|
||||
vector::text
|
||||
FROM "Document" e
|
||||
WHERE e."type" = ${type}::"DocumentType"
|
||||
AND e."referenceId" = ${referenceId}
|
||||
FROM "Document" d
|
||||
WHERE d."type" = ${type}::"DocumentType"
|
||||
AND d."referenceId" = ${referenceId}
|
||||
`;
|
||||
|
||||
const documents = prismaDocuments.map((prismaDocument) => {
|
||||
@@ -104,3 +105,45 @@ export const getDocumentsByTypeAndReferenceId = reactCache(
|
||||
}
|
||||
)()
|
||||
);
|
||||
|
||||
export const findNearestDocuments = async (
|
||||
environmentId: string,
|
||||
vector: number[],
|
||||
limit: number = 5
|
||||
): Promise<TDocument[]> => {
|
||||
validateInputs([environmentId, ZId]);
|
||||
const threshold = 0.8; //0.2;
|
||||
// Convert the embedding array to a JSON-like string representation
|
||||
const vectorString = `[${vector.join(",")}]`;
|
||||
|
||||
// Execute raw SQL query to find nearest neighbors and exclude the vector column
|
||||
const prismaDocuments: TPrismaDocument[] = await prisma.$queryRaw`
|
||||
SELECT
|
||||
id,
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt",
|
||||
type,
|
||||
text,
|
||||
"referenceId",
|
||||
vector::text
|
||||
FROM "Document" d
|
||||
WHERE d."environmentId" = ${environmentId}
|
||||
AND d."vector" <=> ${vectorString}::vector(512) <= ${threshold}
|
||||
ORDER BY d."vector" <=> ${vectorString}::vector(512)
|
||||
LIMIT ${limit};
|
||||
`;
|
||||
|
||||
const documents = prismaDocuments.map((prismaDocument) => {
|
||||
// Convert the string representation of the vector back to an array of numbers
|
||||
const vector = prismaDocument.vector
|
||||
.slice(1, -1) // Remove the surrounding square brackets
|
||||
.split(",") // Split the string into an array of strings
|
||||
.map(Number); // Convert each string to a number
|
||||
return {
|
||||
...prismaDocument,
|
||||
vector,
|
||||
};
|
||||
});
|
||||
|
||||
return documents;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user