Expose Overview IPC load failures inline
Overview previously treated rejected history and credential IPC calls like empty data, which made backend read failures indistinguishable from a clean first run. The page now keeps separate error flags, shows provider status as unreadable when credential status cannot load, and replaces history-derived metrics/recent rows with retryable inline failure states. Constraint: Issue #311 asks for per-card inline feedback without changing backend handler signatures or adding global toast UI.\nRejected: Global error boundary or toast | broader UI behavior than needed for the Overview failure path.\nConfidence: high\nScope-risk: narrow\nTested: cd openless-all/app && git diff --check && npm run build\nNot-tested: Live Tauri IPC failure injection.\nRelated: https://github.com/appergb/openless/issues/311
This commit is contained in:
1 parent
c4a7ebe62c
commit
d32da5c36c
6 files changed
+88
-22
No files matched your search
@@ -122,12 +122,15 @@ export const en: typeof zhCN = {
|
||||
llmNotConfigured: 'Not configured',
|
||||
statusConfigured: 'Configured',
|
||||
statusNotConfigured: 'Not configured',
|
||||
statusUnknown: 'Unavailable',
|
||||
credentialsLoadError: 'Could not read credential status',
|
||||
metricChars: 'Characters today',
|
||||
metricSegments: '{{count}} segments',
|
||||
metricDuration: 'Total duration today',
|
||||
metricAvg: 'Avg per segment',
|
||||
metricAvgTrend: "Today's average",
|
||||
metricNoData: 'No data',
|
||||
historyLoadError: 'History load failed',
|
||||
metricTotal: 'Total records',
|
||||
metricTotalTrend: 'Local archive (max 200)',
|
||||
weekTitle: 'Last 7 days',
|
||||
@@ -135,6 +138,8 @@ export const en: typeof zhCN = {
|
||||
recentTitle: 'Recent transcripts',
|
||||
recentAll: 'View all →',
|
||||
recentEmpty: 'No records yet. Press {{trigger}} to start your first recording.',
|
||||
recentLoadFailed: 'Could not load recent transcripts. Please retry.',
|
||||
historyRetry: 'Retry',
|
||||
weekDays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
||||
},
|
||||
history: {
|
||||
|
||||
@@ -124,12 +124,15 @@ export const ja: typeof zhCN = {
|
||||
llmNotConfigured: '未設定',
|
||||
statusConfigured: '設定済み',
|
||||
statusNotConfigured: '未設定',
|
||||
statusUnknown: '読み取れません',
|
||||
credentialsLoadError: '認証情報の状態を読み取れません',
|
||||
metricChars: '本日の文字数',
|
||||
metricSegments: '{{count}} セグメント',
|
||||
metricDuration: '本日の合計時間',
|
||||
metricAvg: '平均セグメント',
|
||||
metricAvgTrend: '本日の平均',
|
||||
metricNoData: 'データなし',
|
||||
historyLoadError: '履歴の読み込みに失敗',
|
||||
metricTotal: '累計記録',
|
||||
metricTotalTrend: 'ローカル保存(上限 200)',
|
||||
weekTitle: '直近 7 日',
|
||||
@@ -137,6 +140,8 @@ export const ja: typeof zhCN = {
|
||||
recentTitle: '最近の認識',
|
||||
recentAll: 'すべて表示 →',
|
||||
recentEmpty: '記録がありません。{{trigger}} を押して最初の録音を始めましょう。',
|
||||
recentLoadFailed: '最近の認識を読み込めません。再試行してください。',
|
||||
historyRetry: '再試行',
|
||||
weekDays: ['日', '月', '火', '水', '木', '金', '土'],
|
||||
},
|
||||
history: {
|
||||
|
||||
@@ -124,12 +124,15 @@ export const ko: typeof zhCN = {
|
||||
llmNotConfigured: '구성되지 않음',
|
||||
statusConfigured: '구성됨',
|
||||
statusNotConfigured: '구성되지 않음',
|
||||
statusUnknown: '읽을 수 없음',
|
||||
credentialsLoadError: '자격 증명 상태를 읽을 수 없습니다',
|
||||
metricChars: '오늘 글자 수',
|
||||
metricSegments: '{{count}} 세그먼트',
|
||||
metricDuration: '오늘 총 시간',
|
||||
metricAvg: '평균 세그먼트',
|
||||
metricAvgTrend: '오늘 평균',
|
||||
metricNoData: '데이터 없음',
|
||||
historyLoadError: '기록 로드 실패',
|
||||
metricTotal: '누적 기록',
|
||||
metricTotalTrend: '로컬 보관(상한 200)',
|
||||
weekTitle: '최근 7일',
|
||||
@@ -137,6 +140,8 @@ export const ko: typeof zhCN = {
|
||||
recentTitle: '최근 인식',
|
||||
recentAll: '전체 보기 →',
|
||||
recentEmpty: '아직 기록이 없습니다. {{trigger}} 를 눌러 첫 녹음을 시작하세요.',
|
||||
recentLoadFailed: '최근 인식 기록을 불러올 수 없습니다. 다시 시도해 주세요.',
|
||||
historyRetry: '다시 시도',
|
||||
weekDays: ['일', '월', '화', '수', '목', '금', '토'],
|
||||
},
|
||||
history: {
|
||||
|
||||
@@ -120,12 +120,15 @@ export const zhCN = {
|
||||
llmNotConfigured: '未配置',
|
||||
statusConfigured: '已配置',
|
||||
statusNotConfigured: '未配置',
|
||||
statusUnknown: '无法读取',
|
||||
credentialsLoadError: '无法读取凭据状态',
|
||||
metricChars: '今日字数',
|
||||
metricSegments: '{{count}} 段',
|
||||
metricDuration: '今日总时长',
|
||||
metricAvg: '平均段落',
|
||||
metricAvgTrend: '今日均值',
|
||||
metricNoData: '暂无数据',
|
||||
historyLoadError: '历史读取失败',
|
||||
metricTotal: '累计记录',
|
||||
metricTotalTrend: '本机存档 (上限 200)',
|
||||
weekTitle: '近 7 天',
|
||||
@@ -133,6 +136,8 @@ export const zhCN = {
|
||||
recentTitle: '最近识别',
|
||||
recentAll: '全部记录 →',
|
||||
recentEmpty: '还没有记录。按 {{trigger}} 开始第一次录音。',
|
||||
recentLoadFailed: '无法读取最近识别,请重试。',
|
||||
historyRetry: '重试',
|
||||
weekDays: ['日', '一', '二', '三', '四', '五', '六'],
|
||||
},
|
||||
history: {
|
||||
|
||||
@@ -122,12 +122,15 @@ export const zhTW: typeof zhCN = {
|
||||
llmNotConfigured: '未配置',
|
||||
statusConfigured: '已配置',
|
||||
statusNotConfigured: '未配置',
|
||||
statusUnknown: '無法讀取',
|
||||
credentialsLoadError: '無法讀取憑據狀態',
|
||||
metricChars: '今日字數',
|
||||
metricSegments: '{{count}} 段',
|
||||
metricDuration: '今日總時長',
|
||||
metricAvg: '平均段落',
|
||||
metricAvgTrend: '今日均值',
|
||||
metricNoData: '暫無數據',
|
||||
historyLoadError: '歷史讀取失敗',
|
||||
metricTotal: '累計記錄',
|
||||
metricTotalTrend: '本機存檔 (上限 200)',
|
||||
weekTitle: '近 7 天',
|
||||
@@ -135,6 +138,8 @@ export const zhTW: typeof zhCN = {
|
||||
recentTitle: '最近識別',
|
||||
recentAll: '全部記錄 →',
|
||||
recentEmpty: '還沒有記錄。按 {{trigger}} 開始第一次錄音。',
|
||||
recentLoadFailed: '無法讀取最近識別,請重試。',
|
||||
historyRetry: '重試',
|
||||
weekDays: ['日', '一', '二', '三', '四', '五', '六'],
|
||||
},
|
||||
history: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Overview.tsx — 真实指标,从 listHistory + getCredentials 派生。
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon } from '../components/Icon';
|
||||
import { formatComboLabel } from '../lib/hotkey';
|
||||
@@ -50,6 +50,8 @@ export function Overview({ onOpenHistory }: OverviewProps) {
|
||||
const { t } = useTranslation();
|
||||
const modeLabel = useModeLabels();
|
||||
const [history, setHistory] = useState<DictationSession[]>([]);
|
||||
const [historyError, setHistoryError] = useState(false);
|
||||
const [credsError, setCredsError] = useState(false);
|
||||
const [creds, setCreds] = useState<CredentialsStatus>({
|
||||
activeAsrProvider: 'volcengine',
|
||||
activeLlmProvider: 'ark',
|
||||
@@ -60,11 +62,29 @@ export function Overview({ onOpenHistory }: OverviewProps) {
|
||||
});
|
||||
const { prefs } = useHotkeySettings();
|
||||
|
||||
useEffect(() => {
|
||||
listHistory().then(setHistory);
|
||||
getCredentials().then(setCreds);
|
||||
const refreshHistory = useCallback(() => {
|
||||
setHistoryError(false);
|
||||
listHistory()
|
||||
.then(setHistory)
|
||||
.catch(error => {
|
||||
console.error('[overview] failed to load history', error);
|
||||
setHistoryError(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refreshHistory();
|
||||
getCredentials()
|
||||
.then(status => {
|
||||
setCreds(status);
|
||||
setCredsError(false);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('[overview] failed to load credentials status', error);
|
||||
setCredsError(true);
|
||||
});
|
||||
}, [refreshHistory]);
|
||||
|
||||
const metrics = useMemo(() => {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
@@ -138,21 +158,21 @@ export function Overview({ onOpenHistory }: OverviewProps) {
|
||||
kind={t('overview.asrKind')}
|
||||
name={asrProviderName}
|
||||
subname={asrProviderId}
|
||||
configured={creds.asrConfigured}
|
||||
status={credsError ? 'error' : creds.asrConfigured ? 'configured' : 'notConfigured'}
|
||||
/>
|
||||
<ProviderCard
|
||||
kind={t('overview.llmKind')}
|
||||
name={llmProviderName}
|
||||
subname={llmProviderId}
|
||||
configured={creds.llmConfigured}
|
||||
status={credsError ? 'error' : creds.llmConfigured ? 'configured' : 'notConfigured'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12, marginBottom: 18 }}>
|
||||
<Metric icon="hash" label={t('overview.metricChars')} value={metrics.charsToday.toLocaleString()} trend={t('overview.metricSegments', { count: metrics.segmentsToday })} />
|
||||
<Metric icon="mic" label={t('overview.metricDuration')} value={formatDuration(metrics.totalDurationMs, t)} trend="" />
|
||||
<Metric icon="clock" label={t('overview.metricAvg')} value={formatDuration(metrics.avgLatencyMs, t)} trend={metrics.segmentsToday > 0 ? t('overview.metricAvgTrend') : t('overview.metricNoData')} />
|
||||
<Metric icon="bolt" label={t('overview.metricTotal')} value={String(history.length)} trend={t('overview.metricTotalTrend')} accent />
|
||||
<Metric icon="hash" label={t('overview.metricChars')} value={historyError ? '—' : metrics.charsToday.toLocaleString()} trend={historyError ? t('overview.historyLoadError') : t('overview.metricSegments', { count: metrics.segmentsToday })} />
|
||||
<Metric icon="mic" label={t('overview.metricDuration')} value={historyError ? '—' : formatDuration(metrics.totalDurationMs, t)} trend={historyError ? t('overview.historyLoadError') : ''} />
|
||||
<Metric icon="clock" label={t('overview.metricAvg')} value={historyError ? '—' : formatDuration(metrics.avgLatencyMs, t)} trend={historyError ? t('overview.historyLoadError') : metrics.segmentsToday > 0 ? t('overview.metricAvgTrend') : t('overview.metricNoData')} />
|
||||
<Metric icon="bolt" label={t('overview.metricTotal')} value={historyError ? '—' : String(history.length)} trend={historyError ? t('overview.historyLoadError') : t('overview.metricTotalTrend')} accent />
|
||||
</div>
|
||||
|
||||
{/* 底部一行 = flex:1 撑满剩余高度(父 wrapper 是 display:flex/column)。
|
||||
@@ -164,7 +184,13 @@ export function Overview({ onOpenHistory }: OverviewProps) {
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--ol-ink-2)' }}>{t('overview.weekTitle')}</span>
|
||||
<span style={{ fontSize: 11, color: 'var(--ol-ink-4)' }}>{t('overview.weekUnit')}</span>
|
||||
</div>
|
||||
<WeekChart data={weekly} />
|
||||
{historyError ? (
|
||||
<div style={{ height: 100, display: 'flex', alignItems: 'center', justifyContent: 'center', textAlign: 'center', fontSize: 12, color: 'var(--ol-ink-4)' }}>
|
||||
{t('overview.historyLoadError')}
|
||||
</div>
|
||||
) : (
|
||||
<WeekChart data={weekly} />
|
||||
)}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 10, color: 'var(--ol-ink-4)', marginTop: 8 }}>
|
||||
{weekDayLabels(t('overview.weekDays', { returnObjects: true }) as string[]).map((d, i) => <span key={i}>{d}</span>)}
|
||||
</div>
|
||||
@@ -176,14 +202,23 @@ export function Overview({ onOpenHistory }: OverviewProps) {
|
||||
<Btn size="sm" variant="ghost" onClick={onOpenHistory}>{t('overview.recentAll')}</Btn>
|
||||
</div>
|
||||
<div className="ol-thinscroll" style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
|
||||
{history.length === 0 && (
|
||||
<div style={{ padding: 24, textAlign: 'center', fontSize: 12, color: 'var(--ol-ink-4)' }}>
|
||||
{t('overview.recentEmpty', { trigger: prefs ? formatComboLabel(prefs.dictationHotkey) : '' })}
|
||||
{historyError ? (
|
||||
<div style={{ padding: 24, textAlign: 'center', fontSize: 12, color: 'var(--ol-ink-4)', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10 }}>
|
||||
<span>{t('overview.recentLoadFailed')}</span>
|
||||
<Btn size="sm" variant="ghost" onClick={refreshHistory}>{t('overview.historyRetry')}</Btn>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{history.length === 0 && (
|
||||
<div style={{ padding: 24, textAlign: 'center', fontSize: 12, color: 'var(--ol-ink-4)' }}>
|
||||
{t('overview.recentEmpty', { trigger: prefs ? formatComboLabel(prefs.dictationHotkey) : '' })}
|
||||
</div>
|
||||
)}
|
||||
{history.slice(0, 5).map(s => (
|
||||
<RecentRow key={s.id} session={s} modeLabel={modeLabel} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{history.slice(0, 5).map(s => (
|
||||
<RecentRow key={s.id} session={s} modeLabel={modeLabel} />
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -195,10 +230,10 @@ interface ProviderCardProps {
|
||||
kind: string;
|
||||
name: string;
|
||||
subname: string;
|
||||
configured: boolean;
|
||||
status: 'configured' | 'notConfigured' | 'error';
|
||||
}
|
||||
|
||||
function ProviderCard({ kind, name, subname, configured }: ProviderCardProps) {
|
||||
function ProviderCard({ kind, name, subname, status }: ProviderCardProps) {
|
||||
const { t } = useTranslation();
|
||||
// ASR 卡用 mic 图标,其他用 sparkle —— 通过比较译文判断会随语言改变,故改用本地化无关的字面量比较。
|
||||
const isAsr = kind === t('overview.asrKind');
|
||||
@@ -217,17 +252,23 @@ function ProviderCard({ kind, name, subname, configured }: ProviderCardProps) {
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 2 }}>
|
||||
<span style={{ fontSize: 11, color: 'var(--ol-ink-4)', fontWeight: 600, letterSpacing: '.06em', textTransform: 'uppercase' }}>{kind}</span>
|
||||
{configured ? (
|
||||
{status === 'configured' && (
|
||||
<Pill tone="ok" size="sm">
|
||||
<span style={{ width: 5, height: 5, borderRadius: 999, background: 'var(--ol-ok)' }} />
|
||||
{t('overview.statusConfigured')}
|
||||
</Pill>
|
||||
) : (
|
||||
)}
|
||||
{status === 'notConfigured' && (
|
||||
<Pill tone="outline" size="sm">{t('overview.statusNotConfigured')}</Pill>
|
||||
)}
|
||||
{status === 'error' && (
|
||||
<Pill tone="outline" size="sm" style={{ color: 'var(--ol-red, #ef4444)', borderColor: 'rgba(239,68,68,0.24)' }}>{t('overview.statusUnknown')}</Pill>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--ol-ink)' }}>{name}</div>
|
||||
<div style={{ fontSize: 11.5, color: 'var(--ol-ink-3)', marginTop: 1, fontFamily: 'var(--ol-font-mono)' }}>{subname}</div>
|
||||
<div style={{ fontSize: 11.5, color: status === 'error' ? 'var(--ol-red, #ef4444)' : 'var(--ol-ink-3)', marginTop: 1, fontFamily: status === 'error' ? undefined : 'var(--ol-font-mono)' }}>
|
||||
{status === 'error' ? t('overview.credentialsLoadError') : subname}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user