release: ship v1.5.0 with on-device writing suggestions
Adds next-sentence suggestions while typing, weekly input insights and a personal phrase memory to the desktop app, and fixes custom instructions so they process the text instead of inserting the instruction's own wording. Local model requests are now bounded and individually cancellable. Bumps the product version to 1.5.0 (Android/iOS build 1050000), refreshes the landing and web download links, and records the new INPUT feature rows and the open verification gaps in the infrastructure map.
This commit is contained in:
parent
99f06c253c
commit
5c11ee2fde
104 changed files with 14410 additions and 174 deletions
|
|
@ -1,7 +1,7 @@
|
|||
// src/renderer/components/SettingsModal.tsx
|
||||
// 설계서 03: Settings React Modal — General(음성 모드+핫키)/Audio/STT/LLM 탭
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
|
|
@ -22,19 +22,21 @@ import {
|
|||
Button,
|
||||
Paper,
|
||||
} from '@mui/material'
|
||||
import { X, Keyboard, Mic, Lock, Cloud, RefreshCw, Play, Cpu, HelpCircle } from 'lucide-react'
|
||||
import { X, Keyboard, Mic, Lock, Cloud, RefreshCw, Play, Cpu, HelpCircle, TextCursorInput } from 'lucide-react'
|
||||
import { d3roPalette, d3roFontMono, d3roShadow, d3roRadius, d3roTypo } from '@d3ro/ui/theme'
|
||||
import { Led } from '@d3ro/ui/components/ds'
|
||||
import { LicenseTab } from './LicenseTab'
|
||||
import { CloudSyncSection } from './CloudSyncSection'
|
||||
import { STTTab } from './STTTab'
|
||||
import { InputConsentPanel } from './input-insights/InputConsentPanel'
|
||||
import { KeyBindingField } from './keybinding/KeyBindingField'
|
||||
import { asTranslationKey } from './keybinding/translation-key'
|
||||
import { useI18n, LOCALE_META } from '@d3ro/i18n'
|
||||
import type { Locale } from '@d3ro/i18n'
|
||||
import type { KeyBindingActionGroup } from '@d3ro/core/keybinding'
|
||||
import { KEYBINDING_ACTIONS } from '@d3ro/core/keybinding'
|
||||
import { auditKeyBindingMap, KEYBINDING_ACTIONS } from '@d3ro/core/keybinding'
|
||||
import type { ThemeMode, AppConfig, AudioDevice, LLMModel, LLMStatus } from '@d3ro/core/types'
|
||||
import { useKeyBindingMap } from '../hooks/useKeyBindingMap'
|
||||
|
||||
interface SettingsModalProps {
|
||||
open: boolean
|
||||
|
|
@ -57,9 +59,10 @@ function TabPanel({ children, value, index }: TabPanelProps): React.ReactElement
|
|||
const ACTION_GROUP_LABEL_KEYS: Readonly<Record<KeyBindingActionGroup, string>> = {
|
||||
voice: 'keybinding.ui.sectionVoice',
|
||||
window: 'keybinding.ui.sectionWindow',
|
||||
input: 'keybinding.ui.sectionInput',
|
||||
}
|
||||
|
||||
const ACTION_GROUP_ORDER: readonly KeyBindingActionGroup[] = ['voice', 'window']
|
||||
const ACTION_GROUP_ORDER: readonly KeyBindingActionGroup[] = ['voice', 'window', 'input']
|
||||
|
||||
export function SettingsModal({ open, initialTab = 0, onClose }: SettingsModalProps): React.ReactElement {
|
||||
const { t, locale, setLocale } = useI18n()
|
||||
|
|
@ -67,6 +70,11 @@ export function SettingsModal({ open, initialTab = 0, onClose }: SettingsModalPr
|
|||
const [config, setConfig] = useState<Partial<AppConfig>>({})
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [appVersion, setAppVersion] = useState<string | null>(null)
|
||||
const keybindingMap = useKeyBindingMap()
|
||||
const keybindingIssues = useMemo(
|
||||
() => (keybindingMap === null ? [] : auditKeyBindingMap(keybindingMap)),
|
||||
[keybindingMap]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (open && initialTab !== undefined) {
|
||||
|
|
@ -229,6 +237,7 @@ export function SettingsModal({ open, initialTab = 0, onClose }: SettingsModalPr
|
|||
<Tab label={t('settings.tabs.audio')} icon={<Mic size={14} />} iconPosition="start" />
|
||||
<Tab label={t('settings.tabs.stt')} />
|
||||
<Tab label={t('settings.tabs.llm')} />
|
||||
<Tab label={t('input.tab')} icon={<TextCursorInput size={14} />} iconPosition="start" />
|
||||
<Tab label={t('license.nav')} icon={<Lock size={14} />} iconPosition="start" />
|
||||
<Tab label={t('settings.tabs.cloud') ?? 'Cloud'} icon={<Cloud size={14} />} iconPosition="start" />
|
||||
<Tab label={t('settings.tabs.about')} />
|
||||
|
|
@ -259,6 +268,45 @@ export function SettingsModal({ open, initialTab = 0, onClose }: SettingsModalPr
|
|||
/>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
p: 1.25,
|
||||
borderRadius: d3roRadius.inner,
|
||||
border: `1px solid ${keybindingIssues.length === 0 ? d3roPalette.border.subtle : d3roPalette.status.warning}`,
|
||||
bgcolor: d3roPalette.bg.inset,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 0.5
|
||||
}}
|
||||
aria-live="polite"
|
||||
>
|
||||
<Typography sx={{ fontSize: d3roTypo.label.size, color: d3roPalette.text.secondary }}>
|
||||
{keybindingIssues.length === 0
|
||||
? t('keybinding.ui.auditClear')
|
||||
: t('keybinding.ui.auditIssues', { count: keybindingIssues.length })}
|
||||
</Typography>
|
||||
{keybindingIssues.slice(0, 3).map((issue) => {
|
||||
const action = KEYBINDING_ACTIONS.find((entry) => entry.id === issue.actionId)
|
||||
const actionLabel = action ? t(asTranslationKey(action.labelKey)) : issue.actionId
|
||||
const conflictLabels = issue.conflictActionIds
|
||||
.map((actionId) => {
|
||||
const conflict = KEYBINDING_ACTIONS.find((entry) => entry.id === actionId)
|
||||
return conflict ? t(asTranslationKey(conflict.labelKey)) : actionId
|
||||
})
|
||||
.join(', ')
|
||||
return (
|
||||
<Typography key={`${issue.actionId}-${issue.bindingIndex}-${issue.kind}`} sx={{ fontSize: d3roTypo.small.size, color: d3roPalette.text.secondary }}>
|
||||
{issue.kind === 'invalid'
|
||||
? t('keybinding.ui.auditInvalid', {
|
||||
action: actionLabel,
|
||||
reason: issue.reasonKey ? t(asTranslationKey(issue.reasonKey)) : t('keybinding.ui.auditUnknown')
|
||||
})
|
||||
: t('keybinding.ui.auditConflict', { action: actionLabel, conflicts: conflictLabels })}
|
||||
</Typography>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
|
||||
{ACTION_GROUP_ORDER.map((group) => {
|
||||
const actions = KEYBINDING_ACTIONS.filter((action) => action.group === group)
|
||||
if (actions.length === 0) return null
|
||||
|
|
@ -718,16 +766,20 @@ export function SettingsModal({ open, initialTab = 0, onClose }: SettingsModalPr
|
|||
|
||||
{/* ── 라이선스 탭 ────────────────────────────── */}
|
||||
<TabPanel value={activeTab} index={4}>
|
||||
<InputConsentPanel />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={activeTab} index={5}>
|
||||
<LicenseTab />
|
||||
</TabPanel>
|
||||
|
||||
{/* ── Cloud Sync 탭 ──────────────────────────── */}
|
||||
<TabPanel value={activeTab} index={5}>
|
||||
<TabPanel value={activeTab} index={6}>
|
||||
<CloudSyncSection />
|
||||
</TabPanel>
|
||||
|
||||
{/* ── 정보 탭 ──────────────────────────────── */}
|
||||
<TabPanel value={activeTab} index={6}>
|
||||
<TabPanel value={activeTab} index={7}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2.5 }}>
|
||||
<Typography variant="overline" sx={{ color: d3roPalette.text.label, letterSpacing: '1.5px' }}>
|
||||
D3RO-VOICE
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue