I am building an application in Next.js, TailwindCSS, Appwrite, and Zustand. Fix the issue of 'two children with the same key'.  Optimize the API for better cost efficiency. Also Improve prompts and fix bugs. Provide the full updated code.



"use client";
import React, { useState, useEffect, useCallback, useRef } from "react";
import {
  FaBriefcase,
  FaPlus,
  FaPaperPlane,
  FaCheck,
  FaSquare,
  FaChevronDown,
  FaChevronUp,
} from "react-icons/fa";
import useProfileStore from "../../../store/profileStore";
import useUserStore from "../../../store/userStore";
import useResumeProjectStore from "../../../store/resumeProjectStore";
import useDocumentViewStore from "../../../store/documentViewStore";
import ProfileForm from "../../dashboard-component/ProfileForm";
import Button from "./ButtonCV";
import Modal from "./ModalCV";
import ChatBubble from "./ChatBubbleCV";

const SECTIONS = [
  "summary",
  "experience",
  "skills",
  "education",
  "certifications",
  "projects",
  "languages",
  "awards",
];

export default function ChatView() {
  const { user } = useUserStore();
  const { userProfiles, fetchProfiles, createProfile } = useProfileStore();
  const { currentProject, updateProject } = useResumeProjectStore();
  const { setCurrentResume, setCurrentCoverLetter, setCurrentEmail } = useDocumentViewStore();

  const [selectedProfile, setSelectedProfile] = useState("");
  const [jobDescription, setJobDescription] = useState("");
  const [chatInput, setChatInput] = useState("");
  const [isProfileJobModalOpen, setIsProfileJobModalOpen] = useState(false);
  const [isCreateProfileModalOpen, setIsCreateProfileModalOpen] = useState(false);
  const [selectedOptions, setSelectedOptions] = useState({
    resume: false,
    cover: false,
    email: false,
  });
  const [isGenerating, setIsGenerating] = useState(false);
  const [isSending, setIsSending] = useState(false);
  const [resumeGenerated, setResumeGenerated] = useState(false);
  const [isRegenerateOpen, setIsRegenerateOpen] = useState(false);
  const [selectedSections, setSelectedSections] = useState({});
  const [initialMessageShown, setInitialMessageShown] = useState(false);

  const chatContainerRef = useRef(null);

  const scrollToBottom = useCallback(() => {
    if (chatContainerRef.current) {
      chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
    }
  }, []);

  useEffect(() => {
    if (user) {
      fetchProfiles(user.$id);
    }
  }, [user, fetchProfiles]);

  useEffect(() => {
    if (currentProject) {
      setSelectedProfile(currentProject.profileId || "");
      setJobDescription(currentProject.jobDescription || "");
      if (currentProject.resume && Object.keys(currentProject.resume).length > 0) {
        setResumeGenerated(true);
      } else {
        setResumeGenerated(false);
      }
      if (
        currentProject.chatHistory &&
        currentProject.chatHistory.length === 0 &&
        !initialMessageShown
      ) {
        setInitialMessageShown(true);
      }
    }
    // Remove initialMessageShown from dependencies to prevent infinite loop
  }, [currentProject]);

  useEffect(() => {
    scrollToBottom();
  }, [currentProject?.chatHistory, scrollToBottom]);

  const toggleOption = useCallback((option) => {
    setSelectedOptions((prev) => ({ ...prev, [option]: !prev[option] }));
  }, []);

  const handleCreateProfile = useCallback(
    async (profileData) => {
      if (user) {
        try {
          await createProfile(user.$id, profileData);
          setIsCreateProfileModalOpen(false);
          fetchProfiles(user.$id, true);
        } catch (error) {
          console.error("Error creating profile:", error);
        }
      }
    },
    [user, createProfile, fetchProfiles]
  );

  const handleSaveProfileAndJob = useCallback(async () => {
    if (user && currentProject) {
      try {
        await updateProject(user.$id, currentProject.id, {
          profileId: selectedProfile,
          jobDescription,
        });
        setIsProfileJobModalOpen(false);
      } catch (error) {
        console.error("Error updating project:", error);
      }
    }
  }, [user, currentProject, selectedProfile, jobDescription, updateProject]);

  const generateContent = useCallback(
    async (type, profile, jobDescription) => {
      try {
        const response = await fetch("/api/generate", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ type, profile, jobDescription }),
        });
        const data = await response.json();
        if (!response.ok || !data.result) throw new Error(data.error || "Failed to generate content");

        const updatedChatHistory = [
          ...currentProject.chatHistory,
          { type: "system", content: `Generated ${type} successfully.` },
        ];
        await updateProject(user.$id, currentProject.id, {
          [type === "resume" ? "resume" : type === "cover" ? "coverLetter" : "email"]: data.result,
          chatHistory: updatedChatHistory,
        });

        if (type === "resume") {
          setCurrentResume(data.result);
          setResumeGenerated(true);
        } else if (type === "cover") {
          setCurrentCoverLetter(data.result);
        } else if (type === "email") {
          setCurrentEmail(data.result);
        }

        return data.result;
      } catch (error) {
        console.error(`Error generating ${type}:`, error);
        await updateProject(user.$id, currentProject.id, {
          chatHistory: [
            ...currentProject.chatHistory,
            {
              type: "system",
              content: `Error generating ${type}: ${error.message}`,
            },
          ],
        });
        throw error;
      }
    },
    [currentProject, setCurrentResume, setCurrentCoverLetter, setCurrentEmail, updateProject, user]
  );

  const handleGenerate = useCallback(async () => {
    if (!selectedProfile || !jobDescription) {
      alert("Please select a profile and enter a job description first.");
      return;
    }
    setIsGenerating(true);
    setResumeGenerated(false);
    const profile = userProfiles.find((p) => p.id === selectedProfile);
    const types = Object.entries(selectedOptions)
      .filter(([, isSelected]) => isSelected)
      .map(([type]) => type);

    try {
      await Promise.all(types.map((type) => generateContent(type, profile, jobDescription)));
    } catch (error) {
      console.error("Error generating content:", error);
    } finally {
      setIsGenerating(false);
    }
  }, [selectedProfile, jobDescription, userProfiles, selectedOptions, generateContent]);

  const handleSendMessage = useCallback(async () => {
    if (!chatInput.trim()) return;
    setIsSending(true);
    setIsGenerating(true);
    const message = chatInput.trim();
    setChatInput("");

    const sectionsToRegenerate = Object.entries(selectedSections)
      .filter(([_, isSelected]) => isSelected)
      .map(([section]) => section);

    try {
      const newUserMessage = { type: "user", content: message };
      const updatedChatHistory = [...currentProject.chatHistory, newUserMessage];

      await updateProject(user.$id, currentProject.id, {
        chatHistory: updatedChatHistory,
      });

      let apiEndpoint = "/api/resume";
      let apiBody = {
        action: "chat",
        message,
        chatHistory: updatedChatHistory,
        resume: currentProject.resume,
        jobDescription: currentProject.jobDescription,
      };

      if (sectionsToRegenerate.length > 0) {
        const originalSections = sectionsToRegenerate.reduce((acc, section) => {
          acc[section] = currentProject.resume.sections[section];
          return acc;
        }, {});

        apiBody = {
          action: "regenerate",
          selectedSections: sectionsToRegenerate,
          profile: userProfiles.find((p) => p.id === selectedProfile),
          jobDescription: currentProject.jobDescription,
          currentResume: currentProject.resume,
          message,
          originalSections,
        };
      }

      const response = await fetch(apiEndpoint, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(apiBody),
      });

      if (!response.ok) throw new Error("Failed to get response");
      const data = await response.json();

      if (sectionsToRegenerate.length > 0) {
        await updateProject(user.$id, currentProject.id, {
          resume: data.result,
        });
        setCurrentResume(data.result);
      }

      const newAssistantMessage = { type: "assistant", content: data.reply };

      await updateProject(user.$id, currentProject.id, {
        chatHistory: [...updatedChatHistory, newAssistantMessage],
      });

      scrollToBottom();
    } catch (error) {
      console.error("Error sending message:", error);
    } finally {
      setIsSending(false);
      setIsGenerating(false);
      setSelectedSections({});
      setIsRegenerateOpen(false);
    }
  }, [
    chatInput,
    currentProject,
    selectedSections,
    selectedProfile,
    setCurrentResume,
    updateProject,
    user,
    userProfiles,
    scrollToBottom,
  ]);

  const handleSectionSelect = useCallback((section) => {
    setSelectedSections((prev) => ({
      ...prev,
      [section]: !prev[section],
    }));
  }, []);

  return (
    <div className="flex flex-col h-full relative">
      {/* Chat history */}
      <div ref={chatContainerRef} className="flex-1 overflow-y-auto p-4">
        {initialMessageShown && (
          <ChatBubble
            message={{ type: "assistant", content: "initial" }}
            isInitialMessage={true}
            showRegenerate={false}
          />
        )}
        {currentProject?.chatHistory?.map((message) => (
          <ChatBubble
            key={message.id || message.content} // Use a unique key
            message={message}
            isInitialMessage={false}
            showRegenerate={false}
          />
        ))}
        {isGenerating && (
          <ChatBubble
            message={{ type: "assistant", content: "Generating..." }}
            isLoading={true}
            showRegenerate={false}
          />
        )}
      </div>

      {/* Regenerate section */}
      {resumeGenerated && (
        <div className="bg-gray-50 border-t border-gray-200 p-3">
          <button
            className="text-indigo-600 font-semibold flex items-center"
            onClick={() => setIsRegenerateOpen((prev) => !prev)}
          >
            {isRegenerateOpen ? (
              <FaChevronUp className="mr-1" />
            ) : (
              <FaChevronDown className="mr-1" />
            )}
            Regenerate Sections
          </button>
          {isRegenerateOpen && (
            <div className="mt-2">
              <div className="flex flex-wrap gap-2">
                {SECTIONS.map((section) => (
                  <button
                    key={section}
                    onClick={() => handleSectionSelect(section)}
                    className={`text-xs px-3 py-1.5 rounded-md flex items-center ${
                      selectedSections[section]
                        ? "bg-indigo-600 text-white hover:bg-indigo-700"
                        : "bg-gray-200 text-gray-700 hover:bg-gray-300"
                    }`}
                  >
                    {selectedSections[section] ? (
                      <FaCheck className="mr-1.5" />
                    ) : (
                      <FaSquare className="mr-1.5 rounded-md" />
                    )}
                    {section.charAt(0).toUpperCase() + section.slice(1)}
                  </button>
                ))}
              </div>
            </div>
          )}
        </div>
      )}

      {/* Options */}
      <div className="bg-gray-50 border-t border-gray-200 p-3">
        <div className="flex flex-wrap items-center justify-between gap-2">
          <Button
            onClick={() => setIsProfileJobModalOpen(true)}
            className="text-xs px-3 py-1.5 flex items-center"
          >
            <FaBriefcase className="mr-1.5" /> Set Profile & Job
          </Button>
          <div className="flex flex-wrap gap-2">
            {["resume", "cover", "email"].map((option) => (
              <button
                key={option}
                onClick={() => toggleOption(option)}
                className={`text-xs px-3 py-1.5 rounded-md flex items-center ${
                  selectedOptions[option]
                    ? "bg-indigo-600 text-white hover:bg-indigo-700"
                    : "bg-gray-200 text-gray-700 hover:bg-gray-300"
                }`}
              >
                {selectedOptions[option] ? (
                  <FaCheck className="mr-1.5" />
                ) : (
                  <FaSquare className="mr-1.5 rounded-md" />
                )}
                {option === "cover"
                  ? "Cover Letter"
                  : option.charAt(0).toUpperCase() + option.slice(1)}
              </button>
            ))}
          </div>
        </div>
      </div>

      {/* Input area */}
      <div className="bg-white border-t border-gray-200 p-4">
        <div className="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2">
          <input
            type="text"
            value={chatInput}
            onChange={(e) => setChatInput(e.target.value)}
            placeholder="Type your message..."
            className="flex-grow px-4 py-2 text-sm text-gray-700 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-600"
            onKeyDown={(e) => {
              if (e.key === "Enter") handleSendMessage();
            }}
          />
          <div className="flex space-x-2">
            <Button
              onClick={handleSendMessage}
              className="flex-grow sm:flex-grow-0"
              disabled={!chatInput.trim() || isSending}
            >
              <FaPaperPlane className="mr-2" /> {isSending ? "Sending..." : "Send"}
            </Button>
            <Button
              onClick={handleGenerate}
              className="flex-grow sm:flex-grow-0 bg-green-600 hover:bg-green-700 focus:ring-green-600"
              disabled={
                isGenerating ||
                !selectedProfile ||
                !jobDescription ||
                !Object.values(selectedOptions).some(Boolean)
              }
            >
              {isGenerating ? "Generating..." : "Generate"}
            </Button>
          </div>
        </div>
      </div>

      {/* Modals */}
      <Modal
        isOpen={isProfileJobModalOpen}
        onClose={() => setIsProfileJobModalOpen(false)}
        title="Set Profile & Job Description"
      >
        <div className="space-y-4">
          <div>
            <label htmlFor="profile" className="block text-sm font-medium text-gray-700 mb-1">
              Select a Profile
            </label>
            {userProfiles && userProfiles.length > 0 ? (
              <select
                id="profile"
                value={selectedProfile}
                onChange={(e) => setSelectedProfile(e.target.value)}
                className="w-full px-3 py-2 text-sm text-gray-700 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-600"
              >
                <option value="">Select a profile</option>
                {userProfiles.map((profile) => (
                  <option key={profile.id} value={profile.id}>
                    {profile.profileName}
                  </option>
                ))}
              </select>
            ) : (
              <div className="text-sm text-gray-500">No profiles found. Please create a profile.</div>
            )}
            <Button onClick={() => setIsCreateProfileModalOpen(true)} className="mt-2">
              <FaPlus className="mr-2" /> Create New Profile
            </Button>
          </div>
          <div>
            <label htmlFor="jobDescription" className="block text-sm font-medium text-gray-700 mb-1">
              Job Description
            </label>
            <textarea
              id="jobDescription"
              value={jobDescription}
              onChange={(e) => setJobDescription(e.target.value)}
              placeholder="Paste job description here..."
              className="w-full px-3 py-2 text-sm text-gray-700 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-600"
              rows="6"
            ></textarea>
          </div>
          <div className="flex justify-end">
            <Button onClick={handleSaveProfileAndJob}>
              <FaCheck className="mr-2" /> Confirm
            </Button>
          </div>
        </div>
      </Modal>

      <Modal
        isOpen={isCreateProfileModalOpen}
        onClose={() => setIsCreateProfileModalOpen(false)}
        title="Create New Profile"
      >
        <ProfileForm onSubmit={handleCreateProfile} />
      </Modal>
    </div>
  );
}




import { NextResponse } from "next/server";
import Together from "together-ai";

const together = new Together({ apiKey: process.env.TOGETHER_API_KEY });

export async function POST(request) {
  try {
    const { action, ...data } = await request.json();

    switch (action) {
      case "regenerate":
        return await handleRegenerate(data);
      case "chat":
        return await handleChat(data);
      default:
        return NextResponse.json({ error: "Invalid action" }, { status: 400 });
    }
  } catch (error) {
    console.error("Error in API:", error);
    return NextResponse.json(
      { error: error.message || "Internal server error" },
      { status: 500 }
    );
  }
}

async function handleRegenerate({
  selectedSections,
  profile,
  jobDescription,
  currentResume,
  message,
  originalSections,
}) {
  if (
    !selectedSections ||
    !profile ||
    !jobDescription ||
    !currentResume ||
    !originalSections ||
    !message
  ) {
    return NextResponse.json(
      { error: "Missing required fields" },
      { status: 400 }
    );
  }

  const prompt = generateRegenerationPrompt(
    selectedSections,
    profile,
    jobDescription,
    currentResume,
    message,
    originalSections
  );

  const response = await together.chat.completions.create({
    messages: [{ role: "user", content: prompt }],
    model: "meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo",
    max_tokens: 2048,
    temperature: 0.7,
    top_p: 0.9,
    n: 1,
    stop: ["
", "
"],
  });

  const generatedContent = response.choices[0].message.content.trim();
  const jsonString = extractJson(generatedContent);

  try {
    const jsonContent = JSON.parse(jsonString);
    const validatedContent = validateResumeContent(jsonContent);

    const replyPrompt = generateReplyPrompt(
      selectedSections,
      originalSections,
      validatedContent,
      message
    );

    const replyResponse = await together.chat.completions.create({
      messages: [{ role: "user", content: replyPrompt }],
      model: "meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo",
      max_tokens: 1024,
      temperature: 0.7,
      top_p: 0.9,
      n: 1,
      stop: ["
", "
"],
    });

    const reply = replyResponse.choices[0].message.content.trim();

    return NextResponse.json({ result: validatedContent, reply });
  } catch (parseError) {
    console.error("Error parsing generated content:", parseError);
    return NextResponse.json(
      { error: "Generated content is not valid JSON", generatedContent },
      { status: 500 }
    );
  }
}

async function handleChat({ message, chatHistory, resume, jobDescription }) {
  if (!message || !chatHistory || !resume || !jobDescription) {
    return NextResponse.json(
      { error: "Missing required fields" },
      { status: 400 }
    );
  }

  const formattedHistory = chatHistory.map((msg) => ({
    role: msg.type === "user" ? "user" : "assistant",
    content: msg.content,
  }));

  const systemMessage = `
    You are a helpful assistant for resume and job application related queries.
    You have access to the following information:
    Resume: ${JSON.stringify(resume, null, 2)}
    Job Description: ${jobDescription}
    Please use this information to provide accurate and tailored responses to the user's questions about their resume and job application.
  `;

  const response = await together.chat.completions.create({
    messages: [
      { role: "system", content: systemMessage },
      ...formattedHistory,
      { role: "user", content: message },
    ],
    model: "meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo",
    max_tokens: 1024,
    temperature: 0.7,
    top_p: 0.9,
    n: 1,
    stop: ["
", "
"],
  });

  const reply = response.choices[0].message.content.trim();

  return NextResponse.json({ reply });
}

const sectionPrompts = {
  summary:
    "Update the summary to highlight key achievements and skills relevant to the job description.",
  experience:
    "Enhance the experience section to emphasize relevant responsibilities and accomplishments.",
  skills:
    "Modify the skills section to showcase skills that align closely with the job requirements.",
  education:
    "Adjust the education section to highlight relevant courses or honors.",
};

function generateRegenerationPrompt(
  selectedSections,
  profile,
  jobDescription,
  currentResume,
  message,
  originalSections
) {
  const sectionInstructions = selectedSections
    .map((section) => {
      const instruction = sectionPrompts[section] || "";
      return `Section: ${section}\nInstruction: ${instruction}\nOriginal Content: ${JSON.stringify(originalSections[section], null, 2)}`;
    })
    .join("\n\n");

  return `
    You are a resume writing assistant. Please regenerate the following sections of the resume based on the profile, job description, and current resume. Only modify the selected sections:

    Selected Sections: ${selectedSections.join(", ")}
    Profile: ${JSON.stringify(profile, null, 2)}
    Job Description: ${jobDescription}
    Current Resume: ${JSON.stringify(currentResume, null, 2)}
    User Message: ${message}
    Instructions:
    For each of the selected sections, follow these specific instructions:
    ${sectionInstructions}

    General Instructions:
    - Only modify the sections specified in the Selected Sections.
    - Maintain the same structure and format as the current resume.
    - Tailor the content to better match the job description and highlight relevant skills and experiences.
    - Ensure that the regenerated sections seamlessly integrate with the rest of the resume.
    - Consider the user's message when making changes: "${message}"
    - Improve the content more than the original, addressing the user's request.
    - Return the entire resume JSON object, including both modified and unmodified sections.

    Return only the JSON object, no additional text.
  `;
}

function generateReplyPrompt(
  selectedSections,
  originalSections,
  validatedContent,
  userMessage
) {
  const regeneratedSections = selectedSections.reduce((acc, section) => {
    acc[section] = validatedContent.sections[section];
    return acc;
  }, {});

  return `
    You have just regenerated the following sections of a resume: ${selectedSections.join(", ")}.
    Here are the original sections: ${JSON.stringify(originalSections, null, 2)}
    And here are the regenerated sections: ${JSON.stringify(regeneratedSections, null, 2)}
    Provide a brief summary of the changes made and any suggestions for further improvements.
    Address the user's request to "${userMessage}".
  `;
}

function extractJson(text) {
  const jsonStart = text.indexOf("{");
  const jsonEnd = text.lastIndexOf("}");
  if (jsonStart !== -1 && jsonEnd !== -1) {
    return text.substring(jsonStart, jsonEnd + 1);
  }
  throw new Error("JSON data not found in the generated content");
}

function validateResumeContent(content) {
  const requiredFields = [
    "title",
    "summary",
    "email",
    "phone",
    "location",
    "sections",
  ];
  const requiredSections = [
    "socialLinks",
    "education",
    "experience",
    "skills",
    "certifications",
    "projects",
    "languages",
    "awards",
  ];

  requiredFields.forEach((field) => {
    if (!content[field]) {
      content[field] = "";
    }
  });

  if (!content.sections || typeof content.sections !== "object") {
    content.sections = {};
  }

  requiredSections.forEach((section) => {
    if (!content.sections[section] || !Array.isArray(content.sections[section])) {
      content.sections[section] = [];
    }
  });

  requiredSections.forEach((section) => {
    content.sections[section] = content.sections[section].map((item) => {
      const validatedItem = { ...item, id: item.id || generateUniqueId() };
      ["startDate", "endDate", "issueDate", "expirationDate", "dateReceived"].forEach((dateField) => {
        if (validatedItem[dateField]) {
          validatedItem[dateField] = validateDate(validatedItem[dateField]);
        }
      });
      return validatedItem;
    });
  });

  return content;
}

function validateDate(dateString) {
  if (!dateString || dateString.trim() === "") return "";

  const date = new Date(dateString);
  if (isNaN(date.getTime())) return "";

  return date.toISOString().split("T")[0];
}

function generateUniqueId() {
  return Math.random().toString(36).substr(2, 9);
}