Thursday

19-06-2025 Vol 19

LocoLama โ€“ Local AI Chat Terminal Using Ollama + Next.js (DROP002)

LocoLama: Membangun Terminal Chat AI Lokal dengan Ollama dan Next.js (DROP002)

Di era AI generatif yang berkembang pesat, kemampuan untuk berinteraksi dengan model AI secara lokal menjadi semakin penting. Privasi data, latensi rendah, dan kemampuan untuk beroperasi secara offline adalah beberapa keuntungan utama. Dalam tutorial ini, kita akan menjelajahi bagaimana cara membangun terminal chat AI lokal Anda sendiri bernama LocoLama menggunakan Ollama dan Next.js. Proyek ini, yang kami sebut DROP002, akan membimbing Anda melalui setiap langkah, mulai dari pengaturan lingkungan pengembangan hingga menyebarkan aplikasi Anda.

Mengapa Terminal Chat AI Lokal?

Sebelum kita menyelami detail implementasi, mari kita pahami mengapa membangun terminal chat AI lokal itu penting:

  1. Privasi Data: Data Anda tetap berada di mesin Anda. Tidak ada data sensitif yang dikirim ke server eksternal.
  2. Latensi Rendah: Respons lebih cepat karena inferensi AI terjadi secara lokal, menghilangkan ketergantungan pada koneksi internet dan server jarak jauh.
  3. Operasi Offline: Terus menggunakan aplikasi AI Anda bahkan tanpa koneksi internet.
  4. Kustomisasi: Kendalikan penuh model AI yang Anda gunakan dan bagaimana mereka dilatih.
  5. Biaya: Hilangkan biaya yang terkait dengan API pihak ketiga dan penggunaan berbasis cloud.

Prasyarat

Sebelum memulai, pastikan Anda memiliki hal-hal berikut terinstal:

  1. Node.js dan npm (atau yarn): Next.js membutuhkan Node.js. Unduh dan instal dari nodejs.org. npm biasanya sudah termasuk dalam instalasi Node.js.
  2. Ollama: Unduh dan instal Ollama dari ollama.com. Ikuti petunjuk instalasi khusus untuk sistem operasi Anda.
  3. Editor Kode: Visual Studio Code (VS Code) sangat direkomendasikan, tetapi editor teks apa pun yang Anda sukai akan berfungsi.

Langkah 1: Menyiapkan Proyek Next.js

Pertama, kita akan membuat proyek Next.js baru. Next.js menyediakan kerangka kerja yang kuat untuk membangun aplikasi web React dengan server-side rendering (SSR) dan kemampuan static site generation (SSG).

  1. Buat Proyek Next.js: Buka terminal Anda dan jalankan perintah berikut:
    npx create-next-app locolama

    Ini akan membuat direktori baru bernama `locolama` dengan templat Next.js dasar.

  2. Navigasi ke Direktori Proyek:
    cd locolama
  3. Mulai Server Pengembangan:
    npm run dev

    Ini akan memulai server pengembangan di `http://localhost:3000`. Buka di browser Anda untuk melihat aplikasi Next.js default.

Langkah 2: Menginstal Dependensi yang Diperlukan

Kita membutuhkan beberapa dependensi tambahan untuk berinteraksi dengan Ollama dan membangun antarmuka pengguna yang menarik.

  1. Instal Paket: Jalankan perintah berikut untuk menginstal dependensi yang diperlukan:
    npm install ollama react-markdown react-syntax-highlighter

    Berikut penjelasan singkat tentang setiap paket:

    • ollama: Library klien JavaScript untuk berinteraksi dengan API Ollama.
    • react-markdown: Komponen React untuk merender teks Markdown menjadi HTML. Ini akan berguna untuk menampilkan respons AI.
    • react-syntax-highlighter: Komponen React untuk menyorot kode dalam respons AI.

Langkah 3: Membangun Antarmuka Pengguna

Sekarang, mari kita buat antarmuka pengguna untuk terminal chat kita. Kita akan mulai dengan tata letak dasar dan kemudian menambahkan fungsionalitas interaksi.

  1. Edit `pages/index.js`: Buka file `pages/index.js` di editor kode Anda. File ini berisi komponen utama untuk halaman beranda aplikasi Anda.
  2. Ganti Konten dengan Markup Berikut:
    
    import { useState, useEffect } from 'react';
    import ReactMarkdown from 'react-markdown';
    import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
    import { dark } from 'react-syntax-highlighter/dist/esm/styles/prism';
    
    export default function Home() {
      const [inputText, setInputText] = useState('');
      const [chatHistory, setChatHistory] = useState([]);
      const [isLoading, setIsLoading] = useState(false);
    
      const handleSubmit = async (e) => {
        e.preventDefault();
        if (!inputText.trim()) return;
    
        setIsLoading(true);
        setChatHistory([...chatHistory, { type: 'user', text: inputText }]);
        setInputText('');
    
        try {
          const response = await fetch('/api/chat', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
            },
            body: JSON.stringify({ prompt: inputText }),
          });
    
          if (!response.ok) {
            throw new Error(\`HTTP error! status: ${response.status}\`);
          }
    
          const data = await response.json();
          setChatHistory([...chatHistory, { type: 'user', text: inputText }, { type: 'bot', text: data.response }]);
    
        } catch (error) {
          console.error("Failed to fetch:", error);
          setChatHistory([...chatHistory, { type: 'user', text: inputText }, { type: 'bot', text: "Error: Could not process your request." }]);
        } finally {
          setIsLoading(false);
        }
      };
    
      return (
        <div className="container">
          <h1>LocoLama Chat</h1>
          <div className="chat-container">
            {chatHistory.map((message, index) => (
              <div key={index} className={\`message \${message.type}\`}>
                <strong>{message.type === 'user' ? 'You:' : 'Bot:'}</strong><br/>
                <ReactMarkdown
                  components={{
                    code({ node, inline, className, children, ...props }) {
                      const match = (className || '').match(/language-(\\w+)/);
                      return !inline && match ? (
                        <SyntaxHighlighter
                          style={dark}
                          language={match[1]}
                          PreTag="div"
                          {...props}
                        >
                          {String(children).replace(/\\n$/, '')}
                        </SyntaxHighlighter>
                      ) : (
                        <code className={className} {...props}>
                          {children}
                        </code>
                      );
                    }
                  }}
                >
                  {message.text}
                </ReactMarkdown>
              </div>
            ))}
            {isLoading && <div className="message bot"><strong>Bot:</strong><br/>Thinking...</div>}
          </div>
          <form onSubmit={handleSubmit}>
            <input
              type="text"
              value={inputText}
              onChange={(e) => setInputText(e.target.value)}
              placeholder="Type your message..."
              disabled={isLoading}
            />
            <button type="submit" disabled={isLoading}>
              {isLoading ? 'Sending...' : 'Send'}
            </button>
          </form>
    
          <style jsx>{\`
            .container {
              max-width: 800px;
              margin: 20px auto;
              padding: 20px;
              border: 1px solid #ddd;
              border-radius: 8px;
              font-family: sans-serif;
            }
    
            h1 {
              text-align: center;
              margin-bottom: 20px;
            }
    
            .chat-container {
              margin-bottom: 20px;
              padding: 10px;
              border: 1px solid #ddd;
              border-radius: 4px;
              height: 400px;
              overflow-y: scroll;
            }
    
            .message {
              padding: 8px 12px;
              border-radius: 4px;
              margin-bottom: 8px;
              word-wrap: break-word;
            }
    
            .message.user {
              background-color: #e2f0ff;
              text-align: right;
            }
    
            .message.bot {
              background-color: #f9f9f9;
              text-align: left;
            }
    
            form {
              display: flex;
            }
    
            input[type="text"] {
              flex: 1;
              padding: 10px;
              border: 1px solid #ddd;
              border-radius: 4px 0 0 4px;
              font-size: 16px;
            }
    
            button {
              padding: 10px 15px;
              background-color: #0070f3;
              color: white;
              border: none;
              border-radius: 0 4px 4px 0;
              cursor: pointer;
              font-size: 16px;
            }
    
            button:disabled {
              background-color: #999;
              cursor: not-allowed;
            }
          \`}</style>
        </div>
      );
    }
          

Kode ini membuat antarmuka obrolan dasar dengan riwayat obrolan, bidang input, dan tombol kirim. Ini juga menangani status pemuatan untuk memberikan umpan balik visual ketika aplikasi sedang menunggu respons AI.

Langkah 4: Membuat Endpoint API untuk Berinteraksi dengan Ollama

Selanjutnya, kita perlu membuat endpoint API yang akan menerima masukan pengguna dan mengirimkannya ke Ollama untuk pemrosesan.

  1. Buat File `pages/api/chat.js`: Buat file baru di direktori `pages/api/` bernama `chat.js`.
  2. Tambahkan Kode Berikut ke `chat.js`:
    
    import { Ollama } from 'ollama';
    
    const ollama = new Ollama({ host: 'http://localhost:11434' });
    
    export default async function handler(req, res) {
      if (req.method === 'POST') {
        const { prompt } = req.body;
    
        try {
          const response = await ollama.chat({
            model: 'llama2', // Ganti dengan model yang ingin Anda gunakan
            messages: [{ role: 'user', content: prompt }],
          });
    
          res.status(200).json({ response: response.message.content });
        } catch (error) {
          console.error("Ollama error:", error);
          res.status(500).json({ error: 'Failed to generate response' });
        }
      } else {
        res.status(405).json({ error: 'Method Not Allowed' });
      }
    }
          

Kode ini membuat endpoint API yang:

  • Menerima permintaan POST yang berisi `prompt`.
  • Menginisialisasi klien Ollama. Pastikan `host` cocok dengan alamat Ollama Anda. Secara default, Ollama berjalan di `http://localhost:11434`.
  • Menggunakan metode `ollama.chat` untuk mengirimkan prompt ke model AI. Penting: Ganti `’llama2’` dengan nama model yang telah Anda unduh dan ingin Anda gunakan dengan Ollama. Anda dapat menemukan daftar model yang tersedia menggunakan perintah `ollama list`.
  • Mengembalikan respons AI sebagai JSON.
  • Menangani kesalahan dan mengembalikan kode status 500 jika terjadi kesalahan.

Langkah 5: Memastikan Ollama Berjalan dan Model Sudah Diunduh

Sebelum menjalankan aplikasi, pastikan Ollama berjalan dan Anda telah mengunduh model yang ingin Anda gunakan.

  1. Mulai Ollama: Buka terminal baru dan jalankan perintah `ollama serve`. Ini akan memulai server Ollama.
  2. Unduh Model: Di terminal lain, jalankan perintah `ollama pull llama2` (atau model lain yang Anda inginkan). Ini akan mengunduh model yang diperlukan.

Langkah 6: Menjalankan Aplikasi

Sekarang Anda memiliki semua bagian yang diperlukan, jalankan aplikasi Next.js Anda:

npm run dev

Buka `http://localhost:3000` di browser Anda. Anda sekarang dapat berinteraksi dengan terminal chat AI lokal Anda!

Peningkatan Lebih Lanjut

Berikut beberapa peningkatan lebih lanjut yang dapat Anda lakukan pada proyek LocoLama:

  1. Pilih Model: Tambahkan dropdown untuk memungkinkan pengguna memilih model AI yang berbeda.
  2. Streaming Respons: Implementasikan streaming respons untuk menampilkan respons AI secara real-time alih-alih menunggu seluruh respons selesai sebelum menampilkannya. Ini dapat meningkatkan pengalaman pengguna secara signifikan.
  3. Riwayat Obrolan Persisten: Simpan riwayat obrolan di penyimpanan lokal atau database agar obrolan tetap ada di antara sesi.
  4. Penyesuaian UI: Sesuaikan UI agar lebih ramah pengguna dan estetis.
  5. Dukungan untuk Multimodal Models: Integrate support for models that can handle images, audio, and video.
  6. Fungsi Tambahan: Tambahkan fungsi seperti kemampuan untuk menyalin kode, mengunduh respons, atau mengekspor riwayat obrolan.

Streaming Respons dengan Ollama dan Next.js

Salah satu peningkatan paling signifikan yang dapat Anda lakukan adalah menerapkan streaming respons. Secara default, Ollama mengirimkan seluruh respons setelah diproses. Streaming memungkinkan Anda menampilkan respons secara bertahap saat dibuat, sehingga meningkatkan pengalaman pengguna.

  1. Ubah Endpoint API (`pages/api/chat.js`): Perbarui endpoint API untuk menggunakan streaming dari Ollama.
    
    import { Ollama } from 'ollama';
    
    const ollama = new Ollama({ host: 'http://localhost:11434' });
    
    export const config = {
      runtime: 'edge',
    };
    
    export default async function handler(req) {
      if (req.method === 'POST') {
        const { prompt } = await req.json();
    
        const stream = await ollama.chat({
          model: 'llama2',
          messages: [{ role: 'user', content: prompt }],
          stream: true, // Aktifkan streaming
        });
    
        return new Response(
          new ReadableStream({
            async start(controller) {
              for await (const part of stream) {
                const textEncoder = new TextEncoder();
                controller.enqueue(textEncoder.encode(part.message.content));
              }
              controller.close();
            },
          }),
          {
            headers: { 'Content-Type': 'text/plain' },
          }
        );
      } else {
        return new Response(JSON.stringify({ error: 'Method Not Allowed' }), {
          status: 405,
          headers: { 'Content-Type': 'application/json' },
        });
      }
    }
        

    Perubahan utama:

    • Menambahkan `export const config = { runtime: ‘edge’ };` untuk mengaktifkan fungsi Edge di Next.js, yang diperlukan untuk streaming.
    • Mengaktifkan opsi `stream: true` dalam metode `ollama.chat`.
    • Menggunakan `ReadableStream` untuk secara bertahap mengirimkan respons kembali ke klien.
  2. Perbarui Komponen Frontend (`pages/index.js`): Ubah frontend untuk menangani respons streaming.
    
    import { useState, useEffect, useRef } from 'react';
    import ReactMarkdown from 'react-markdown';
    import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
    import { dark } from 'react-syntax-highlighter/dist/esm/styles/prism';
    
    export default function Home() {
      const [inputText, setInputText] = useState('');
      const [chatHistory, setChatHistory] = useState([]);
      const [isLoading, setIsLoading] = useState(false);
      const [currentResponse, setCurrentResponse] = useState(''); // Simpan respons streaming
      const chatContainerRef = useRef(null);
    
      useEffect(() => {
        if (chatContainerRef.current) {
          chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
        }
      }, [chatHistory, currentResponse]);
    
      const handleSubmit = async (e) => {
        e.preventDefault();
        if (!inputText.trim()) return;
    
        setIsLoading(true);
        setCurrentResponse(''); // Reset respons saat ini
        setChatHistory([...chatHistory, { type: 'user', text: inputText }]);
    
        try {
          const response = await fetch('/api/chat', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
            },
            body: JSON.stringify({ prompt: inputText }),
          });
    
          if (!response.ok) {
            throw new Error(\`HTTP error! status: ${response.status}\`);
          }
    
          const reader = response.body.getReader();
          const decoder = new TextDecoder();
          let accumulatedResponse = '';
    
          while (true) {
            const { done, value } = await reader.read();
            if (done) {
              break;
            }
            const decodedChunk = decoder.decode(value);
            accumulatedResponse += decodedChunk;
            setCurrentResponse(accumulatedResponse);
          }
    
          setChatHistory(prevChatHistory => [...prevChatHistory, { type: 'user', text: inputText }, { type: 'bot', text: accumulatedResponse }]);
    
        } catch (error) {
          console.error("Failed to fetch:", error);
          setChatHistory(prevChatHistory => [...prevChatHistory, { type: 'user', text: inputText }, { type: 'bot', text: "Error: Could not process your request." }]);
        } finally {
          setIsLoading(false);
          setInputText('');
        }
      };
    
      return (
        <div className="container">
          <h1>LocoLama Chat</h1>
          <div className="chat-container" ref={chatContainerRef}>
            {chatHistory.map((message, index) => (
              <div key={index} className={\`message \${message.type}\`}>
                <strong>{message.type === 'user' ? 'You:' : 'Bot:'}</strong><br/>
                <ReactMarkdown
                  components={{
                    code({ node, inline, className, children, ...props }) {
                      const match = (className || '').match(/language-(\\w+)/);
                      return !inline && match ? (
                        <SyntaxHighlighter
                          style={dark}
                          language={match[1]}
                          PreTag="div"
                          {...props}
                        >
                          {String(children).replace(/\\n$/, '')}
                        </SyntaxHighlighter>
                      ) : (
                        <code className={className} {...props}>
                          {children}
                        </code>
                      );
                    }
                  }}
                >
                  {message.text}
                </ReactMarkdown>
              </div>
            ))}
            {isLoading && (
              <div className="message bot">
                <strong>Bot:</strong><br />
                <ReactMarkdown
                    components={{
                      code({ node, inline, className, children, ...props }) {
                        const match = (className || '').match(/language-(\\w+)/);
                        return !inline && match ? (
                          <SyntaxHighlighter
                            style={dark}
                            language={match[1]}
                            PreTag="div"
                            {...props}
                          >
                            {String(children).replace(/\\n$/, '')}
                          </SyntaxHighlighter>
                        ) : (
                          <code className={className} {...props}>
                            {children}
                          </code>
                        );
                      }
                    }}
                  >
                    {currentResponse}
                </ReactMarkdown>
              </div>
            )}
          </div>
          <form onSubmit={handleSubmit}>
            <input
              type="text"
              value={inputText}
              onChange={(e) => setInputText(e.target.value)}
              placeholder="Type your message..."
              disabled={isLoading}
            />
            <button type="submit" disabled={isLoading}>
              {isLoading ? 'Sending...' : 'Send'}
            </button>
          </form>
    
          <style jsx>{\`
            .container {
              max-width: 800px;
              margin: 20px auto;
              padding: 20px;
              border: 1px solid #ddd;
              border-radius: 8px;
              font-family: sans-serif;
            }
    
            h1 {
              text-align: center;
              margin-bottom: 20px;
            }
    
            .chat-container {
              margin-bottom: 20px;
              padding: 10px;
              border: 1px solid #ddd;
              border-radius: 4px;
              height: 400px;
              overflow-y: scroll;
              display: flex;
              flex-direction: column;
            }
    
            .message {
              padding: 8px 12px;
              border-radius: 4px;
              margin-bottom: 8px;
              word-wrap: break-word;
            }
    
            .message.user {
              background-color: #e2f0ff;
              text-align: right;
            }
    
            .message.bot {
              background-color: #f9f9f9;
              text-align: left;
            }
    
            form {
              display: flex;
            }
    
            input[type="text"] {
              flex: 1;
              padding: 10px;
              border: 1px solid #ddd;
              border-radius: 4px 0 0 4px;
              font-size: 16px;
            }
    
            button {
              padding: 10px 15px;
              background-color: #0070f3;
              color: white;
              border: none;
              border-radius: 0 4px 4px 0;
              cursor: pointer;
              font-size: 16px;
            }
    
            button:disabled {
              background-color: #999;
              cursor: not-allowed;
            }
          \`}</style>
        </div>
      );
    }
          

    Perubahan utama:

    • Menambahkan state `currentResponse` untuk menyimpan respons streaming.
    • Menggunakan `response.body.getReader()` untuk membaca respons secara bertahap.
    • Memperbarui state `currentResponse` dengan setiap chunk yang diterima.
    • Menampilkan `currentResponse` dalam komponen chat.

Mengatasi Masalah Umum

Saat membangun LocoLama, Anda mungkin mengalami beberapa masalah umum:

  • Ollama Tidak Berjalan: Pastikan server Ollama berjalan dengan menjalankan `ollama serve` di terminal terpisah.
  • Model Tidak Ditemukan: Verifikasi bahwa Anda telah mengunduh model yang benar dengan menggunakan perintah `ollama list` dan pastikan nama model yang sesuai digunakan dalam endpoint API.
  • Masalah CORS: Jika Anda mengalami kesalahan Cross-Origin Resource Sharing (CORS), pastikan bahwa server Ollama dikonfigurasi untuk mengizinkan permintaan dari domain aplikasi Next.js Anda. Anda dapat melakukan ini dengan memulai Ollama dengan flag berikut: `OLLAMA_ORIGINS=* ollama serve`. Perhatian: Menggunakan `*` untuk mengizinkan semua asal tidak disarankan untuk lingkungan produksi karena alasan keamanan. Sebaiknya tentukan asal tertentu secara eksplisit.
  • Tidak ada Respons: Periksa konsol browser dan log server untuk kesalahan. Pastikan bahwa endpoint API berfungsi dengan benar dan tidak ada kesalahan dalam kode Anda.
  • Respons Lambat: Respons yang lambat dapat disebabkan oleh berbagai faktor, termasuk perangkat keras yang tidak memadai, model yang besar, atau masalah jaringan. Pertimbangkan untuk menggunakan model yang lebih kecil atau mengoptimalkan kode Anda untuk meningkatkan kinerja.

Kesimpulan

Selamat! Anda telah berhasil membangun terminal chat AI lokal menggunakan Ollama dan Next.js. Proyek LocoLama ini menunjukkan bagaimana Anda dapat memanfaatkan kekuatan AI secara lokal, menawarkan privasi, latensi rendah, dan kemampuan untuk beroperasi secara offline. Dengan peningkatan dan penyesuaian lebih lanjut, Anda dapat membangun berbagai aplikasi AI yang disesuaikan dengan kebutuhan khusus Anda. Jangan ragu untuk menjelajahi model Ollama lain dan bereksperimen dengan fitur Next.js untuk membuat pengalaman chat AI yang unik dan bermanfaat.

Ingat, ini hanyalah titik awal. Dunia AI generatif terus berkembang, jadi teruslah belajar, bereksperimen, dan membangun!

“`

omcoding

Leave a Reply

Your email address will not be published. Required fields are marked *