refactor dashboard room activity fetching
Refactor selected-room dashboard activity fetching, split Rooms i18n, and tighten mobile usage layout.
This commit is contained in:
@@ -34,7 +34,6 @@ import {
|
|||||||
fetchAccounts,
|
fetchAccounts,
|
||||||
fetchDashboardData,
|
fetchDashboardData,
|
||||||
fetchModelConfig,
|
fetchModelConfig,
|
||||||
fetchRoomsTimelineBatch,
|
|
||||||
runInboxAction,
|
runInboxAction,
|
||||||
runServiceAction,
|
runServiceAction,
|
||||||
runScheduledTaskAction,
|
runScheduledTaskAction,
|
||||||
@@ -52,6 +51,10 @@ import {
|
|||||||
type Locale,
|
type Locale,
|
||||||
type Messages,
|
type Messages,
|
||||||
} from './i18n';
|
} from './i18n';
|
||||||
|
import {
|
||||||
|
useSelectedRoomActivity,
|
||||||
|
type RoomActivityMap,
|
||||||
|
} from './useRoomActivity';
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
|
|
||||||
interface DashboardState {
|
interface DashboardState {
|
||||||
@@ -60,8 +63,6 @@ interface DashboardState {
|
|||||||
tasks: DashboardTask[];
|
tasks: DashboardTask[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type RoomActivityMap = Record<string, DashboardRoomActivity>;
|
|
||||||
|
|
||||||
type UsageRow = DashboardOverview['usage']['rows'][number];
|
type UsageRow = DashboardOverview['usage']['rows'][number];
|
||||||
type InboxItem = DashboardOverview['inbox'][number];
|
type InboxItem = DashboardOverview['inbox'][number];
|
||||||
type RiskLevel = 'ok' | 'warn' | 'critical';
|
type RiskLevel = 'ok' | 'warn' | 'critical';
|
||||||
@@ -2568,7 +2569,9 @@ function RoomBoardV2({
|
|||||||
roomActivity,
|
roomActivity,
|
||||||
roomActivityLoading,
|
roomActivityLoading,
|
||||||
roomMessageKey,
|
roomMessageKey,
|
||||||
|
selectedJid,
|
||||||
locale,
|
locale,
|
||||||
|
onSelectedJidChange,
|
||||||
snapshots,
|
snapshots,
|
||||||
t,
|
t,
|
||||||
}: {
|
}: {
|
||||||
@@ -2585,13 +2588,14 @@ function RoomBoardV2({
|
|||||||
roomActivity: RoomActivityMap;
|
roomActivity: RoomActivityMap;
|
||||||
roomActivityLoading: boolean;
|
roomActivityLoading: boolean;
|
||||||
roomMessageKey: string | null;
|
roomMessageKey: string | null;
|
||||||
|
selectedJid: string | null;
|
||||||
locale: Locale;
|
locale: Locale;
|
||||||
|
onSelectedJidChange: (jid: string | null) => void;
|
||||||
snapshots: StatusSnapshot[];
|
snapshots: StatusSnapshot[];
|
||||||
t: Messages;
|
t: Messages;
|
||||||
}) {
|
}) {
|
||||||
const [filter, setFilter] = useState<RoomFilter>('all');
|
const [filter, setFilter] = useState<RoomFilter>('all');
|
||||||
const [sort, setSort] = useState<RoomSort>('recent');
|
const [sort, setSort] = useState<RoomSort>('recent');
|
||||||
const [selectedJid, setSelectedJid] = useState<string | null>(null);
|
|
||||||
const [drafts, setDrafts] = useState<Record<string, string>>({});
|
const [drafts, setDrafts] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
const allEntries: RoomEntryWithService[] = snapshots.flatMap((snapshot) =>
|
const allEntries: RoomEntryWithService[] = snapshots.flatMap((snapshot) =>
|
||||||
@@ -2601,10 +2605,6 @@ function RoomBoardV2({
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (allEntries.length === 0) {
|
|
||||||
return <EmptyState>{t.rooms.empty}</EmptyState>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const counts = {
|
const counts = {
|
||||||
all: allEntries.length,
|
all: allEntries.length,
|
||||||
processing: allEntries.filter((e) => e.status === 'processing').length,
|
processing: allEntries.filter((e) => e.status === 'processing').length,
|
||||||
@@ -2632,6 +2632,15 @@ function RoomBoardV2({
|
|||||||
const selectedEntry =
|
const selectedEntry =
|
||||||
sorted.find((e) => e.jid === selectedJid) ?? sorted[0] ?? null;
|
sorted.find((e) => e.jid === selectedJid) ?? sorted[0] ?? null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const nextJid = selectedEntry?.jid ?? null;
|
||||||
|
if (nextJid !== selectedJid) onSelectedJidChange(nextJid);
|
||||||
|
}, [onSelectedJidChange, selectedEntry?.jid, selectedJid]);
|
||||||
|
|
||||||
|
if (allEntries.length === 0) {
|
||||||
|
return <EmptyState>{t.rooms.empty}</EmptyState>;
|
||||||
|
}
|
||||||
|
|
||||||
function setDraft(jid: string, value: string) {
|
function setDraft(jid: string, value: string) {
|
||||||
setDrafts((previous) => ({ ...previous, [jid]: value }));
|
setDrafts((previous) => ({ ...previous, [jid]: value }));
|
||||||
}
|
}
|
||||||
@@ -2705,7 +2714,7 @@ function RoomBoardV2({
|
|||||||
aria-current={active ? 'page' : undefined}
|
aria-current={active ? 'page' : undefined}
|
||||||
className={`rooms-list-item status-${entry.status}${active ? ' is-active' : ''}`}
|
className={`rooms-list-item status-${entry.status}${active ? ' is-active' : ''}`}
|
||||||
key={`${entry.serviceId}:${entry.jid}`}
|
key={`${entry.serviceId}:${entry.jid}`}
|
||||||
onClick={() => setSelectedJid(entry.jid)}
|
onClick={() => onSelectedJidChange(entry.jid)}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span className={`room-pulse pulse-${entry.status}`}>
|
<span className={`room-pulse pulse-${entry.status}`}>
|
||||||
@@ -3788,8 +3797,15 @@ function App() {
|
|||||||
const [serviceActionKey, setServiceActionKey] =
|
const [serviceActionKey, setServiceActionKey] =
|
||||||
useState<ServiceActionKey | null>(null);
|
useState<ServiceActionKey | null>(null);
|
||||||
const [roomMessageKey, setRoomMessageKey] = useState<string | null>(null);
|
const [roomMessageKey, setRoomMessageKey] = useState<string | null>(null);
|
||||||
const [roomActivity, setRoomActivity] = useState<RoomActivityMap>({});
|
const [selectedRoomJid, setSelectedRoomJid] = useState<string | null>(null);
|
||||||
const [roomActivityLoading, setRoomActivityLoading] = useState(false);
|
const {
|
||||||
|
refreshRoom: refreshRoomActivity,
|
||||||
|
roomActivity,
|
||||||
|
roomActivityLoading,
|
||||||
|
} = useSelectedRoomActivity({
|
||||||
|
active: activeView === 'rooms',
|
||||||
|
selectedRoomJid: selectedRoomJid,
|
||||||
|
});
|
||||||
const [pendingMessages, setPendingMessages] = useState<
|
const [pendingMessages, setPendingMessages] = useState<
|
||||||
Record<string, Array<DashboardRoomActivity['messages'][number]>>
|
Record<string, Array<DashboardRoomActivity['messages'][number]>>
|
||||||
>({});
|
>({});
|
||||||
@@ -3956,8 +3972,7 @@ function App() {
|
|||||||
try {
|
try {
|
||||||
await sendRoomMessage(roomJid, text, requestId, nickname || null);
|
await sendRoomMessage(roomJid, text, requestId, nickname || null);
|
||||||
try {
|
try {
|
||||||
const fresh = await fetchRoomsTimelineBatch();
|
await refreshRoomActivity(roomJid);
|
||||||
setRoomActivity(fresh);
|
|
||||||
} catch {
|
} catch {
|
||||||
/* refresh will retry on next poll */
|
/* refresh will retry on next poll */
|
||||||
}
|
}
|
||||||
@@ -4125,102 +4140,6 @@ function App() {
|
|||||||
return () => window.clearInterval(id);
|
return () => window.clearInterval(id);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const roomsJidsKey = useMemo(() => {
|
|
||||||
if (!data) return '';
|
|
||||||
return [
|
|
||||||
...new Set(
|
|
||||||
data.snapshots.flatMap((snapshot) =>
|
|
||||||
snapshot.entries.map((entry) => entry.jid),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
.sort()
|
|
||||||
.join('|');
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const roomsLastUpdatedKey = useMemo(() => {
|
|
||||||
if (!data) return '';
|
|
||||||
return data.snapshots
|
|
||||||
.map((s) => s.updatedAt)
|
|
||||||
.sort()
|
|
||||||
.join('|');
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (activeView !== 'rooms' || !data) return;
|
|
||||||
if (!roomsJidsKey) {
|
|
||||||
setRoomActivity({});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cancelled = false;
|
|
||||||
let inFlight = false;
|
|
||||||
let pollIntervalId: number | null = null;
|
|
||||||
let es: EventSource | null = null;
|
|
||||||
|
|
||||||
const applyMap = (map: RoomActivityMap) => {
|
|
||||||
if (!cancelled) setRoomActivity(map);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchOnce = async (initial: boolean) => {
|
|
||||||
if (cancelled || inFlight) return;
|
|
||||||
inFlight = true;
|
|
||||||
if (initial) setRoomActivityLoading(true);
|
|
||||||
try {
|
|
||||||
applyMap(await fetchRoomsTimelineBatch());
|
|
||||||
} catch {
|
|
||||||
/* keep last good state */
|
|
||||||
} finally {
|
|
||||||
inFlight = false;
|
|
||||||
if (!cancelled && initial) setRoomActivityLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const startPollingFallback = () => {
|
|
||||||
if (pollIntervalId !== null) return;
|
|
||||||
pollIntervalId = window.setInterval(() => void fetchOnce(false), 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const stopPollingFallback = () => {
|
|
||||||
if (pollIntervalId === null) return;
|
|
||||||
window.clearInterval(pollIntervalId);
|
|
||||||
pollIntervalId = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined' && 'EventSource' in window) {
|
|
||||||
void fetchOnce(true);
|
|
||||||
try {
|
|
||||||
es = new EventSource('/api/stream');
|
|
||||||
es.addEventListener('rooms-timeline', (event) => {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(
|
|
||||||
(event as MessageEvent).data,
|
|
||||||
) as RoomActivityMap;
|
|
||||||
applyMap(parsed);
|
|
||||||
stopPollingFallback();
|
|
||||||
} catch {
|
|
||||||
/* malformed event, ignore */
|
|
||||||
}
|
|
||||||
});
|
|
||||||
es.onerror = () => {
|
|
||||||
// Browser auto-reconnects; meanwhile keep data fresh via polling.
|
|
||||||
startPollingFallback();
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
startPollingFallback();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
void fetchOnce(true);
|
|
||||||
startPollingFallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
stopPollingFallback();
|
|
||||||
es?.close();
|
|
||||||
};
|
|
||||||
}, [activeView, roomsJidsKey]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPendingMessages((prev) => {
|
setPendingMessages((prev) => {
|
||||||
const next: typeof prev = {};
|
const next: typeof prev = {};
|
||||||
@@ -4385,6 +4304,8 @@ function App() {
|
|||||||
roomActivityLoading={roomActivityLoading}
|
roomActivityLoading={roomActivityLoading}
|
||||||
roomMessageKey={roomMessageKey}
|
roomMessageKey={roomMessageKey}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
|
onSelectedJidChange={setSelectedRoomJid}
|
||||||
|
selectedJid={selectedRoomJid}
|
||||||
snapshots={data.snapshots}
|
snapshots={data.snapshots}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { roomMessages, type RoomMessages } from './i18n/rooms';
|
||||||
|
|
||||||
export const LOCALES = ['ko', 'en', 'zh', 'ja'] as const;
|
export const LOCALES = ['ko', 'en', 'zh', 'ja'] as const;
|
||||||
|
|
||||||
export type Locale = (typeof LOCALES)[number];
|
export type Locale = (typeof LOCALES)[number];
|
||||||
@@ -152,46 +154,7 @@ export interface Messages {
|
|||||||
confirmDecline: string;
|
confirmDecline: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
rooms: {
|
rooms: RoomMessages;
|
||||||
empty: string;
|
|
||||||
cardsAria: string;
|
|
||||||
room: string;
|
|
||||||
service: string;
|
|
||||||
agent: string;
|
|
||||||
status: string;
|
|
||||||
queue: string;
|
|
||||||
queueWaitingMessages: string;
|
|
||||||
tasks: string;
|
|
||||||
elapsed: string;
|
|
||||||
activity: string;
|
|
||||||
loadingActivity: string;
|
|
||||||
noActivity: string;
|
|
||||||
task: string;
|
|
||||||
noTask: string;
|
|
||||||
currentTurn: string;
|
|
||||||
noTurn: string;
|
|
||||||
round: string;
|
|
||||||
attempt: string;
|
|
||||||
updated: string;
|
|
||||||
output: string;
|
|
||||||
latestOutput: string;
|
|
||||||
outputHistory: string;
|
|
||||||
noOutput: string;
|
|
||||||
recentMessages: string;
|
|
||||||
messageHistory: string;
|
|
||||||
noMessages: string;
|
|
||||||
details: string;
|
|
||||||
message: string;
|
|
||||||
messagePlaceholder: string;
|
|
||||||
send: string;
|
|
||||||
sending: string;
|
|
||||||
error: string;
|
|
||||||
filterAll: string;
|
|
||||||
sortRecent: string;
|
|
||||||
sortName: string;
|
|
||||||
sortQueue: string;
|
|
||||||
sortLabel: string;
|
|
||||||
};
|
|
||||||
usage: {
|
usage: {
|
||||||
empty: string;
|
empty: string;
|
||||||
current: string;
|
current: string;
|
||||||
@@ -465,46 +428,7 @@ export const messages = {
|
|||||||
confirmDecline: 'Decline this item?',
|
confirmDecline: 'Decline this item?',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: roomMessages.ko,
|
||||||
empty: '룸 없음.',
|
|
||||||
cardsAria: '룸 상태 카드',
|
|
||||||
room: '룸',
|
|
||||||
service: '서비스',
|
|
||||||
agent: '에이전트',
|
|
||||||
status: '상태',
|
|
||||||
queue: '큐',
|
|
||||||
queueWaitingMessages: '대기 메시지',
|
|
||||||
tasks: '태스크',
|
|
||||||
elapsed: '경과',
|
|
||||||
activity: '진행',
|
|
||||||
loadingActivity: '진행 로딩 중',
|
|
||||||
noActivity: '진행 없음',
|
|
||||||
task: '태스크',
|
|
||||||
noTask: '태스크 없음',
|
|
||||||
currentTurn: '현재 턴',
|
|
||||||
noTurn: '턴 없음',
|
|
||||||
round: '라운드',
|
|
||||||
attempt: '시도',
|
|
||||||
updated: '갱신',
|
|
||||||
output: '출력',
|
|
||||||
latestOutput: '마지막 출력',
|
|
||||||
outputHistory: '출력 기록',
|
|
||||||
noOutput: '출력 없음',
|
|
||||||
recentMessages: '메시지 기록',
|
|
||||||
messageHistory: '메시지 기록',
|
|
||||||
noMessages: '메시지 없음',
|
|
||||||
details: '세부',
|
|
||||||
message: '메시지',
|
|
||||||
messagePlaceholder: '요청 입력...',
|
|
||||||
send: '전송',
|
|
||||||
sending: '전송 중...',
|
|
||||||
error: '오류',
|
|
||||||
filterAll: '전체',
|
|
||||||
sortRecent: '최근 활동',
|
|
||||||
sortName: '이름순',
|
|
||||||
sortQueue: '큐 많은 순',
|
|
||||||
sortLabel: '정렬',
|
|
||||||
},
|
|
||||||
usage: {
|
usage: {
|
||||||
empty: '사용량 스냅샷 없음. 수집기 확인.',
|
empty: '사용량 스냅샷 없음. 수집기 확인.',
|
||||||
current: '사용중',
|
current: '사용중',
|
||||||
@@ -762,46 +686,7 @@ export const messages = {
|
|||||||
confirmDecline: 'Decline this item?',
|
confirmDecline: 'Decline this item?',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: roomMessages.en,
|
||||||
empty: 'No rooms yet.',
|
|
||||||
cardsAria: 'Room status cards',
|
|
||||||
room: 'room',
|
|
||||||
service: 'service',
|
|
||||||
agent: 'agent',
|
|
||||||
status: 'status',
|
|
||||||
queue: 'queue',
|
|
||||||
queueWaitingMessages: 'pending messages',
|
|
||||||
tasks: 'tasks',
|
|
||||||
elapsed: 'elapsed',
|
|
||||||
activity: 'activity',
|
|
||||||
loadingActivity: 'Loading activity',
|
|
||||||
noActivity: 'No activity',
|
|
||||||
task: 'task',
|
|
||||||
noTask: 'No task',
|
|
||||||
currentTurn: 'Current turn',
|
|
||||||
noTurn: 'No turn',
|
|
||||||
round: 'round',
|
|
||||||
attempt: 'attempt',
|
|
||||||
updated: 'updated',
|
|
||||||
output: 'output',
|
|
||||||
latestOutput: 'latest output',
|
|
||||||
outputHistory: 'output log',
|
|
||||||
noOutput: 'No output',
|
|
||||||
recentMessages: 'Message log',
|
|
||||||
messageHistory: 'Message log',
|
|
||||||
noMessages: 'No messages',
|
|
||||||
details: 'details',
|
|
||||||
message: 'message',
|
|
||||||
messagePlaceholder: 'Type request...',
|
|
||||||
send: 'Send',
|
|
||||||
sending: 'Sending',
|
|
||||||
error: 'Error',
|
|
||||||
filterAll: 'All',
|
|
||||||
sortRecent: 'Recent activity',
|
|
||||||
sortName: 'Name',
|
|
||||||
sortQueue: 'Queue size',
|
|
||||||
sortLabel: 'Sort',
|
|
||||||
},
|
|
||||||
usage: {
|
usage: {
|
||||||
empty: 'No usage snapshot. Check collector.',
|
empty: 'No usage snapshot. Check collector.',
|
||||||
current: 'Active',
|
current: 'Active',
|
||||||
@@ -1059,46 +944,7 @@ export const messages = {
|
|||||||
confirmDecline: '确认拒绝此项?',
|
confirmDecline: '确认拒绝此项?',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: roomMessages.zh,
|
||||||
empty: '暂无房间。',
|
|
||||||
cardsAria: '房间状态卡片',
|
|
||||||
room: '房间',
|
|
||||||
service: '服务',
|
|
||||||
agent: '代理',
|
|
||||||
status: '状态',
|
|
||||||
queue: '队列',
|
|
||||||
queueWaitingMessages: '待处理消息',
|
|
||||||
tasks: '任务',
|
|
||||||
elapsed: '耗时',
|
|
||||||
activity: '进展',
|
|
||||||
loadingActivity: '正在加载进展',
|
|
||||||
noActivity: '暂无进展',
|
|
||||||
task: '任务',
|
|
||||||
noTask: '无任务',
|
|
||||||
currentTurn: '当前回合',
|
|
||||||
noTurn: '无回合',
|
|
||||||
round: '轮次',
|
|
||||||
attempt: '尝试',
|
|
||||||
updated: '更新',
|
|
||||||
output: '输出',
|
|
||||||
latestOutput: '最新输出',
|
|
||||||
outputHistory: '输出记录',
|
|
||||||
noOutput: '暂无输出',
|
|
||||||
recentMessages: '消息记录',
|
|
||||||
messageHistory: '消息记录',
|
|
||||||
noMessages: '暂无消息',
|
|
||||||
details: '详情',
|
|
||||||
message: 'Message',
|
|
||||||
messagePlaceholder: '输入请求...',
|
|
||||||
send: '发送',
|
|
||||||
sending: '发送中...',
|
|
||||||
error: '错误',
|
|
||||||
filterAll: '全部',
|
|
||||||
sortRecent: '最近活动',
|
|
||||||
sortName: '名称',
|
|
||||||
sortQueue: '队列大小',
|
|
||||||
sortLabel: '排序',
|
|
||||||
},
|
|
||||||
usage: {
|
usage: {
|
||||||
empty: '暂无用量快照。检查采集器。',
|
empty: '暂无用量快照。检查采集器。',
|
||||||
current: '使用中',
|
current: '使用中',
|
||||||
@@ -1358,46 +1204,7 @@ export const messages = {
|
|||||||
confirmDecline: 'この項目を拒否しますか?',
|
confirmDecline: 'この項目を拒否しますか?',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: roomMessages.ja,
|
||||||
empty: 'ルームなし。',
|
|
||||||
cardsAria: 'ルーム状態カード',
|
|
||||||
room: 'ルーム',
|
|
||||||
service: 'サービス',
|
|
||||||
agent: 'エージェント',
|
|
||||||
status: '状態',
|
|
||||||
queue: 'キュー',
|
|
||||||
queueWaitingMessages: '待機メッセージ',
|
|
||||||
tasks: 'タスク',
|
|
||||||
elapsed: '経過',
|
|
||||||
activity: '進行',
|
|
||||||
loadingActivity: '進行を読み込み中',
|
|
||||||
noActivity: '進行なし',
|
|
||||||
task: 'タスク',
|
|
||||||
noTask: 'タスクなし',
|
|
||||||
currentTurn: '現在ターン',
|
|
||||||
noTurn: 'ターンなし',
|
|
||||||
round: 'ラウンド',
|
|
||||||
attempt: '試行',
|
|
||||||
updated: '更新',
|
|
||||||
output: '出力',
|
|
||||||
latestOutput: '最新出力',
|
|
||||||
outputHistory: '出力履歴',
|
|
||||||
noOutput: '出力なし',
|
|
||||||
recentMessages: 'メッセージ履歴',
|
|
||||||
messageHistory: 'メッセージ履歴',
|
|
||||||
noMessages: 'メッセージなし',
|
|
||||||
details: '詳細',
|
|
||||||
message: 'メッセージ',
|
|
||||||
messagePlaceholder: '依頼を入力...',
|
|
||||||
send: '送信',
|
|
||||||
sending: '送信中...',
|
|
||||||
error: 'エラー',
|
|
||||||
filterAll: '全て',
|
|
||||||
sortRecent: '最近の活動',
|
|
||||||
sortName: '名前順',
|
|
||||||
sortQueue: 'キュー順',
|
|
||||||
sortLabel: '並び替え',
|
|
||||||
},
|
|
||||||
usage: {
|
usage: {
|
||||||
empty: '使用量スナップショットなし。収集器を確認。',
|
empty: '使用量スナップショットなし。収集器を確認。',
|
||||||
current: '使用中',
|
current: '使用中',
|
||||||
|
|||||||
205
apps/dashboard/src/i18n/rooms.ts
Normal file
205
apps/dashboard/src/i18n/rooms.ts
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import type { Locale } from '../i18n';
|
||||||
|
|
||||||
|
export interface RoomMessages {
|
||||||
|
empty: string;
|
||||||
|
cardsAria: string;
|
||||||
|
room: string;
|
||||||
|
service: string;
|
||||||
|
agent: string;
|
||||||
|
status: string;
|
||||||
|
queue: string;
|
||||||
|
queueWaitingMessages: string;
|
||||||
|
tasks: string;
|
||||||
|
elapsed: string;
|
||||||
|
activity: string;
|
||||||
|
loadingActivity: string;
|
||||||
|
noActivity: string;
|
||||||
|
task: string;
|
||||||
|
noTask: string;
|
||||||
|
currentTurn: string;
|
||||||
|
noTurn: string;
|
||||||
|
round: string;
|
||||||
|
attempt: string;
|
||||||
|
updated: string;
|
||||||
|
output: string;
|
||||||
|
latestOutput: string;
|
||||||
|
outputHistory: string;
|
||||||
|
noOutput: string;
|
||||||
|
recentMessages: string;
|
||||||
|
messageHistory: string;
|
||||||
|
noMessages: string;
|
||||||
|
details: string;
|
||||||
|
message: string;
|
||||||
|
messagePlaceholder: string;
|
||||||
|
send: string;
|
||||||
|
sending: string;
|
||||||
|
error: string;
|
||||||
|
filterAll: string;
|
||||||
|
sortRecent: string;
|
||||||
|
sortName: string;
|
||||||
|
sortQueue: string;
|
||||||
|
sortLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const roomMessages = {
|
||||||
|
ko: {
|
||||||
|
empty: '룸 없음.',
|
||||||
|
cardsAria: '룸 상태 카드',
|
||||||
|
room: '룸',
|
||||||
|
service: '서비스',
|
||||||
|
agent: '에이전트',
|
||||||
|
status: '상태',
|
||||||
|
queue: '큐',
|
||||||
|
queueWaitingMessages: '대기 메시지',
|
||||||
|
tasks: '태스크',
|
||||||
|
elapsed: '경과',
|
||||||
|
activity: '진행',
|
||||||
|
loadingActivity: '진행 로딩 중',
|
||||||
|
noActivity: '진행 없음',
|
||||||
|
task: '태스크',
|
||||||
|
noTask: '태스크 없음',
|
||||||
|
currentTurn: '현재 턴',
|
||||||
|
noTurn: '턴 없음',
|
||||||
|
round: '라운드',
|
||||||
|
attempt: '시도',
|
||||||
|
updated: '갱신',
|
||||||
|
output: '출력',
|
||||||
|
latestOutput: '마지막 출력',
|
||||||
|
outputHistory: '출력 기록',
|
||||||
|
noOutput: '출력 없음',
|
||||||
|
recentMessages: '메시지 기록',
|
||||||
|
messageHistory: '메시지 기록',
|
||||||
|
noMessages: '메시지 없음',
|
||||||
|
details: '세부',
|
||||||
|
message: '메시지',
|
||||||
|
messagePlaceholder: '요청 입력...',
|
||||||
|
send: '전송',
|
||||||
|
sending: '전송 중...',
|
||||||
|
error: '오류',
|
||||||
|
filterAll: '전체',
|
||||||
|
sortRecent: '최근 활동',
|
||||||
|
sortName: '이름순',
|
||||||
|
sortQueue: '큐 많은 순',
|
||||||
|
sortLabel: '정렬',
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
empty: 'No rooms yet.',
|
||||||
|
cardsAria: 'Room status cards',
|
||||||
|
room: 'room',
|
||||||
|
service: 'service',
|
||||||
|
agent: 'agent',
|
||||||
|
status: 'status',
|
||||||
|
queue: 'queue',
|
||||||
|
queueWaitingMessages: 'pending messages',
|
||||||
|
tasks: 'tasks',
|
||||||
|
elapsed: 'elapsed',
|
||||||
|
activity: 'activity',
|
||||||
|
loadingActivity: 'Loading activity',
|
||||||
|
noActivity: 'No activity',
|
||||||
|
task: 'task',
|
||||||
|
noTask: 'No task',
|
||||||
|
currentTurn: 'Current turn',
|
||||||
|
noTurn: 'No turn',
|
||||||
|
round: 'round',
|
||||||
|
attempt: 'attempt',
|
||||||
|
updated: 'updated',
|
||||||
|
output: 'output',
|
||||||
|
latestOutput: 'latest output',
|
||||||
|
outputHistory: 'output log',
|
||||||
|
noOutput: 'No output',
|
||||||
|
recentMessages: 'Message log',
|
||||||
|
messageHistory: 'Message log',
|
||||||
|
noMessages: 'No messages',
|
||||||
|
details: 'details',
|
||||||
|
message: 'message',
|
||||||
|
messagePlaceholder: 'Type request...',
|
||||||
|
send: 'Send',
|
||||||
|
sending: 'Sending',
|
||||||
|
error: 'Error',
|
||||||
|
filterAll: 'All',
|
||||||
|
sortRecent: 'Recent activity',
|
||||||
|
sortName: 'Name',
|
||||||
|
sortQueue: 'Queue size',
|
||||||
|
sortLabel: 'Sort',
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
empty: '暂无房间。',
|
||||||
|
cardsAria: '房间状态卡片',
|
||||||
|
room: '房间',
|
||||||
|
service: '服务',
|
||||||
|
agent: '代理',
|
||||||
|
status: '状态',
|
||||||
|
queue: '队列',
|
||||||
|
queueWaitingMessages: '待处理消息',
|
||||||
|
tasks: '任务',
|
||||||
|
elapsed: '耗时',
|
||||||
|
activity: '进展',
|
||||||
|
loadingActivity: '正在加载进展',
|
||||||
|
noActivity: '暂无进展',
|
||||||
|
task: '任务',
|
||||||
|
noTask: '无任务',
|
||||||
|
currentTurn: '当前回合',
|
||||||
|
noTurn: '无回合',
|
||||||
|
round: '轮次',
|
||||||
|
attempt: '尝试',
|
||||||
|
updated: '更新',
|
||||||
|
output: '输出',
|
||||||
|
latestOutput: '最新输出',
|
||||||
|
outputHistory: '输出记录',
|
||||||
|
noOutput: '暂无输出',
|
||||||
|
recentMessages: '消息记录',
|
||||||
|
messageHistory: '消息记录',
|
||||||
|
noMessages: '暂无消息',
|
||||||
|
details: '详情',
|
||||||
|
message: 'Message',
|
||||||
|
messagePlaceholder: '输入请求...',
|
||||||
|
send: '发送',
|
||||||
|
sending: '发送中...',
|
||||||
|
error: '错误',
|
||||||
|
filterAll: '全部',
|
||||||
|
sortRecent: '最近活动',
|
||||||
|
sortName: '名称',
|
||||||
|
sortQueue: '队列大小',
|
||||||
|
sortLabel: '排序',
|
||||||
|
},
|
||||||
|
ja: {
|
||||||
|
empty: 'ルームなし。',
|
||||||
|
cardsAria: 'ルーム状態カード',
|
||||||
|
room: 'ルーム',
|
||||||
|
service: 'サービス',
|
||||||
|
agent: 'エージェント',
|
||||||
|
status: '状態',
|
||||||
|
queue: 'キュー',
|
||||||
|
queueWaitingMessages: '待機メッセージ',
|
||||||
|
tasks: 'タスク',
|
||||||
|
elapsed: '経過',
|
||||||
|
activity: '進行',
|
||||||
|
loadingActivity: '進行を読み込み中',
|
||||||
|
noActivity: '進行なし',
|
||||||
|
task: 'タスク',
|
||||||
|
noTask: 'タスクなし',
|
||||||
|
currentTurn: '現在ターン',
|
||||||
|
noTurn: 'ターンなし',
|
||||||
|
round: 'ラウンド',
|
||||||
|
attempt: '試行',
|
||||||
|
updated: '更新',
|
||||||
|
output: '出力',
|
||||||
|
latestOutput: '最新出力',
|
||||||
|
outputHistory: '出力履歴',
|
||||||
|
noOutput: '出力なし',
|
||||||
|
recentMessages: 'メッセージ履歴',
|
||||||
|
messageHistory: 'メッセージ履歴',
|
||||||
|
noMessages: 'メッセージなし',
|
||||||
|
details: '詳細',
|
||||||
|
message: 'メッセージ',
|
||||||
|
messagePlaceholder: '依頼を入力...',
|
||||||
|
send: '送信',
|
||||||
|
sending: '送信中...',
|
||||||
|
error: 'エラー',
|
||||||
|
filterAll: '全て',
|
||||||
|
sortRecent: '最近の活動',
|
||||||
|
sortName: '名前順',
|
||||||
|
sortQueue: 'キュー順',
|
||||||
|
sortLabel: '並び替え',
|
||||||
|
},
|
||||||
|
} satisfies Record<Locale, RoomMessages>;
|
||||||
@@ -3004,35 +3004,66 @@ progress::-moz-progress-bar {
|
|||||||
|
|
||||||
.usage-row {
|
.usage-row {
|
||||||
grid-template-columns:
|
grid-template-columns:
|
||||||
minmax(86px, 0.86fr) minmax(62px, 0.54fr) minmax(62px, 0.54fr)
|
minmax(72px, 0.82fr) minmax(52px, 0.48fr) minmax(52px, 0.48fr)
|
||||||
minmax(58px, 0.4fr);
|
minmax(48px, 0.36fr);
|
||||||
gap: 5px;
|
gap: 4px;
|
||||||
padding: 9px;
|
padding: 7px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usage-account {
|
.usage-account {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 5px;
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-account div {
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-account strong {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-account .pill,
|
||||||
|
.usage-account .mono-chip {
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usage-quota,
|
.usage-quota,
|
||||||
.usage-speed {
|
.usage-speed {
|
||||||
gap: 4px;
|
gap: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usage-quota {
|
.usage-quota {
|
||||||
padding: 5px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usage-quota div {
|
.usage-quota div {
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-quota span,
|
||||||
|
.usage-speed span {
|
||||||
|
font-size: 9px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-quota strong,
|
||||||
|
.usage-speed strong {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-speed {
|
||||||
|
min-height: 0;
|
||||||
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usage-quota small {
|
.usage-quota small {
|
||||||
overflow: hidden;
|
display: none;
|
||||||
font-size: 10px;
|
}
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
.usage-speed small {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.record-card-grid {
|
.record-card-grid {
|
||||||
@@ -3168,7 +3199,7 @@ progress::-moz-progress-bar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.usage-summary {
|
.usage-summary {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usage-summary div:last-child {
|
.usage-summary div:last-child {
|
||||||
@@ -3176,19 +3207,19 @@ progress::-moz-progress-bar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.usage-row {
|
.usage-row {
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
grid-template-columns:
|
||||||
|
minmax(68px, 0.8fr) minmax(48px, 0.48fr) minmax(48px, 0.48fr)
|
||||||
|
minmax(44px, 0.34fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.usage-account {
|
.usage-account {
|
||||||
grid-column: 1 / -1;
|
grid-column: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usage-speed {
|
.usage-speed {
|
||||||
grid-column: 1 / -1;
|
grid-column: auto;
|
||||||
display: flex;
|
display: grid;
|
||||||
min-height: auto;
|
min-height: 0;
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton-grid,
|
.skeleton-grid,
|
||||||
@@ -4582,6 +4613,35 @@ progress::-moz-progress-bar {
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.usage-matrix {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-row {
|
||||||
|
grid-template-columns: minmax(0, 1fr) 62px 62px !important;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-speed {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-account strong {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-account .pill,
|
||||||
|
.usage-account .mono-chip {
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-quota {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.rooms-grid {
|
.rooms-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
68
apps/dashboard/src/useRoomActivity.ts
Normal file
68
apps/dashboard/src/useRoomActivity.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { fetchRoomTimeline, type DashboardRoomActivity } from './api';
|
||||||
|
|
||||||
|
export type RoomActivityMap = Record<string, DashboardRoomActivity>;
|
||||||
|
|
||||||
|
interface UseSelectedRoomActivityOptions {
|
||||||
|
active: boolean;
|
||||||
|
pollMs?: number;
|
||||||
|
selectedRoomJid: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSelectedRoomActivity({
|
||||||
|
active,
|
||||||
|
pollMs = 2000,
|
||||||
|
selectedRoomJid,
|
||||||
|
}: UseSelectedRoomActivityOptions): {
|
||||||
|
refreshRoom: (roomJid: string) => Promise<DashboardRoomActivity>;
|
||||||
|
roomActivity: RoomActivityMap;
|
||||||
|
roomActivityLoading: boolean;
|
||||||
|
} {
|
||||||
|
const [roomActivity, setRoomActivity] = useState<RoomActivityMap>({});
|
||||||
|
const [roomActivityLoading, setRoomActivityLoading] = useState(false);
|
||||||
|
|
||||||
|
const refreshRoom = useCallback(async (roomJid: string) => {
|
||||||
|
const activity = await fetchRoomTimeline(roomJid);
|
||||||
|
setRoomActivity((previous) => ({
|
||||||
|
...previous,
|
||||||
|
[roomJid]: activity,
|
||||||
|
}));
|
||||||
|
return activity;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!active || !selectedRoomJid) {
|
||||||
|
setRoomActivityLoading(false);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
let inFlight = false;
|
||||||
|
let pollIntervalId: number | null = null;
|
||||||
|
|
||||||
|
const fetchOnce = async (initial: boolean) => {
|
||||||
|
if (cancelled || inFlight) return;
|
||||||
|
inFlight = true;
|
||||||
|
if (initial) setRoomActivityLoading(true);
|
||||||
|
try {
|
||||||
|
await refreshRoom(selectedRoomJid);
|
||||||
|
} catch {
|
||||||
|
/* Keep the last good timeline snapshot. */
|
||||||
|
} finally {
|
||||||
|
inFlight = false;
|
||||||
|
if (!cancelled && initial) setRoomActivityLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void fetchOnce(true);
|
||||||
|
pollIntervalId = window.setInterval(() => void fetchOnce(false), pollMs);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
if (pollIntervalId !== null) window.clearInterval(pollIntervalId);
|
||||||
|
};
|
||||||
|
}, [active, pollMs, refreshRoom, selectedRoomJid]);
|
||||||
|
|
||||||
|
return { refreshRoom, roomActivity, roomActivityLoading };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user