Code viewer for World: LLM Financial Advice
let openaiCubeAngle = 0;
let anthropicCubeAngle = 0;
let googleCubeAngle = 0;

let isProcessingOpenAI = false;
let isProcessingAnthropic = false;
let isProcessingGoogle = false;

const cubeSize = 100;

var openaiTexture, anthropicTexture, googleTexture;

let openaiResult = "Processing...";
let anthropicResult = "Processing...";
let googleResult = "Processing...";

//setup the ui elemetns
AB.world.newRun = function () {
    const inputStyle = `
        position: absolute;
        padding: 10px;
        font-size: 14px;
        border: 1px solid #ccc;
        border-radius: 4px;
        box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
        width: 300px;
        text-align: left;`
        ;
    
    let openaiInput = document.createElement("input");
    openaiInput.type = "password";
    openaiInput.id = "openaiInput";
    openaiInput.placeholder = "Enter your OpenAI API key";
    openaiInput.style.cssText = `${inputStyle}; top: 220px; left: calc(33% - 150px);`;
    document.body.appendChild(openaiInput);

    let openaiRenderArea = document.createElement("p");
    openaiRenderArea.id = "openaiRenderArea";
    openaiRenderArea.innerText = "";
    openaiRenderArea.style.cssText = `position: absolute; top: 600px; left: calc(33% - 150px); text-align: center; width: 250px;`;
    document.body.appendChild(openaiRenderArea);

    let anthropicInput = document.createElement("input");
    anthropicInput.type = "password";
    anthropicInput.id = "anthropicInput";
    anthropicInput.placeholder = "Enter your Anthropic API key";
    anthropicInput.style.cssText = `${inputStyle}; top: 220px; left: calc(50% - 150px);`;
    document.body.appendChild(anthropicInput);

    let anthropicRenderArea = document.createElement("p");
    anthropicRenderArea.id = "anthropicRenderArea";
    anthropicRenderArea.innerText = "";
    anthropicRenderArea.style.cssText = `position: absolute; top: 600px; left: calc(50% - 150px); text-align: center; width: 250px;`;
    document.body.appendChild(anthropicRenderArea);

    let googleInput = document.createElement("input");
    googleInput.type = "password";
    googleInput.id = "googleInput";
    googleInput.placeholder = "Enter your Google Access Token";
    googleInput.style.cssText = `${inputStyle}; top: 220px; left: calc(67% - 150px);`;
    document.body.appendChild(googleInput);

    let googleRenderArea = document.createElement("p");
    googleRenderArea.id = "googleRenderArea";
    googleRenderArea.innerText = "";
    googleRenderArea.style.cssText = `position: absolute; top: 600px; left: calc(67% - 150px); text-align: center; width: 250px;`;
    document.body.appendChild(googleRenderArea);

    let tickerInput = document.createElement("input");
    tickerInput.type = "text";
    tickerInput.id = "tickerInput";
    tickerInput.placeholder = "Enter stock ticker (e.g., AAPL)";
    tickerInput.style.cssText = `${inputStyle}; top: 10px; left: 50%; transform: translateX(-50%);`;
    document.body.appendChild(tickerInput);

    let contentInput = document.createElement("textarea");
    contentInput.id = "contentInput";
    contentInput.placeholder = "Paste the tweet or article content here...";
    contentInput.style.cssText = `${inputStyle}; top: 70px; left: 50%; transform: translateX(-50%); height: 100px;`;
    document.body.appendChild(contentInput);
    
    let submitButton = document.createElement("button");
    submitButton.id = "submitButton";
    submitButton.innerText = "Analyse Sentiment";
    submitButton.style.cssText = `
        position: absolute;
        top: 270px;
        left: 50%;
        transform: translateX(-50%);
        padding: 10px 20px;
        font-size: 14px;
        border: none;
        background-color: #007bff;
        color: #fff;
        border-radius: 4px;
        cursor: pointer;
        box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
    `;
    submitButton.onclick = analyseSentiment;
    document.body.appendChild(submitButton);

    //this used to be where all the outputs ended up, but I didn't bother to change it.
    let resultArea = document.createElement("p");
    resultArea.id = "resultArea";
    resultArea.innerText = "Enter your API key, stock ticker, and content, then click analyse.";
    resultArea.style.cssText = `
        position: absolute;
        top: 180px;
        left: 50%;
        transform: translateX(-50%);
        text-align: center;
        width: 60%;
        font-size: 14px;
        color: #333;
    `;
    document.body.appendChild(resultArea);
    
    document.querySelectorAll("input, textarea, button, p").forEach((element) => {
    element.style.position = "absolute";
    element.style.zIndex = "10";
    });
};

function preload() {
    openaiTexture = loadImage ('/uploads/meadeb2/openai-logo.png');
    anthropicTexture = loadImage ('/uploads/meadeb2/anthropic-logo.png');
    googleTexture = loadImage ('/uploads/meadeb2/google-logo.jpg');
}

//issue with canvas being place over the interactive elemnts, set z index to -1 so it would be under the other elemnts.
function setup() {
    const canvas = createCanvas(window.innerWidth, window.innerHeight, WEBGL);
    canvas.style('z-index', '-1');
    canvas.position(0, 0);
}


function draw() {
    background(240);

    push();
    translate(-330, 0, 0);
    texture(openaiTexture);
    if (isProcessingOpenAI) rotateY(openaiCubeAngle);
    if (isProcessingOpenAI) rotateZ(openaiCubeAngle);
    box(cubeSize);
    pop();

    push();
    translate(0, 0, 0);
    texture(anthropicTexture);
    if (isProcessingAnthropic) rotateX(anthropicCubeAngle);
    if (isProcessingAnthropic) rotateY(anthropicCubeAngle);
    box(cubeSize);
    pop();

    push();
    translate(330, 0, 0);
    texture(googleTexture);
    if (isProcessingGoogle) rotateZ(googleCubeAngle);
    if (isProcessingGoogle) rotateX(googleCubeAngle);
    box(cubeSize);
    pop();

    if (isProcessingOpenAI) openaiCubeAngle += 0.05;
    if (isProcessingAnthropic) anthropicCubeAngle += 0.05;
    if (isProcessingGoogle) googleCubeAngle += 0.05;
}

//a collapsible response box
function createResponseBox(id, summary, details) {
    const container = document.createElement("div");
    container.style.cssText = `
        background-color: white;
        border: 1px solid #ccc;
        border-radius: 4px;
        padding: 15px;
        width: 250px;
        margin: 7px auto;
        cursor: pointer;
        box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
        text-align: center;
    `;

    const summaryElement = document.createElement("p");
    summaryElement.style.cssText = `
        font-size: 20px;
        font-weight: bold;
        margin: 0;
        color: #007bff;
        font-family: Arial, sans-serif;
    `;
    summaryElement.innerText = summary;

    const detailsElement = document.createElement("p");
    detailsElement.style.cssText = `
        font-size: 14px;
        margin-top: 10px;
        display: none;
    `;
    detailsElement.innerText = details;

    container.onclick = () => {
        const isHidden = detailsElement.style.display === "none";
        detailsElement.style.display = isHidden ? "block" : "none";
    };

    container.appendChild(summaryElement);
    container.appendChild(detailsElement);

    const renderArea = document.getElementById(id);
    renderArea.innerHTML = "";
    renderArea.appendChild(container);
}

//calls each available API and organises their responses
async function analyseSentiment() {
    
    const openaiKey = document.getElementById("openaiInput").value.trim();
    const anthropicKey = document.getElementById("anthropicInput").value.trim();
    const googleKey = document.getElementById("googleInput").value.trim();
    
    
    const ticker = document.getElementById("tickerInput").value.trim();
    const content = document.getElementById("contentInput").value.trim();

    document.getElementById("openaiRenderArea").innerText = "";
    document.getElementById("anthropicRenderArea").innerText = "";
    document.getElementById("googleRenderArea").innerText = "";

    if (!ticker) {
        document.getElementById("resultArea").innerText = "Please enter a stock ticker.";
        return;
    }
    if (!content) {
        document.getElementById("resultArea").innerText = "Please paste some content to analyse.";
        return;
    }

    document.getElementById("resultArea").innerText = "";
    isProcessingOpenAI = !!openaiKey;
    isProcessingAnthropic = !!anthropicKey;
    isProcessingGoogle = !!googleKey;

    //check which API Keys are available to call APIs asynchronously
    try {
        if (openaiKey) {
            document.getElementById("openaiRenderArea").innerText = "";
            const openaiResult = await callOpenAI(openaiKey, ticker, content);
            const [summary, ...details] = openaiResult.split("\n");
            createResponseBox("openaiRenderArea", summary, details.join("\n"));
        }

        if (anthropicKey) {
            document.getElementById("anthropicRenderArea").innerText = "";
            const anthropicResult = await callAnthropic(anthropicKey, ticker, content);
            const [summary, ...details] = anthropicResult.split("\n");
            createResponseBox("anthropicRenderArea", summary, details.join("\n"));
        }

        if (googleKey) {
            document.getElementById("googleRenderArea").innerText = "";
            const googleResult = await callGoogleGemini(googleKey, ticker, content);
            const [summary, ...details] = googleResult.split("\n");
            createResponseBox("googleRenderArea", summary, details.join("\n"));
        }

        document.getElementById("resultArea").innerText = "";
    } catch (error) {
        document.getElementById("resultArea").innerText = "An error occurred during the analysis. Please try again.";
        console.error(error);
    } finally {
        isProcessingOpenAI = false;
        isProcessingAnthropic = false;
        isProcessingGoogle = false;
    }
}

//OpenAI API Homepage
//https://platform.openai.com
//used this tutorial at the start https://www.youtube.com/watch?v=aqdWSYWC_LI
//decided on 0.5 tempurature, to allow a little bit of creativity in responses
async function callOpenAI(apiKey, ticker, content) {
    try {
        const response = await fetch("https://api.openai.com/v1/chat/completions", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "Authorization": `Bearer ${apiKey}`
            },
            body: JSON.stringify({
                model: "gpt-3.5-turbo",
                messages: [
                    {
                        role: "system",
                        content: "You are an assistant that provides concise sentiment analysis with a positive percentage score (0-100%)focused on a specific stock ticker. Respond with the following format only: 'BUY!/SELL!/HOLD!' as the recommendation, then on a new line the positive sentiment percentage as a percentage (0-100%) and a brief justification. Use the specific stock ticker as the focal point for your analysis. Take into account how what is said in the submission might affect the market value of a company beyond simply positive and negative connotations. Focus on how the content might influence the market value of ${ticker}, considering direct and indirect effects. Provide a concise yet clear justification, no longer than three sentences, focusing on the impact on the specified ticker."
                    },
                    {
                        role: "user",
                        content: `Analyse the sentiment of the following content for the stock ticker ${ticker}:\n\n${content}`
                    }
                ],
                max_tokens: 100,
                temperature: 0.5
            })
        });

        const analysisData = await response.json();
        if (!response.ok) {
            throw new Error(analysisData.error.message);
        }

        return analysisData.choices[0].message.content.trim();
    } catch (error) {
        console.error("OpenAI API error:", error);
        return "OpenAI API call failed.";
    }
}

//Anthropic API Homepage
//https://www.anthropic.com/api
async function callAnthropic(apiKey, ticker, content) {
    const systemPrompt = `You are an assistant that provides concise sentiment analysis with a positive percentage score (0-100%) focused on a specific stock ticker. Respond with the following format only: 'BUY!/SELL!/HOLD!' as the recommendation then on a new line the positive sentiment percentage as a percentage (0-100%) and a brief justification. Use the specific stock ticker as the focal point for your analysis. Take into account how what is said in the submission might affect the market value of a company beyond simply positive and negative connotations. Focus on how the content might influence the market value of ${ticker}, considering direct and indirect effects. Provide a concise yet clear justification, no longer than three sentences, focusing on the impact on the specified ticker.`;

    const messages = [
        {
            role: "user",
            content: `Analyse the sentiment of the following content for the stock ticker ${ticker}:\n\n${content}`
        }
    ];

    try {
        const response = await fetch("https://api.anthropic.com/v1/messages", {
            method: "POST",
            headers: {
                "x-api-key": apiKey,
                "Content-Type": "application/json",
                "anthropic-version": "2023-06-01",
                //https://github.com/anthropics/anthropic-sdk-typescript/blob/e400d2e8a54aa736717ed849ef8b44a3490fce68/src/index.ts#L151
                //this took way too long to figure out, thank you Anthropic
                "anthropic-dangerous-direct-browser-access": "true"
            },
            body: JSON.stringify({
                model: "claude-3-5-sonnet-20241022",
                system: systemPrompt,
                messages: messages,
                max_tokens: 100,
                temperature: 0.5
            })
        });

        const data = await response.json();
        if (!response.ok) {
            throw new Error(data.error?.message || `Anthropic API status: ${response.status}`);
        }

        
        const assistantResponse = data.content?.map(block => block.text).join("") || "No response content.";
        return assistantResponse.trim();
    } catch (error) {
        console.error("Anthropic API Error:", error);
        return "Anthropic API call failed.";
    }
}

//Google Vertex AI Homepage
//https://cloud.google.com/vertex-ai?hl=en
async function callGoogleGemini(apiKey, ticker, content) {
    const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=${apiKey}`;

    const requestBody = {
        contents: [
            {
                parts: [
                    {
                        text: `You are an assistant that provides concise sentiment analysis with a positive percentage score (0-100%) focused on the stock ticker ${ticker}. Respond with the following format only: 'BUY!/SELL!/HOLD!' as the recommendation then on a new line the positive sentiment percentage as a percentage (0-100%) and a brief justification. Use the specific stock ticker as the focal point for your analysis. Take into account how what is said in the submission might affect the market value of a company beyond simply positive and negative connotations. Focus on how the content might influence the market value of ${ticker}, considering direct and indirect effects. Provide a concise yet clear justification, no longer than three sentences, focusing on the impact on the specified ticker. Analyse the following content:\n\n${content}`
                    }
                ]
            }
        ],
        generationConfig: {
            maxOutputTokens: 256,
            temperature: 0.5
        }
    };

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

        const data = await response.json();

        if (!response.ok) {
            throw new Error(data.error?.message || `Google API status: ${response.status}`);
        }

        console.log("full Google API Response:", data);

        //google responses are weirdly formatted, had to look into the developer console to find where the text response was.
        const output = data.candidates?.[0]?.content?.parts?.map(part => part.text).join("") || "No response content.";
        return output.trim();
    } catch (error) {
        console.error("GoogleAPI error:", error);
        return "Google API call failed.";
    }
}

AB.world.endRun = function () {
    document.getElementById("openaiInput").remove();
    document.getElementById("anthropicInput").remove();
    document.getElementById("tickerInput").remove();
    document.getElementById("contentInput").remove();
    document.getElementById("submitButton").remove();
    document.getElementById("resultArea").remove();
};