aIkal1n commited on
Commit
32ff0e3
Β·
1 Parent(s): 3bf56aa

initial interface

Browse files
Files changed (3) hide show
  1. app.js +695 -0
  2. style/style.css +557 -0
  3. view/index.html +467 -0
app.js ADDED
@@ -0,0 +1,695 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ════════════════════════════════════════
2
+ AI ORCHESTRATION PLATFORM β€” APP LOGIC
3
+ ════════════════════════════════════════ */
4
+
5
+ const { createApp, ref, reactive, computed, nextTick, onMounted } = Vue;
6
+
7
+ /* ════════════════════════════════════════
8
+ SEED / STATIC DATA
9
+ ════════════════════════════════════════ */
10
+
11
+ const SEED_AGENTS = [
12
+ { agent_id: 'agt-001', agent_name: 'Planner', agent_role: 'Task decomposition', status: 'idle', color: '#6ee7b7' },
13
+ { agent_id: 'agt-002', agent_name: 'Researcher', agent_role: 'Web & data retrieval', status: 'busy', color: '#818cf8' },
14
+ { agent_id: 'agt-003', agent_name: 'Coder', agent_role: 'Code generation', status: 'idle', color: '#f472b6' },
15
+ { agent_id: 'agt-004', agent_name: 'Reviewer', agent_role: 'QA & validation', status: 'idle', color: '#fbbf24' },
16
+ ];
17
+
18
+ const SEED_PIPELINES = [
19
+ { pipeline_id: 'pip-001', pipeline_name: 'Auto Orchestrate', pipeline_description: 'Pilih agen secara otomatis' },
20
+ { pipeline_id: 'pip-002', pipeline_name: 'Research Mode', pipeline_description: 'Fokus riset & analisis' },
21
+ { pipeline_id: 'pip-003', pipeline_name: 'Code Generator', pipeline_description: 'Fokus pembuatan kode' },
22
+ { pipeline_id: 'pip-004', pipeline_name: 'Review Pipeline', pipeline_description: 'Review & QA saja' },
23
+ ];
24
+
25
+ const SEED_ACTIVE_TASKS = [
26
+ { task_id: 'tsk-001', task_name: 'Market analysis', assigned_agent: 'Researcher', progress_percent: 65, task_color: '#818cf8' },
27
+ { task_id: 'tsk-002', task_name: 'Code review', assigned_agent: 'Reviewer', progress_percent: 30, task_color: '#fbbf24' },
28
+ ];
29
+
30
+ const SEED_TOKEN_USAGE = [
31
+ { model_name: 'claude-3-opus', tokens_used: 48200, token_limit: 100000, bar_color: '#6ee7b7' },
32
+ { model_name: 'claude-3-haiku', tokens_used: 12500, token_limit: 100000, bar_color: '#818cf8' },
33
+ ];
34
+
35
+ const SEED_SYSTEM_LOGS = [
36
+ { log_id: 'log-001', log_time: '14:02', log_level: 'success', log_message: 'Orchestrator ready' },
37
+ { log_id: 'log-002', log_time: '14:03', log_level: '', log_message: 'Researcher agent connected' },
38
+ { log_id: 'log-003', log_time: '14:05', log_level: 'warn', log_message: 'High latency detected (>500ms)' },
39
+ ];
40
+
41
+ const SUGGESTIONS = [
42
+ { title: 'πŸ” Research Task', desc: 'Riset topik dan buat ringkasan', prompt: 'Riset perkembangan terbaru machine learning di 2024 dan buat ringkasan eksekutif' },
43
+ { title: 'πŸ’» Generate Code', desc: 'Buat kode dari deskripsi', prompt: 'Buat REST API sederhana menggunakan Node.js + Express untuk manajemen todo list' },
44
+ { title: 'πŸ“‹ Plan a Project', desc: 'Dekomposisi proyek jadi task', prompt: 'Buat rencana proyek untuk membangun platform e-commerce dari nol dalam 3 bulan' },
45
+ { title: 'βœ… Review & QA', desc: 'Review output dari agen lain', prompt: 'Review dan evaluasi kualitas kode Python berikut untuk deteksi anomali data sensor' },
46
+ ];
47
+
48
+ /**
49
+ * Mock SSE event sequences β€” simulasi stream dari AI backend.
50
+ *
51
+ * CATATAN INTEGRASI (SSE):
52
+ * Nanti ganti seluruh fungsi mockSSEStream() dengan:
53
+ *
54
+ * const es = new EventSource('/api/stream?task_id=...');
55
+ * es.addEventListener('message', (e) => pushSSEEntry('msg', e.data));
56
+ * es.addEventListener('tool', (e) => pushSSEEntry('tool', e.data));
57
+ * es.addEventListener('done', (e) => { pushSSEEntry('done', e.data); es.close(); });
58
+ * es.addEventListener('error', (e) => { pushSSEEntry('error', 'Stream error'); es.close(); });
59
+ */
60
+ const SSE_MOCK_SEQUENCES = [
61
+ [
62
+ { event_type: 'msg', data: 'Parsing task intent…', delay: 200 },
63
+ { event_type: 'msg', data: 'Selecting optimal agent…', delay: 400 },
64
+ { event_type: 'tool', data: 'repo_read: fetching README', delay: 600 },
65
+ { event_type: 'tool', data: 'repo_read: scanning src/', delay: 500 },
66
+ { event_type: 'msg', data: 'Analysing code structure…', delay: 700 },
67
+ { event_type: 'stream', data: 'Generating response', delay: 300 },
68
+ { event_type: 'done', data: 'Task completed (1.4s)', delay: 500 },
69
+ ],
70
+ [
71
+ { event_type: 'msg', data: 'Decomposing task into steps…', delay: 300 },
72
+ { event_type: 'tool', data: 'web_search: query sent', delay: 500 },
73
+ { event_type: 'tool', data: 'web_search: 12 results found', delay: 400 },
74
+ { event_type: 'msg', data: 'Summarising findings…', delay: 800 },
75
+ { event_type: 'stream', data: 'Streaming answer tokens', delay: 600 },
76
+ { event_type: 'done', data: 'Task completed (2.1s)', delay: 400 },
77
+ ],
78
+ [
79
+ { event_type: 'msg', data: 'Loading repository context…', delay: 400 },
80
+ { event_type: 'tool', data: 'git_diff: comparing changes', delay: 700 },
81
+ { event_type: 'tool', data: 'code_lint: running checks', delay: 600 },
82
+ { event_type: 'msg', data: 'Writing fix…', delay: 500 },
83
+ { event_type: 'tool', data: 'git_commit: staging files', delay: 400 },
84
+ { event_type: 'tool', data: 'git_push: pushing branch', delay: 500 },
85
+ { event_type: 'done', data: 'Branch pushed (2.8s)', delay: 300 },
86
+ ],
87
+ ];
88
+
89
+ /* ════════════════════════════════════════
90
+ HELPERS
91
+ ════════════════════════════════════════ */
92
+
93
+ const delay = (ms) => new Promise((r) => setTimeout(r, ms));
94
+ const rand = (min, max) => Math.floor(Math.random() * (max - min) + min);
95
+ const pickRand = (arr) => arr[Math.floor(Math.random() * arr.length)];
96
+ const nowTime = () => new Date().toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' });
97
+ const nowTs = () => new Date().toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
98
+ const makeId = (p, n) => `${p}-${String(n).padStart(3, '0')}`;
99
+
100
+ /**
101
+ * Parse "https://github.com/owner/repo" β†’ { owner, repo_name }
102
+ * Returns null when invalid.
103
+ */
104
+ function parseGithubUrl(url) {
105
+ try {
106
+ const u = new URL(url.trim());
107
+ if (u.hostname !== 'github.com') return null;
108
+ const parts = u.pathname.replace(/^\//, '').replace(/\/$/, '').split('/');
109
+ if (parts.length < 2) return null;
110
+ return { owner: parts[0], repo_name: parts[1] };
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Build GitHub API headers from PAT.
118
+ * CATATAN INTEGRASI: headers ini yang dikirim ke endpoint /api/github/*
119
+ * atau langsung ke api.github.com.
120
+ */
121
+ function githubHeaders(pat) {
122
+ return {
123
+ Authorization: `Bearer ${pat}`,
124
+ Accept: 'application/vnd.github+json',
125
+ 'X-GitHub-Api-Version': '2022-11-28',
126
+ };
127
+ }
128
+
129
+ /* ════════════════════════════════════════
130
+ VUE APP
131
+ ════════════════════════════════════════ */
132
+
133
+ createApp({
134
+ setup() {
135
+
136
+ /* ── GitHub Auth state ──
137
+ * Data contract: GitHubAuth
138
+ * {
139
+ * is_authenticated : boolean
140
+ * pat_input : string (cleared after connect)
141
+ * pat : string (stored in memory only, never localStorage)
142
+ * username : string
143
+ * avatar_url : string
144
+ * }
145
+ */
146
+ const github_auth = reactive({
147
+ is_authenticated: false,
148
+ pat_input: '',
149
+ pat: '',
150
+ username: '',
151
+ avatar_url: '',
152
+ });
153
+
154
+ /* ── Repo state ──
155
+ * Data contract: RepoState
156
+ * {
157
+ * url_input : string
158
+ * is_analysing : boolean
159
+ * info : RepoInfo | null
160
+ * }
161
+ *
162
+ * RepoInfo:
163
+ * {
164
+ * full_name : string (e.g. "owner/repo")
165
+ * language : string
166
+ * stars_count : number
167
+ * open_issues_count : number
168
+ * default_branch : string
169
+ * is_contributor : boolean (true if authed user can push)
170
+ * }
171
+ */
172
+ const repo = reactive({
173
+ url_input: '',
174
+ is_analysing: false,
175
+ info: null,
176
+ });
177
+
178
+ /* ── SSE Stream state ──
179
+ * CATATAN INTEGRASI:
180
+ * is_active β†’ true saat EventSource sedang terbuka
181
+ * entries β†’ array of { ts, event_type, data }
182
+ * event_type β†’ 'msg' | 'tool' | 'done' | 'error' | 'stream'
183
+ */
184
+ const sse_stream = reactive({
185
+ is_active: false,
186
+ entries: [],
187
+ });
188
+
189
+ /* ── Commit result (last) ──
190
+ * Data contract: CommitResult
191
+ * {
192
+ * branch_name : string
193
+ * commit_sha : string
194
+ * commit_message : string
195
+ * commits_url : string (link ke tab commits branch)
196
+ * pr_url : string (link buat PR)
197
+ * }
198
+ */
199
+ const last_commit_result = ref(null);
200
+
201
+ /* ── Core state ── */
202
+ const agents = ref(SEED_AGENTS.map((a) => ({ ...a })));
203
+ const pipelines = ref(SEED_PIPELINES);
204
+ const active_pipeline = ref(pipelines.value[0]);
205
+ const selected_agent_id = ref('agt-001');
206
+ const messages = ref([]);
207
+ const session_stats = reactive({ total_tasks: 0, success_rate: 98, avg_latency_ms: 312 });
208
+ const active_tasks = ref(SEED_ACTIVE_TASKS.map((t) => ({ ...t })));
209
+ const token_usage = ref(SEED_TOKEN_USAGE.map((u) => ({ ...u })));
210
+ const system_logs = ref(SEED_SYSTEM_LOGS.map((l) => ({ ...l })));
211
+ const suggestions = SUGGESTIONS;
212
+
213
+ const input_text = ref('');
214
+ const is_loading = ref(false);
215
+ const messageContainer = ref(null);
216
+ const sseLogArea = ref(null);
217
+ const textarea = ref(null);
218
+
219
+ const modal = reactive({
220
+ show: false,
221
+ title: '',
222
+ body: '',
223
+ confirm_label: '',
224
+ confirm_class: 'primary',
225
+ on_confirm: () => {},
226
+ });
227
+
228
+ let msg_counter = 0;
229
+ let log_counter = SEED_SYSTEM_LOGS.length;
230
+
231
+ /* ════════════════════════════════════════
232
+ GITHUB AUTH
233
+ ════════════════════════════════════════ */
234
+
235
+ /**
236
+ * connectGithub β€” verifikasi PAT dan ambil profil user.
237
+ *
238
+ * CATATAN INTEGRASI:
239
+ * Ganti fetch di bawah dengan endpoint backend kamu, misalnya:
240
+ * POST /api/auth/github { pat } β†’ { username, avatar_url }
241
+ *
242
+ * Backend perlu forward ke: GET https://api.github.com/user
243
+ * dengan header Authorization: Bearer <pat>
244
+ * dan kembalikan { login, avatar_url } ke frontend.
245
+ */
246
+ async function connectGithub() {
247
+ const pat = github_auth.pat_input.trim();
248
+ if (!pat) return;
249
+
250
+ addLog('', 'Verifying GitHub PAT…');
251
+
252
+ /* --- MOCK: simulasi verifikasi token --- */
253
+ await delay(800);
254
+ const is_valid = pat.startsWith('ghp_') || pat.length > 20;
255
+
256
+ if (!is_valid) {
257
+ addLog('error', 'GitHub PAT invalid');
258
+ showModal('Authentication Failed', '<p>PAT tidak valid. Pastikan token dimulai dengan <code>ghp_</code> dan memiliki scope <code>repo</code>.</p>');
259
+ return;
260
+ }
261
+
262
+ github_auth.pat = pat;
263
+ github_auth.pat_input = '';
264
+ github_auth.is_authenticated = true;
265
+ github_auth.username = 'dev_user'; // ← dari GET /user response: data.login
266
+ github_auth.avatar_url = ''; // ← data.avatar_url
267
+ addLog('success', `GitHub connected as ${github_auth.username}`);
268
+ }
269
+
270
+ function revokeGithub() {
271
+ github_auth.is_authenticated = false;
272
+ github_auth.pat = '';
273
+ github_auth.username = '';
274
+ repo.info = null;
275
+ addLog('warn', 'GitHub session revoked');
276
+ }
277
+
278
+ /* ════════════════════════════════════════
279
+ REPO ANALYSE
280
+ ════════════════════════════════════════ */
281
+
282
+ /**
283
+ * analyseRepo β€” fetch metadata repo dan cek contributor status.
284
+ *
285
+ * CATATAN INTEGRASI (dua endpoint GitHub API):
286
+ *
287
+ * 1. GET https://api.github.com/repos/{owner}/{repo}
288
+ * β†’ ambil full_name, language, stargazers_count, open_issues_count, default_branch
289
+ *
290
+ * 2. GET https://api.github.com/repos/{owner}/{repo}/collaborators/{username}
291
+ * β†’ 204 = contributor, 404 = bukan contributor
292
+ * (endpoint ini butuh PAT dengan scope repo)
293
+ *
294
+ * Kalau user belum auth, is_contributor selalu false.
295
+ */
296
+ async function analyseRepo() {
297
+ const url_str = repo.url_input.trim();
298
+ const parsed = parseGithubUrl(url_str);
299
+
300
+ if (!parsed) {
301
+ showModal('URL Tidak Valid', '<p>Format URL harus: <code>https://github.com/owner/repo</code></p>');
302
+ return;
303
+ }
304
+
305
+ repo.is_analysing = true;
306
+ addLog('', `Analysing ${parsed.owner}/${parsed.repo_name}…`);
307
+
308
+ /* --- MOCK: simulasi API response --- */
309
+ await delay(1200);
310
+
311
+ const is_authed = github_auth.is_authenticated;
312
+ const is_contributor = is_authed && Math.random() > 0.4; // simulasi: 60% kemungkinan contributor
313
+
314
+ repo.info = {
315
+ full_name: `${parsed.owner}/${parsed.repo_name}`,
316
+ language: pickRand(['TypeScript', 'Python', 'Go', 'Rust', 'JavaScript']),
317
+ stars_count: rand(10, 4800),
318
+ open_issues_count: rand(0, 42),
319
+ default_branch: 'main',
320
+ is_contributor,
321
+ };
322
+
323
+ repo.is_analysing = false;
324
+ addLog('success', `Repo loaded: ${repo.info.full_name} (contributor: ${is_contributor})`);
325
+
326
+ /* Auto-trigger agent pipeline */
327
+ 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`;
328
+ await sendMessage();
329
+ }
330
+
331
+ /* ════════════════════════════════════════
332
+ SSE STREAM (MOCK)
333
+ ════════════════════════════════════════ */
334
+
335
+ function pushSSEEntry(event_type, data) {
336
+ sse_stream.entries.push({ ts: nowTs(), event_type, data });
337
+ nextTick(() => {
338
+ if (sseLogArea.value) sseLogArea.value.scrollTop = sseLogArea.value.scrollHeight;
339
+ });
340
+ }
341
+
342
+ /**
343
+ * mockSSEStream β€” simulasi event yang diterima dari backend via SSE.
344
+ *
345
+ * CATATAN INTEGRASI:
346
+ * Hapus fungsi ini dan ganti `await mockSSEStream()` dengan:
347
+ *
348
+ * const es = new EventSource(`/api/tasks/${task_id}/stream`);
349
+ * await new Promise((resolve, reject) => {
350
+ * es.addEventListener('message', (e) => pushSSEEntry('msg', e.data));
351
+ * es.addEventListener('tool', (e) => pushSSEEntry('tool', e.data));
352
+ * es.addEventListener('stream', (e) => pushSSEEntry('stream', e.data));
353
+ * es.addEventListener('done', (e) => { pushSSEEntry('done', e.data); es.close(); resolve(); });
354
+ * es.addEventListener('error', (e) => { pushSSEEntry('error', 'Connection error'); es.close(); reject(); });
355
+ * });
356
+ */
357
+ async function mockSSEStream() {
358
+ sse_stream.is_active = true;
359
+ const seq = pickRand(SSE_MOCK_SEQUENCES);
360
+ for (const event of seq) {
361
+ await delay(event.delay);
362
+ pushSSEEntry(event.event_type, event.data);
363
+ }
364
+ sse_stream.is_active = false;
365
+ }
366
+
367
+ /* ════════════════════════════════════════
368
+ DOWNLOAD REPORT
369
+ ════════════════════════════════════════ */
370
+
371
+ /**
372
+ * downloadReport β€” export session ke file .txt / .md.
373
+ *
374
+ * CATATAN INTEGRASI:
375
+ * Kalau backend sudah punya endpoint laporan, ganti dengan:
376
+ * window.open(`/api/sessions/${session_id}/report?format=pdf`, '_blank');
377
+ *
378
+ * Untuk sekarang, kita generate langsung di browser dari state.
379
+ */
380
+ function downloadReport() {
381
+ const lines = [
382
+ '# AI Orchestration β€” Session Report',
383
+ `Generated : ${new Date().toLocaleString('id-ID')}`,
384
+ `Pipeline : ${active_pipeline.value.pipeline_name}`,
385
+ `Repo : ${repo.info ? repo.info.full_name : '(none)'}`,
386
+ `Total tasks: ${session_stats.total_tasks}`,
387
+ `Success : ${session_stats.success_rate}%`,
388
+ '',
389
+ '---',
390
+ '',
391
+ '## Conversation Log',
392
+ '',
393
+ ];
394
+
395
+ messages.value.forEach((msg) => {
396
+ if (msg.is_typing) return;
397
+ lines.push(`### [${msg.created_at}] ${msg.sender_name} (${msg.sender_type})`);
398
+ lines.push(msg.message_content || '');
399
+ if (msg.pipeline_trace && msg.pipeline_trace.length) {
400
+ lines.push('');
401
+ lines.push('**Pipeline Trace:**');
402
+ msg.pipeline_trace.forEach((s) => lines.push(` β€’ ${s.step_agent} β†’ ${s.step_output} (${s.duration_ms}ms)`));
403
+ }
404
+ if (msg.commit_result) {
405
+ lines.push('');
406
+ lines.push(`**Branch Created:** ${msg.commit_result.branch_name}`);
407
+ lines.push(`**Commit:** ${msg.commit_result.commit_message}`);
408
+ lines.push(`**Commits URL:** ${msg.commit_result.commits_url}`);
409
+ }
410
+ lines.push('');
411
+ });
412
+
413
+ if (sse_stream.entries.length) {
414
+ lines.push('---', '', '## SSE Stream Log', '');
415
+ sse_stream.entries.forEach((e) => lines.push(`[${e.ts}] [${e.event_type}] ${e.data}`));
416
+ }
417
+
418
+ const blob = new Blob([lines.join('\n')], { type: 'text/markdown;charset=utf-8' });
419
+ const url = URL.createObjectURL(blob);
420
+ const a = document.createElement('a');
421
+ a.href = url;
422
+ a.download = `orch-report-${Date.now()}.md`;
423
+ a.click();
424
+ URL.revokeObjectURL(url);
425
+ addLog('success', 'Report downloaded');
426
+ }
427
+
428
+ /* ════════════════════════════════════════
429
+ MODAL HELPER
430
+ ════════════════════════════════════════ */
431
+
432
+ function showModal(title, body, opts = {}) {
433
+ modal.title = title;
434
+ modal.body = body;
435
+ modal.confirm_label = opts.confirm_label || '';
436
+ modal.confirm_class = opts.confirm_class || 'primary';
437
+ modal.on_confirm = opts.on_confirm || (() => { modal.show = false; });
438
+ modal.show = true;
439
+ }
440
+
441
+ /* ════════════════════════════════════════
442
+ CORE UTILITIES
443
+ ════════════════════════════════════════ */
444
+
445
+ function getAgentColor(agent_id) {
446
+ const a = agents.value.find((a) => a.agent_id === agent_id);
447
+ return a ? a.color : '#888';
448
+ }
449
+
450
+ function setAgentStatus(agent_id, status) {
451
+ const a = agents.value.find((a) => a.agent_id === agent_id);
452
+ if (a) a.status = status;
453
+ }
454
+
455
+ function addLog(level, message) {
456
+ system_logs.value.unshift({
457
+ log_id: makeId('log', ++log_counter),
458
+ log_time: nowTime(),
459
+ log_level: level,
460
+ log_message: message,
461
+ });
462
+ if (system_logs.value.length > 30) system_logs.value.pop();
463
+ }
464
+
465
+ async function scrollToBottom() {
466
+ await nextTick();
467
+ if (messageContainer.value) messageContainer.value.scrollTop = messageContainer.value.scrollHeight;
468
+ }
469
+
470
+ function autoResize(e) {
471
+ const el = e.target;
472
+ el.style.height = 'auto';
473
+ el.style.height = Math.min(el.scrollHeight, 160) + 'px';
474
+ }
475
+
476
+ async function useSuggestion(prompt) {
477
+ input_text.value = prompt;
478
+ await sendMessage();
479
+ }
480
+
481
+ function clearChat() {
482
+ messages.value = [];
483
+ last_commit_result.value = null;
484
+ session_stats.total_tasks = 0;
485
+ }
486
+
487
+ /* ── Template class/style helpers ── */
488
+ function avatarClass(msg) {
489
+ if (msg.sender_type === 'user') return 'avatar-user';
490
+ if (msg.sender_type === 'orchestrator') return 'avatar-orchestrator';
491
+ return 'avatar-agent';
492
+ }
493
+ function bubbleClass(msg) {
494
+ return {
495
+ 'bubble-user': msg.sender_type === 'user',
496
+ 'bubble-system': msg.sender_type === 'orchestrator',
497
+ 'bubble-agent': msg.sender_type === 'agent',
498
+ };
499
+ }
500
+ function agentBubbleStyle(agent_id) {
501
+ const color = getAgentColor(agent_id);
502
+ return { borderColor: color + '55', background: color + '0d' };
503
+ }
504
+ function tokenBarWidth(usage) {
505
+ return Math.min((usage.tokens_used / usage.token_limit) * 100, 100);
506
+ }
507
+
508
+ /* ── Message builders ── */
509
+ function buildUserMsg(text) {
510
+ return {
511
+ message_id: makeId('msg', ++msg_counter), session_id: 'ses-001',
512
+ sender_type: 'user', sender_name: 'You', sender_initials: 'ME',
513
+ agent_id: null, message_content: text,
514
+ pipeline_trace: [], commit_result: null,
515
+ is_typing: false, created_at: nowTime(),
516
+ };
517
+ }
518
+ function buildTypingMsg(sender_type, sender_name, sender_initials, agent_id = null) {
519
+ return {
520
+ message_id: makeId('msg', ++msg_counter),
521
+ sender_type, sender_name, sender_initials, agent_id,
522
+ message_content: '', pipeline_trace: [], commit_result: null,
523
+ is_typing: true, created_at: nowTime(),
524
+ };
525
+ }
526
+
527
+ /* ════════════════════════════════════════
528
+ SEND MESSAGE β€” MAIN FLOW
529
+ ════════════════════════════════════════ */
530
+
531
+ /**
532
+ * sendMessage β€” mengirim pesan ke orchestrator.
533
+ *
534
+ * CATATAN INTEGRASI:
535
+ * Ganti blok "simulate agent work" dengan:
536
+ * const task = await fetch('/api/tasks', {
537
+ * method: 'POST',
538
+ * headers: { 'Content-Type': 'application/json' },
539
+ * body: JSON.stringify({
540
+ * session_id : 'ses-001',
541
+ * pipeline_id : active_pipeline.value.pipeline_id,
542
+ * message_content: text,
543
+ * repo_url : repo.url_input,
544
+ * github_pat : github_auth.pat, // jangan log/tampilkan ini
545
+ * })
546
+ * }).then(r => r.json());
547
+ *
548
+ * Kemudian buka SSE:
549
+ * const es = new EventSource(`/api/tasks/${task.task_id}/stream`);
550
+ * …(lihat mockSSEStream untuk event handling)
551
+ *
552
+ * Respons akhir agent dikirim via event 'done' atau endpoint terpisah:
553
+ * GET /api/tasks/{task_id}/result
554
+ */
555
+ async function sendMessage() {
556
+ const text = input_text.value.trim();
557
+ if (!text || is_loading.value) return;
558
+
559
+ /* 1. User message */
560
+ messages.value.push(buildUserMsg(text));
561
+ input_text.value = '';
562
+ if (textarea.value) textarea.value.style.height = 'auto';
563
+ session_stats.total_tasks++;
564
+ await scrollToBottom();
565
+
566
+ /* 2. Orchestrator typing */
567
+ is_loading.value = true;
568
+ const orch_typing = buildTypingMsg('orchestrator', 'Orchestrator', '⬑');
569
+ messages.value.push(orch_typing);
570
+ addLog('', `Task dispatched β†’ ${active_pipeline.value.pipeline_name}`);
571
+ await scrollToBottom();
572
+
573
+ /* 3. SSE stream (mock) β€” jalan paralel */
574
+ const stream_promise = mockSSEStream();
575
+
576
+ await delay(rand(900, 1500));
577
+
578
+ /* 4. Pick agent */
579
+ const picked_agent = pickRand(agents.value);
580
+ setAgentStatus(picked_agent.agent_id, 'busy');
581
+
582
+ /* 5. Replace orchestrator typing β†’ response */
583
+ const orch_idx = messages.value.indexOf(orch_typing);
584
+ messages.value[orch_idx] = {
585
+ ...orch_typing,
586
+ is_typing: false,
587
+ message_content: `Menerima tugas dari pipeline "${active_pipeline.value.pipeline_name}". Mendistribusikan ke ${picked_agent.agent_name}…`,
588
+ pipeline_trace: [
589
+ { step_agent: 'Orchestrator', step_output: 'Parsed intent', step_color: '#6ee7b7', duration_ms: rand(40, 140) },
590
+ { step_agent: picked_agent.agent_name, step_output: 'Assigned', step_color: picked_agent.color, duration_ms: rand(10, 60) },
591
+ ],
592
+ created_at: nowTime(),
593
+ };
594
+ await scrollToBottom();
595
+ await delay(rand(700, 1500));
596
+
597
+ /* 6. Agent typing */
598
+ const agent_typing = buildTypingMsg(
599
+ 'agent',
600
+ picked_agent.agent_name,
601
+ picked_agent.agent_name.slice(0, 2).toUpperCase(),
602
+ picked_agent.agent_id,
603
+ );
604
+ messages.value.push(agent_typing);
605
+ addLog('', `${picked_agent.agent_name} processing…`);
606
+ await scrollToBottom();
607
+ await delay(rand(1200, 2200));
608
+
609
+ /* 7. Determine if this is a code task β†’ generate commit result */
610
+ const is_code_task = active_pipeline.value.pipeline_id === 'pip-003'
611
+ || text.toLowerCase().includes('fix') || text.toLowerCase().includes('refactor')
612
+ || text.toLowerCase().includes('code') || text.toLowerCase().includes('bug');
613
+
614
+ const is_contributor = repo.info && repo.info.is_contributor;
615
+
616
+ let commit_result = null;
617
+ if (is_code_task && github_auth.is_authenticated) {
618
+ if (is_contributor) {
619
+ /* Contributor: bisa push branch baru */
620
+ const branch_name = `ai-fix/${Date.now()}`;
621
+ const parsed = repo.info ? parseGithubUrl(repo.url_input) : null;
622
+ const repo_base = parsed ? `https://github.com/${parsed.owner}/${parsed.repo_name}` : 'https://github.com/owner/repo';
623
+
624
+ commit_result = {
625
+ branch_name,
626
+ commit_sha: rand(100000, 999999).toString(16),
627
+ commit_message: `fix: AI-generated improvements for task "${text.slice(0, 50)}"`,
628
+ commits_url: `${repo_base}/commits/${branch_name}`,
629
+ pr_url: `${repo_base}/compare/${branch_name}?expand=1`,
630
+ };
631
+ last_commit_result.value = commit_result;
632
+ addLog('success', `Branch pushed: ${branch_name}`);
633
+ } else {
634
+ /* Bukan contributor: tidak bisa push */
635
+ addLog('warn', 'Cannot push: user is not a contributor');
636
+ }
637
+ }
638
+
639
+ /* 8. Agent response texts */
640
+ const AGENT_RESPONSES = [
641
+ () => `Saya telah menganalisis permintaan "${text.slice(0, 60)}…"\n\nHasil menunjukkan 3 sub-task utama. Rencana eksekusi sudah disiapkan dengan estimasi waktu ~4 menit.`,
642
+ () => `Permintaan diterima. Ditemukan referensi dari 12 sumber terpercaya. Data menunjukkan tren positif 6 bulan terakhir dengan growth rate 23%.`,
643
+ () => is_contributor
644
+ ? `Kode telah di-generate dan diperbaiki. Branch baru sudah di-push ke repository. Silahkan review pull request untuk merge ke main.`
645
+ : `Kode telah dianalisis dan perbaikan diidentifikasi. Karena kamu bukan contributor, patch tidak dapat di-push otomatis. Salin output di bawah untuk apply manual.`,
646
+ () => `Review selesai. Ditemukan 2 improvement: (1) optimasi query endpoint utama, (2) tambahkan error handling untuk edge case. Tidak ada critical bug. βœ“`,
647
+ ];
648
+ const resp_fn = pickRand(AGENT_RESPONSES);
649
+
650
+ /* 9. Replace agent typing β†’ final response */
651
+ const agent_idx = messages.value.indexOf(agent_typing);
652
+ messages.value[agent_idx] = {
653
+ ...agent_typing,
654
+ is_typing: false,
655
+ message_content: resp_fn(),
656
+ commit_result,
657
+ created_at: nowTime(),
658
+ };
659
+
660
+ setAgentStatus(picked_agent.agent_id, 'idle');
661
+ token_usage.value[0].tokens_used += rand(200, 1000);
662
+ addLog('success', `${picked_agent.agent_name} completed task`);
663
+ is_loading.value = false;
664
+ await Promise.all([scrollToBottom(), stream_promise]);
665
+ }
666
+
667
+ /* ════════════════════════════════════════
668
+ BACKGROUND SIMULATION
669
+ ════════════════════════════════════════ */
670
+
671
+ onMounted(() => {
672
+ setInterval(() => {
673
+ active_tasks.value.forEach((t) => {
674
+ t.progress_percent = Math.min(100, t.progress_percent + rand(0, 4));
675
+ if (t.progress_percent >= 100) t.progress_percent = 0;
676
+ });
677
+ }, 2000);
678
+ });
679
+
680
+ /* ── Expose to template ── */
681
+ return {
682
+ /* state */
683
+ github_auth, repo, sse_stream, last_commit_result,
684
+ agents, pipelines, active_pipeline, selected_agent_id,
685
+ messages, session_stats, active_tasks, token_usage, system_logs,
686
+ suggestions, input_text, is_loading, modal,
687
+ /* refs */
688
+ messageContainer, sseLogArea, textarea,
689
+ /* methods */
690
+ connectGithub, revokeGithub, analyseRepo,
691
+ sendMessage, clearChat, useSuggestion, autoResize, downloadReport,
692
+ getAgentColor, avatarClass, bubbleClass, agentBubbleStyle, tokenBarWidth,
693
+ };
694
+ },
695
+ }).mount('#app');
style/style.css ADDED
@@ -0,0 +1,557 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ════════════════════════════════════════
2
+ AI ORCHESTRATION PLATFORM β€” STYLESHEET
3
+ ════════════════════════════════════════ */
4
+
5
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
6
+
7
+ :root {
8
+ --bg: #0c0c10;
9
+ --surface: #13131a;
10
+ --surface2: #1a1a24;
11
+ --surface3: #22222f;
12
+ --border: #2a2a3a;
13
+ --border2: #383850;
14
+ --accent: #6ee7b7;
15
+ --accent2: #818cf8;
16
+ --accent3: #f472b6;
17
+ --text: #e8e8f0;
18
+ --text2: #9090a8;
19
+ --text3: #5a5a72;
20
+ --danger: #f87171;
21
+ --warn: #fbbf24;
22
+ }
23
+
24
+ html, body {
25
+ height: 100%;
26
+ font-family: 'DM Sans', sans-serif;
27
+ background: var(--bg);
28
+ color: var(--text);
29
+ }
30
+
31
+ /* ════════ LAYOUT ════════ */
32
+ #app { display: flex; height: 100vh; overflow: hidden; }
33
+
34
+ /* ════════ SIDEBAR ════════ */
35
+ .sidebar {
36
+ width: 270px; flex-shrink: 0;
37
+ background: var(--surface); border-right: 1px solid var(--border);
38
+ display: flex; flex-direction: column; overflow-y: auto;
39
+ }
40
+ .sidebar::-webkit-scrollbar { width: 3px; }
41
+ .sidebar::-webkit-scrollbar-thumb { background: var(--border); }
42
+
43
+ .sidebar-header {
44
+ padding: 18px 16px 14px; display: flex; align-items: center; gap: 10px;
45
+ border-bottom: 1px solid var(--border); flex-shrink: 0;
46
+ }
47
+ .logo-mark {
48
+ width: 28px; height: 28px; flex-shrink: 0;
49
+ background: linear-gradient(135deg, var(--accent), var(--accent2));
50
+ border-radius: 6px; display: grid; place-items: center;
51
+ font-family: 'IBM Plex Mono', monospace; font-size: 11px; font-weight: 500; color: #0c0c10;
52
+ }
53
+ .logo-text {
54
+ font-family: 'Syne', sans-serif; font-size: 13px; font-weight: 700;
55
+ letter-spacing: 0.04em; white-space: nowrap; overflow: hidden;
56
+ }
57
+ .logo-text span { color: var(--accent); }
58
+
59
+ .sidebar-section { padding: 12px 8px; }
60
+ .sidebar-label {
61
+ font-size: 10px; font-weight: 500; letter-spacing: 0.1em;
62
+ color: var(--text3); text-transform: uppercase; padding: 0 8px 8px;
63
+ }
64
+
65
+ /* ── GitHub Auth ── */
66
+ .auth-card {
67
+ margin: 0 8px 0;
68
+ background: var(--surface2); border: 1px solid var(--border);
69
+ border-radius: 10px; padding: 12px; transition: border-color 0.15s;
70
+ }
71
+ .auth-card.authenticated { border-color: rgba(110,231,183,0.3); }
72
+ .auth-header { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
73
+ .auth-title { font-size: 12px; font-weight: 600; }
74
+ .auth-sub { font-size: 11px; color: var(--text3); }
75
+ .auth-input-row { display: flex; gap: 6px; }
76
+ .auth-input {
77
+ flex: 1; min-width: 0; background: var(--surface3); border: 1px solid var(--border);
78
+ border-radius: 6px; padding: 6px 8px; font-size: 11px;
79
+ font-family: 'IBM Plex Mono', monospace; color: var(--text); outline: none;
80
+ transition: border-color 0.15s;
81
+ }
82
+ .auth-input::placeholder { color: var(--text3); }
83
+ .auth-input:focus { border-color: var(--accent2); }
84
+ .auth-connect-btn {
85
+ flex-shrink: 0; padding: 6px 10px; border-radius: 6px; border: none;
86
+ background: var(--accent2); color: #fff; font-size: 11px; font-weight: 500;
87
+ cursor: pointer; font-family: 'DM Sans', sans-serif; transition: all 0.15s;
88
+ }
89
+ .auth-connect-btn:hover { background: #6b76e8; }
90
+ .auth-user-row { display: flex; align-items: center; gap: 8px; }
91
+ .auth-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
92
+ .auth-username { font-size: 12px; font-weight: 500; color: var(--accent); }
93
+ .auth-revoke {
94
+ margin-left: auto; font-size: 11px; color: var(--text3); cursor: pointer;
95
+ background: none; border: none; font-family: 'DM Sans', sans-serif; transition: color 0.15s;
96
+ }
97
+ .auth-revoke:hover { color: var(--danger); }
98
+
99
+ /* ── Repo Input ── */
100
+ .repo-section { padding: 8px 8px 4px; }
101
+ .repo-label { font-size: 10px; font-weight: 500; letter-spacing: 0.1em; color: var(--text3); text-transform: uppercase; padding: 0 2px 6px; display: block; }
102
+ .repo-row { display: flex; gap: 6px; }
103
+ .repo-url-input {
104
+ flex: 1; min-width: 0; background: var(--surface2); border: 1px solid var(--border);
105
+ border-radius: 8px; padding: 7px 10px; font-size: 12px;
106
+ font-family: 'IBM Plex Mono', monospace; color: var(--text); outline: none;
107
+ transition: border-color 0.15s;
108
+ }
109
+ .repo-url-input::placeholder { color: var(--text3); }
110
+ .repo-url-input:focus { border-color: var(--accent2); }
111
+ .repo-analyse-btn {
112
+ flex-shrink: 0; padding: 7px 11px; border-radius: 8px; border: none;
113
+ background: var(--accent); color: #0c0c10; font-size: 11px; font-weight: 600;
114
+ cursor: pointer; font-family: 'DM Sans', sans-serif; transition: all 0.15s;
115
+ display: flex; align-items: center; gap: 5px; white-space: nowrap;
116
+ }
117
+ .repo-analyse-btn:hover:not(:disabled) { background: #4dd4a0; }
118
+ .repo-analyse-btn:disabled { background: var(--surface3); color: var(--text3); cursor: not-allowed; }
119
+
120
+ .repo-info-box {
121
+ margin: 6px 8px 0;
122
+ background: rgba(110,231,183,0.05); border: 1px solid rgba(110,231,183,0.15);
123
+ border-radius: 8px; padding: 9px 11px;
124
+ }
125
+ .repo-info-name { font-family: 'IBM Plex Mono', monospace; font-size: 12px; font-weight: 500; color: var(--accent); }
126
+ .repo-info-meta { font-size: 11px; color: var(--text3); margin-top: 2px; }
127
+ .contributor-badge {
128
+ display: inline-block; margin-top: 5px; font-size: 10px;
129
+ padding: 2px 8px; border-radius: 20px; font-family: 'IBM Plex Mono', monospace;
130
+ }
131
+ .contributor-badge.yes { background: rgba(110,231,183,0.1); color: var(--accent); }
132
+ .contributor-badge.no { background: rgba(248,113,113,0.1); color: var(--danger); }
133
+
134
+ /* ── Agent Items ── */
135
+ .agent-item {
136
+ display: flex; align-items: center; gap: 10px;
137
+ padding: 8px 10px; border-radius: 8px; cursor: pointer;
138
+ transition: background 0.15s; margin-bottom: 2px;
139
+ }
140
+ .agent-item:hover { background: var(--surface3); }
141
+ .agent-item.active { background: var(--surface3); }
142
+ .agent-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
143
+ .agent-info { overflow: hidden; flex: 1; }
144
+ .agent-name { font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
145
+ .agent-role { font-size: 11px; color: var(--text3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
146
+ .agent-status {
147
+ font-size: 10px; padding: 2px 7px; border-radius: 20px;
148
+ font-family: 'IBM Plex Mono', monospace; font-weight: 500;
149
+ }
150
+ .status-idle { background: rgba(110,231,183,0.08); color: var(--accent); }
151
+ .status-busy { background: rgba(251,191,36,0.10); color: var(--warn); }
152
+ .status-error { background: rgba(248,113,113,0.10); color: var(--danger); }
153
+
154
+ .divider { height: 1px; background: var(--border); margin: 8px 0; }
155
+
156
+ /* ── Stats ── */
157
+ .stats-row { display: flex; gap: 8px; padding: 4px 8px; }
158
+ .stat-box {
159
+ flex: 1; background: var(--surface2); border: 1px solid var(--border);
160
+ border-radius: 8px; padding: 8px; text-align: center;
161
+ }
162
+ .stat-num { font-family: 'IBM Plex Mono', monospace; font-size: 18px; font-weight: 500; }
163
+ .stat-label { font-size: 10px; color: var(--text3); margin-top: 2px; }
164
+
165
+ /* ── Sidebar Bottom ── */
166
+ .sidebar-bottom { margin-top: auto; padding: 12px 8px; border-top: 1px solid var(--border); flex-shrink: 0; }
167
+ .setting-btn {
168
+ width: 100%; display: flex; align-items: center; gap: 10px;
169
+ padding: 8px 10px; border-radius: 8px; border: none;
170
+ background: transparent; color: var(--text2); cursor: pointer;
171
+ font-family: 'DM Sans', sans-serif; font-size: 13px;
172
+ transition: background 0.15s, color 0.15s;
173
+ }
174
+ .setting-btn:hover { background: var(--surface3); color: var(--text); }
175
+ .setting-btn svg { flex-shrink: 0; }
176
+
177
+ /* ════════ CHAT AREA ════════ */
178
+ .chat-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
179
+
180
+ .chat-topbar {
181
+ height: 52px; flex-shrink: 0;
182
+ background: var(--surface); border-bottom: 1px solid var(--border);
183
+ display: flex; align-items: center; justify-content: space-between;
184
+ padding: 0 20px; gap: 12px;
185
+ }
186
+ .topbar-left { display: flex; align-items: center; gap: 12px; }
187
+ .topbar-title { font-family: 'Syne', sans-serif; font-size: 14px; font-weight: 600; }
188
+ .pipeline-badge {
189
+ font-family: 'IBM Plex Mono', monospace; font-size: 11px;
190
+ padding: 3px 8px; border-radius: 20px;
191
+ background: rgba(110,231,183,0.1); color: var(--accent);
192
+ border: 1px solid rgba(110,231,183,0.2);
193
+ }
194
+ .topbar-right { display: flex; align-items: center; gap: 8px; }
195
+ .icon-btn {
196
+ width: 32px; height: 32px; border-radius: 8px; border: 1px solid var(--border);
197
+ background: transparent; color: var(--text2); cursor: pointer;
198
+ display: grid; place-items: center; transition: all 0.15s;
199
+ }
200
+ .icon-btn:hover { background: var(--surface3); color: var(--text); }
201
+
202
+ /* ── Action Bar (bawah topbar) ── */
203
+ .action-bar {
204
+ flex-shrink: 0; padding: 8px 20px;
205
+ background: var(--surface2); border-bottom: 1px solid var(--border);
206
+ display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
207
+ }
208
+ .action-sep { width: 1px; height: 20px; background: var(--border); margin: 0 2px; }
209
+
210
+ .action-btn {
211
+ display: inline-flex; align-items: center; gap: 6px;
212
+ padding: 5px 12px; border-radius: 7px; border: 1px solid var(--border);
213
+ background: transparent; color: var(--text2); cursor: pointer;
214
+ font-family: 'DM Sans', sans-serif; font-size: 12px; font-weight: 500;
215
+ transition: all 0.15s; white-space: nowrap; text-decoration: none;
216
+ }
217
+ .action-btn:hover:not(:disabled) { border-color: var(--border2); color: var(--text); background: var(--surface3); }
218
+ .action-btn:disabled { opacity: 0.35; cursor: not-allowed; }
219
+ .action-btn svg { flex-shrink: 0; }
220
+
221
+ .action-btn.green { background: rgba(110,231,183,0.07); border-color: rgba(110,231,248,0.2); color: var(--accent); }
222
+ .action-btn.green:hover:not(:disabled) { background: rgba(110,231,183,0.14); }
223
+ .action-btn.purple { background: rgba(129,140,248,0.07); border-color: rgba(129,140,248,0.2); color: var(--accent2); }
224
+ .action-btn.purple:hover:not(:disabled) { background: rgba(129,140,248,0.14); }
225
+ .action-btn.red { background: rgba(248,113,113,0.07); border-color: rgba(248,113,113,0.2); color: var(--danger); }
226
+ .action-btn.red:hover:not(:disabled) { background: rgba(248,113,113,0.14); }
227
+
228
+ .no-contrib-notice {
229
+ display: inline-flex; align-items: center; gap: 6px;
230
+ font-size: 11px; color: var(--danger); padding: 4px 10px;
231
+ background: rgba(248,113,113,0.07); border: 1px solid rgba(248,113,113,0.2);
232
+ border-radius: 7px;
233
+ }
234
+
235
+ /* ── Messages ── */
236
+ .chat-messages {
237
+ flex: 1; overflow-y: auto; padding: 24px 0; scroll-behavior: smooth;
238
+ }
239
+ .chat-messages::-webkit-scrollbar { width: 4px; }
240
+ .chat-messages::-webkit-scrollbar-track { background: transparent; }
241
+ .chat-messages::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
242
+
243
+ .msg-wrapper {
244
+ display: flex; gap: 12px; padding: 6px 24px;
245
+ animation: fadeUp 0.25s ease both;
246
+ }
247
+ @keyframes fadeUp {
248
+ from { opacity: 0; transform: translateY(8px); }
249
+ to { opacity: 1; transform: translateY(0); }
250
+ }
251
+ .msg-wrapper.user { flex-direction: row-reverse; }
252
+
253
+ .msg-avatar {
254
+ width: 30px; height: 30px; border-radius: 8px; flex-shrink: 0;
255
+ display: grid; place-items: center;
256
+ font-family: 'IBM Plex Mono', monospace; font-size: 10px; font-weight: 500;
257
+ }
258
+ .avatar-user { background: var(--accent2); color: #fff; }
259
+ .avatar-orchestrator { background: linear-gradient(135deg, var(--accent), var(--accent2)); color: #0c0c10; }
260
+ .avatar-agent { color: #0c0c10; }
261
+
262
+ .msg-body { flex: 1; max-width: 680px; }
263
+ .msg-wrapper.user .msg-body { align-items: flex-end; display: flex; flex-direction: column; }
264
+
265
+ .msg-meta {
266
+ display: flex; align-items: center; gap: 8px;
267
+ margin-bottom: 6px; font-size: 11px; color: var(--text3);
268
+ }
269
+ .msg-wrapper.user .msg-meta { flex-direction: row-reverse; }
270
+ .msg-sender { font-weight: 500; color: var(--text2); }
271
+ .msg-time { font-family: 'IBM Plex Mono', monospace; }
272
+
273
+ .msg-bubble {
274
+ padding: 12px 16px; border-radius: 12px;
275
+ font-size: 14px; line-height: 1.7; max-width: 100%;
276
+ white-space: pre-wrap;
277
+ }
278
+ .bubble-user { background: var(--accent2); color: #fff; border-bottom-right-radius: 4px; }
279
+ .bubble-system { background: var(--surface2); border: 1px solid var(--border); border-bottom-left-radius: 4px; }
280
+ .bubble-agent { border: 1px solid var(--border); border-bottom-left-radius: 4px; }
281
+
282
+ /* ── Pipeline Trace ── */
283
+ .pipeline-trace {
284
+ margin-top: 10px; padding: 10px 12px;
285
+ background: var(--surface); border-radius: 8px; border: 1px solid var(--border);
286
+ }
287
+ .trace-label {
288
+ font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
289
+ color: var(--text3); margin-bottom: 8px; font-family: 'IBM Plex Mono', monospace;
290
+ }
291
+ .trace-steps { display: flex; flex-direction: column; gap: 6px; }
292
+ .trace-step { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text2); }
293
+ .trace-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
294
+ .trace-arrow { color: var(--text3); font-size: 10px; }
295
+ .trace-step-name { font-family: 'IBM Plex Mono', monospace; }
296
+ .trace-step-time { margin-left: auto; color: var(--text3); font-family: 'IBM Plex Mono', monospace; font-size: 11px; }
297
+
298
+ /* ── Commit Action Card (inside bubble) ── */
299
+ .commit-card {
300
+ margin-top: 10px; padding: 10px 12px;
301
+ background: rgba(129,140,248,0.06); border: 1px solid rgba(129,140,248,0.2);
302
+ border-radius: 8px;
303
+ }
304
+ .commit-card-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
305
+ .commit-card-title { font-size: 12px; font-weight: 600; color: var(--accent2); }
306
+ .commit-branch-name {
307
+ font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: var(--text2);
308
+ background: var(--surface3); padding: 1px 7px; border-radius: 4px; display: inline-block;
309
+ margin-bottom: 6px;
310
+ }
311
+ .commit-desc { font-size: 12px; color: var(--text3); margin-bottom: 10px; line-height: 1.5; }
312
+ .commit-actions { display: flex; gap: 6px; flex-wrap: wrap; }
313
+ .commit-btn {
314
+ display: inline-flex; align-items: center; gap: 5px;
315
+ padding: 5px 11px; border-radius: 6px; border: none; cursor: pointer;
316
+ font-family: 'DM Sans', sans-serif; font-size: 12px; font-weight: 500;
317
+ text-decoration: none; transition: all 0.15s;
318
+ }
319
+ .commit-btn.view-commits {
320
+ background: rgba(129,140,248,0.12); color: var(--accent2);
321
+ border: 1px solid rgba(129,140,248,0.25);
322
+ }
323
+ .commit-btn.view-commits:hover { background: rgba(129,140,248,0.2); }
324
+ .commit-btn.view-pr {
325
+ background: rgba(110,231,183,0.1); color: var(--accent);
326
+ border: 1px solid rgba(110,231,183,0.2);
327
+ }
328
+ .commit-btn.view-pr:hover { background: rgba(110,231,183,0.18); }
329
+
330
+ /* ── Typing Indicator ── */
331
+ .typing-indicator {
332
+ display: flex; align-items: center; gap: 4px;
333
+ padding: 14px 16px; background: var(--surface2);
334
+ border: 1px solid var(--border); border-radius: 12px;
335
+ border-bottom-left-radius: 4px; width: fit-content;
336
+ }
337
+ .typing-dot {
338
+ width: 5px; height: 5px; background: var(--text3);
339
+ border-radius: 50%; animation: typingBounce 1.2s infinite;
340
+ }
341
+ .typing-dot:nth-child(2) { animation-delay: 0.2s; }
342
+ .typing-dot:nth-child(3) { animation-delay: 0.4s; }
343
+ @keyframes typingBounce {
344
+ 0%, 80%, 100% { transform: translateY(0); opacity: 0.4; }
345
+ 40% { transform: translateY(-5px); opacity: 1; }
346
+ }
347
+
348
+ /* ════════ CHAT INPUT ════════ */
349
+ .chat-input-area {
350
+ flex-shrink: 0; padding: 14px 24px 18px;
351
+ background: var(--bg); border-top: 1px solid var(--border);
352
+ }
353
+ .pipeline-selector {
354
+ display: flex; gap: 6px; margin-bottom: 10px;
355
+ overflow-x: auto; padding-bottom: 4px;
356
+ }
357
+ .pipeline-selector::-webkit-scrollbar { height: 2px; }
358
+ .pipeline-selector::-webkit-scrollbar-thumb { background: var(--border); }
359
+ .pipeline-chip {
360
+ flex-shrink: 0; padding: 4px 12px; border-radius: 20px;
361
+ font-size: 12px; cursor: pointer; border: 1px solid var(--border);
362
+ background: transparent; color: var(--text2); font-family: 'DM Sans', sans-serif;
363
+ transition: all 0.15s; white-space: nowrap;
364
+ }
365
+ .pipeline-chip:hover { border-color: var(--border2); color: var(--text); }
366
+ .pipeline-chip.active { background: rgba(110,231,183,0.1); border-color: rgba(110,231,183,0.3); color: var(--accent); }
367
+ .input-box {
368
+ display: flex; align-items: flex-end; gap: 10px;
369
+ background: var(--surface); border: 1px solid var(--border);
370
+ border-radius: 14px; padding: 10px 10px 10px 16px;
371
+ transition: border-color 0.15s;
372
+ }
373
+ .input-box:focus-within { border-color: var(--border2); }
374
+ textarea {
375
+ flex: 1; background: transparent; border: none; outline: none;
376
+ color: var(--text); font-family: 'DM Sans', sans-serif; font-size: 14px;
377
+ line-height: 1.6; resize: none; min-height: 22px; max-height: 160px;
378
+ }
379
+ textarea::placeholder { color: var(--text3); }
380
+ .send-btn {
381
+ width: 36px; height: 36px; border-radius: 10px; border: none;
382
+ background: var(--accent); color: #0c0c10; cursor: pointer;
383
+ display: grid; place-items: center; flex-shrink: 0;
384
+ transition: all 0.15s; align-self: flex-end;
385
+ }
386
+ .send-btn:hover:not(:disabled) { background: #4dd4a0; transform: scale(1.05); }
387
+ .send-btn:disabled { background: var(--surface3); color: var(--text3); cursor: not-allowed; }
388
+ .input-hint { font-size: 11px; color: var(--text3); margin-top: 8px; text-align: center; }
389
+ .input-hint kbd {
390
+ font-family: 'IBM Plex Mono', monospace; font-size: 10px;
391
+ background: var(--surface2); border: 1px solid var(--border);
392
+ border-radius: 4px; padding: 1px 5px;
393
+ }
394
+
395
+ /* ════════ RIGHT PANEL ════════ */
396
+ .right-panel {
397
+ width: 300px; flex-shrink: 0;
398
+ background: var(--surface); border-left: 1px solid var(--border);
399
+ display: flex; flex-direction: column; overflow-y: auto;
400
+ }
401
+ .right-panel::-webkit-scrollbar { width: 4px; }
402
+ .right-panel::-webkit-scrollbar-thumb { background: var(--border); }
403
+
404
+ .panel-section { padding: 14px 16px; border-bottom: 1px solid var(--border); }
405
+ .panel-title {
406
+ font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase;
407
+ color: var(--text3); margin-bottom: 10px; font-family: 'IBM Plex Mono', monospace;
408
+ display: flex; align-items: center; justify-content: space-between;
409
+ }
410
+ .panel-title-badge {
411
+ background: var(--surface3); border-radius: 10px;
412
+ padding: 1px 7px; font-size: 10px; color: var(--text3);
413
+ text-transform: none; letter-spacing: 0;
414
+ }
415
+
416
+ /* ── SSE Log ── */
417
+ .sse-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
418
+ .sse-status-dot {
419
+ width: 6px; height: 6px; border-radius: 50%; background: var(--text3);
420
+ flex-shrink: 0;
421
+ }
422
+ .sse-status-dot.active { background: var(--accent); animation: sseGlow 1.5s infinite; }
423
+ @keyframes sseGlow { 0%,100% { opacity:1; } 50% { opacity:0.3; } }
424
+ .sse-status-label { font-size: 11px; color: var(--text3); }
425
+ .sse-status-label.active { color: var(--accent); }
426
+ .sse-clear-btn {
427
+ margin-left: auto; font-size: 10px; color: var(--text3); cursor: pointer;
428
+ background: none; border: none; font-family: 'DM Sans', sans-serif; transition: color 0.15s;
429
+ }
430
+ .sse-clear-btn:hover { color: var(--text2); }
431
+
432
+ .sse-log-area {
433
+ background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
434
+ font-family: 'IBM Plex Mono', monospace; font-size: 11px; line-height: 1.7;
435
+ max-height: 200px; overflow-y: auto; padding: 8px 10px;
436
+ }
437
+ .sse-log-area::-webkit-scrollbar { width: 3px; }
438
+ .sse-log-area::-webkit-scrollbar-thumb { background: var(--border2); }
439
+ .sse-empty-msg { color: var(--text3); text-align: center; padding: 12px 0; font-size: 11px; }
440
+
441
+ .sse-entry { display: flex; gap: 6px; margin-bottom: 3px; align-items: baseline; }
442
+ .sse-ts { color: var(--text3); flex-shrink: 0; font-size: 10px; }
443
+ .sse-event-tag {
444
+ flex-shrink: 0; font-size: 9px; padding: 1px 5px; border-radius: 3px;
445
+ text-transform: uppercase; letter-spacing: 0.05em;
446
+ }
447
+ .sse-event-tag.msg { background: rgba(129,140,248,0.15); color: var(--accent2); }
448
+ .sse-event-tag.tool { background: rgba(251,191,36,0.12); color: var(--warn); }
449
+ .sse-event-tag.done { background: rgba(110,231,183,0.12); color: var(--accent); }
450
+ .sse-event-tag.error { background: rgba(248,113,113,0.12); color: var(--danger); }
451
+ .sse-event-tag.stream { background: rgba(110,231,183,0.08); color: var(--text3); }
452
+ .sse-data { color: var(--text2); word-break: break-all; flex: 1; }
453
+
454
+ /* ── Task Card ── */
455
+ .task-card {
456
+ background: var(--surface2); border: 1px solid var(--border);
457
+ border-radius: 10px; padding: 11px; margin-bottom: 7px;
458
+ }
459
+ .task-name { font-size: 13px; font-weight: 500; margin-bottom: 3px; }
460
+ .task-agent { font-size: 11px; color: var(--text3); margin-bottom: 8px; }
461
+ .progress-bar { height: 3px; background: var(--surface3); border-radius: 2px; overflow: hidden; }
462
+ .progress-fill { height: 100%; border-radius: 2px; transition: width 0.5s ease; }
463
+
464
+ /* ── Token Usage ── */
465
+ .token-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
466
+ .token-label { font-size: 12px; color: var(--text2); }
467
+ .token-val { font-family: 'IBM Plex Mono', monospace; font-size: 12px; color: var(--text); }
468
+ .token-bar { height: 2px; background: var(--surface3); border-radius: 2px; overflow: hidden; margin-bottom: 10px; }
469
+ .token-fill { height: 100%; border-radius: 2px; }
470
+
471
+ /* ── System Log ── */
472
+ .log-item { display: flex; gap: 8px; margin-bottom: 7px; font-size: 11px; }
473
+ .log-time { font-family: 'IBM Plex Mono', monospace; color: var(--text3); white-space: nowrap; }
474
+ .log-msg { color: var(--text2); line-height: 1.5; }
475
+ .log-msg.success { color: var(--accent); }
476
+ .log-msg.warn { color: var(--warn); }
477
+ .log-msg.error { color: var(--danger); }
478
+
479
+ /* ── System Status ── */
480
+ .system-status {
481
+ display: flex; align-items: center; gap: 8px; padding: 10px 12px;
482
+ background: rgba(110,231,183,0.06); border: 1px solid rgba(110,231,183,0.15); border-radius: 8px;
483
+ }
484
+ .pulse-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); animation: sseGlow 2s infinite; }
485
+ .status-text { font-size: 12px; color: var(--accent); font-weight: 500; }
486
+ .status-sub { font-size: 11px; color: var(--text3); margin-left: auto; font-family: 'IBM Plex Mono', monospace; }
487
+
488
+ /* ════════ WELCOME ════════ */
489
+ .welcome {
490
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
491
+ flex: 1; padding: 40px 24px; text-align: center; gap: 0;
492
+ }
493
+ .welcome-logo {
494
+ width: 56px; height: 56px;
495
+ background: linear-gradient(135deg, var(--accent), var(--accent2));
496
+ border-radius: 14px; display: grid; place-items: center;
497
+ font-family: 'IBM Plex Mono', monospace; font-size: 18px; font-weight: 500;
498
+ color: #0c0c10; margin-bottom: 20px;
499
+ }
500
+ .welcome h1 { font-family: 'Syne', sans-serif; font-size: 22px; font-weight: 700; margin-bottom: 8px; }
501
+ .welcome p { font-size: 14px; color: var(--text2); line-height: 1.7; max-width: 380px; margin-bottom: 28px; }
502
+ .suggestion-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; width: 100%; max-width: 500px; }
503
+ .suggestion-card {
504
+ background: var(--surface2); border: 1px solid var(--border);
505
+ border-radius: 10px; padding: 12px 14px; text-align: left; cursor: pointer; transition: all 0.15s;
506
+ }
507
+ .suggestion-card:hover { border-color: var(--border2); background: var(--surface3); }
508
+ .sugg-title { font-size: 13px; font-weight: 500; margin-bottom: 4px; }
509
+ .sugg-desc { font-size: 11px; color: var(--text3); line-height: 1.5; }
510
+
511
+ /* ════════ MODAL ════════ */
512
+ .modal-overlay {
513
+ position: fixed; inset: 0; background: rgba(0,0,0,0.65);
514
+ display: flex; align-items: center; justify-content: center;
515
+ z-index: 200; backdrop-filter: blur(4px);
516
+ animation: fadeInOverlay 0.15s ease;
517
+ }
518
+ @keyframes fadeInOverlay { from { opacity: 0; } to { opacity: 1; } }
519
+ .modal {
520
+ background: var(--surface); border: 1px solid var(--border2);
521
+ border-radius: 14px; padding: 24px; width: 460px; max-width: 90vw;
522
+ animation: modalSlideUp 0.2s ease;
523
+ }
524
+ @keyframes modalSlideUp {
525
+ from { opacity: 0; transform: translateY(14px); }
526
+ to { opacity: 1; transform: translateY(0); }
527
+ }
528
+ .modal-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 14px; gap: 12px; }
529
+ .modal-title { font-family: 'Syne', sans-serif; font-size: 16px; font-weight: 700; }
530
+ .modal-close {
531
+ width: 28px; height: 28px; flex-shrink: 0; border-radius: 7px; border: none;
532
+ background: var(--surface3); color: var(--text2); cursor: pointer;
533
+ display: grid; place-items: center; transition: all 0.15s;
534
+ }
535
+ .modal-close:hover { background: var(--border); color: var(--text); }
536
+ .modal-body { font-size: 13px; color: var(--text2); line-height: 1.7; }
537
+ .modal-footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px; }
538
+ .modal-btn {
539
+ padding: 8px 18px; border-radius: 8px; border: none;
540
+ font-size: 13px; font-weight: 500; cursor: pointer;
541
+ font-family: 'DM Sans', sans-serif; transition: all 0.15s;
542
+ }
543
+ .modal-btn.secondary { background: var(--surface3); color: var(--text2); border: 1px solid var(--border); }
544
+ .modal-btn.secondary:hover { background: var(--border); color: var(--text); }
545
+ .modal-btn.primary { background: var(--accent2); color: #fff; }
546
+ .modal-btn.primary:hover { background: #6b76e8; }
547
+ .modal-btn.danger { background: rgba(248,113,113,0.15); color: var(--danger); border: 1px solid rgba(248,113,113,0.3); }
548
+
549
+ .modal-info-row {
550
+ display: flex; align-items: center; gap: 8px; padding: 10px 12px;
551
+ background: var(--surface2); border: 1px solid var(--border);
552
+ border-radius: 8px; margin-bottom: 12px;
553
+ }
554
+ .modal-info-row code {
555
+ font-family: 'IBM Plex Mono', monospace; font-size: 12px;
556
+ color: var(--accent2); background: var(--surface3); padding: 2px 7px; border-radius: 4px;
557
+ }
view/index.html ADDED
@@ -0,0 +1,467 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="id">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>AI Orchestration Platform</title>
7
+ <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Syne:wght@400;600;700&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="../style/style.css" />
11
+ </head>
12
+ <body>
13
+ <div id="app">
14
+
15
+ <!-- ════════ SIDEBAR ════════ -->
16
+ <aside class="sidebar">
17
+ <div class="sidebar-header">
18
+ <div class="logo-mark">AI</div>
19
+ <div class="logo-text">ORCH<span>.</span>STUDIO</div>
20
+ </div>
21
+
22
+ <!-- GitHub PAT Auth -->
23
+ <div class="sidebar-section" style="padding-bottom: 4px;">
24
+ <div class="sidebar-label">GitHub Auth</div>
25
+ <div class="auth-card" :class="{ authenticated: github_auth.is_authenticated }">
26
+ <div class="auth-header">
27
+ <!-- GitHub icon -->
28
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="color: var(--text2)">
29
+ <path d="M12 2C6.477 2 2 6.477 2 12c0 4.418 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.009-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.155-1.11-1.463-1.11-1.463-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0 0 22 12c0-5.523-4.477-10-10-10z"/>
30
+ </svg>
31
+ <div>
32
+ <div class="auth-title">GitHub Authentication</div>
33
+ <div class="auth-sub">Personal Access Token</div>
34
+ </div>
35
+ </div>
36
+
37
+ <!-- Not authenticated -->
38
+ <div v-if="!github_auth.is_authenticated" class="auth-input-row">
39
+ <input
40
+ class="auth-input"
41
+ type="password"
42
+ v-model="github_auth.pat_input"
43
+ placeholder="ghp_xxxxxxxxxxxx"
44
+ @keydown.enter="connectGithub"
45
+ />
46
+ <button class="auth-connect-btn" @click="connectGithub" :disabled="!github_auth.pat_input.trim()">
47
+ Connect
48
+ </button>
49
+ </div>
50
+
51
+ <!-- Authenticated -->
52
+ <div v-else class="auth-user-row">
53
+ <div class="auth-dot"></div>
54
+ <span class="auth-username">{{ github_auth.username }}</span>
55
+ <button class="auth-revoke" @click="revokeGithub">Revoke</button>
56
+ </div>
57
+ </div>
58
+ </div>
59
+
60
+ <!-- Repo URL -->
61
+ <div class="repo-section">
62
+ <label class="repo-label">Repository</label>
63
+ <div class="repo-row">
64
+ <input
65
+ class="repo-url-input"
66
+ type="text"
67
+ v-model="repo.url_input"
68
+ placeholder="https://github.com/owner/repo"
69
+ @keydown.enter="analyseRepo"
70
+ />
71
+ <button
72
+ class="repo-analyse-btn"
73
+ @click="analyseRepo"
74
+ :disabled="!repo.url_input.trim() || repo.is_analysing"
75
+ >
76
+ <svg v-if="!repo.is_analysing" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
77
+ <circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
78
+ </svg>
79
+ <svg v-else width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="animation: spin 1s linear infinite;">
80
+ <path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>
81
+ </svg>
82
+ {{ repo.is_analysing ? 'Analysing…' : 'Analyse' }}
83
+ </button>
84
+ </div>
85
+
86
+ <!-- Repo info result -->
87
+ <div v-if="repo.info" class="repo-info-box">
88
+ <div class="repo-info-name">{{ repo.info.full_name }}</div>
89
+ <div class="repo-info-meta">{{ repo.info.language }} · ⭐ {{ repo.info.stars_count }} · {{ repo.info.open_issues_count }} issues</div>
90
+ <span class="contributor-badge" :class="repo.info.is_contributor ? 'yes' : 'no'">
91
+ {{ repo.info.is_contributor ? 'βœ“ Contributor' : 'βœ— Read-only' }}
92
+ </span>
93
+ </div>
94
+ </div>
95
+
96
+ <div class="divider" style="margin: 4px 16px;"></div>
97
+
98
+ <!-- Active Agents -->
99
+ <div class="sidebar-section">
100
+ <div class="sidebar-label">Active Agents</div>
101
+ <div
102
+ v-for="agent in agents"
103
+ :key="agent.agent_id"
104
+ class="agent-item"
105
+ :class="{ active: selected_agent_id === agent.agent_id }"
106
+ @click="selected_agent_id = agent.agent_id"
107
+ >
108
+ <div class="agent-dot" :style="{ background: agent.color }"></div>
109
+ <div class="agent-info">
110
+ <div class="agent-name">{{ agent.agent_name }}</div>
111
+ <div class="agent-role">{{ agent.agent_role }}</div>
112
+ </div>
113
+ <span class="agent-status" :class="'status-' + agent.status">{{ agent.status }}</span>
114
+ </div>
115
+ </div>
116
+
117
+ <div class="divider" style="margin: 0 16px;"></div>
118
+
119
+ <!-- Session Stats -->
120
+ <div class="sidebar-section">
121
+ <div class="sidebar-label">Session Stats</div>
122
+ <div class="stats-row">
123
+ <div class="stat-box">
124
+ <div class="stat-num" style="color: var(--accent)">{{ session_stats.total_tasks }}</div>
125
+ <div class="stat-label">Tasks</div>
126
+ </div>
127
+ <div class="stat-box">
128
+ <div class="stat-num" style="color: var(--accent2)">{{ session_stats.success_rate }}%</div>
129
+ <div class="stat-label">Success</div>
130
+ </div>
131
+ <div class="stat-box">
132
+ <div class="stat-num" style="color: var(--warn)">{{ session_stats.avg_latency_ms }}ms</div>
133
+ <div class="stat-label">Latency</div>
134
+ </div>
135
+ </div>
136
+ </div>
137
+
138
+ <div class="sidebar-bottom">
139
+ <button class="setting-btn">
140
+ <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
141
+ <circle cx="12" cy="12" r="3"/>
142
+ <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
143
+ </svg>
144
+ Settings
145
+ </button>
146
+ <button class="setting-btn" style="color: var(--danger)">
147
+ <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
148
+ <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
149
+ <polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/>
150
+ </svg>
151
+ Logout
152
+ </button>
153
+ </div>
154
+ </aside>
155
+
156
+ <!-- ════════ CHAT AREA ════════ -->
157
+ <main class="chat-area">
158
+
159
+ <!-- Topbar -->
160
+ <header class="chat-topbar">
161
+ <div class="topbar-left">
162
+ <div class="topbar-title">AI Orchestration Chat</div>
163
+ <span class="pipeline-badge">{{ active_pipeline.pipeline_name }}</span>
164
+ </div>
165
+ <div class="topbar-right">
166
+ <button class="icon-btn" title="Clear chat" @click="clearChat">
167
+ <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
168
+ <polyline points="3 6 5 6 21 6"/>
169
+ <path d="M19 6l-1 14H6L5 6"/>
170
+ <path d="M10 11v6M14 11v6M9 6V4h6v2"/>
171
+ </svg>
172
+ </button>
173
+ </div>
174
+ </header>
175
+
176
+ <!-- Action Bar -->
177
+ <div class="action-bar">
178
+ <!-- Download Reports -->
179
+ <button class="action-btn green" @click="downloadReport" :disabled="messages.length === 0">
180
+ <svg width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
181
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
182
+ <polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
183
+ </svg>
184
+ Download Report
185
+ </button>
186
+
187
+ <div class="action-sep"></div>
188
+
189
+ <!-- View Commits β€” hanya muncul kalau ada commit_result di chat -->
190
+ <template v-if="last_commit_result">
191
+ <a
192
+ class="action-btn purple"
193
+ :href="last_commit_result.commits_url"
194
+ target="_blank"
195
+ rel="noopener"
196
+ >
197
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
198
+ <circle cx="12" cy="12" r="3"/><line x1="12" y1="3" x2="12" y2="9"/><line x1="12" y1="15" x2="12" y2="21"/>
199
+ <line x1="3" y1="12" x2="9" y2="12"/><line x1="15" y1="12" x2="21" y2="12"/>
200
+ </svg>
201
+ View Commits
202
+ </a>
203
+
204
+ <!-- Pull Request β€” hanya contributor -->
205
+ <template v-if="repo.info && repo.info.is_contributor">
206
+ <a class="action-btn green" :href="last_commit_result.pr_url" target="_blank" rel="noopener">
207
+ <svg width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
208
+ <circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/>
209
+ <path d="M6 9v6M15.5 6.5l-3 3.5 3 3.5M18 6v9"/>
210
+ </svg>
211
+ Open Pull Request
212
+ </a>
213
+ </template>
214
+
215
+ <div class="action-sep"></div>
216
+ </template>
217
+
218
+ <!-- Non-contributor notice -->
219
+ <div v-if="repo.info && !repo.info.is_contributor" class="no-contrib-notice">
220
+ <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
221
+ <circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
222
+ </svg>
223
+ Read-only β€” commit &amp; push disabled
224
+ </div>
225
+ </div>
226
+
227
+ <!-- Messages -->
228
+ <div class="chat-messages" ref="messageContainer">
229
+
230
+ <!-- Welcome -->
231
+ <div v-if="messages.length === 0" class="welcome">
232
+ <div class="welcome-logo">⬑</div>
233
+ <h1>AI Orchestration Studio</h1>
234
+ <p>Masukkan link repo GitHub dan token PAT di sidebar, lalu klik Analyse untuk memulai agent.</p>
235
+ <div class="suggestion-grid">
236
+ <div
237
+ v-for="s in suggestions" :key="s.title"
238
+ class="suggestion-card"
239
+ @click="useSuggestion(s.prompt)"
240
+ >
241
+ <div class="sugg-title">{{ s.title }}</div>
242
+ <div class="sugg-desc">{{ s.desc }}</div>
243
+ </div>
244
+ </div>
245
+ </div>
246
+
247
+ <!-- Message list -->
248
+ <template v-else>
249
+ <div
250
+ v-for="msg in messages" :key="msg.message_id"
251
+ class="msg-wrapper"
252
+ :class="{ user: msg.sender_type === 'user' }"
253
+ >
254
+ <div
255
+ class="msg-avatar"
256
+ :class="avatarClass(msg)"
257
+ :style="msg.sender_type === 'agent' ? { background: getAgentColor(msg.agent_id) } : {}"
258
+ >
259
+ {{ msg.sender_type === 'user' ? 'ME' : msg.sender_initials }}
260
+ </div>
261
+
262
+ <div class="msg-body">
263
+ <div class="msg-meta">
264
+ <span class="msg-sender">{{ msg.sender_name }}</span>
265
+ <span class="msg-time">{{ msg.created_at }}</span>
266
+ </div>
267
+
268
+ <div v-if="msg.is_typing" class="typing-indicator">
269
+ <div class="typing-dot"></div>
270
+ <div class="typing-dot"></div>
271
+ <div class="typing-dot"></div>
272
+ </div>
273
+
274
+ <div
275
+ v-else
276
+ class="msg-bubble"
277
+ :class="bubbleClass(msg)"
278
+ :style="msg.sender_type === 'agent' ? agentBubbleStyle(msg.agent_id) : {}"
279
+ >
280
+ {{ msg.message_content }}
281
+
282
+ <!-- Pipeline trace -->
283
+ <div v-if="msg.pipeline_trace && msg.pipeline_trace.length" class="pipeline-trace">
284
+ <div class="trace-label">Pipeline Trace</div>
285
+ <div class="trace-steps">
286
+ <div v-for="(step, idx) in msg.pipeline_trace" :key="idx" class="trace-step">
287
+ <div class="trace-dot" :style="{ background: step.step_color }"></div>
288
+ <span class="trace-step-name">{{ step.step_agent }}</span>
289
+ <span class="trace-arrow">β†’</span>
290
+ <span style="color: var(--text2)">{{ step.step_output }}</span>
291
+ <span class="trace-step-time">{{ step.duration_ms }}ms</span>
292
+ </div>
293
+ </div>
294
+ </div>
295
+
296
+ <!-- Commit result card -->
297
+ <div v-if="msg.commit_result" class="commit-card">
298
+ <div class="commit-card-header">
299
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style="color: var(--accent2)">
300
+ <path d="M12 2C6.477 2 2 6.477 2 12c0 4.418 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.009-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.155-1.11-1.463-1.11-1.463-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0 0 22 12c0-5.523-4.477-10-10-10z"/>
301
+ </svg>
302
+ <span class="commit-card-title">Branch Created</span>
303
+ </div>
304
+ <div class="commit-branch-name">{{ msg.commit_result.branch_name }}</div>
305
+ <div class="commit-desc">{{ msg.commit_result.commit_message }}</div>
306
+ <div class="commit-actions">
307
+ <a class="commit-btn view-commits" :href="msg.commit_result.commits_url" target="_blank" rel="noopener">
308
+ <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
309
+ <circle cx="12" cy="12" r="3"/><line x1="12" y1="3" x2="12" y2="9"/><line x1="12" y1="15" x2="12" y2="21"/>
310
+ <line x1="3" y1="12" x2="9" y2="12"/><line x1="15" y1="12" x2="21" y2="12"/>
311
+ </svg>
312
+ View Commits
313
+ </a>
314
+ <a
315
+ v-if="repo.info && repo.info.is_contributor"
316
+ class="commit-btn view-pr"
317
+ :href="msg.commit_result.pr_url"
318
+ target="_blank"
319
+ rel="noopener"
320
+ >
321
+ <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
322
+ <circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/>
323
+ <path d="M6 9v6M15.5 6.5l-3 3.5 3 3.5M18 6v9"/>
324
+ </svg>
325
+ Open PR
326
+ </a>
327
+ </div>
328
+ </div>
329
+ </div>
330
+ </div>
331
+ </div>
332
+ </template>
333
+ </div>
334
+
335
+ <!-- Input area -->
336
+ <div class="chat-input-area">
337
+ <div class="pipeline-selector">
338
+ <button
339
+ v-for="p in pipelines" :key="p.pipeline_id"
340
+ class="pipeline-chip"
341
+ :class="{ active: active_pipeline.pipeline_id === p.pipeline_id }"
342
+ @click="active_pipeline = p"
343
+ >
344
+ {{ p.pipeline_name }}
345
+ </button>
346
+ </div>
347
+ <div class="input-box">
348
+ <textarea
349
+ v-model="input_text"
350
+ placeholder="Chat nonaktif β€” agent bekerja otomatis saat repo di-Analyse"
351
+ disabled
352
+ rows="1"
353
+ ></textarea>
354
+ </div>
355
+ <div class="input-hint">Mode otomatis Β· Analyse repo untuk memulai agent</div>
356
+ </div>
357
+ </main>
358
+
359
+ <!-- ════════ RIGHT PANEL ════════ -->
360
+ <aside class="right-panel">
361
+
362
+ <!-- System status -->
363
+ <div class="panel-section">
364
+ <div class="system-status">
365
+ <div class="pulse-dot"></div>
366
+ <span class="status-text">System Online</span>
367
+ <span class="status-sub">v2.4.1</span>
368
+ </div>
369
+ </div>
370
+
371
+ <!-- SSE / AI Response Logs -->
372
+ <div class="panel-section">
373
+ <div class="panel-title">
374
+ AI Response Stream
375
+ <span class="panel-title-badge">SSE</span>
376
+ </div>
377
+ <div class="sse-header">
378
+ <div class="sse-status-dot" :class="{ active: sse_stream.is_active }"></div>
379
+ <span class="sse-status-label" :class="{ active: sse_stream.is_active }">
380
+ {{ sse_stream.is_active ? 'Streaming…' : 'Idle' }}
381
+ </span>
382
+ <button class="sse-clear-btn" @click="sse_stream.entries = []">Clear</button>
383
+ </div>
384
+ <div class="sse-log-area" ref="sseLogArea">
385
+ <div v-if="!sse_stream.entries.length" class="sse-empty-msg">
386
+ Waiting for stream events…
387
+ </div>
388
+ <div v-for="(entry, idx) in sse_stream.entries" :key="idx" class="sse-entry">
389
+ <span class="sse-ts">{{ entry.ts }}</span>
390
+ <span class="sse-event-tag" :class="entry.event_type">{{ entry.event_type }}</span>
391
+ <span class="sse-data">{{ entry.data }}</span>
392
+ </div>
393
+ </div>
394
+ </div>
395
+
396
+ <!-- Active tasks -->
397
+ <div class="panel-section">
398
+ <div class="panel-title">
399
+ Active Tasks
400
+ <span class="panel-title-badge">{{ active_tasks.length }}</span>
401
+ </div>
402
+ <div v-for="task in active_tasks" :key="task.task_id" class="task-card">
403
+ <div class="task-name">{{ task.task_name }}</div>
404
+ <div class="task-agent">{{ task.assigned_agent }}</div>
405
+ <div class="progress-bar">
406
+ <div class="progress-fill" :style="{ width: task.progress_percent + '%', background: task.task_color }"></div>
407
+ </div>
408
+ </div>
409
+ <div v-if="!active_tasks.length" style="font-size: 12px; color: var(--text3); text-align: center; padding: 8px 0;">
410
+ No active tasks
411
+ </div>
412
+ </div>
413
+
414
+ <!-- Token usage -->
415
+ <div class="panel-section">
416
+ <div class="panel-title">Token Usage</div>
417
+ <div v-for="usage in token_usage" :key="usage.model_name">
418
+ <div class="token-row">
419
+ <span class="token-label">{{ usage.model_name }}</span>
420
+ <span class="token-val">{{ usage.tokens_used.toLocaleString() }}</span>
421
+ </div>
422
+ <div class="token-bar">
423
+ <div class="token-fill" :style="{ width: tokenBarWidth(usage) + '%', background: usage.bar_color }"></div>
424
+ </div>
425
+ </div>
426
+ </div>
427
+
428
+ <!-- System log -->
429
+ <div class="panel-section">
430
+ <div class="panel-title">System Log</div>
431
+ <div v-for="log in system_logs" :key="log.log_id" class="log-item">
432
+ <span class="log-time">{{ log.log_time }}</span>
433
+ <span class="log-msg" :class="log.log_level">{{ log.log_message }}</span>
434
+ </div>
435
+ </div>
436
+ </aside>
437
+
438
+ <!-- ════════ MODAL ════════ -->
439
+ <div v-if="modal.show" class="modal-overlay" @click.self="modal.show = false">
440
+ <div class="modal">
441
+ <div class="modal-header">
442
+ <div class="modal-title">{{ modal.title }}</div>
443
+ <button class="modal-close" @click="modal.show = false">
444
+ <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
445
+ <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
446
+ </svg>
447
+ </button>
448
+ </div>
449
+ <div class="modal-body" v-html="modal.body"></div>
450
+ <div class="modal-footer">
451
+ <button class="modal-btn secondary" @click="modal.show = false">Close</button>
452
+ <button v-if="modal.confirm_label" class="modal-btn" :class="modal.confirm_class" @click="modal.on_confirm">
453
+ {{ modal.confirm_label }}
454
+ </button>
455
+ </div>
456
+ </div>
457
+ </div>
458
+
459
+ </div>
460
+
461
+ <style>
462
+ @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
463
+ </style>
464
+
465
+ <script src="../app.js"></script>
466
+ </body>
467
+ </html>