Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions openless-all/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,19 +122,24 @@ 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',
weekUnit: 'count / day',
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: {
Expand Down
5 changes: 5 additions & 0 deletions openless-all/app/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,19 +124,24 @@ export const ja: typeof zhCN = {
llmNotConfigured: '未設定',
statusConfigured: '設定済み',
statusNotConfigured: '未設定',
statusUnknown: '読み取れません',
credentialsLoadError: '認証情報の状態を読み取れません',
metricChars: '本日の文字数',
metricSegments: '{{count}} セグメント',
metricDuration: '本日の合計時間',
metricAvg: '平均セグメント',
metricAvgTrend: '本日の平均',
metricNoData: 'データなし',
historyLoadError: '履歴の読み込みに失敗',
metricTotal: '累計記録',
metricTotalTrend: 'ローカル保存(上限 200)',
weekTitle: '直近 7 日',
weekUnit: '件 / 日',
recentTitle: '最近の認識',
recentAll: 'すべて表示 →',
recentEmpty: '記録がありません。{{trigger}} を押して最初の録音を始めましょう。',
recentLoadFailed: '最近の認識を読み込めません。再試行してください。',
historyRetry: '再試行',
weekDays: ['日', '月', '火', '水', '木', '金', '土'],
},
history: {
Expand Down
5 changes: 5 additions & 0 deletions openless-all/app/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,19 +124,24 @@ export const ko: typeof zhCN = {
llmNotConfigured: '구성되지 않음',
statusConfigured: '구성됨',
statusNotConfigured: '구성되지 않음',
statusUnknown: '읽을 수 없음',
credentialsLoadError: '자격 증명 상태를 읽을 수 없습니다',
metricChars: '오늘 글자 수',
metricSegments: '{{count}} 세그먼트',
metricDuration: '오늘 총 시간',
metricAvg: '평균 세그먼트',
metricAvgTrend: '오늘 평균',
metricNoData: '데이터 없음',
historyLoadError: '기록 로드 실패',
metricTotal: '누적 기록',
metricTotalTrend: '로컬 보관(상한 200)',
weekTitle: '최근 7일',
weekUnit: '건/일',
recentTitle: '최근 인식',
recentAll: '전체 보기 →',
recentEmpty: '아직 기록이 없습니다. {{trigger}} 를 눌러 첫 녹음을 시작하세요.',
recentLoadFailed: '최근 인식 기록을 불러올 수 없습니다. 다시 시도해 주세요.',
historyRetry: '다시 시도',
weekDays: ['일', '월', '화', '수', '목', '금', '토'],
},
history: {
Expand Down
5 changes: 5 additions & 0 deletions openless-all/app/src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,19 +120,24 @@ export const zhCN = {
llmNotConfigured: '未配置',
statusConfigured: '已配置',
statusNotConfigured: '未配置',
statusUnknown: '无法读取',
credentialsLoadError: '无法读取凭据状态',
metricChars: '今日字数',
metricSegments: '{{count}} 段',
metricDuration: '今日总时长',
metricAvg: '平均段落',
metricAvgTrend: '今日均值',
metricNoData: '暂无数据',
historyLoadError: '历史读取失败',
metricTotal: '累计记录',
metricTotalTrend: '本机存档 (上限 200)',
weekTitle: '近 7 天',
weekUnit: '条数 / 天',
recentTitle: '最近识别',
recentAll: '全部记录 →',
recentEmpty: '还没有记录。按 {{trigger}} 开始第一次录音。',
recentLoadFailed: '无法读取最近识别,请重试。',
historyRetry: '重试',
weekDays: ['日', '一', '二', '三', '四', '五', '六'],
},
history: {
Expand Down
5 changes: 5 additions & 0 deletions openless-all/app/src/i18n/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,19 +122,24 @@ export const zhTW: typeof zhCN = {
llmNotConfigured: '未配置',
statusConfigured: '已配置',
statusNotConfigured: '未配置',
statusUnknown: '無法讀取',
credentialsLoadError: '無法讀取憑據狀態',
metricChars: '今日字數',
metricSegments: '{{count}} 段',
metricDuration: '今日總時長',
metricAvg: '平均段落',
metricAvgTrend: '今日均值',
metricNoData: '暫無數據',
historyLoadError: '歷史讀取失敗',
metricTotal: '累計記錄',
metricTotalTrend: '本機存檔 (上限 200)',
weekTitle: '近 7 天',
weekUnit: '條數 / 天',
recentTitle: '最近識別',
recentAll: '全部記錄 →',
recentEmpty: '還沒有記錄。按 {{trigger}} 開始第一次錄音。',
recentLoadFailed: '無法讀取最近識別,請重試。',
historyRetry: '重試',
weekDays: ['日', '一', '二', '三', '四', '五', '六'],
},
history: {
Expand Down
85 changes: 63 additions & 22 deletions openless-all/app/src/pages/Overview.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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',
Expand All @@ -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);
Expand Down Expand Up @@ -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)。
Expand All @@ -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>
Expand All @@ -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>
Expand All @@ -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');
Expand All @@ -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>
);
Expand Down
Loading