Banner z obietnicami

Jeżeli należysz do naszego klubu superbohaterów, musisz go chwalebnie reprezentować. Powinieneś byś silny, wytrwały i nieugięty, jak każdy superbohater. Tak przynajmniej może się wydawać. Prawda jest jednak taka, że nawet najlepsi z nas czasami potrzebują wsparcia. Szczególnie ostatnie lata, lockdown, inflacja i wiele podobnych rzeczy każdego z nas może przemielić jak maszynka do mięsa.

Czy wiesz, że w obecnych czasach co piąte dziecko ma problemy psychiczne, a w naszym kraju mężczyźni znajdują się czołówce samobójstw w Europie? Można by tak wymieniać i wymieniać. Czasy nie są za miłe, a my nie jesteśmy niezniszczalni.

Gdzieś w początkowych rozdziałach wspomniałem tobie o pewnej najważniejszej rzeczy. Dalej jestem zdania, że jest to rzecz najważniejsza w programowaniu. Ale jak to mówi przysłowie - "w zdrowym ciele zdrowy duch". Ale i bez zdrowego ducha ciało nie może być zdrowe. I w tym miejscu wkraczają współtowarzysze superbohaterów - trenerzy, dietetycy ale i psychoterapeuci. Wszyscy oni będą mieli coraz więcej do zrobienia.


W poniższym tekście zajmiemy się stworzeniem przykładowego baneru. Osobiście nie jestem jakimś ekspertem od reklamy. Ale... Ostatnimi czasy coraz częściej męczę AI o różne tematy, tak i tym razem pogadałem sobie z nią o powyższych problemach. To z kolei wydało mi się ciekawym pomysłem na stworzenie naszego dzieła.

W każdym razie będziemy tworzyć takie cacko:

Banner jest prezentacją oryginalnej rozmowy, jaką odbyłem z ChatGPT. Żeby ją przedstawić, będziemy potrzebowali kilku "klocków", z których zbudujemy banner. Po pierwsze przyda nam się mechanizm, który będzie służył do wypisywania tekstów litera po literze. Po drugie musimy zaanimować przejścia między kolejnymi wypowiedziami - tu zastopować, tam płynnie przejść itp.

Naszą pracę podzielmy na kilka punktów:

  1. napisanie klasy do wypisywania tekstów
  2. dodatkowe funkcje do animacji i spowalniania
  3. realne odpytywanie AI?

Klasycznie jak w poprzednich projektach na HTML i CSS dostaliśmy gotowe...

Pokaż HTML

<a href="https://magdalenaratajczyk.pl" target="_blank" class="banner">
    <div class="banner-madeline"></div>
    <div class="banner-contact">
        <h1 class="banner-contact-title">Magdalena Ratajczyk</h1>
        <div class="banner-contact-text">Psychoterapeuta, pedagog</div>
    </div>
    <div class="banner-content">
        <div class="banner-inner">
            <div class="banner-chat">
                <div class="banner-el banner-el1">
                    <div class="banner-el-icon banner-el-icon-me"></div>
                    <div class="banner-el-text">
                    </div>
                </div>
                <div class="banner-el banner-el2">
                    <div class="banner-el-icon banner-el-icon-chat"></div>
                    <div class="banner-el-text">
                    </div>
                </div>
                <div class="banner-el banner-el3">
                    <div class="banner-el-icon banner-el-icon-me"></div>
                    <div class="banner-el-text">
                    </div>
                </div>
                <div class="banner-el banner-el4">
                    <div class="banner-el-icon banner-el-icon-chat"></div>
                    <div class="banner-el-text">
                    </div>
                </div>
            </div>
        </div>
    </div>
</a>

Pokaż CSS

.banner {
    font-size: 10px;
    max-width: 1000px;
    height: 400px;
    background: #29353F url(bg-desktop.png) center right / cover;
    overflow: hidden;
    font-family: 'Work Sans', sans-serif;
    color: #fff;
    position: relative;
    text-decoration: none;
    display: flex;
    margin: 50px auto;
    border-radius: 5px;
    flex-direction: row-reverse;
}
.banner:hover {
    color: #fff;
}
.banner-madeline {
    background: url(magdalena.jpg) center top / cover no-repeat;
    width: 330px;
    height: 100%;
    text-align: center;
    display: flex;
    justify-content: flex-end;
    flex-direction: column;
    padding-bottom: 30px;
    text-shadow: 0 0 30px #29353F, 0 0 20px #29353F, 0 0 30px #29353F, 0 0 30px #29353F;
    font-size: 10px;
    flex-shrink: 0;
}
.banner-contact-title {
    font-size: 2em;
    line-height: 1;
    margin: 10px 0;
    z-index: 0;
    position: relative;
}
.banner-contact-text {
    text-transform: uppercase;
    font-size: 1.3em;
    letter-spacing: 2px;
    line-height: 1;
    color: #EFD7BC;
    font-weight: bold;
    z-index: 1;
    position: relative;
}
.banner-content {
    flex: 1;
    margin: 35px;
    font-size: 13px;
    line-height: 1.6em;
    font-weight: 300;
    overflow: hidden;
}
.banner-chat {
    width: calc(100% + 30px);
    padding-right: 30px;
    box-sizing: border-box;
    height: 100%;
    overflow-y: scroll;
}

.banner-el {
    min-height: 30px;
    display: flex;
    gap: 15px;
    padding-bottom: 15px;
    padding-top: 15px;
    border-bottom: 1px solid rgba(255 255 255 / 0.2);
    opacity: 0;
}
.banner-el:first-child {
    padding-top: 0;
}
.banner-el:last-child {
    border: 0;
}
.banner-el-icon {
    width: 30px;
    height: 30px;
    background-size: cover;
}
.banner-el-icon-chat {
    background-image: url(icon-chat.png);
}
.banner-el-icon-me {
    background-image: url(icon-me.png);
}
.banner-el-text {
    flex: 1;
    padding-top: 5px;
}
.banner-el-text .mark {
    font-weight: bold;
    color: #10A37F;
}

.banner-el.is-wait .banner-el-text::before {
    content: "";
    position: relative;
    height: 1em;
    width: 0.5em;
    background: rgba(255 255 255 / 0.7);
    display: inline-block;
    animation: cursorWait 0.2s 0s infinite alternate;
    vertical-align: text-top;
}

@keyframes cursorWait {
    0% {
        opacity: 0;
    }
    100% {
        opacity: 1;
    }
}

@media (max-width: 800px) {
    .banner-madeline {
        width: 230px;
    }
    .banner-contact {
        font-size: 8px;
        width: 230px;
        right: 20px;
    }
}
@media (max-width: 600px) {
    .banner {
        background-image: url(bg-mobile.png);
        background-position: center bottom;
        height: 830px;
        flex-direction: column;
    }
    .banner-madeline {
        width: 100%;
        height: 400px;
    }
    .banner-content {
        margin: 15px;
    }
    .banner-chat {
        padding-right: 0;
    }
}

Klasa TypeWriter

Naszą pracę rozpoczniemy od stworzenia klasy TypeWriter.

Zasada działania naszej maszyny piszącej będzie całkiem prosta. Całe pisanie będzie składało się z wykonywanych po sobie kolejnych akcji, które za pomocą funkcji będziemy odkładać do osobnej tablicy.


export class TypeMachine {
    #actions = []; //tablica akcji
    #text = ''; //tekst który aktualnie wypisujemy
    #count = 0; //dodatkowa zmienna - przyda się do liczenia którą literę wypisujemy

    constructor() {
        //na razie pusty
    }
}

Wszystkie powyższe zmienne dzięki poprzedzeniu je znakiem # stworzyliśmy jako prywatne. W konstruktorze tworzymy opcje naszej maszyny - działanie takie samo jak w innych naszych projektach.

Funkcja write

Jak u Hitchcocka. Zaczynamy od najcięższej rzeczy czyli funkcji write().


class TypeMachine {
    ...

    write(text = "", time = 0) {
        const actionWrite = () => new Promise((resolve, reject) => {
            this.#text += text;
            if (time === 0) {
                this.#count += text.length;
                this.#typeText(this.#count, time)
                resolve();
            } else {
                const timeInt = setInterval(() => {
                    this.#count++;
                    this.#typeText(this.#count, time)
                    if (this.#count >= this.#text.length) {
                        clearInterval(timeInt);
                        resolve();
                    }
                }, time)
            }
        });
        this.#actions.push(actionWrite);
        return this;
    }

    ...

}

Każda odkładana akcja będzie obietnicą na której wykonanie spokojnie poczekamy.

Po pierwsze dokładamy tekst, który za chwilę za pomocą funkcji typeText() i interwału będziemy wypisywać.

Tekst wypisywany będzie za każdym razem od jego początku do zmiennej #count, którą zwiększamy w interwale. Przykładowo:


const tm = new TypeMachine();

tm
.write("Ala", 10);
//"A"   -->  #count = 0
//"Al"   -->  #count = 1
//"Ala"   -->  #count = 2

.write(" ma", 10);
//"Ala "   -->  #count = 3
//"Ala m"   -->  #count = 4
//"Ala ma"   -->  #count = 5
//itd

Jeżeli ktoś odpali naszą funkcję z czasem 0, cały przekazany w parametrze tekst od razu wypiszemy dodając do zmiennej #count jego długość. Przyda to się, jeżeli w wypisywanym tekście będziemy chcieli zmienić wygląd za pomocą html.


const tm = new TypeMachine();

tm
.write("Ala", 10);
//"A"   -->  #count = 0
//"Al"   -->  #count = 1
//"Ala"   -->  #count = 2

.write("<b style='color:red'>", 0);
//"Ala<b style='color:red'>"   -->  #count = 23

.write(" ma", 10);
//"Ala<b style='color:red'> "   -->  #count = 24
//"Ala<b style='color:red'> m"   -->  #count = 25
//"Ala<b style='color:red'> ma"   -->  #count = 26

.write("</b>", 0);
//"Ala<b style='color:red'> ma</b>"   -->  #count = 30

Tak to będzie działać. Na koniec funkcji zwracamy this czyli samego siebie. Dzięki temu wszystkie funkcje będziemy mogli łączyć w łańcuch tak samo jak w przypadku innych metod dla innych obiektów:


"Lorem ipsum"
    .toUpperCase()
    .toLowerCase()
    .substring(2)

tm
    .write("Ala", 10)
    .write(" ma", 10)
    .write(" kota", 10)

Funkcja typeText

Kolejną metodą, którą napiszemy będzie prywatna funkcja typeText(). Posłuży ona do wypisywania samego tekst. Nasz tekst może być wypisywany w różnych miejscach. Czasami będziemy chcieli wypisać go w konsoli debuggera, a innym razem w kilku częściach baneru na stronie. Nie możemy więc tego zakodować na sztywno. Zamiast tego użyjemy funkcji zwrotnej, którą przekażemy do konstruktora:


class TypeMachine {
    ...

    constructor(cb) {
        this.cb = cb;
    }

    #typeText(indexTo, time = 0) {
        this.cb(this.#text.slice(0, indexTo), indexTo);
        return this;
    }

    ...

}

Dzięki temu odpowiedni kod przekażemy przy tworzeniu pojedynczej instancji naszej maszyny:


const tm = new TypeMachine((text, count) => {
    console.log(text, count);
})
tm.write("Ala ma kota");

Funkcja pause

Jak przypatrzysz się rozmowie z banera, wypisywanie tekstu nie jest jednolite. W pierwszej wypowiedzi zawahałem się (pauza), wprowadziłem korektę (usunięcie kilku liter), znowu zawahałem się, po czym pisałem dalej.

Zacznijmy od funkcji dodającej możliwość zaczekania z pisaniem kolejnych liter:


class TypeMachine {
    ...

    pause(time = 0) {
        const actionPause = () => new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve();
            }, time)
        });
        this.#actions.push(actionPause);
        return this;
    }

    ...
}

Funkcja back

Żeby zasymulować kasowanie tekstu, musimy cofnąć się ze zmienną #count. Posłuży do tego funkcja back():


class TypeMachine {
    ...

    back(count = 0, time = 0) {
        const actionBack = () => new Promise((resolve, reject) => {
            if (count === -1) {
                count = this.#text.length;
            }
            let tick = 0;
            const timeInt = setInterval(() => {
                this.#count--;
                tick++;
                this.#typeText(this.#count, time)
                if (tick >= count) {
                    this.#text = this.#text.substr(0, this.#text.length - tick);
                    clearInterval(timeInt);
                    resolve();
                }
            }, time)
        });
    }

    ...
}

Funkcja wymaga podania liczby znaków do cofnięcia. Jeżeli zamiast liczby podamy -1, wtedy zacznie kasować wszystkie litery aż do początku.

Możemy też stworzyć dodatkową funkcję pomocniczą, która posłuży do kasowania całego tekstu:


class TypeMachine {
    ...

    eraseAll(time = 0) {
        this.back(-1, time);
        return this;
    }

}

Funkcja runFn

Kolejną funkcją, która nam się przyda, będzie ta, która posłuży do odpalania naszego "customowego" kodu. Podczas pisania tekstu możesz chcieć w pewnej chwili odpalać dodatkowe funkcje - np. podczas cofania kursora może się dodawać jakaś dodatkowa klasa, albo zmieniać tło. Posłuż do tego funkcja runFn():


class TypeMachine {
    ...

    runFn(cb) {
        this.#actions.push(async () => await cb()); //funkcja async to też obietnica
        return this;
    }

}

Funkcja start

Ostatnią funkcją będzie start(), która posłuży do wystartowania całego pisania.


class TypeMachine {
    ...

    async start() {
        for (const action of this._actions) {
            const a = await action();
        }
        return this;
    }

    ...
}

Nasza klasa jest gotowa :). Przykładowe jej użycie może mieć postać:


const div = document.querySelector("div");
const button = document.querySelector("button");
button.addEventListener("click", () => {
    button.disabled = true;
    startWrite();
})

function startWrite() {
    const tm = new TypeMachine((text, count) => {
        div.innerHTML = text;
    });

    tm
    .write("Ala ma kota,", 50)
    .write(" a kot ma Ulę.", 50)
    .pause(500)
    .runFn(() => {
        div.style.background = "gold"
    })
    .pause(1000)
    .back(4, 50)
    .pause(1000)
    .write("<b style='color: red'>")
    .write("Alę.", 50)
    .write("</b>")
    .pause(500)
    .runFn(() => {
        div.style.background = ""
    })
    .pause(1000)
    .write(" Ala go kocha,", 50)
    .write(" a kot ją wcale.", 50)
    .pause(1000)
    .runFn(() => {
        button.disabled = false;
        //lub startWrite() jeżeli chcemy zapętlić
    })
    .start();
}

Animowanie dymków rozmowy

Pierwszy krok mamy załatwiony. Przejdźmy więc do kolejnych, czyli animowania całej rozmowy.

Zacznijmy od pobrania odpowiednich elementów i utworzenia podstawowych zmiennych:


let writeSpeedAI = 15; //jak szybko pisze AI
let writeSpeedMe = 50; //jak szybko piszę ja
let thinkTimeAI = 2000; //jak długo myśli ai

//wszystkie elementy pobrane ze strony zgrupuję w obiekcie
//dzięki czemu łatwiej będzie mi się do nich odwoływać
const DOM = {};

DOM.banner = document.querySelector(".banner");

DOM.el1 = DOM.banner.querySelector(".banner-el1");
DOM.el1Text = DOM.el1.querySelector(".banner-el-text");

DOM.el2 = DOM.banner.querySelector(".banner-el2");
DOM.el2Text = DOM.el2.querySelector(".banner-el-text");

DOM.el3 = DOM.banner.querySelector(".banner-el3");
DOM.el3Text = DOM.el3.querySelector(".banner-el-text");

DOM.el4 = DOM.banner.querySelector(".banner-el4");
DOM.el4Text = DOM.el4.querySelector(".banner-el-text");

DOM.inner = DOM.banner.querySelector(".banner-inner");

...

Nasza rozmowa składa się z 4 dymków, które mieszczą się w elemencie .banner-inner. Pokażmy pierwszy dymek, po czym użyjmy w nim naszej piszącej maszyny. Do pokazania dymku moglibyśmy użyć biblioteki GreenSock, która nie tylko ma praktycznie nieograniczone możliwości animacji, ale i jest bajecznie prosta w użyciu:


<!-- dodajemy bibliotekę do HTML -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.5/gsap.min.js">

...

async function startPoint1() {
    gsap.to(DOM.el1, {opacity: 1, duration: 1});
}

startPoint1();

Mega wygodna sprawa (polecam!)...

W naszym przypadku jednak chcemy tylko animować zwykłe opacity. Możemy to zrobić samym CSS i dodaniem odpowiedniej klasy dla elementu. Ja skorzystam tutaj z metody animate.


...

async function startPoint1() {
    return new Promise((resolve, reject) => {
        const anim = DOM.el1.animate([
            {opacity: 0},
            {opacity: 1}
        ], {
            duration: 1000,
            fill: "both"
        });
        anim.onfinish = () => resolve()
    })
}

startPoint1();

Trochę więcej kodu, ale mniej do ściągnięcia danych przez użytkownika. Będziemy chcieli pokazywać resztę dymków, dlatego kod odpowiedzialny za animację wynieśmy do osobnej funkcji:


...

async function showElement(el, timeInSecond) {
    return new Promise((resolve, reject) => {
        const anim = el.animate([
            {opacity: 0},
            {opacity: 1}
        ], {
            duration: timeInSecond * 1000,
            fill: "both"
        });
        anim.onfinish = () => resolve()
    })
}

async function startPoint1() {
    await showElement(DOM.el1, 1);
}

startPoint1();

Po pokazaniu dymka, chcemy chwilkę odczekać zanim tekst zacznie się pisać, dzięki czemu efekt będzie bardziej realny. Napiszmy dodatkową funkcję służącą do opóźniania dalszego kodu:.


...

function pause(time = 100) {
    return new Promise((resolve, reject) => {
        setTimeout(() => { resolve() }, time)
    });
}

async function showElement(el, timeInSecond) {
    ...
}

async function startPoint1() {
    await showElement(DOM.el1, 1);
    await pause(1000);
}

startPoint1();

A następnie dodajmy samo pisanie tekstu korzystając z naszej klasy:


...

function pause(time = 100) {
    ...
}

async function showElement(el, timeInSecond) {
    ...
}

async function startPoint1() {
    await showElement(DOM.el1, 0.5);
    await pause(1000);

    const tm = new TypeMachine((text, count) => {
        DOM.el1Text.innerHTML = text;
        DOM.content.scrollTop = DOM.content.scrollHeight; //aby podczas pisania przenosił na koniec tekstu
    });
    tm.write("dzisiaj dowiedz", writeSpeedMe)
    tm.pause(400)
    tm.back(7, writeSpeedMe)
    tm.pause(400)
    tm.write("czytałem ze ponoć 20% młodzieży ma problemy depresyjne", writeSpeedMe); //tekst zostawiam w oryginalne postaci :)
    tm.runFn(() => {
        setTimeout(() => startPoint2(), 500);
    })
    tm.start();
}

startPoint1();

Drugim dymkiem będzie odpowiedź ChatGPT. Żeby to zasygnalizować, dodamy do dymku dodatkową klasę .is-wait, która za pomocą odpowiedniego stylowania stworzy dodatkowy element ::before, który będzie symulował mrugający kursor:


.banner-el.is-wait .banner-el-text::before {
    content: "";
    position: relative;
    height: 1em;
    width: 0.5em;
    background: rgba(255 255 255 / 0.7);
    display: inline-block;
    animation: cursorWait 0.2s 0s infinite alternate;
    vertical-align: text-top;
}

@keyframes cursorWait {
    0% {
        opacity: 0;
    }
    100% {
        opacity: 1;
    }
}

Pozostaje napisać funkcję pokazującą drugi dymek:


...

async function startPoint2() {
    await show(DOM.el2, 0.5);

    //głupie AI myśli
    DOM.el2.classList.add("is-wait");
    await pause(thinkTimeAI);
    DOM.el2.classList.remove("is-wait");

    const tm = new TypeMachine((text, count) => {
        DOM.el2Text.innerHTML = text;
        DOM.content.scrollTop = DOM.content.scrollHeight;
    });
    tm.write("Niestety, faktycznie depresja i inne zaburzenia nastroju są coraz bardziej powszechne wśród młodzieży. Według danych Światowej Organizacji Zdrowia, około 20% młodych ludzi w wieku 10-24 lat doświadcza zaburzeń psychicznych, takich jak depresja czy lęki.", writeSpeedAI);
    tm.runFn(() => {
        setTimeout(() => startPoint3(), 500);
    });
    tm.start();
}

i podobnie pozostałe dwa dymki:


...

async function startPoint3() {
    await show(DOM.el3, 0.5);
    await pause(1000);

    const tm = new TypeMachine((text, count) => {
        DOM.el3Text.innerHTML = text;
        DOM.content.scrollTop = DOM.content.scrollHeight;
    });
    tm.write("Czy narzędzie AI jest w stanie rozwiązać problemy psychiczne?", writeSpeedMe);
    tm.runFn(() => {
        setTimeout(() => startPoint4(), 500);
    });
    tm.start();
}

async function startPoint4() {
    await show(DOM.el4, 0.5);

    DOM.el4.classList.add("is-wait");
    await pause(thinkTimeAI);
    DOM.el4.classList.remove("is-wait");

    const tm = new TypeMachine((text, count) => {
        DOM.el4Text.innerHTML = text;
        DOM.content.scrollTop = DOM.content.scrollHeight;
    });
    tm.write("Narzędzia sztucznej inteligencji mogą być pomocne w procesie diagnostyki i leczenia problemów psychicznych, jednak nie są w stanie zastąpić pracy wykwalifikowanego psychologa czy psychiatry.", writeSpeedAI);
    tm.write("<b class='mark'> ")
    tm.write("Terapia to proces indywidualny, który wymaga zaangażowania i współpracy pomiędzy terapeutą a pacjentem, oraz uwzględnienia kontekstu życiowego i emocjonalnego każdej osoby.", writeSpeedAI)
    tm.write("</b>")
    tm.start();
}

Demo masz na początku strony...

Jedyną rzeczą, którą w nim dodałem, do odpalanie całej animacji w momencie, gdy baner zacznie wjeżdżać na ekran.


let startPlay = false;
let observer = new IntersectionObserver(elements => {
    for (let el of elements) {
        if (el.isIntersecting) {
            if (!startPlay) {
                startPoint1();
                startPlay = true;
            }
        }
    }
}, {
    rootMargin: '0px',
    threshold: 0.5
});

observer.observe(DOM.banner);

Bonus - odpowiedzi z ChatGPT

W powyższym kodzie teksty były wpisane na sztywno (ale wiedz, że to są praktycznie oryginalne teksty).

W ramach testu możemy pokusić się o użycie na naszym banerze realnych odpowiedzi, które uzyskamy bezpośrednio od ChatGPT.

Po pierwsze musisz wejść na stronę https://platform.openai.com/docs/api-reference i w prawym górnym rogu się zarejestrować. Po zarejestrowaniu znowu na tej same stronie przy twoim awatarze (prawy górny róg) otwórz menu i z opcji wybierz "View API keys". Wygeneruj nowy klucz. Będziesz musiał go użyć przy połączeniu z API.

Połączenie zrobimy za pomocą zwykłego fetch według opisu ze strony api. W naszym przypadku musimy zmienić tylko funkcje, gdzie odpowiada AI.


...

const API_KEY = 'TUTAJ TWÓJ KLUCZ';

function pause(time = 100) {
    ...
}

async function showElement(el, timeInSecond) {
    ...
}

async function startPoint1() {
    ...
}

async function startPoint2() {
    await show(DOM.el2, 0.5);

    DOM.el2.classList.add("is-wait");

    const tm = new TypeMachine((text, count) => {
            DOM.el2Text.innerHTML = text;
            DOM.content.scrollTop = DOM.content.scrollHeight;
    });

    const q = "czytałem ze ponoć 20% młodzieży ma problemy depresyjne";

    try {
        const r = await fetch("https://api.openai.com/v1/chat/completions", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "Authorization": `Bearer ${API_KEY}`,
            },
            body: JSON.stringify({
                model: "gpt-3.5-turbo",
                messages: [{ role: "user", content: q }],
            }),
        });

        const json = await r.json();

        DOM.el2.classList.remove("is-wait");

        for (let el of json.choices) {
            tm.write(el.message.content, writeSpeedAI);
        }
        tm.runFn(() => {
            setTimeout(() => startPoint3(), 500);
        });
        tm.start();
    } catch(err) {
        throw Error(err);
    }
}

...

startPoint1();

...

async function startPoint3() {
    ...
}

async function startPoint4() {
    await show(DOM.el4, 0.5);

    DOM.el4.classList.add("is-wait");

    const tm = new TypeMachine({
        onWrite : (text, count) => {
            DOM.el4Text.innerHTML = text;
            DOM.content.scrollTop = DOM.content.scrollHeight;
        }
    });
    const q = "Czy narzędzie AI jest w stanie rozwiązać problemy psychiczne?";

    try {
        const r = await fetch("https://api.openai.com/v1/chat/completions", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "Authorization": `Bearer ${API_KEY}`,
            },
            body: JSON.stringify({
                model: "gpt-3.5-turbo",
                messages: [{ role: "user", content: q }],
            }),
        });

        const json = await r.json();

        DOM.el4.classList.remove("is-wait");

        for (let el of json.choices) {
            tm.write(el.message.content, writeSpeedAI);
        }
        tm.start();
    } catch(err) {
        throw Error(err);
    }
}

startPoint1();

Czy to się nadaje na realny baner? Nie za bardzo. Poniżej nagrałem ci film z pierwszego lepszego testu. Ogólnie jest nieźle, ale w poniższym przypadku dostałem odpowiedzi po polsku, po 3 próbie część przyszła po angielsku. Podejrzewam, że trzeba by się przyjrzeć parametrom, które udostępnia api. Ważniejsze jednak jest to, skąd mam wiedzieć, w jakim humorze AI będzie za kilka dni?. A jeżeli zacznie wrzucać ludzkości? Wtedy akurat najlepiej zacząć przenosić swoje kodowanie do schronu, no ale i nasz baner by ucierpiał...

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem.
Aha - i ta strona korzysta z ciasteczek.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.