Untitled

 avatar
unknown
plain_text
11 days ago
7.4 kB
9
Indexable
"server only";

import { auth } from "@clerk/nextjs/server";
import { generateText } from 'ai';
import { google } from '@ai-sdk/google';
import { v4 as uuidv4 } from "uuid";
import { desc, eq } from "drizzle-orm";
import { fal } from "@fal-ai/client";
import { z } from "zod";
import { getChatMessages, insertMessage } from "./messages";
import { db } from "@/db/db";
import { character, chatParticipants } from "@/db/schema";
  
const CharacterType = z.object({
  name: z.string(),
  age: z.string(),
  profession: z.string(),
  physical_appearance: z.string(),
  personality: z.string(),
  background: z.string(),
  tone_and_speech: z.string(),
  habits_and_mannerisms: z.string(),
  initialMessage: z.string(),
});

export const createCharacter = async () => {
  if (!process.env.GOOGLE_GENERATIVE_AI_API_KEY) {

    throw new Error("Google API key is not configured");

  }

  const result = await db.select().from(character);

  // Initialize Google AI model with proper configuration
  const model = google('gemini-1.5-flash', {
    apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY, // Add this line
    structuredOutputs: true,  });

  try {
    const { text } = await generateText({
      model,
      messages: [
        {
          role: "user",
          content: [
            {
              type: "text",
              text: `Return ONLY a JSON object with no markdown formatting or additional text. The JSON should have this exact structure:
              {
                "name": string,
                "age": string,
                "profession": string,
                "physical_appearance": string,
                "personality": string,
                "background": string,
                "tone_and_speech": string,
                "habits_and_mannerisms": string,
                "initialMessage": string
              }

              Requirements:
              - Name: Create a memorable name (NOT Zara)
              - Age: Between 10 and 80
              - Profession: Real-world profession (NOT urban forger, NO sci-fi)
              - Physical Appearance: Detailed facial description
              - Personality: 2-3 realistic traits
              - Background: Brief, realistic backstory
              - Tone and Speech: Casual, modern communication style
              - Habits: Realistic behaviors and quirks
              - Initial Message: Casual, friendly first text message

              Communication style:
              - Use casual language (omg, lol, k, yeah, nah)
              - Include natural typos
              - Use contractions
              - Sound human and relatable

              IMPORTANT: Character must be different from these existing characters:
              ${JSON.stringify(result)}

              Must be a realistic, modern person - NO sci-fi or anime characters.`
            }
          ]
        }
      ]
    });
    let cleanedText = text;

    if (text.includes('```json')) {

      cleanedText = text.replace(/```json\n|\n```/g, '');

    }
    // Parse and validate the response
    let characterData;

    try {

      characterData = CharacterType.parse(JSON.parse(cleanedText));

    } catch (parseError) {

      console.error("JSON Parse Error:", parseError);

      console.error("Raw text:", text);

      throw new Error("Invalid JSON response from AI");

    }
    // Generate profile image
    const { imageUrl } = await createProfileImage(characterData.physical_appearance);

    // Insert character into database
    return insertCharacter({
      ...characterData,
      imageUrl,
    });

  } catch (error) {
    console.error("Error creating character:", error);
        // Improve error handling with more specific messages

      if (error.status === 401) {

        throw new Error("Authentication failed - check your Google AI API key");
  
      }
    throw new Error("Failed to generate character");
  }
};

const createProfileImage = async (description: string) => {
  const result = await fal.subscribe("fal-ai/flux-pro/v1.1-ultra", {
    input: {
      prompt:
        description +
        `\n Please  make the picture of the person, like it were a profile picture taken for a social media, there full face has to be visible. It's just a picture of them and not a screenshot of a website or profile. Consider this to be an image for their social media profile picture.
        `,
    },
    logs: true,
    onQueueUpdate: (update) => {
      if (update.status === "IN_PROGRESS") {
        update.logs.map((log) => log.message).forEach(console.log);
      }
    },
  });
  return {
    imageUrl: result.data?.images?.[0]?.url,
  };
};


const insertCharacter = async ({
  name,
  age,
  profession,
  physical_appearance,
  personality,
  background,
  tone_and_speech,
  habits_and_mannerisms,
  imageUrl,
  initialMessage,
}: any) => {
  await db.insert(character).values({
    name,
    age,
    profession,
    physical_appearance,
    personality,
    background,
    tone_and_speech,
    habits_and_mannerisms,
    profile_image: imageUrl,
    initial_message: initialMessage,
  });
};

export const swapCharacter = async () => {
  const { userId } = await auth();

  await db
    .delete(chatParticipants)
    .where(eq(chatParticipants.user_id, userId!));

  return await getCharacter();
};

let lastCharacterIndex = -1;

function getRandomIndex(length: number) {
  let newIndex;
  do {
    newIndex = Math.floor(Math.random() * length);
  } while (newIndex === lastCharacterIndex && length > 1);

  lastCharacterIndex = newIndex;
  return newIndex;
}

export const getCharacter = async () => {
  const { userId } = await auth();

  // Check if the user already has an assigned character
  const existingParticipant = await db
    .select()
    .from(chatParticipants)
    .where(eq(chatParticipants.user_id, userId!))
    .limit(1);

  if (existingParticipant.length > 0) {
    // Fetch the character based on the existing participant
    const existingCharacter = await db
      .select()
      .from(character)
      .where(eq(character.id, existingParticipant[0].character_id!))
      .limit(1);

    const messages = await getChatMessages(existingParticipant?.[0]?.chat_id);

    return {
      character: existingCharacter?.[0],
      chatParticipants: existingParticipant?.[0],
      messages,
    };
  }

  const characters = await db.select().from(character);

  const charLength = characters.length;
  // Check if there are any characters to avoid errors
  if (charLength === 0) {    
    await createCharacter();
    
    const newCharacters = await db.select().from(character);
    await console.log(newCharacters);
  }

  // Generate a random index that's different from the last one
  const randomIndex = getRandomIndex(charLength);

  const info = await createChat({
    char: characters[randomIndex],
  });

  // Return the random character
  return info;
};

const createChat = async ({ char }: any) => {
  const { userId } = await auth();
  const chatId = uuidv4();

  await db.insert(chatParticipants).values({
    chat_id: chatId,
    character_id: char?.id,
    user_id: userId,
  });

  if (char?.initial_message) {
    await insertMessage(chatId, "assistant", char?.initial_message!);
  }

  return {
    character: char,
    messages: [],
  };
};

export const getAllCharacters = async () => {
  return db.select().from(character).orderBy(desc(character.id));
};
Leave a Comment