# PROMPT CODING AGENT — FE Nuxt 4: Chat Agent Pekerjaan (Centrifugo)

> Tempel seluruh isi ke coding agent frontend. Versi 3.0 — realtime via **Centrifugo (WebSocket)**, arsitektur FE → BE (Golang) → Hermes.

---

Kamu adalah coding agent frontend yang membangun fitur **Chat Agent Pekerjaan** di proyek **Nuxt 4** yang sudah ada. Kamu HANYA membangun sisi frontend. Semua komunikasi dengan AI melewati **backend Golang milik tim sendiri** — frontend TIDAK PERNAH memanggil Hermes secara langsung dan TIDAK menyimpan API key apa pun. Real-time memakai **Centrifugo** (server WebSocket pub/sub standalone yang dikelola tim backend).

## 1. Konteks & arsitektur

- Proyek: **Nuxt 4** (Vue 3 + Nitro, struktur `app/`). Bahasa UI: Bahasa Indonesia.
- Backend: **Golang** (sudah ada). AI di belakang BE: Hermes Agent (satu mesin per user).
- Alur: user mengirim **data diri, CV, foto** lalu menyuruh agent mendaftar & melamar pekerjaan. User harus bisa: (a) melihat aktivitas agent secara real-time, (b) menjawab pertanyaan agent di tengah proses (pretest question, dll), (c) melihat status & sisa kuota, (d) menutup tab tanpa merusak proses (agent tetap jalan, jawaban tetap sampai), (e) **melihat & mengoperasikan browser agent** (login ke job portal sendiri) lewat **viewer VNC tertanam** di halaman.

```
Browser (Nuxt 4 FE)
   ├─ kirim pesan:  POST {API_BASE}/api/v1/chat          (HTTP, balas 202 cepat)
   └─ terima event: WebSocket ─► Centrifugo ─► channel "user:<user_id>"
                      (centrifuge-js; reconnect otomatis + history replay)
```

**Pola penting:** FE TIDAK membaca stream SSE dari BE. Semua peristiwa agent datang sebagai **event Centrifugo** di channel pribadi user. FE hanya memakai HTTP untuk: kirim pesan, upload file, ambil token realtime, ambil URL viewer VNC, reset sesi, dan baca kuota/riwayat lamaran.

## 2. Endpoint BE yang dipakai FE

Base URL dari `NUXT_PUBLIC_API_BASE` (public runtime config — bukan rahasia). Auth = mekanisme FE→BE yang sudah ada.

| Method & Path | Fungsi |
|---|---|
| `POST {API_BASE}/api/v1/chat` | Kirim pesan. Body `{"message":"..."}`. Respons **202** `{"accepted":true,"run_id":"..."}` — agent jalan di background, hasilnya lewat Centrifugo. Kalau agent sedang bekerja → **429** `{"error":{"code":"agent_busy","message":"..."}}`. |
| `POST {API_BASE}/api/v1/chat/session` | Reset sesi percakapan. `{"ok":true}`. |
| `GET {API_BASE}/api/v1/realtime/token` | Ambil JWT Centrifugo untuk user ini. Respons `{"token":"..."}`. |
| `GET {API_BASE}/api/v1/realtime/vnc` | Ambil URL WebSocket viewer VNC **milik user ini saja**. Respons `{"url":"wss://..."}`. BE TIDAK boleh mengembalikan URL mesin user lain. |
| `POST {API_BASE}/api/v1/upload` | Upload file (multipart, field `file`). `{"url":"...","name":"..."}`. |
| `GET {API_BASE}/api/v1/quota` | `{"used_today":3,"limit_per_day":10,"remaining_today":7,"window_days":30,"total_used":12,"total_limit":300}` |
| `GET {API_BASE}/api/v1/applications` | Riwayat lamaran user. |
| `GET {API_BASE}/api/v1/portal-status` | Status koneksi job portal user: `[{"portal":"jobstreet","name":"JobStreet","status":"connected","display_name":"ena***@yahoo.com","verified_at":"..."}]` (`name` dari tabel `portals` BE). |
| `GET {API_BASE}/api/v1/daily-reports` | Histori daily report (untuk kartu ringkasan; yang terbaru duluan). |

## 3. Koneksi Centrifugo (wajib)

- Dependency baru yang diizinkan (hanya ini): **`centrifuge`** (paket npm resmi centrifuge-js) dan **`@novnc/novnc`** (untuk viewer VNC).
- Alur: setelah auth FE→BE siap, panggil `GET /api/v1/realtime/token` → `new Centrifuge(API_BASE + '/connection/websocket', { token })` → `client.connect()`.
- Subscribe channel **`user:<user_id>`** (user_id dari sesi auth FE yang sudah ada).
- `client.on('disconnected')` → tampilkan indikator "menghubungkan kembali…"; jangan reset state chat. `client.on('connected')` → sembunyikan.
- **History replay:** setelah subscribe, panggil `client.history(channel)` — BE/Centrifugo menyimpan event agent terbaru. Replay event yang belum dirender (pakai `seq`/`epoch` dari Centrifugo untuk idempotensi — catat seq terakhir yang sudah diproses, abaikan yang lebih kecil/sama). Tujuannya: tab ditutup saat agent bekerja → buka lagi → jawaban agent tetap tampil.
- Jangan tampilkan raw payload Centrifugo ke user.

## 4. Event Centrifugo (channel `user:<user_id>`)

| Event (`data.type`) | Payload | Aksi FE |
|---|---|---|
| `agent.status` | `{state:"idle"\|"working"\|"waiting_user"}` | Indikator status (chip di header/atas input): "sedang bekerja…" / "menunggu jawabanmu" / kosong. Saat `working` → nonaktifkan tombol kirim; saat `waiting_user` atau `idle` → aktifkan. |
| `agent.activity` | `{tool,emoji,label,toolCallId}` | Tambah ke activity feed. |
| `agent.text_delta` | `{delta}` | Append ke bubble assistant yang sedang berjalan (streaming). |
| `agent.text_done` | `{text}` | Finalisasi bubble assistant (teks sudah tanpa marker). |
| `agent.ask_user` | `{question,choices?,askId}` | Tampilkan kartu pertanyaan (bagian 5). |
| `agent.finished` | `{ok}` | Turn selesai; refresh badge kuota (`GET /api/v1/quota`). |
| `agent.error` | `{code,message}` | Bubble error ramah. |
| `quota.updated` | `{used_today,remaining_today,total_used,total_limit}` | Update badge kuota langsung (tanpa refetch). |
| `daily_report` | `{date,applied,failed,quota_left,portals_ok,portals_expired,no_match}` | Tampilkan kartu ringkasan daily run (bagian 6.12). |

Urutan khas satu turn: `agent.status(working)` → `agent.activity` (beberapa) → `agent.text_delta` (banyak) → `agent.text_done` → (`agent.ask_user` + `agent.status(waiting_user)`) ATAU (`agent.status(idle)` + `agent.finished`).

## 5. Kartu `ask_user` (dari event, bukan parse teks)

- Muncul dari event `agent.ask_user` — **TIDAK ada regex/parse marker apa pun di FE** (BE sudah mengubahnya jadi event).
- Tampilan: judul "🤖 Agent butuh jawabanmu", teks `question`. Jika `choices` ada & non-kosong → tombol per pilihan (maks 4) + opsi "Ketik jawaban sendiri…" (muncul input teks). Tanpa `choices` → input teks langsung.
- Saat user menjawab: `POST /api/v1/chat` dengan `{"message":"Jawaban user: <teks>"}` (teks = label tombol atau isi input). Tandai kartu sebagai terjawab (tampilkan jawabannya di kartu).
- Saat state `waiting_user`, input chat TETAP aktif (user bisa jawab atau ngobrol lain). Kartu tetap tampil sampai terjawab.

## 6. Fitur UI yang harus dibuat

Semua teks Bahasa Indonesia.

1. **Chat streaming** — bubble user (kanan, biru) & assistant (kiri, abu) dari `agent.text_delta`/`text_done`. Enter = kirim, Shift+Enter = baris baru.
2. **Activity feed** — dari `agent.activity` (`[emoji] [label]`), animasi ringan, auto-scroll ke terbaru.
3. **Indikator status agent** — dari `agent.status` (bagian 4). Saat `working`, tombol kirim nonaktif + label "Agent sedang bekerja…".
4. **Kartu `ask_user`** — bagian 5.
5. **Upload CV & foto** — tombol/area upload dekat input: PDF/DOC/DOCX (CV), JPG/JPEG/PNG (foto), maks 10 MB, validasi client (BE juga validasi). Upload via `POST /api/v1/upload`. Sukses → susun otomatis ke kolom input: `Saya upload CV: <url_cv> dan foto: <url_foto>. Gunakan data ini untuk proses lamaran saya.` (sesuaikan jika satu file).
6. **Badge kuota** — dari `GET /api/v1/quota` + update real-time via `quota.updated`. Format: "Kuota lamaran hari ini: 7/10". Jika 0 → peringatan halus.
7. **Tombol "Sesi Baru"** — `POST /api/v1/chat/session`, bersihkan chat + activity feed + kartu (konfirmasi dulu). State `waiting_user`/`working` di-reset ke `idle`.
8. **Error handling** — pesan ramah: HTTP error BE (429 `agent_busy` → "Agent masih bekerja, tunggu sebentar"), WS putus ("Menghubungkan kembali…"), event `agent.error`. Jangan tampilkan stack trace.
9. **Responsif** — mobile & desktop.
10. **Viewer VNC (`VncViewer`)** — menampilkan browser agent (desktop mesin Hermes) di halaman chat:
    - Koneksi: `import RFB from '@novnc/novnc'` → `new RFB(el, url, {})` dengan `url` dari `GET /api/v1/realtime/vnc` (panggil LAZY — saat viewer pertama dibuka, bukan saat halaman load).
    - `rfb.scaleViewport = true` (ikuti ukuran kontainer); `rfb.resizeSession = false`. FE menyesuaikan tampilan (fit/zoom) terhadap ukuran desktop apa pun (desktop mesin: 1600×900, rasio 16:9) — jangan paksa rasio tertentu di sisi server.
    - Tombol buka/tutup viewer (drawer/modal di samping chat; bisa di-drag/resize sesuka desain).
    - **Auto-show:** saat menerima event `agent.status` = `waiting_user` ATAU `agent.ask_user` → buka viewer otomatis (agent butuh user di browser, mis. login portal) + tampilkan hint "Agent menunggu kamu di browser — login dulu ya".
    - Saat `agent.status` = `working`/`idle` → viewer boleh tetap terbuka (user bisa nonton), tidak wajib ditutup.
    - Status koneksi viewer ditampilkan kecil ("terhubung / terputus / menghubungkan…"); tombol reconnect.
    - USER BERINTERAKSI (mengetik) di viewer — ini bukan view-only. Mouse/keyboard input dikirim ke browser agent.
    - **Toolbar bantu (WAJIB — untuk user non-teknis)**: floating di atas canvas viewer, SELALU terlihat (termasuk saat remote fullscreen/maximized — mis. popup login Google yang menutupi layar). Tombol berlabel jelas, Bahasa Indonesia:
      - "🗙 **Tutup jendela**" → kirim **Alt+F4** ke remote (tutup jendela/popup Chrome yang aktif).
      - "⏏ **Keluar**" → kirim **Esc** (keluar fullscreen/overlay).
      - "⛶ **Layar penuh**" → toggle fullscreen VIEWER (bukan remote).
      - "🆘 **Popup nempel?**" → kirim `POST /api/v1/chat` `{"message":"Tutup popup browser yang terbuka"}` → agent tutup via CDP (fallback andal untuk popup bandel).
      - Implementasi kirim key (`@novnc/novnc`): `rfb.sendKey(keysym, code)` — keysym: Escape `0xFF1B` · Alt_L `0xFFE9` · F4 `0xFFC1` · Tab `0xFF09`. Kombinasi Alt+F4 = urutan down/up: Alt down → F4 down → F4 up → Alt up; jika `sendKey` versi package hanya press+release tunggal, gunakan `rfb._sock.sendKeyEvent(keysym, code, down)` (internal) sesuai versi package.
      - User non-teknis TIDAK kenal shortcut keyboard — JANGAN andalkan keyboard; semua aksi harus lewat tombol yang terlihat.
    - **URL mengandung JWT pendek** (`?token=<jwt>`, masa berlaku ±1 jam dari BE): jangan cache/simpan URL lebih lama dari sesi viewer aktif; jika koneksi ditolak/terputus (token kedaluwarsa) → ambil URL BARU dari `GET /api/v1/realtime/vnc` lalu reconnect otomatis (maks 2×).
11. **Status koneksi portal** — daftar portal user dari `GET /api/v1/portal-status`:
    - Tampilkan per portal: nama (dari `name`) + status (`connected` → "Terhubung ✓" hijau · `expired`/`disconnected` → "Perlu login ulang" oranye · `connecting` → spinner).
    - Klik portal yang butuh login → buka `VncViewer` (agent akan buka halaman login; user login sendiri).
    - **Tombol "🔄 Perbarui status login"** di daftar portal (untuk user non-teknis): kirim `POST /api/v1/chat` dengan `{"message":"Cek dan perbarui status portal saya"}` → agent verifikasi sesi & sync status SEKARANG (tanpa menunggu watchdog 10 menit) → hasil tampil via event `quota.updated`/bubble jawaban + refresh `GET /api/v1/portal-status`.
    - Auto-refresh saat menerima event `agent.status(waiting_user)` atau setelah event `daily_report`.
12. **Kartu daily report** — ringkasan run harian dari event `daily_report` (dan histori via `GET /api/v1/daily-reports`):
    - Tampilkan: "📊 Daily report [tanggal] — applied: X · failed: Y · sisa kuota: Z" + portal yang bermasalah (`portals_expired`) + `no_match` ("Tidak ada lowongan cocok").
    - Kartu ringkas, bisa ditutup, histori terakhir tampil di bawah chat.

## 7. Struktur file (Nuxt 4 — direktori `app/`)

```
app/composables/useHermesChat.ts      # koneksi Centrifugo + state chat/feed/kartu + kirim pesan
app/components/HermesChatInput.vue    # input + tombol kirim + area upload
app/components/HermesActivityFeed.vue # feed aktivitas agent
app/components/HermesAskCard.vue      # kartu pertanyaan (event ask_user)
app/components/HermesMessageList.vue  # daftar bubble chat
app/components/HermesQuotaBadge.vue   # chip kuota
app/components/HermesStatusBadge.vue  # indikator status agent (working/waiting/idle)
app/components/HermesPortalList.vue    # status koneksi job portal (dari portal-status)
app/components/HermesDailyReport.vue   # kartu ringkasan daily run
app/components/VncViewer.vue          # viewer browser agent (RFB dari @novnc/novnc), auto-show saat waiting_user
app/pages/index.vue (atau halaman chat yang ada)  # rakit komponen
nuxt.config.ts                        # runtimeConfig.public.apiBase (+ public.mockChat)
package.json                          # + centrifuge, @novnc/novnc
.env.example                          # + NUXT_PUBLIC_API_BASE=
```

Catatan:
- `nuxt.config.ts`: `runtimeConfig: { public: { apiBase: process.env.NUXT_PUBLIC_API_BASE || 'http://localhost:8080', mockChat: process.env.NUXT_PUBLIC_MOCK_CHAT === 'true' } }`.
- `$fetch` dengan `baseURL: useRuntimeConfig().public.apiBase`, `credentials: 'include'`.
- Koneksi Centrifugo dibuat setelah auth siap; jangan blok render halaman karenanya (tampilkan state "menghubungkan…").

## 8. Mock mode (opsional tapi disarankan) — tes UI tanpa BE, Centrifugo, dan Hermes

Jika `NUXT_PUBLIC_MOCK_CHAT=true`: composable TIDAK connect Centrifugo / TIDAK memanggil BE. Gunakan event emitter lokal (setTimeout) yang mengirim urutan event SAMA PERSIS seperti server:
1. `agent.status(working)` → beberapa `agent.activity` (`🔍 membuka halaman lowongan...`, `📝 mengisi form...`) → `agent.text_delta` berulang → `agent.text_done` → `agent.ask_user{question,choices}` → `agent.status(waiting_user)`.
2. Jawaban user (turn mock) → balas `(MOCK) Jawaban diterima: <jawaban>` lewat `text_delta`/`text_done` → `agent.status(idle)` → `agent.finished`.
3. Sesekali kirim `quota.updated` (mis. setelah turn ke-2), lalu `daily_report` (`{date:"2026-08-13",applied:3,failed:0,quota_left:2,portals_ok:["jobstreet"],portals_expired:[],no_match:false}`) di akhir.
4. Mock `GET /api/v1/portal-status` (bila dipanggil): 2 portal, `jobstreet` connected & `linkedin` expired.
Tujuan: seluruh UI teruji tanpa backend.

## 9. Keamanan

- TIDAK ADA API key di frontend.
- Jangan log isi pesan (data pribadi) ke console.
- Render teks dari event sebagai teks biasa (bukan HTML).
- Jangan hardcode channel user lain — selalu `user:<user_id>` dari sesi auth sendiri.
- URL VNC hanya untuk user pemilik mesin: tampilkan hanya milik user sendiri, jangan di-log, jangan disimpan di localStorage/state global yang bisa dibaca user lain, dan jangan pernah dirender untuk user lain.

## 10. Kriteria penerimaan

1. `npm run dev` jalan tanpa error console.
2. Connect Centrifugo sukses (token valid), subscribe `user:<id>`.
3. Kirim pesan → 202; bubble assistant muncul streaming dari `agent.text_delta`; `agent.finished` mengakhiri turn.
4. Activity feed live dari `agent.activity`.
5. Indikator status: `working` (input nonaktif) → `waiting_user` → `idle` mengikuti event `agent.status`.
6. Event `agent.ask_user` → kartu dengan tombol; klik → `POST /chat {"message":"Jawaban user: ..."}`; kartu terjawab.
7. **Tab ditutup saat agent `working` → buka lagi → jawaban agent yang terlewat tampil dari history replay (tidak dobel).**
8. Badge kuota ter-update real-time dari `quota.updated` (tanpa reload).
9. Upload CV & foto sukses; tipe salah ditolak client; pesan otomatis tersusun.
10. Kirim saat `working` → tombol nonaktif (dan jika tetap terjadi via API → 429 ditangani ramah).
11. Tombol "Sesi Baru" → reset + UI bersih.
12. Mock mode lolos poin 3–8 tanpa backend.
13. Viewer VNC: URL didapat dari `GET /api/v1/realtime/vnc`; viewer connect & menampilkan desktop; auto-show saat `waiting_user`; tombol buka/tutup & reconnect jalan; user bisa mengetik di viewer; koneksi ditolak → ambil URL baru & reconnect.
14. Portal status tampil (`Terhubung ✓` / `Perlu login ulang`); klik portal yang expired → VncViewer terbuka.
15. Kartu daily report muncul dari event `daily_report`; histori dari `GET /api/v1/daily-reports` tampil benar.

## 11. Batasan

- JANGAN buat server route Nuxt untuk proxy apa pun (FE murni client; semua lewat BE).
- JANGAN parse SSE / regex marker — semua event sudah typed dari Centrifugo.
- JANGAN hardcode URL/secret; semua lewat runtimeConfig public.
- Dependency baru yang diizinkan HANYA: `centrifuge` dan `@novnc/novnc`. Jangan tambah yang lain tanpa alasan kuat.
- JANGAN buat sistem login baru — pakai auth FE→BE yang ada.
