Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/(main)/@header/forms/create/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import MainHeader from "@/components/layout-ui/header/main-header";

export default function CatchAllHeaderSlot() {
return <MainHeader showHeader={false} />;
}
37 changes: 37 additions & 0 deletions app/(main)/forms/create/create-form.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use server";

import { CreateFormRequest } from "./use-cases/assistant";
import { createForm } from "@/services/api";

export interface CreateFormDraftResult {
isSuccess: boolean;
error?: string;
formId?: string;
}

export async function createFormDraft(
request: CreateFormRequest,
): Promise<CreateFormDraftResult> {
const result: CreateFormDraftResult = {
isSuccess: false,
};

if (!request) {
result.error = "Request is null";
return result;
}

try {
const formDraft = await createForm(request);
if (formDraft.id?.length > 0) {
result.formId = formDraft.id;
result.isSuccess = true;
} else {
result.error = "Failed to create form draft";
}
} catch (er) {

Check warning on line 32 in app/(main)/forms/create/create-form.action.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

The catch parameter `er` should be named `error_`.

See more on https://sonarcloud.io/project/issues?id=endatix_endatix-hub&issues=AZsS7wFGqtNBt7OY7qwP&open=AZsS7wFGqtNBt7OY7qwP&pullRequest=53
result.error = `Failed to create form draft. Details: ${er}`;
} finally {
return result;

Check failure on line 35 in app/(main)/forms/create/create-form.action.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unsafe usage of ReturnStatement.

See more on https://sonarcloud.io/project/issues?id=endatix_endatix-hub&issues=AZsS7wFGqtNBt7OY7qwQ&open=AZsS7wFGqtNBt7OY7qwQ&pullRequest=53
}
}
40 changes: 40 additions & 0 deletions app/(main)/forms/create/define-form.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"use server";

import { defineForm } from "@/services/ai-api";
import {
PromptResult,
IPromptResult,
} from "@/app/(main)/forms/create/prompt-result";
import { Model } from "survey-core";
import { DefineFormRequest } from "@/app/(main)/forms/create/use-cases/assistant";

export async function defineFormAction(
prevState: IPromptResult,
formData: FormData,
): Promise<IPromptResult> {
const prompt = formData.get("prompt");
const threadId = formData.get("threadId");
const assistantId = formData.get("assistantId");

try {
const request = {
prompt: prompt as string,
} as DefineFormRequest;

if (threadId && assistantId) {
request.threadId = threadId as string;
request.assistantId = assistantId as string;
}

const response = await defineForm(request);

const validatedModel = new Model(response.definition);
response.definition = validatedModel.toJSON();

return PromptResult.Success(response);
} catch {
return PromptResult.Error(
"Failed to process your prompt. Please try again and if the issue persists, contact support.",
);
}
}
216 changes: 216 additions & 0 deletions app/(main)/forms/create/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
"use client";

import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import { NextPage } from "next";
import ChatBox from "./ui/chat-box";
import PreviewFormContainer from "./ui/preview-form-container";
import ChatThread from "./ui/chat-thread";
import { useEffect, useRef, useState, useTransition } from "react";
import {
AssistantStore,
CreateFormRequest,
DefineFormCommand,
Message,
} from "./use-cases/assistant";
import DotLoader from "@/components/loaders/dot-loader";
import {
ChevronLeft,
ChevronRight,
FilePenLine,
Globe,
PlusCircle,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { ImperativePanelHandle } from "react-resizable-panels";
import { redirect } from "next/navigation";
import { createFormDraft } from "./create-form.action";
import { SurveyModel } from "survey-react-ui";
const SHEET_CSS = "absolute inset-x-0 top-0 h-screen";
const CRITICAL_WIDTH = 600;

const CreateForm: NextPage = () => {
const chatPanelRef = useRef<ImperativePanelHandle>(null);
const [isCollapsed, setIsCollapsed] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const [shouldType, setShouldType] = useState(false);
const [isWaiting, setIsWaiting] = useState(false);
const [messages, setMessages] = useState(new Array<Message>());
const [formModel, setFormModel] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();

useEffect(() => {
const contextStore = new AssistantStore();
const currentContext = contextStore.getChatContext();

if (currentContext?.isInitialPrompt) {
setShouldType(true);
}

if (currentContext?.messages) {
setMessages(currentContext.messages);
}
const formModel = contextStore.getFormModel();
if (formModel) {
setFormModel(formModel);
}

const checkWidth = () => {
setIsMobile(window.innerWidth < CRITICAL_WIDTH);
if (window.innerWidth < CRITICAL_WIDTH) {
chatPanelRef.current?.collapse();
}
};

checkWidth();
window.addEventListener("resize", checkWidth);
return () => window.removeEventListener("resize", checkWidth);
}, []);

const defineFormHandler = (stateCommand: DefineFormCommand) => {
const contextStore = new AssistantStore();
switch (stateCommand) {

Check warning on line 75 in app/(main)/forms/create/page.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this "switch" statement by "if" statements to increase readability.

See more on https://sonarcloud.io/project/issues?id=endatix_endatix-hub&issues=AZsS7wE0qtNBt7OY7qwJ&open=AZsS7wE0qtNBt7OY7qwJ&pullRequest=53
case DefineFormCommand.fullStateUpdate:
const formContext = contextStore.getChatContext();

Check warning on line 77 in app/(main)/forms/create/page.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected lexical declaration in case block.

See more on https://sonarcloud.io/project/issues?id=endatix_endatix-hub&issues=AZsS7wE0qtNBt7OY7qwK&open=AZsS7wE0qtNBt7OY7qwK&pullRequest=53
const formModel = contextStore.getFormModel();

Check warning on line 78 in app/(main)/forms/create/page.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected lexical declaration in case block.

See more on https://sonarcloud.io/project/issues?id=endatix_endatix-hub&issues=AZsS7wE0qtNBt7OY7qwL&open=AZsS7wE0qtNBt7OY7qwL&pullRequest=53
setShouldType(true);
setMessages(formContext.messages);
setFormModel(formModel);
break;
default:
break;
}
};

const toggleCollapse = () => {
const chatPanel = chatPanelRef.current;
if (chatPanel?.isCollapsed()) {
chatPanel.expand();
} else {
chatPanel?.collapse();
}
};

const handleResize = (size: number) => {
if (size > 300 && isCollapsed == false) {

Check warning on line 98 in app/(main)/forms/create/page.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor the code to avoid using this boolean literal.

See more on https://sonarcloud.io/project/issues?id=endatix_endatix-hub&issues=AZsS7wE0qtNBt7OY7qwM&open=AZsS7wE0qtNBt7OY7qwM&pullRequest=53
toggleCollapse();
return;

Check warning on line 100 in app/(main)/forms/create/page.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this redundant jump.

See more on https://sonarcloud.io/project/issues?id=endatix_endatix-hub&issues=AZsS7wE0qtNBt7OY7qwN&open=AZsS7wE0qtNBt7OY7qwN&pullRequest=53
}
};

const openFormInEditor = async () => {
startTransition(async () => {
const survey = new SurveyModel(formModel);
const request: CreateFormRequest = {
name: survey.title,
isEnabled: false,
description: survey.description,
formDefinitionJsonData: JSON.stringify(formModel),
};
const formResult = await createFormDraft(request);
if (formResult.isSuccess && formResult.formId) {
redirect(`/forms/${formResult.formId}`);
} else {
alert(formResult.error);
}
});
};

if (isMobile) {
console.log("isMobile", isMobile);
}

return (
<ResizablePanelGroup
direction="horizontal"
className={`${SHEET_CSS} flex flex-1 space-y-2`}
>
<ResizablePanel defaultSize={70}>
<div className="flex h-screen sm:pl-14 lg-pl-16 sm:pt-12 md:pt-4">
{formModel && (
<PreviewFormContainer model={formModel} />
)}
</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel
ref={chatPanelRef}
defaultSize={30}
minSize={30}
collapsible={true}
collapsedSize={4}
onCollapse={() => setIsCollapsed(true)}
onExpand={() => setIsCollapsed(false)}
onResize={(size) => handleResize(size)}
className="transition-all duration-300 ease-in-out"
>
<div className="flex h-screen shrink-0 z-50 bg-background border-l pt-6 md:px-4">
<Button
variant="ghost"
size="icon"
className="absolute sm:pl-0 pl-4 opacity-50
`${isMobile ? 'hidden' : 'block'}`"
onClick={toggleCollapse}
>
{isCollapsed ? (
<ChevronLeft className="h-8 w-8 " />
) : (
<ChevronRight className="h-8 w-8" />
)}
</Button>
{!isCollapsed && (
<div className="flex flex-col gap-4 sm:pt-12 p-6">
<ChatThread isTyping={shouldType} messages={messages} />
{isWaiting && (
<DotLoader className="flex flex-none items-center m-auto" />
)}
<div className="items-center gap-2 flex">
<Button
variant="outline"
size="sm"
className="h-8 border-dashed"
>
<Globe className="mr-2 h-4 w-4" />
Add languages
</Button>
<Button
variant="outline"
size="sm"
className="h-8 border-dashed"
>
<PlusCircle className="mr-2 h-4 w-4" />
Generate submissions
</Button>
<Button
disabled={isPending}
onClick={openFormInEditor}
variant="default"
size="sm"
className="h-8 border-dashed"
>
<FilePenLine className="mr-2 h-4 w-4" />
{isPending ? "Creating Form..." : "Continue in Editor"}
</Button>
</div>
<ChatBox
className="flex-end flex-none"
placeholder="Ask a follow up (⌘F), ↑ to select"
onPendingChange={(pending) => {
setIsWaiting(pending);
}}
onStateChange={(stateCommand) => {
defineFormHandler(stateCommand);
}}
/>
</div>
)}
</div>
</ResizablePanel>
</ResizablePanelGroup>
);
};

export default CreateForm;
44 changes: 44 additions & 0 deletions app/(main)/forms/create/prompt-result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { DefineFormContext } from "@/app/(main)/forms/create/use-cases/assistant";

export interface IPromptResult {
success?: boolean;
errorMessage?: string;
value?: DefineFormContext;
}

export class PromptResult implements IPromptResult {
success?: boolean;
errorMessage?: string;
value?: DefineFormContext;

private constructor(
success?: boolean,
errorMessage?: string,
value?: DefineFormContext,
) {
this.success = success;
this.errorMessage = errorMessage;
this.value = value;
}

isError = (): boolean =>
this.success === false && this.errorMessage !== undefined;

static Success(value: DefineFormContext): IPromptResult {
return {
success: true,
value: value,
};
}

static Error(errorMessage: string): IPromptResult {
return {
success: false,
errorMessage: errorMessage,
};
}

static InitialState(): IPromptResult {
return {};
}
}
Loading