From d32da5c36cda29bdb72ae25a997adeae461201a7 Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Fri, 8 May 2026 07:25:38 +0800 Subject: [PATCH] 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 --- openless-all/app/src/i18n/en.ts | 5 ++ openless-all/app/src/i18n/ja.ts | 5 ++ openless-all/app/src/i18n/ko.ts | 5 ++ openless-all/app/src/i18n/zh-CN.ts | 5 ++ openless-all/app/src/i18n/zh-TW.ts | 5 ++ openless-all/app/src/pages/Overview.tsx | 85 ++++++++++++++++++------- 6 files changed, 88 insertions(+), 22 deletions(-) diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index 27c668c8..21a02056 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -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: { diff --git a/openless-all/app/src/i18n/ja.ts b/openless-all/app/src/i18n/ja.ts index d2870995..5763acfd 100644 --- a/openless-all/app/src/i18n/ja.ts +++ b/openless-all/app/src/i18n/ja.ts @@ -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: { diff --git a/openless-all/app/src/i18n/ko.ts b/openless-all/app/src/i18n/ko.ts index 916d3d2c..1394b458 100644 --- a/openless-all/app/src/i18n/ko.ts +++ b/openless-all/app/src/i18n/ko.ts @@ -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: { diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index e1b45682..658e5505 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -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: { diff --git a/openless-all/app/src/i18n/zh-TW.ts b/openless-all/app/src/i18n/zh-TW.ts index cbc3af95..42147ac1 100644 --- a/openless-all/app/src/i18n/zh-TW.ts +++ b/openless-all/app/src/i18n/zh-TW.ts @@ -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: { diff --git a/openless-all/app/src/pages/Overview.tsx b/openless-all/app/src/pages/Overview.tsx index 30b7c87f..8bde860c 100644 --- a/openless-all/app/src/pages/Overview.tsx +++ b/openless-all/app/src/pages/Overview.tsx @@ -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([]); + const [historyError, setHistoryError] = useState(false); + const [credsError, setCredsError] = useState(false); const [creds, setCreds] = useState({ 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'} />
- - - 0 ? t('overview.metricAvgTrend') : t('overview.metricNoData')} /> - + + + 0 ? t('overview.metricAvgTrend') : t('overview.metricNoData')} /> +
{/* 底部一行 = flex:1 撑满剩余高度(父 wrapper 是 display:flex/column)。 @@ -164,7 +184,13 @@ export function Overview({ onOpenHistory }: OverviewProps) { {t('overview.weekTitle')} {t('overview.weekUnit')} - + {historyError ? ( +
+ {t('overview.historyLoadError')} +
+ ) : ( + + )}
{weekDayLabels(t('overview.weekDays', { returnObjects: true }) as string[]).map((d, i) => {d})}
@@ -176,14 +202,23 @@ export function Overview({ onOpenHistory }: OverviewProps) { {t('overview.recentAll')}
- {history.length === 0 && ( -
- {t('overview.recentEmpty', { trigger: prefs ? formatComboLabel(prefs.dictationHotkey) : '' })} + {historyError ? ( +
+ {t('overview.recentLoadFailed')} + {t('overview.historyRetry')}
+ ) : ( + <> + {history.length === 0 && ( +
+ {t('overview.recentEmpty', { trigger: prefs ? formatComboLabel(prefs.dictationHotkey) : '' })} +
+ )} + {history.slice(0, 5).map(s => ( + + ))} + )} - {history.slice(0, 5).map(s => ( - - ))}
@@ -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) {
{kind} - {configured ? ( + {status === 'configured' && ( {t('overview.statusConfigured')} - ) : ( + )} + {status === 'notConfigured' && ( {t('overview.statusNotConfigured')} )} + {status === 'error' && ( + {t('overview.statusUnknown')} + )}
{name}
-
{subname}
+
+ {status === 'error' ? t('overview.credentialsLoadError') : subname} +
);