import { type KeyboardEvent, useEffect, useMemo, useRef, useState, } from 'react'; import { Check, Inbox, Mic, RefreshCw, Send, X } from 'lucide-react'; import { type DashboardInboxAction, type DashboardOverview, type StatusSnapshot, } from './api'; import { formatDate } from './dashboardHelpers'; import type { Locale } from './i18n'; type InboxItem = DashboardOverview['inbox'][number]; type GlassesMode = 'queue' | 'voice'; interface RoomChoice { jid: string; label: string; status: StatusSnapshot['entries'][number]['status']; pendingTasks: number; } interface SpeechRecognitionResultLike { readonly 0?: { transcript?: string }; } interface SpeechRecognitionEventLike { readonly results: ArrayLike; } interface SpeechRecognitionLike { continuous: boolean; interimResults: boolean; lang: string; maxAlternatives: number; onend: (() => void) | null; onerror: (() => void) | null; onresult: ((event: SpeechRecognitionEventLike) => void) | null; start: () => void; } type SpeechRecognitionConstructor = new () => SpeechRecognitionLike; interface SpeechWindow extends Window { SpeechRecognition?: SpeechRecognitionConstructor; webkitSpeechRecognition?: SpeechRecognitionConstructor; } export interface GlassesPanelProps { createRequestId: () => string; error: string | null; freshnessText: string; inboxActionKey: `${string}:${DashboardInboxAction}` | null; locale: Locale; onInboxAction: ( item: InboxItem, action: DashboardInboxAction, ) => Promise; onRefresh: () => void; onSendRoomMessage: ( roomJid: string, text: string, requestId: string, ) => Promise; overview: DashboardOverview; refreshing: boolean; roomMessageKey: string | null; snapshots: StatusSnapshot[]; } const QUEUE_KIND_ORDER: Record = { approval: 0, 'reviewer-request': 1, 'arbiter-request': 2, 'ci-failure': 3, mention: 4, 'pending-room': 5, }; const SEVERITY_ORDER: Record = { error: 0, warn: 1, info: 2, }; const SPEECH_LOCALES: Record = { en: 'en-US', ja: 'ja-JP', ko: 'ko-KR', zh: 'zh-CN', }; function getSpeechRecognition(): SpeechRecognitionConstructor | null { if (typeof window === 'undefined') return null; const speechWindow = window as SpeechWindow; return ( speechWindow.SpeechRecognition ?? speechWindow.webkitSpeechRecognition ?? null ); } function inboxActionsFor(item: InboxItem): DashboardInboxAction[] { if ( item.source === 'paired-task' && (item.kind === 'reviewer-request' || item.kind === 'approval' || item.kind === 'arbiter-request') ) { return ['run', 'decline', 'dismiss']; } return ['dismiss']; } function actionLabel(item: InboxItem, action: DashboardInboxAction): string { if (action === 'dismiss') return '닫기'; if (action === 'decline') return '거절'; if (item.kind === 'approval') return '최종화'; if (item.kind === 'reviewer-request') return '리뷰'; if (item.kind === 'arbiter-request') return '중재'; return '실행'; } function actionIcon(action: DashboardInboxAction) { if (action === 'decline') return ; if (action === 'dismiss') return ; return ; } function sortInboxItems(items: InboxItem[]): InboxItem[] { return [...items].sort((a, b) => { const severity = SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]; if (severity !== 0) return severity; const kind = QUEUE_KIND_ORDER[a.kind] - QUEUE_KIND_ORDER[b.kind]; if (kind !== 0) return kind; return b.lastOccurredAt.localeCompare(a.lastOccurredAt); }); } function buildRoomChoices(snapshots: StatusSnapshot[]): RoomChoice[] { const rooms = new Map(); for (const snapshot of snapshots) { const entries = Array.isArray(snapshot.entries) ? snapshot.entries : []; for (const entry of entries) { const existing = rooms.get(entry.jid); const label = entry.name || entry.folder || entry.jid; if (!existing) { rooms.set(entry.jid, { jid: entry.jid, label, pendingTasks: entry.pendingTasks, status: entry.status, }); continue; } if ( entry.status === 'processing' || entry.pendingTasks > existing.pendingTasks ) { rooms.set(entry.jid, { jid: entry.jid, label, pendingTasks: entry.pendingTasks, status: entry.status, }); } } } return [...rooms.values()].sort((a, b) => { if (a.status === 'processing' && b.status !== 'processing') return -1; if (b.status === 'processing' && a.status !== 'processing') return 1; return b.pendingTasks - a.pendingTasks || a.label.localeCompare(b.label); }); } function clampIndex(index: number, length: number): number { if (length <= 0) return 0; return Math.max(0, Math.min(index, length - 1)); } function nextIndex(index: number, length: number, delta: number): number { if (length <= 0) return 0; return (index + delta + length) % length; } function readTranscript(event: SpeechRecognitionEventLike): string { return Array.from(event.results) .map((result) => result[0]?.transcript?.trim() ?? '') .filter(Boolean) .join(' ') .trim(); } interface QueueCardProps { inboxActionKey: `${string}:${DashboardInboxAction}` | null; inboxActions: DashboardInboxAction[]; inboxItems: InboxItem[]; locale: Locale; onInboxAction: ( item: InboxItem, action: DashboardInboxAction, ) => Promise; selectedActionIndex: number; selectedInbox: InboxItem | undefined; selectedInboxIndex: number; setSelectedActionIndex: (index: number) => void; } function QueueCard({ inboxActionKey, inboxActions, inboxItems, locale, onInboxAction, selectedActionIndex, selectedInbox, selectedInboxIndex, setSelectedActionIndex, }: QueueCardProps) { if (!selectedInbox) { return (
Queue clear 새 요청 없음
); } return (
{selectedInbox.kind} {selectedInboxIndex + 1}/{inboxItems.length}

{selectedInbox.title}

{selectedInbox.summary}

Target
{selectedInbox.roomName ?? selectedInbox.groupFolder ?? selectedInbox.roomJid ?? selectedInbox.taskId ?? '-'}
Time
{formatDate(selectedInbox.lastOccurredAt, locale)}
{inboxActions.map((action, index) => { const actionKey = `${selectedInbox.id}:${action}`; const busy = inboxActionKey === actionKey; return ( ); })}
); } interface VoiceCardProps { canListen: boolean; listening: boolean; onSendVoiceText: () => void; onStartListening: () => void; selectedRoom: RoomChoice | undefined; sendingVoice: boolean; setSelectedRoomIndex: (index: number) => void; setVoiceText: (value: string) => void; rooms: RoomChoice[]; voiceText: string; } function VoiceCard({ canListen, listening, onSendVoiceText, onStartListening, selectedRoom, sendingVoice, setSelectedRoomIndex, setVoiceText, rooms, voiceText, }: VoiceCardProps) { return (
voice {selectedRoom?.label ?? 'No room'}