codeGuard / app.js
aIkal1n's picture
initial interface
32ff0e3
Raw
History Blame Contribute Delete
29.7 kB
/* ════════════════════════════════════════
AI ORCHESTRATION PLATFORM β€” APP LOGIC
════════════════════════════════════════ */
const { createApp, ref, reactive, computed, nextTick, onMounted } = Vue;
/* ════════════════════════════════════════
SEED / STATIC DATA
════════════════════════════════════════ */
const SEED_AGENTS = [
{ agent_id: 'agt-001', agent_name: 'Planner', agent_role: 'Task decomposition', status: 'idle', color: '#6ee7b7' },
{ agent_id: 'agt-002', agent_name: 'Researcher', agent_role: 'Web & data retrieval', status: 'busy', color: '#818cf8' },
{ agent_id: 'agt-003', agent_name: 'Coder', agent_role: 'Code generation', status: 'idle', color: '#f472b6' },
{ agent_id: 'agt-004', agent_name: 'Reviewer', agent_role: 'QA & validation', status: 'idle', color: '#fbbf24' },
];
const SEED_PIPELINES = [
{ pipeline_id: 'pip-001', pipeline_name: 'Auto Orchestrate', pipeline_description: 'Pilih agen secara otomatis' },
{ pipeline_id: 'pip-002', pipeline_name: 'Research Mode', pipeline_description: 'Fokus riset & analisis' },
{ pipeline_id: 'pip-003', pipeline_name: 'Code Generator', pipeline_description: 'Fokus pembuatan kode' },
{ pipeline_id: 'pip-004', pipeline_name: 'Review Pipeline', pipeline_description: 'Review & QA saja' },
];
const SEED_ACTIVE_TASKS = [
{ task_id: 'tsk-001', task_name: 'Market analysis', assigned_agent: 'Researcher', progress_percent: 65, task_color: '#818cf8' },
{ task_id: 'tsk-002', task_name: 'Code review', assigned_agent: 'Reviewer', progress_percent: 30, task_color: '#fbbf24' },
];
const SEED_TOKEN_USAGE = [
{ model_name: 'claude-3-opus', tokens_used: 48200, token_limit: 100000, bar_color: '#6ee7b7' },
{ model_name: 'claude-3-haiku', tokens_used: 12500, token_limit: 100000, bar_color: '#818cf8' },
];
const SEED_SYSTEM_LOGS = [
{ log_id: 'log-001', log_time: '14:02', log_level: 'success', log_message: 'Orchestrator ready' },
{ log_id: 'log-002', log_time: '14:03', log_level: '', log_message: 'Researcher agent connected' },
{ log_id: 'log-003', log_time: '14:05', log_level: 'warn', log_message: 'High latency detected (>500ms)' },
];
const SUGGESTIONS = [
{ title: 'πŸ” Research Task', desc: 'Riset topik dan buat ringkasan', prompt: 'Riset perkembangan terbaru machine learning di 2024 dan buat ringkasan eksekutif' },
{ title: 'πŸ’» Generate Code', desc: 'Buat kode dari deskripsi', prompt: 'Buat REST API sederhana menggunakan Node.js + Express untuk manajemen todo list' },
{ title: 'πŸ“‹ Plan a Project', desc: 'Dekomposisi proyek jadi task', prompt: 'Buat rencana proyek untuk membangun platform e-commerce dari nol dalam 3 bulan' },
{ title: 'βœ… Review & QA', desc: 'Review output dari agen lain', prompt: 'Review dan evaluasi kualitas kode Python berikut untuk deteksi anomali data sensor' },
];
/**
* Mock SSE event sequences β€” simulasi stream dari AI backend.
*
* CATATAN INTEGRASI (SSE):
* Nanti ganti seluruh fungsi mockSSEStream() dengan:
*
* const es = new EventSource('/api/stream?task_id=...');
* es.addEventListener('message', (e) => pushSSEEntry('msg', e.data));
* es.addEventListener('tool', (e) => pushSSEEntry('tool', e.data));
* es.addEventListener('done', (e) => { pushSSEEntry('done', e.data); es.close(); });
* es.addEventListener('error', (e) => { pushSSEEntry('error', 'Stream error'); es.close(); });
*/
const SSE_MOCK_SEQUENCES = [
[
{ event_type: 'msg', data: 'Parsing task intent…', delay: 200 },
{ event_type: 'msg', data: 'Selecting optimal agent…', delay: 400 },
{ event_type: 'tool', data: 'repo_read: fetching README', delay: 600 },
{ event_type: 'tool', data: 'repo_read: scanning src/', delay: 500 },
{ event_type: 'msg', data: 'Analysing code structure…', delay: 700 },
{ event_type: 'stream', data: 'Generating response', delay: 300 },
{ event_type: 'done', data: 'Task completed (1.4s)', delay: 500 },
],
[
{ event_type: 'msg', data: 'Decomposing task into steps…', delay: 300 },
{ event_type: 'tool', data: 'web_search: query sent', delay: 500 },
{ event_type: 'tool', data: 'web_search: 12 results found', delay: 400 },
{ event_type: 'msg', data: 'Summarising findings…', delay: 800 },
{ event_type: 'stream', data: 'Streaming answer tokens', delay: 600 },
{ event_type: 'done', data: 'Task completed (2.1s)', delay: 400 },
],
[
{ event_type: 'msg', data: 'Loading repository context…', delay: 400 },
{ event_type: 'tool', data: 'git_diff: comparing changes', delay: 700 },
{ event_type: 'tool', data: 'code_lint: running checks', delay: 600 },
{ event_type: 'msg', data: 'Writing fix…', delay: 500 },
{ event_type: 'tool', data: 'git_commit: staging files', delay: 400 },
{ event_type: 'tool', data: 'git_push: pushing branch', delay: 500 },
{ event_type: 'done', data: 'Branch pushed (2.8s)', delay: 300 },
],
];
/* ════════════════════════════════════════
HELPERS
════════════════════════════════════════ */
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
const rand = (min, max) => Math.floor(Math.random() * (max - min) + min);
const pickRand = (arr) => arr[Math.floor(Math.random() * arr.length)];
const nowTime = () => new Date().toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' });
const nowTs = () => new Date().toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
const makeId = (p, n) => `${p}-${String(n).padStart(3, '0')}`;
/**
* Parse "https://github.com/owner/repo" β†’ { owner, repo_name }
* Returns null when invalid.
*/
function parseGithubUrl(url) {
try {
const u = new URL(url.trim());
if (u.hostname !== 'github.com') return null;
const parts = u.pathname.replace(/^\//, '').replace(/\/$/, '').split('/');
if (parts.length < 2) return null;
return { owner: parts[0], repo_name: parts[1] };
} catch {
return null;
}
}
/**
* Build GitHub API headers from PAT.
* CATATAN INTEGRASI: headers ini yang dikirim ke endpoint /api/github/*
* atau langsung ke api.github.com.
*/
function githubHeaders(pat) {
return {
Authorization: `Bearer ${pat}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
};
}
/* ════════════════════════════════════════
VUE APP
════════════════════════════════════════ */
createApp({
setup() {
/* ── GitHub Auth state ──
* Data contract: GitHubAuth
* {
* is_authenticated : boolean
* pat_input : string (cleared after connect)
* pat : string (stored in memory only, never localStorage)
* username : string
* avatar_url : string
* }
*/
const github_auth = reactive({
is_authenticated: false,
pat_input: '',
pat: '',
username: '',
avatar_url: '',
});
/* ── Repo state ──
* Data contract: RepoState
* {
* url_input : string
* is_analysing : boolean
* info : RepoInfo | null
* }
*
* RepoInfo:
* {
* full_name : string (e.g. "owner/repo")
* language : string
* stars_count : number
* open_issues_count : number
* default_branch : string
* is_contributor : boolean (true if authed user can push)
* }
*/
const repo = reactive({
url_input: '',
is_analysing: false,
info: null,
});
/* ── SSE Stream state ──
* CATATAN INTEGRASI:
* is_active β†’ true saat EventSource sedang terbuka
* entries β†’ array of { ts, event_type, data }
* event_type β†’ 'msg' | 'tool' | 'done' | 'error' | 'stream'
*/
const sse_stream = reactive({
is_active: false,
entries: [],
});
/* ── Commit result (last) ──
* Data contract: CommitResult
* {
* branch_name : string
* commit_sha : string
* commit_message : string
* commits_url : string (link ke tab commits branch)
* pr_url : string (link buat PR)
* }
*/
const last_commit_result = ref(null);
/* ── Core state ── */
const agents = ref(SEED_AGENTS.map((a) => ({ ...a })));
const pipelines = ref(SEED_PIPELINES);
const active_pipeline = ref(pipelines.value[0]);
const selected_agent_id = ref('agt-001');
const messages = ref([]);
const session_stats = reactive({ total_tasks: 0, success_rate: 98, avg_latency_ms: 312 });
const active_tasks = ref(SEED_ACTIVE_TASKS.map((t) => ({ ...t })));
const token_usage = ref(SEED_TOKEN_USAGE.map((u) => ({ ...u })));
const system_logs = ref(SEED_SYSTEM_LOGS.map((l) => ({ ...l })));
const suggestions = SUGGESTIONS;
const input_text = ref('');
const is_loading = ref(false);
const messageContainer = ref(null);
const sseLogArea = ref(null);
const textarea = ref(null);
const modal = reactive({
show: false,
title: '',
body: '',
confirm_label: '',
confirm_class: 'primary',
on_confirm: () => {},
});
let msg_counter = 0;
let log_counter = SEED_SYSTEM_LOGS.length;
/* ════════════════════════════════════════
GITHUB AUTH
════════════════════════════════════════ */
/**
* connectGithub β€” verifikasi PAT dan ambil profil user.
*
* CATATAN INTEGRASI:
* Ganti fetch di bawah dengan endpoint backend kamu, misalnya:
* POST /api/auth/github { pat } β†’ { username, avatar_url }
*
* Backend perlu forward ke: GET https://api.github.com/user
* dengan header Authorization: Bearer <pat>
* dan kembalikan { login, avatar_url } ke frontend.
*/
async function connectGithub() {
const pat = github_auth.pat_input.trim();
if (!pat) return;
addLog('', 'Verifying GitHub PAT…');
/* --- MOCK: simulasi verifikasi token --- */
await delay(800);
const is_valid = pat.startsWith('ghp_') || pat.length > 20;
if (!is_valid) {
addLog('error', 'GitHub PAT invalid');
showModal('Authentication Failed', '<p>PAT tidak valid. Pastikan token dimulai dengan <code>ghp_</code> dan memiliki scope <code>repo</code>.</p>');
return;
}
github_auth.pat = pat;
github_auth.pat_input = '';
github_auth.is_authenticated = true;
github_auth.username = 'dev_user'; // ← dari GET /user response: data.login
github_auth.avatar_url = ''; // ← data.avatar_url
addLog('success', `GitHub connected as ${github_auth.username}`);
}
function revokeGithub() {
github_auth.is_authenticated = false;
github_auth.pat = '';
github_auth.username = '';
repo.info = null;
addLog('warn', 'GitHub session revoked');
}
/* ════════════════════════════════════════
REPO ANALYSE
════════════════════════════════════════ */
/**
* analyseRepo β€” fetch metadata repo dan cek contributor status.
*
* CATATAN INTEGRASI (dua endpoint GitHub API):
*
* 1. GET https://api.github.com/repos/{owner}/{repo}
* β†’ ambil full_name, language, stargazers_count, open_issues_count, default_branch
*
* 2. GET https://api.github.com/repos/{owner}/{repo}/collaborators/{username}
* β†’ 204 = contributor, 404 = bukan contributor
* (endpoint ini butuh PAT dengan scope repo)
*
* Kalau user belum auth, is_contributor selalu false.
*/
async function analyseRepo() {
const url_str = repo.url_input.trim();
const parsed = parseGithubUrl(url_str);
if (!parsed) {
showModal('URL Tidak Valid', '<p>Format URL harus: <code>https://github.com/owner/repo</code></p>');
return;
}
repo.is_analysing = true;
addLog('', `Analysing ${parsed.owner}/${parsed.repo_name}…`);
/* --- MOCK: simulasi API response --- */
await delay(1200);
const is_authed = github_auth.is_authenticated;
const is_contributor = is_authed && Math.random() > 0.4; // simulasi: 60% kemungkinan contributor
repo.info = {
full_name: `${parsed.owner}/${parsed.repo_name}`,
language: pickRand(['TypeScript', 'Python', 'Go', 'Rust', 'JavaScript']),
stars_count: rand(10, 4800),
open_issues_count: rand(0, 42),
default_branch: 'main',
is_contributor,
};
repo.is_analysing = false;
addLog('success', `Repo loaded: ${repo.info.full_name} (contributor: ${is_contributor})`);
/* Auto-trigger agent pipeline */
input_text.value = `Analisis repository ${repo.info.full_name} secara menyeluruh β€” ${repo.info.language}, ${repo.info.stars_count} stars, ${repo.info.open_issues_count} issues`;
await sendMessage();
}
/* ════════════════════════════════════════
SSE STREAM (MOCK)
════════════════════════════════════════ */
function pushSSEEntry(event_type, data) {
sse_stream.entries.push({ ts: nowTs(), event_type, data });
nextTick(() => {
if (sseLogArea.value) sseLogArea.value.scrollTop = sseLogArea.value.scrollHeight;
});
}
/**
* mockSSEStream β€” simulasi event yang diterima dari backend via SSE.
*
* CATATAN INTEGRASI:
* Hapus fungsi ini dan ganti `await mockSSEStream()` dengan:
*
* const es = new EventSource(`/api/tasks/${task_id}/stream`);
* await new Promise((resolve, reject) => {
* es.addEventListener('message', (e) => pushSSEEntry('msg', e.data));
* es.addEventListener('tool', (e) => pushSSEEntry('tool', e.data));
* es.addEventListener('stream', (e) => pushSSEEntry('stream', e.data));
* es.addEventListener('done', (e) => { pushSSEEntry('done', e.data); es.close(); resolve(); });
* es.addEventListener('error', (e) => { pushSSEEntry('error', 'Connection error'); es.close(); reject(); });
* });
*/
async function mockSSEStream() {
sse_stream.is_active = true;
const seq = pickRand(SSE_MOCK_SEQUENCES);
for (const event of seq) {
await delay(event.delay);
pushSSEEntry(event.event_type, event.data);
}
sse_stream.is_active = false;
}
/* ════════════════════════════════════════
DOWNLOAD REPORT
════════════════════════════════════════ */
/**
* downloadReport β€” export session ke file .txt / .md.
*
* CATATAN INTEGRASI:
* Kalau backend sudah punya endpoint laporan, ganti dengan:
* window.open(`/api/sessions/${session_id}/report?format=pdf`, '_blank');
*
* Untuk sekarang, kita generate langsung di browser dari state.
*/
function downloadReport() {
const lines = [
'# AI Orchestration β€” Session Report',
`Generated : ${new Date().toLocaleString('id-ID')}`,
`Pipeline : ${active_pipeline.value.pipeline_name}`,
`Repo : ${repo.info ? repo.info.full_name : '(none)'}`,
`Total tasks: ${session_stats.total_tasks}`,
`Success : ${session_stats.success_rate}%`,
'',
'---',
'',
'## Conversation Log',
'',
];
messages.value.forEach((msg) => {
if (msg.is_typing) return;
lines.push(`### [${msg.created_at}] ${msg.sender_name} (${msg.sender_type})`);
lines.push(msg.message_content || '');
if (msg.pipeline_trace && msg.pipeline_trace.length) {
lines.push('');
lines.push('**Pipeline Trace:**');
msg.pipeline_trace.forEach((s) => lines.push(` β€’ ${s.step_agent} β†’ ${s.step_output} (${s.duration_ms}ms)`));
}
if (msg.commit_result) {
lines.push('');
lines.push(`**Branch Created:** ${msg.commit_result.branch_name}`);
lines.push(`**Commit:** ${msg.commit_result.commit_message}`);
lines.push(`**Commits URL:** ${msg.commit_result.commits_url}`);
}
lines.push('');
});
if (sse_stream.entries.length) {
lines.push('---', '', '## SSE Stream Log', '');
sse_stream.entries.forEach((e) => lines.push(`[${e.ts}] [${e.event_type}] ${e.data}`));
}
const blob = new Blob([lines.join('\n')], { type: 'text/markdown;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `orch-report-${Date.now()}.md`;
a.click();
URL.revokeObjectURL(url);
addLog('success', 'Report downloaded');
}
/* ════════════════════════════════════════
MODAL HELPER
════════════════════════════════════════ */
function showModal(title, body, opts = {}) {
modal.title = title;
modal.body = body;
modal.confirm_label = opts.confirm_label || '';
modal.confirm_class = opts.confirm_class || 'primary';
modal.on_confirm = opts.on_confirm || (() => { modal.show = false; });
modal.show = true;
}
/* ════════════════════════════════════════
CORE UTILITIES
════════════════════════════════════════ */
function getAgentColor(agent_id) {
const a = agents.value.find((a) => a.agent_id === agent_id);
return a ? a.color : '#888';
}
function setAgentStatus(agent_id, status) {
const a = agents.value.find((a) => a.agent_id === agent_id);
if (a) a.status = status;
}
function addLog(level, message) {
system_logs.value.unshift({
log_id: makeId('log', ++log_counter),
log_time: nowTime(),
log_level: level,
log_message: message,
});
if (system_logs.value.length > 30) system_logs.value.pop();
}
async function scrollToBottom() {
await nextTick();
if (messageContainer.value) messageContainer.value.scrollTop = messageContainer.value.scrollHeight;
}
function autoResize(e) {
const el = e.target;
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 160) + 'px';
}
async function useSuggestion(prompt) {
input_text.value = prompt;
await sendMessage();
}
function clearChat() {
messages.value = [];
last_commit_result.value = null;
session_stats.total_tasks = 0;
}
/* ── Template class/style helpers ── */
function avatarClass(msg) {
if (msg.sender_type === 'user') return 'avatar-user';
if (msg.sender_type === 'orchestrator') return 'avatar-orchestrator';
return 'avatar-agent';
}
function bubbleClass(msg) {
return {
'bubble-user': msg.sender_type === 'user',
'bubble-system': msg.sender_type === 'orchestrator',
'bubble-agent': msg.sender_type === 'agent',
};
}
function agentBubbleStyle(agent_id) {
const color = getAgentColor(agent_id);
return { borderColor: color + '55', background: color + '0d' };
}
function tokenBarWidth(usage) {
return Math.min((usage.tokens_used / usage.token_limit) * 100, 100);
}
/* ── Message builders ── */
function buildUserMsg(text) {
return {
message_id: makeId('msg', ++msg_counter), session_id: 'ses-001',
sender_type: 'user', sender_name: 'You', sender_initials: 'ME',
agent_id: null, message_content: text,
pipeline_trace: [], commit_result: null,
is_typing: false, created_at: nowTime(),
};
}
function buildTypingMsg(sender_type, sender_name, sender_initials, agent_id = null) {
return {
message_id: makeId('msg', ++msg_counter),
sender_type, sender_name, sender_initials, agent_id,
message_content: '', pipeline_trace: [], commit_result: null,
is_typing: true, created_at: nowTime(),
};
}
/* ════════════════════════════════════════
SEND MESSAGE β€” MAIN FLOW
════════════════════════════════════════ */
/**
* sendMessage β€” mengirim pesan ke orchestrator.
*
* CATATAN INTEGRASI:
* Ganti blok "simulate agent work" dengan:
* const task = await fetch('/api/tasks', {
* method: 'POST',
* headers: { 'Content-Type': 'application/json' },
* body: JSON.stringify({
* session_id : 'ses-001',
* pipeline_id : active_pipeline.value.pipeline_id,
* message_content: text,
* repo_url : repo.url_input,
* github_pat : github_auth.pat, // jangan log/tampilkan ini
* })
* }).then(r => r.json());
*
* Kemudian buka SSE:
* const es = new EventSource(`/api/tasks/${task.task_id}/stream`);
* …(lihat mockSSEStream untuk event handling)
*
* Respons akhir agent dikirim via event 'done' atau endpoint terpisah:
* GET /api/tasks/{task_id}/result
*/
async function sendMessage() {
const text = input_text.value.trim();
if (!text || is_loading.value) return;
/* 1. User message */
messages.value.push(buildUserMsg(text));
input_text.value = '';
if (textarea.value) textarea.value.style.height = 'auto';
session_stats.total_tasks++;
await scrollToBottom();
/* 2. Orchestrator typing */
is_loading.value = true;
const orch_typing = buildTypingMsg('orchestrator', 'Orchestrator', '⬑');
messages.value.push(orch_typing);
addLog('', `Task dispatched β†’ ${active_pipeline.value.pipeline_name}`);
await scrollToBottom();
/* 3. SSE stream (mock) β€” jalan paralel */
const stream_promise = mockSSEStream();
await delay(rand(900, 1500));
/* 4. Pick agent */
const picked_agent = pickRand(agents.value);
setAgentStatus(picked_agent.agent_id, 'busy');
/* 5. Replace orchestrator typing β†’ response */
const orch_idx = messages.value.indexOf(orch_typing);
messages.value[orch_idx] = {
...orch_typing,
is_typing: false,
message_content: `Menerima tugas dari pipeline "${active_pipeline.value.pipeline_name}". Mendistribusikan ke ${picked_agent.agent_name}…`,
pipeline_trace: [
{ step_agent: 'Orchestrator', step_output: 'Parsed intent', step_color: '#6ee7b7', duration_ms: rand(40, 140) },
{ step_agent: picked_agent.agent_name, step_output: 'Assigned', step_color: picked_agent.color, duration_ms: rand(10, 60) },
],
created_at: nowTime(),
};
await scrollToBottom();
await delay(rand(700, 1500));
/* 6. Agent typing */
const agent_typing = buildTypingMsg(
'agent',
picked_agent.agent_name,
picked_agent.agent_name.slice(0, 2).toUpperCase(),
picked_agent.agent_id,
);
messages.value.push(agent_typing);
addLog('', `${picked_agent.agent_name} processing…`);
await scrollToBottom();
await delay(rand(1200, 2200));
/* 7. Determine if this is a code task β†’ generate commit result */
const is_code_task = active_pipeline.value.pipeline_id === 'pip-003'
|| text.toLowerCase().includes('fix') || text.toLowerCase().includes('refactor')
|| text.toLowerCase().includes('code') || text.toLowerCase().includes('bug');
const is_contributor = repo.info && repo.info.is_contributor;
let commit_result = null;
if (is_code_task && github_auth.is_authenticated) {
if (is_contributor) {
/* Contributor: bisa push branch baru */
const branch_name = `ai-fix/${Date.now()}`;
const parsed = repo.info ? parseGithubUrl(repo.url_input) : null;
const repo_base = parsed ? `https://github.com/${parsed.owner}/${parsed.repo_name}` : 'https://github.com/owner/repo';
commit_result = {
branch_name,
commit_sha: rand(100000, 999999).toString(16),
commit_message: `fix: AI-generated improvements for task "${text.slice(0, 50)}"`,
commits_url: `${repo_base}/commits/${branch_name}`,
pr_url: `${repo_base}/compare/${branch_name}?expand=1`,
};
last_commit_result.value = commit_result;
addLog('success', `Branch pushed: ${branch_name}`);
} else {
/* Bukan contributor: tidak bisa push */
addLog('warn', 'Cannot push: user is not a contributor');
}
}
/* 8. Agent response texts */
const AGENT_RESPONSES = [
() => `Saya telah menganalisis permintaan "${text.slice(0, 60)}…"\n\nHasil menunjukkan 3 sub-task utama. Rencana eksekusi sudah disiapkan dengan estimasi waktu ~4 menit.`,
() => `Permintaan diterima. Ditemukan referensi dari 12 sumber terpercaya. Data menunjukkan tren positif 6 bulan terakhir dengan growth rate 23%.`,
() => is_contributor
? `Kode telah di-generate dan diperbaiki. Branch baru sudah di-push ke repository. Silahkan review pull request untuk merge ke main.`
: `Kode telah dianalisis dan perbaikan diidentifikasi. Karena kamu bukan contributor, patch tidak dapat di-push otomatis. Salin output di bawah untuk apply manual.`,
() => `Review selesai. Ditemukan 2 improvement: (1) optimasi query endpoint utama, (2) tambahkan error handling untuk edge case. Tidak ada critical bug. βœ“`,
];
const resp_fn = pickRand(AGENT_RESPONSES);
/* 9. Replace agent typing β†’ final response */
const agent_idx = messages.value.indexOf(agent_typing);
messages.value[agent_idx] = {
...agent_typing,
is_typing: false,
message_content: resp_fn(),
commit_result,
created_at: nowTime(),
};
setAgentStatus(picked_agent.agent_id, 'idle');
token_usage.value[0].tokens_used += rand(200, 1000);
addLog('success', `${picked_agent.agent_name} completed task`);
is_loading.value = false;
await Promise.all([scrollToBottom(), stream_promise]);
}
/* ════════════════════════════════════════
BACKGROUND SIMULATION
════════════════════════════════════════ */
onMounted(() => {
setInterval(() => {
active_tasks.value.forEach((t) => {
t.progress_percent = Math.min(100, t.progress_percent + rand(0, 4));
if (t.progress_percent >= 100) t.progress_percent = 0;
});
}, 2000);
});
/* ── Expose to template ── */
return {
/* state */
github_auth, repo, sse_stream, last_commit_result,
agents, pipelines, active_pipeline, selected_agent_id,
messages, session_stats, active_tasks, token_usage, system_logs,
suggestions, input_text, is_loading, modal,
/* refs */
messageContainer, sseLogArea, textarea,
/* methods */
connectGithub, revokeGithub, analyseRepo,
sendMessage, clearChat, useSuggestion, autoResize, downloadReport,
getAgentColor, avatarClass, bubbleClass, agentBubbleStyle, tokenBarWidth,
};
},
}).mount('#app');