feat(ui): Midnight Glass v2 마감 — lucide 아이콘 통일 + 몰입 패널 웨이브 + 팝업 정합
- @mui/icons-material → lucide-react 전환 (23파일, 잔여 0) - VoiceRecordingPanel: 9바 DOM 웨이브 → GradientWave (canvas) - Vanilla 팝업 5종 style.css v2 정합 (글래스 네이비/헤어라인/블루, CSS 변수 주입 구조 유지 — 테마 반응형 보존)
This commit is contained in:
parent
a8a1d6e546
commit
082ed2ef02
30 changed files with 212 additions and 289 deletions
|
|
@ -4,10 +4,7 @@
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Box, Button, Stack, Alert, CircularProgress } from '@mui/material'
|
import { Box, Button, Stack, Alert, CircularProgress } from '@mui/material'
|
||||||
import CloudIcon from '@mui/icons-material/Cloud'
|
import { Cloud, CloudCheck, Globe, GitBranch } from 'lucide-react'
|
||||||
import CloudDoneIcon from '@mui/icons-material/CloudDone'
|
|
||||||
import GoogleIcon from '@mui/icons-material/Google'
|
|
||||||
import GitHubIcon from '@mui/icons-material/GitHub'
|
|
||||||
import { d3roPalette, typoSx } from '@d3ro/ui/theme'
|
import { d3roPalette, typoSx } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '@d3ro/i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
|
|
||||||
|
|
@ -128,9 +125,9 @@ export function CloudSyncSection(): React.ReactElement {
|
||||||
<Box sx={{ p: 3, bgcolor: d3roPalette.bg.elevated, borderRadius: 2 }}>
|
<Box sx={{ p: 3, bgcolor: d3roPalette.bg.elevated, borderRadius: 2 }}>
|
||||||
<Stack direction="row" alignItems="center" spacing={1.5} sx={{ mb: 2 }}>
|
<Stack direction="row" alignItems="center" spacing={1.5} sx={{ mb: 2 }}>
|
||||||
{state.authenticated ? (
|
{state.authenticated ? (
|
||||||
<CloudDoneIcon sx={{ color: d3roPalette.tag.green }} />
|
<CloudCheck size={24} style={{ color: d3roPalette.tag.green }} />
|
||||||
) : (
|
) : (
|
||||||
<CloudIcon sx={{ color: d3roPalette.text.label }} />
|
<Cloud size={24} style={{ color: d3roPalette.text.label }} />
|
||||||
)}
|
)}
|
||||||
<Box sx={{ ...typoSx('heading'), color: d3roPalette.text.primary }}>Cloud Sync</Box>
|
<Box sx={{ ...typoSx('heading'), color: d3roPalette.text.primary }}>Cloud Sync</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
@ -141,7 +138,7 @@ export function CloudSyncSection(): React.ReactElement {
|
||||||
<Stack direction="row" spacing={1}>
|
<Stack direction="row" spacing={1}>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
startIcon={<GoogleIcon />}
|
startIcon={<Globe size={18} />}
|
||||||
onClick={() => void handleSignIn('google')}
|
onClick={() => void handleSignIn('google')}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
>
|
>
|
||||||
|
|
@ -149,7 +146,7 @@ export function CloudSyncSection(): React.ReactElement {
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
startIcon={<GitHubIcon />}
|
startIcon={<GitBranch size={18} />}
|
||||||
onClick={() => void handleSignIn('github')}
|
onClick={() => void handleSignIn('github')}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,7 @@
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
import { Box, LinearProgress, IconButton, Tooltip } from '@mui/material'
|
import { Box, LinearProgress, IconButton, Tooltip } from '@mui/material'
|
||||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
import { Copy, X, FileUp } from 'lucide-react'
|
||||||
import CloseIcon from '@mui/icons-material/Close'
|
|
||||||
import UploadFileIcon from '@mui/icons-material/UploadFile'
|
|
||||||
import { MetalCard, PhosphorText, Led } from '@d3ro/ui/components/ds'
|
import { MetalCard, PhosphorText, Led } from '@d3ro/ui/components/ds'
|
||||||
import { d3roPalette, d3roTypo } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roTypo } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '@d3ro/i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
|
|
@ -136,7 +134,7 @@ export function FileDropZone(): React.ReactElement {
|
||||||
'&:hover': { borderColor: d3roPalette.accent.amber },
|
'&:hover': { borderColor: d3roPalette.accent.amber },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<UploadFileIcon sx={{ fontSize: 40, color: d3roPalette.text.inactive, mb: 1 }} />
|
<FileUp size={40} style={{ color: d3roPalette.text.inactive, marginBottom: 8 }} />
|
||||||
<PhosphorText variant="body" sx={{ color: d3roPalette.text.secondary }}>
|
<PhosphorText variant="body" sx={{ color: d3roPalette.text.secondary }}>
|
||||||
{t('fileTranscription.dropZone')}
|
{t('fileTranscription.dropZone')}
|
||||||
</PhosphorText>
|
</PhosphorText>
|
||||||
|
|
@ -216,11 +214,11 @@ export function FileDropZone(): React.ReactElement {
|
||||||
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
||||||
<Tooltip title={copied ? 'Copied!' : t('fileTranscription.copyAll')}>
|
<Tooltip title={copied ? 'Copied!' : t('fileTranscription.copyAll')}>
|
||||||
<IconButton size="small" onClick={handleCopy}>
|
<IconButton size="small" onClick={handleCopy}>
|
||||||
<ContentCopyIcon sx={{ fontSize: 16, color: copied ? d3roPalette.accent.amber : d3roPalette.text.inactive }} />
|
<Copy size={16} style={{ color: copied ? d3roPalette.accent.amber : d3roPalette.text.inactive }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<IconButton size="small" onClick={handleReset}>
|
<IconButton size="small" onClick={handleReset}>
|
||||||
<CloseIcon sx={{ fontSize: 16, color: d3roPalette.text.inactive }} />
|
<X size={16} style={{ color: d3roPalette.text.inactive }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,7 @@ import {
|
||||||
Divider,
|
Divider,
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import CloseIcon from '@mui/icons-material/Close'
|
import { X, CircleCheck, XCircle } from 'lucide-react'
|
||||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel'
|
|
||||||
import { MetalCard, PhosphorText, Led, PhysicalButton } from '@d3ro/ui/components/ds'
|
import { MetalCard, PhosphorText, Led, PhysicalButton } from '@d3ro/ui/components/ds'
|
||||||
import { d3roPalette, d3roTypo, d3roRadius, d3roShadow, d3roFontMono } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roTypo, d3roRadius, d3roShadow, d3roFontMono } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '@d3ro/i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
|
|
@ -61,7 +59,7 @@ export function LicenseModal({ open, onClose }: LicenseModalProps): React.ReactE
|
||||||
>
|
>
|
||||||
<PhosphorText variant="meta">{t('license.title')}</PhosphorText>
|
<PhosphorText variant="meta">{t('license.title')}</PhosphorText>
|
||||||
<IconButton size="small" onClick={onClose} sx={{ color: d3roPalette.text.inactive }}>
|
<IconButton size="small" onClick={onClose} sx={{ color: d3roPalette.text.inactive }}>
|
||||||
<CloseIcon sx={{ fontSize: 18 }} />
|
<X size={18} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
|
||||||
|
|
@ -325,10 +323,10 @@ function UpgradeButton({ tier, t, onUpgrade }: { tier: 'pro' | 'pro_plus'; t: (k
|
||||||
|
|
||||||
function TierCell({ value }: { value: boolean | string }): React.ReactElement {
|
function TierCell({ value }: { value: boolean | string }): React.ReactElement {
|
||||||
if (value === true) {
|
if (value === true) {
|
||||||
return <CheckCircleIcon sx={{ fontSize: 14, color: d3roPalette.tag.green }} />
|
return <CircleCheck size={14} style={{ color: d3roPalette.tag.green }} />
|
||||||
}
|
}
|
||||||
if (value === false) {
|
if (value === false) {
|
||||||
return <CancelIcon sx={{ fontSize: 14, color: d3roPalette.text.disabled }} />
|
return <XCircle size={14} style={{ color: d3roPalette.text.disabled }} />
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<PhosphorText variant="dim" sx={{ fontSize: d3roTypo.small.size, color: d3roPalette.accent.amber }}>
|
<PhosphorText variant="dim" sx={{ fontSize: d3roTypo.small.size, color: d3roPalette.accent.amber }}>
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,7 @@ import {
|
||||||
Divider,
|
Divider,
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
import { CircleCheck, XCircle } from 'lucide-react'
|
||||||
import CancelIcon from '@mui/icons-material/Cancel'
|
|
||||||
import { Led, PhosphorText, PhysicalButton, MetalCard } from '@d3ro/ui/components/ds'
|
import { Led, PhosphorText, PhysicalButton, MetalCard } from '@d3ro/ui/components/ds'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '@d3ro/i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
|
|
@ -316,10 +315,10 @@ function ComparisonTable({ t, comparison }: { t: (k: string) => string; comparis
|
||||||
|
|
||||||
function TierCell({ value }: { value: boolean | string }): React.ReactElement {
|
function TierCell({ value }: { value: boolean | string }): React.ReactElement {
|
||||||
if (value === true) {
|
if (value === true) {
|
||||||
return <CheckCircleIcon sx={{ fontSize: 14, color: d3roPalette.tag.green }} />
|
return <CircleCheck size={14} style={{ color: d3roPalette.tag.green }} />
|
||||||
}
|
}
|
||||||
if (value === false) {
|
if (value === false) {
|
||||||
return <CancelIcon sx={{ fontSize: 14, color: d3roPalette.text.disabled }} />
|
return <XCircle size={14} style={{ color: d3roPalette.text.disabled }} />
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<PhosphorText variant="dim" sx={{ fontSize: d3roTypo.small.size, color: d3roPalette.accent.amber }}>
|
<PhosphorText variant="dim" sx={{ fontSize: d3roTypo.small.size, color: d3roPalette.accent.amber }}>
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@ import {
|
||||||
Divider,
|
Divider,
|
||||||
IconButton,
|
IconButton,
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import CloseIcon from '@mui/icons-material/Close'
|
import { X, ExternalLink, Copy } from 'lucide-react'
|
||||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
|
||||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
|
||||||
import { d3roPalette, d3roFontMono, d3roShadow } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roShadow } from '@d3ro/ui/theme'
|
||||||
import { Led } from '@d3ro/ui/components/ds'
|
import { Led } from '@d3ro/ui/components/ds'
|
||||||
import { useI18n } from '@d3ro/i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
|
|
@ -46,7 +44,7 @@ function CodeBlock({ children }: { children: string }): React.ReactElement {
|
||||||
onClick={() => navigator.clipboard.writeText(children)}
|
onClick={() => navigator.clipboard.writeText(children)}
|
||||||
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}
|
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}
|
||||||
>
|
>
|
||||||
<ContentCopyIcon sx={{ fontSize: 14 }} />
|
<Copy size={14} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|
@ -88,7 +86,7 @@ export function OllamaGuideModal({ open, onClose }: OllamaGuideModalProps): Reac
|
||||||
>
|
>
|
||||||
{t('ollama.title')}
|
{t('ollama.title')}
|
||||||
<IconButton onClick={onClose} size="small" sx={{ color: d3roPalette.text.inactive }}>
|
<IconButton onClick={onClose} size="small" sx={{ color: d3roPalette.text.inactive }}>
|
||||||
<CloseIcon sx={{ fontSize: 18 }} />
|
<X size={18} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<Divider sx={{ borderColor: d3roPalette.border.subtle }} />
|
<Divider sx={{ borderColor: d3roPalette.border.subtle }} />
|
||||||
|
|
@ -108,7 +106,7 @@ export function OllamaGuideModal({ open, onClose }: OllamaGuideModalProps): Reac
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
endIcon={<OpenInNewIcon sx={{ fontSize: 14 }} />}
|
endIcon={<ExternalLink size={14} />}
|
||||||
onClick={() => handleOpenLink('https://ollama.com/download')}
|
onClick={() => handleOpenLink('https://ollama.com/download')}
|
||||||
sx={{ fontFamily: d3roFontMono, fontSize: '11px' }}
|
sx={{ fontFamily: d3roFontMono, fontSize: '11px' }}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,7 @@ import {
|
||||||
Box,
|
Box,
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'
|
import { CheckCircle2, AlertCircle, CloudDownload } from 'lucide-react'
|
||||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'
|
|
||||||
import CloudDownloadIcon from '@mui/icons-material/CloudDownload'
|
|
||||||
import { d3roPalette, d3roRadius, typoSx } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roRadius, typoSx } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '@d3ro/i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
|
|
||||||
|
|
@ -281,11 +279,11 @@ export function OnboardingModal({ open, onClose }: OnboardingModalProps): React.
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isSuccess ? (
|
{isSuccess ? (
|
||||||
<CheckCircleOutlineIcon sx={{ color: colorSuccess }} />
|
<CheckCircle2 size={24} style={{ color: colorSuccess }} />
|
||||||
) : isFailed ? (
|
) : isFailed ? (
|
||||||
<ErrorOutlineIcon sx={{ color: colorDanger }} />
|
<AlertCircle size={24} style={{ color: colorDanger }} />
|
||||||
) : (
|
) : (
|
||||||
<CloudDownloadIcon sx={{ color: colorAccent }} />
|
<CloudDownload size={24} style={{ color: colorAccent }} />
|
||||||
)}
|
)}
|
||||||
{t('onboarding.title')}
|
{t('onboarding.title')}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
// shows lock overlay with PRO badge if locked.
|
// shows lock overlay with PRO badge if locked.
|
||||||
|
|
||||||
import { Box, Typography } from '@mui/material'
|
import { Box, Typography } from '@mui/material'
|
||||||
import LockIcon from '@mui/icons-material/Lock'
|
import { Lock } from 'lucide-react'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme'
|
||||||
import { useProFeature } from '../hooks/useProFeature'
|
import { useProFeature } from '../hooks/useProFeature'
|
||||||
import { useI18n } from '@d3ro/i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
|
|
@ -59,7 +59,7 @@ export function ProBadge({ feature, children }: ProBadgeProps): React.ReactEleme
|
||||||
border: `1px solid ${d3roPalette.border.default}`,
|
border: `1px solid ${d3roPalette.border.default}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LockIcon sx={{ fontSize: 14, color: d3roPalette.accent.amber }} />
|
<Lock size={14} style={{ color: d3roPalette.accent.amber }} />
|
||||||
<Typography
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
fontFamily: d3roFontMono,
|
fontFamily: d3roFontMono,
|
||||||
|
|
|
||||||
|
|
@ -24,14 +24,7 @@ import {
|
||||||
Stack,
|
Stack,
|
||||||
Paper,
|
Paper,
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import CloseIcon from '@mui/icons-material/Close'
|
import { X, Keyboard, Pencil, Mic, Lock, Cloud } from 'lucide-react'
|
||||||
import KeyboardIcon from '@mui/icons-material/Keyboard'
|
|
||||||
import EditIcon from '@mui/icons-material/Edit'
|
|
||||||
import MicIcon from '@mui/icons-material/Mic'
|
|
||||||
import LockIcon from '@mui/icons-material/Lock'
|
|
||||||
import CloudIcon from '@mui/icons-material/Cloud'
|
|
||||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel'
|
|
||||||
import { d3roPalette, d3roFontMono, d3roShadow } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roShadow } from '@d3ro/ui/theme'
|
||||||
import { HotkeyRecordModal } from './HotkeyRecordModal'
|
import { HotkeyRecordModal } from './HotkeyRecordModal'
|
||||||
import { formatHotkeyLabel, formatHotkeySegments } from '../utils/format-hotkey'
|
import { formatHotkeyLabel, formatHotkeySegments } from '../utils/format-hotkey'
|
||||||
|
|
@ -100,7 +93,7 @@ function HotkeyDisplay({
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
<IconButton size="small" onClick={onEdit} sx={{ color: d3roPalette.text.inactive, ml: 'auto' }}>
|
<IconButton size="small" onClick={onEdit} sx={{ color: d3roPalette.text.inactive, ml: 'auto' }}>
|
||||||
<EditIcon sx={{ fontSize: 16 }} />
|
<Pencil size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|
@ -342,7 +335,7 @@ export function SettingsModal({ open, onClose }: SettingsModalProps): React.Reac
|
||||||
>
|
>
|
||||||
{t('settings.title')}
|
{t('settings.title')}
|
||||||
<IconButton onClick={onClose} size="small" sx={{ color: d3roPalette.text.inactive }}>
|
<IconButton onClick={onClose} size="small" sx={{ color: d3roPalette.text.inactive }}>
|
||||||
<CloseIcon sx={{ fontSize: 18 }} />
|
<X size={18} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<Divider sx={{ borderColor: d3roPalette.border.subtle }} />
|
<Divider sx={{ borderColor: d3roPalette.border.subtle }} />
|
||||||
|
|
@ -370,12 +363,12 @@ export function SettingsModal({ open, onClose }: SettingsModalProps): React.Reac
|
||||||
'& .MuiTabs-indicator': { bgcolor: d3roPalette.accent.amber, height: 2 },
|
'& .MuiTabs-indicator': { bgcolor: d3roPalette.accent.amber, height: 2 },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tab label={t('settings.tabs.general')} icon={<KeyboardIcon sx={{ fontSize: 14 }} />} iconPosition="start" />
|
<Tab label={t('settings.tabs.general')} icon={<Keyboard size={14} />} iconPosition="start" />
|
||||||
<Tab label={t('settings.tabs.audio')} icon={<MicIcon sx={{ fontSize: 14 }} />} iconPosition="start" />
|
<Tab label={t('settings.tabs.audio')} icon={<Mic size={14} />} iconPosition="start" />
|
||||||
<Tab label={t('settings.tabs.stt')} />
|
<Tab label={t('settings.tabs.stt')} />
|
||||||
<Tab label={t('settings.tabs.llm')} />
|
<Tab label={t('settings.tabs.llm')} />
|
||||||
<Tab label={t('license.nav')} icon={<LockIcon sx={{ fontSize: 14 }} />} iconPosition="start" />
|
<Tab label={t('license.nav')} icon={<Lock size={14} />} iconPosition="start" />
|
||||||
<Tab label={t('settings.tabs.cloud') ?? 'Cloud'} icon={<CloudIcon sx={{ fontSize: 14 }} />} iconPosition="start" />
|
<Tab label={t('settings.tabs.cloud') ?? 'Cloud'} icon={<Cloud size={14} />} iconPosition="start" />
|
||||||
<Tab label={t('settings.tabs.about')} />
|
<Tab label={t('settings.tabs.about')} />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ p: 3 }}>
|
||||||
|
|
@ -531,7 +524,7 @@ export function SettingsModal({ open, onClose }: SettingsModalProps): React.Reac
|
||||||
setSelectedDeviceId(deviceId)
|
setSelectedDeviceId(deviceId)
|
||||||
window.electronAPI.audio.setSelectedDevice({ deviceId })
|
window.electronAPI.audio.setSelectedDevice({ deviceId })
|
||||||
}}
|
}}
|
||||||
startAdornment={<MicIcon sx={{ color: d3roPalette.text.inactive, mr: 1, fontSize: 18 }} />}
|
startAdornment={<Mic size={18} style={{ color: d3roPalette.text.inactive, marginRight: 8 }} />}
|
||||||
>
|
>
|
||||||
{audioDevices.map((device, idx) => (
|
{audioDevices.map((device, idx) => (
|
||||||
<MenuItem key={`${device.deviceId}-${idx}`} value={device.deviceId}>
|
<MenuItem key={`${device.deviceId}-${idx}`} value={device.deviceId}>
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,7 @@
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { Box, Button, IconButton, Dialog, DialogTitle, DialogContent, DialogActions, TextField, Tooltip } from '@mui/material'
|
import { Box, Button, IconButton, Dialog, DialogTitle, DialogContent, DialogActions, TextField, Tooltip } from '@mui/material'
|
||||||
import AddIcon from '@mui/icons-material/Add'
|
import { Plus, Trash2, Pencil, Play } from 'lucide-react'
|
||||||
import DeleteIcon from '@mui/icons-material/Delete'
|
|
||||||
import EditIcon from '@mui/icons-material/Edit'
|
|
||||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow'
|
|
||||||
import { MetalCard, PhosphorText, Led, PhysicalButton } from '@d3ro/ui/components/ds'
|
import { MetalCard, PhosphorText, Led, PhysicalButton } from '@d3ro/ui/components/ds'
|
||||||
import { PageHeader, EmptyStateCard } from './shared'
|
import { PageHeader, EmptyStateCard } from './shared'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme'
|
||||||
|
|
@ -128,7 +125,7 @@ export function TemplateSection(): React.ReactElement {
|
||||||
title={t('template.title').toUpperCase()}
|
title={t('template.title').toUpperCase()}
|
||||||
action={
|
action={
|
||||||
<PhysicalButton size="small" onClick={openCreate}>
|
<PhysicalButton size="small" onClick={openCreate}>
|
||||||
<AddIcon sx={{ fontSize: 14, mr: 0.5 }} /> {t('template.create')}
|
<Plus size={14} style={{ marginRight: 4 }} /> {t('template.create')}
|
||||||
</PhysicalButton>
|
</PhysicalButton>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
@ -182,7 +179,7 @@ export function TemplateSection(): React.ReactElement {
|
||||||
disabled={!!session}
|
disabled={!!session}
|
||||||
sx={{ color: d3roPalette.accent.amber, '&:hover': { opacity: 0.8 } }}
|
sx={{ color: d3roPalette.accent.amber, '&:hover': { opacity: 0.8 } }}
|
||||||
>
|
>
|
||||||
<PlayArrowIcon sx={{ fontSize: 18 }} />
|
<Play size={18} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|
@ -190,7 +187,7 @@ export function TemplateSection(): React.ReactElement {
|
||||||
onClick={() => openEdit(tmpl)}
|
onClick={() => openEdit(tmpl)}
|
||||||
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}
|
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}
|
||||||
>
|
>
|
||||||
<EditIcon sx={{ fontSize: 16 }} />
|
<Pencil size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{!tmpl.isBuiltin && (
|
{!tmpl.isBuiltin && (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|
@ -198,7 +195,7 @@ export function TemplateSection(): React.ReactElement {
|
||||||
onClick={() => handleDelete(tmpl.id)}
|
onClick={() => handleDelete(tmpl.id)}
|
||||||
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}
|
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}
|
||||||
>
|
>
|
||||||
<DeleteIcon sx={{ fontSize: 16 }} />
|
<Trash2 size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -260,12 +257,12 @@ export function TemplateSection(): React.ReactElement {
|
||||||
disabled={formFields.length <= 1}
|
disabled={formFields.length <= 1}
|
||||||
sx={{ color: d3roPalette.text.inactive }}
|
sx={{ color: d3roPalette.text.inactive }}
|
||||||
>
|
>
|
||||||
<DeleteIcon sx={{ fontSize: 16 }} />
|
<Trash2 size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<Button onClick={addField} startIcon={<AddIcon />} size="small" sx={{ alignSelf: 'flex-start' }}>
|
<Button onClick={addField} startIcon={<Plus size={16} />} size="small" sx={{ alignSelf: 'flex-start' }}>
|
||||||
{t('template.addField')}
|
{t('template.addField')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,7 @@ import {
|
||||||
Box,
|
Box,
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import LockIcon from '@mui/icons-material/Lock'
|
import { Lock, CheckCircle2 } from 'lucide-react'
|
||||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'
|
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius, d3roShadow } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius, d3roShadow } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '@d3ro/i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
import type { UpgradePromptEvent, UsageQuota } from '@d3ro/core/types'
|
import type { UpgradePromptEvent, UsageQuota } from '@d3ro/core/types'
|
||||||
|
|
@ -80,7 +79,7 @@ export function UpgradePromptModal(): React.ReactElement {
|
||||||
color: d3roPalette.accent.amber,
|
color: d3roPalette.accent.amber,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LockIcon sx={{ fontSize: 20 }} />
|
<Lock size={20} />
|
||||||
{isQuota
|
{isQuota
|
||||||
? t('license.quotaExceeded.title', { feature: featureLabel })
|
? t('license.quotaExceeded.title', { feature: featureLabel })
|
||||||
: isLoginRequired
|
: isLoginRequired
|
||||||
|
|
@ -134,9 +133,7 @@ export function UpgradePromptModal(): React.ReactElement {
|
||||||
key={benefit}
|
key={benefit}
|
||||||
sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.5 }}
|
sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.5 }}
|
||||||
>
|
>
|
||||||
<CheckCircleOutlineIcon
|
<CheckCircle2 size={14} style={{ color: d3roPalette.tag.green }} />
|
||||||
sx={{ fontSize: 14, color: d3roPalette.tag.green }}
|
|
||||||
/>
|
|
||||||
<Typography
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
fontFamily: d3roFontMono,
|
fontFamily: d3roFontMono,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import { useCallback, useRef } from 'react'
|
import { useCallback, useRef } from 'react'
|
||||||
import { Box } from '@mui/material'
|
import { Box } from '@mui/material'
|
||||||
import DeleteIcon from '@mui/icons-material/Delete'
|
import { Trash2 } from 'lucide-react'
|
||||||
import { MarkdownEditor } from './MarkdownEditor'
|
import { MarkdownEditor } from './MarkdownEditor'
|
||||||
import { ExportMenu } from './ExportMenu'
|
import { ExportMenu } from './ExportMenu'
|
||||||
import { PhysicalButton } from '@d3ro/ui/components/ds'
|
import { PhysicalButton } from '@d3ro/ui/components/ds'
|
||||||
|
|
@ -80,7 +80,7 @@ export function DocumentTab({
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
sx={{ height: 32, fontSize: d3roTypo.engrave.size }}
|
sx={{ height: 32, fontSize: d3roTypo.engrave.size }}
|
||||||
>
|
>
|
||||||
<DeleteIcon sx={{ fontSize: 14, mr: 0.5 }} />
|
<Trash2 size={14} style={{ marginRight: 4 }} />
|
||||||
{t('common.delete')}
|
{t('common.delete')}
|
||||||
</PhysicalButton>
|
</PhysicalButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import { useState, useCallback } from 'react'
|
import { useState, useCallback } from 'react'
|
||||||
import { Menu, MenuItem } from '@mui/material'
|
import { Menu, MenuItem } from '@mui/material'
|
||||||
import FileDownloadIcon from '@mui/icons-material/FileDownload'
|
import { Download } from 'lucide-react'
|
||||||
import { PhysicalButton } from '@d3ro/ui/components/ds'
|
import { PhysicalButton } from '@d3ro/ui/components/ds'
|
||||||
import { d3roPalette, d3roTypo, d3roFontMono, d3roRadius, d3roShadow } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roTypo, d3roFontMono, d3roRadius, d3roShadow } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '@d3ro/i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
|
|
@ -54,7 +54,7 @@ export function ExportMenu({ onExport, onCopyToClipboard }: ExportMenuProps): Re
|
||||||
onClick={handleOpen}
|
onClick={handleOpen}
|
||||||
sx={{ height: 32, fontSize: d3roTypo.engrave.size }}
|
sx={{ height: 32, fontSize: d3roTypo.engrave.size }}
|
||||||
>
|
>
|
||||||
<FileDownloadIcon sx={{ fontSize: 14, mr: 0.5 }} />
|
<Download size={14} style={{ marginRight: 4 }} />
|
||||||
{t('meeting.exportFormat')}
|
{t('meeting.exportFormat')}
|
||||||
</PhysicalButton>
|
</PhysicalButton>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,7 @@
|
||||||
|
|
||||||
import { useState, useCallback, useEffect, useRef, useMemo } from 'react'
|
import { useState, useCallback, useEffect, useRef, useMemo } from 'react'
|
||||||
import { Box, TextField, IconButton, Tooltip } from '@mui/material'
|
import { Box, TextField, IconButton, Tooltip } from '@mui/material'
|
||||||
import SendIcon from '@mui/icons-material/Send'
|
import { Send, Trash2, ChevronUp, ChevronDown } from 'lucide-react'
|
||||||
import DeleteSweepIcon from '@mui/icons-material/DeleteSweep'
|
|
||||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess'
|
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
|
||||||
import { PhosphorText } from '@d3ro/ui/components/ds'
|
import { PhosphorText } from '@d3ro/ui/components/ds'
|
||||||
import { PhysicalButton } from '@d3ro/ui/components/ds'
|
import { PhysicalButton } from '@d3ro/ui/components/ds'
|
||||||
import { isImeComposingEvent } from '../../utils/keyboard'
|
import { isImeComposingEvent } from '../../utils/keyboard'
|
||||||
|
|
@ -187,16 +184,16 @@ export function MeetingChatPanel({ sessionId }: MeetingChatPanelProps): React.Re
|
||||||
onClick={handleClear}
|
onClick={handleClear}
|
||||||
disabled={messages.length === 0 && !streaming}
|
disabled={messages.length === 0 && !streaming}
|
||||||
>
|
>
|
||||||
<DeleteSweepIcon sx={{ fontSize: 15, color: d3roPalette.text.inactive }} />
|
<Trash2 size={15} style={{ color: d3roPalette.text.inactive }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip title={collapsed ? t('meeting.chatExpand') : t('meeting.chatCollapse')}>
|
<Tooltip title={collapsed ? t('meeting.chatExpand') : t('meeting.chatCollapse')}>
|
||||||
<IconButton size="small" onClick={handleToggleCollapse}>
|
<IconButton size="small" onClick={handleToggleCollapse}>
|
||||||
{collapsed ? (
|
{collapsed ? (
|
||||||
<ExpandLessIcon sx={{ fontSize: 15, color: d3roPalette.text.inactive }} />
|
<ChevronUp size={15} style={{ color: d3roPalette.text.inactive }} />
|
||||||
) : (
|
) : (
|
||||||
<ExpandMoreIcon sx={{ fontSize: 15, color: d3roPalette.text.inactive }} />
|
<ChevronDown size={15} style={{ color: d3roPalette.text.inactive }} />
|
||||||
)}
|
)}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
@ -408,7 +405,7 @@ export function MeetingChatPanel({ sessionId }: MeetingChatPanelProps): React.Re
|
||||||
disabled={!inputValue.trim() || streaming}
|
disabled={!inputValue.trim() || streaming}
|
||||||
sx={{ flexShrink: 0, height: 36, minWidth: 36, px: 1 }}
|
sx={{ flexShrink: 0, height: 36, minWidth: 36, px: 1 }}
|
||||||
>
|
>
|
||||||
<SendIcon sx={{ fontSize: 14 }} />
|
<Send size={14} />
|
||||||
</PhysicalButton>
|
</PhysicalButton>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,7 @@ import {
|
||||||
TextField,
|
TextField,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
|
import { ArrowLeft, Plus, Pencil, Check } from 'lucide-react'
|
||||||
import AddIcon from '@mui/icons-material/Add'
|
|
||||||
import EditIcon from '@mui/icons-material/Edit'
|
|
||||||
import CheckIcon from '@mui/icons-material/Check'
|
|
||||||
import { TranscriptTab } from './TranscriptTab'
|
import { TranscriptTab } from './TranscriptTab'
|
||||||
import { DocumentTab } from './DocumentTab'
|
import { DocumentTab } from './DocumentTab'
|
||||||
import { AddDocumentDialog } from './AddDocumentDialog'
|
import { AddDocumentDialog } from './AddDocumentDialog'
|
||||||
|
|
@ -231,7 +228,7 @@ export function MeetingDetailTabs({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconButton size="small" onClick={onBack}>
|
<IconButton size="small" onClick={onBack}>
|
||||||
<ArrowBackIcon sx={{ fontSize: 18, color: d3roPalette.text.secondary }} />
|
<ArrowLeft size={18} style={{ color: d3roPalette.text.secondary }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
{editingTitle ? (
|
{editingTitle ? (
|
||||||
|
|
@ -251,7 +248,7 @@ export function MeetingDetailTabs({
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<IconButton size="small" onClick={handleSaveTitle}>
|
<IconButton size="small" onClick={handleSaveTitle}>
|
||||||
<CheckIcon sx={{ fontSize: 16, color: d3roPalette.accent.amber }} />
|
<Check size={16} style={{ color: d3roPalette.accent.amber }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -264,7 +261,7 @@ export function MeetingDetailTabs({
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => { setEditingTitle(true); setTitleDraft(detail.title ?? '') }}
|
onClick={() => { setEditingTitle(true); setTitleDraft(detail.title ?? '') }}
|
||||||
>
|
>
|
||||||
<EditIcon sx={{ fontSize: 15, color: d3roPalette.text.inactive }} />
|
<Pencil size={15} style={{ color: d3roPalette.text.inactive }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
|
|
@ -303,7 +300,7 @@ export function MeetingDetailTabs({
|
||||||
<Tab key={doc.id} label={doc.title ?? t('meeting.untitled')} />
|
<Tab key={doc.id} label={doc.title ?? t('meeting.untitled')} />
|
||||||
))}
|
))}
|
||||||
<Tab
|
<Tab
|
||||||
icon={<AddIcon sx={{ fontSize: 16 }} />}
|
icon={<Plus size={16} />}
|
||||||
label={t('meeting.addDocument')}
|
label={t('meeting.addDocument')}
|
||||||
iconPosition="start"
|
iconPosition="start"
|
||||||
sx={{ minWidth: 120 }}
|
sx={{ minWidth: 120 }}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,7 @@ import { PhysicalButton } from '@d3ro/ui/components/ds'
|
||||||
import { PhosphorText } from '@d3ro/ui/components/ds'
|
import { PhosphorText } from '@d3ro/ui/components/ds'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '@d3ro/i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
import FileDownloadIcon from '@mui/icons-material/FileDownload'
|
import { Download, WandSparkles, Speech } from 'lucide-react'
|
||||||
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'
|
|
||||||
import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver'
|
|
||||||
|
|
||||||
interface TranscriptTabProps {
|
interface TranscriptTabProps {
|
||||||
sessionId: string
|
sessionId: string
|
||||||
|
|
@ -129,7 +127,7 @@ export function TranscriptTab({
|
||||||
disabled={polishing || diarizing}
|
disabled={polishing || diarizing}
|
||||||
sx={{ height: 30, fontSize: d3roTypo.engrave.size }}
|
sx={{ height: 30, fontSize: d3roTypo.engrave.size }}
|
||||||
>
|
>
|
||||||
<AutoFixHighIcon sx={{ fontSize: 13, mr: 0.5 }} />
|
<WandSparkles size={13} style={{ marginRight: 4 }} />
|
||||||
{polishing ? t('meeting.polishing') : t('meeting.polish')}
|
{polishing ? t('meeting.polishing') : t('meeting.polish')}
|
||||||
</PhysicalButton>
|
</PhysicalButton>
|
||||||
{onDiarize && (
|
{onDiarize && (
|
||||||
|
|
@ -139,7 +137,7 @@ export function TranscriptTab({
|
||||||
disabled={polishing || diarizing}
|
disabled={polishing || diarizing}
|
||||||
sx={{ height: 30, fontSize: d3roTypo.engrave.size }}
|
sx={{ height: 30, fontSize: d3roTypo.engrave.size }}
|
||||||
>
|
>
|
||||||
<RecordVoiceOverIcon sx={{ fontSize: 13, mr: 0.5 }} />
|
<Speech size={13} style={{ marginRight: 4 }} />
|
||||||
{diarizing ? t('meeting.diarizing') : t('meeting.diarize')}
|
{diarizing ? t('meeting.diarizing') : t('meeting.diarize')}
|
||||||
</PhysicalButton>
|
</PhysicalButton>
|
||||||
)}
|
)}
|
||||||
|
|
@ -148,7 +146,7 @@ export function TranscriptTab({
|
||||||
onClick={handleDownloadTxt}
|
onClick={handleDownloadTxt}
|
||||||
sx={{ height: 30, fontSize: d3roTypo.engrave.size }}
|
sx={{ height: 30, fontSize: d3roTypo.engrave.size }}
|
||||||
>
|
>
|
||||||
<FileDownloadIcon sx={{ fontSize: 13, mr: 0.5 }} />
|
<Download size={13} style={{ marginRight: 4 }} />
|
||||||
{t('meeting.downloadTranscript')}
|
{t('meeting.downloadTranscript')}
|
||||||
</PhysicalButton>
|
</PhysicalButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,7 @@
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { Box, IconButton, Tooltip, Chip } from '@mui/material'
|
import { Box, IconButton, Tooltip, Chip } from '@mui/material'
|
||||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
import { Copy, Trash2, Tag, X, ScrollText, ChevronDown } from 'lucide-react'
|
||||||
import DeleteIcon from '@mui/icons-material/Delete'
|
|
||||||
import LocalOfferIcon from '@mui/icons-material/LocalOffer'
|
|
||||||
import CloseIcon from '@mui/icons-material/Close'
|
|
||||||
import SummarizeIcon from '@mui/icons-material/Summarize'
|
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
|
||||||
import { MetalCard, Led } from '@d3ro/ui/components/ds'
|
import { MetalCard, Led } from '@d3ro/ui/components/ds'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '@d3ro/i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
|
|
@ -149,7 +144,7 @@ export function HistoryEntryCard({ entry, onCopy, onDelete, showTags = false, on
|
||||||
<span>{entry.mode.toUpperCase()}</span>
|
<span>{entry.mode.toUpperCase()}</span>
|
||||||
{hasSummary && (
|
{hasSummary && (
|
||||||
<Chip
|
<Chip
|
||||||
icon={<SummarizeIcon sx={{ fontSize: '12px !important' }} />}
|
icon={<ScrollText size={12} />}
|
||||||
label={t('meetingSummary.title')}
|
label={t('meetingSummary.title')}
|
||||||
size="small"
|
size="small"
|
||||||
onClick={handleToggleSummary}
|
onClick={handleToggleSummary}
|
||||||
|
|
@ -176,7 +171,7 @@ export function HistoryEntryCard({ entry, onCopy, onDelete, showTags = false, on
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => onTagClick?.(tag.tag)}
|
onClick={() => onTagClick?.(tag.tag)}
|
||||||
onDelete={() => handleRemoveTag(tag.tag)}
|
onDelete={() => handleRemoveTag(tag.tag)}
|
||||||
deleteIcon={<CloseIcon sx={{ fontSize: '12px !important' }} />}
|
deleteIcon={<X size={12} />}
|
||||||
sx={{
|
sx={{
|
||||||
height: 20,
|
height: 20,
|
||||||
fontFamily: d3roFontMono,
|
fontFamily: d3roFontMono,
|
||||||
|
|
@ -219,7 +214,7 @@ export function HistoryEntryCard({ entry, onCopy, onDelete, showTags = false, on
|
||||||
onClick={() => setShowTagInput(true)}
|
onClick={() => setShowTagInput(true)}
|
||||||
sx={{ p: 0.25, color: d3roPalette.text.muted, '&:hover': { color: d3roPalette.accent.amber } }}
|
sx={{ p: 0.25, color: d3roPalette.text.muted, '&:hover': { color: d3roPalette.accent.amber } }}
|
||||||
>
|
>
|
||||||
<LocalOfferIcon sx={{ fontSize: 14 }} />
|
<Tag size={14} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
@ -234,7 +229,7 @@ export function HistoryEntryCard({ entry, onCopy, onDelete, showTags = false, on
|
||||||
onClick={() => onCopy(displayText)}
|
onClick={() => onCopy(displayText)}
|
||||||
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}
|
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}
|
||||||
>
|
>
|
||||||
<ContentCopyIcon sx={{ fontSize: 16 }} />
|
<Copy size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
@ -245,7 +240,7 @@ export function HistoryEntryCard({ entry, onCopy, onDelete, showTags = false, on
|
||||||
onClick={() => onDelete(entry.id)}
|
onClick={() => onDelete(entry.id)}
|
||||||
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}
|
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}
|
||||||
>
|
>
|
||||||
<DeleteIcon sx={{ fontSize: 16 }} />
|
<Trash2 size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
@ -290,7 +285,7 @@ export function HistoryEntryCard({ entry, onCopy, onDelete, showTags = false, on
|
||||||
<Box sx={{ display: 'flex', gap: 1, mt: 1 }}>
|
<Box sx={{ display: 'flex', gap: 1, mt: 1 }}>
|
||||||
<Tooltip title={t('meetingSummary.exportMarkdown')} arrow>
|
<Tooltip title={t('meetingSummary.exportMarkdown')} arrow>
|
||||||
<IconButton size="small" onClick={handleExportSummary} sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}>
|
<IconButton size="small" onClick={handleExportSummary} sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}>
|
||||||
<SummarizeIcon sx={{ fontSize: 14 }} />
|
<ScrollText size={14} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -324,7 +319,7 @@ export function HistoryEntryCard({ entry, onCopy, onDelete, showTags = false, on
|
||||||
sx={{ mt: 1, textAlign: 'center', cursor: 'pointer', color: d3roPalette.text.muted, '&:hover': { color: d3roPalette.accent.amber } }}
|
sx={{ mt: 1, textAlign: 'center', cursor: 'pointer', color: d3roPalette.text.muted, '&:hover': { color: d3roPalette.accent.amber } }}
|
||||||
onClick={handleToggleSummary}
|
onClick={handleToggleSummary}
|
||||||
>
|
>
|
||||||
<ExpandMoreIcon sx={{ fontSize: 16 }} />
|
<ChevronDown size={16} />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</MetalCard>
|
</MetalCard>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// 공유: 모노 폰트 검색 입력 필드
|
// 공유: 모노 폰트 검색 입력 필드
|
||||||
|
|
||||||
import { TextField, InputAdornment } from '@mui/material'
|
import { TextField, InputAdornment } from '@mui/material'
|
||||||
import SearchIcon from '@mui/icons-material/Search'
|
import { Search } from 'lucide-react'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme'
|
||||||
|
|
||||||
interface SearchInputProps {
|
interface SearchInputProps {
|
||||||
|
|
@ -30,7 +30,7 @@ export function SearchInput({ value, onChange, placeholder }: SearchInputProps):
|
||||||
input: {
|
input: {
|
||||||
startAdornment: (
|
startAdornment: (
|
||||||
<InputAdornment position="start">
|
<InputAdornment position="start">
|
||||||
<SearchIcon sx={{ color: d3roPalette.text.inactive, fontSize: 18 }} />
|
<Search size={18} style={{ color: d3roPalette.text.inactive }} />
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,20 @@
|
||||||
// src/renderer/components/voice-conversation/VoiceRecordingPanel.tsx
|
// src/renderer/components/voice-conversation/VoiceRecordingPanel.tsx
|
||||||
// Phase 5 Part 6: Voice Conversation 몰입 녹음 패널
|
// Voice Conversation 몰입 녹음 패널 (리디자인 v2)
|
||||||
// recording-tip 팝업의 9바 waveform (cos 분포, 100ms, smoothing)을 React로 포팅.
|
// 기존 9바 DOM waveform을 DS의 GradientWave(canvas 스펙트럼 웨이브)로 교체.
|
||||||
// 기존 구현: src/renderer/popups/recording-tip/script.js (Vanilla JS, 팝업 전용)
|
|
||||||
// 스펙: docs/design/03-db-and-ui.md:428+
|
|
||||||
//
|
//
|
||||||
// AudioLevelSource:
|
// AudioLevelSource:
|
||||||
// - VoiceConversationService가 listening 상태에서만 AUDIO_LEVEL 채널로 forwarding.
|
// - VoiceConversationService가 listening 상태에서만 AUDIO_LEVEL 채널로 forwarding.
|
||||||
// - level 값은 0.0~1.0. 구독자가 없는 thinking/speaking에는 Panel 자체가 언마운트되므로
|
// - level 값은 0.0~1.0. 구독자가 없는 thinking/speaking에는 Panel 자체가 언마운트되므로
|
||||||
// useEffect cleanup으로 subscription이 자동 해제됨.
|
// useEffect cleanup으로 subscription이 자동 해제됨.
|
||||||
|
// - 레벨 스무딩/유휴 진동은 GradientWave 내부에서 처리하므로 여기서는 raw level만 전달.
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { Box } from '@mui/material'
|
import { Box } from '@mui/material'
|
||||||
import { Led, PhosphorText } from '@d3ro/ui/components/ds'
|
import { GradientWave, Led, PhosphorText } from '@d3ro/ui/components/ds'
|
||||||
import { d3roPalette, d3roFontMono } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '@d3ro/i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
|
|
||||||
// ── Waveform 상수 (recording-tip/script.js 수치 그대로) ──
|
const WAVE_HEIGHT = 48
|
||||||
const BAR_COUNT = 9
|
|
||||||
const UPDATE_INTERVAL = 100
|
|
||||||
const MIN_HEIGHT = 2
|
|
||||||
const MAX_HEIGHT = 28
|
|
||||||
const SMOOTHING = 0.5
|
|
||||||
const RANDOM_FACTOR = 0.35
|
|
||||||
|
|
||||||
/** 코사인 분포 가중치 (중앙이 가장 높음). 설계서 03. */
|
|
||||||
const weights: readonly number[] = Array.from({ length: BAR_COUNT }, (_, i) => {
|
|
||||||
const center = (BAR_COUNT - 1) / 2
|
|
||||||
const normalized = (i - center) / center
|
|
||||||
return Math.cos((normalized * Math.PI) / 2)
|
|
||||||
})
|
|
||||||
|
|
||||||
function formatDuration(ms: number): string {
|
function formatDuration(ms: number): string {
|
||||||
const elapsed = Math.floor(ms / 1000)
|
const elapsed = Math.floor(ms / 1000)
|
||||||
|
|
@ -39,59 +25,34 @@ function formatDuration(ms: number): string {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Voice Conversation이 listening 상태일 때만 렌더되는 몰입 녹음 패널.
|
* Voice Conversation이 listening 상태일 때만 렌더되는 몰입 녹음 패널.
|
||||||
* - 9바 phosphor waveform (audio-level 기반)
|
* - GradientWave 스펙트럼 웨이브 (audio-level 기반, 유휴 애니메이션 내장)
|
||||||
* - REC LED + elapsed 타이머
|
* - REC LED + elapsed 타이머
|
||||||
* - "SPEAK NOW · PRESS ⏹ TO SEND" 힌트 (i18n)
|
* - "SPEAK NOW · PRESS ⏹ TO SEND" 힌트 (i18n)
|
||||||
*/
|
*/
|
||||||
export function VoiceRecordingPanel(): React.ReactElement {
|
export function VoiceRecordingPanel(): React.ReactElement {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const [heights, setHeights] = useState<number[]>(() =>
|
const [audioLevel, setAudioLevel] = useState(0)
|
||||||
new Array(BAR_COUNT).fill(MIN_HEIGHT),
|
|
||||||
)
|
|
||||||
const [elapsedMs, setElapsedMs] = useState(0)
|
const [elapsedMs, setElapsedMs] = useState(0)
|
||||||
|
|
||||||
const heightsRef = useRef<number[]>(new Array(BAR_COUNT).fill(MIN_HEIGHT))
|
|
||||||
const audioLevelRef = useRef(0)
|
|
||||||
const startedAtRef = useRef<number>(Date.now())
|
const startedAtRef = useRef<number>(Date.now())
|
||||||
|
|
||||||
// audio-level 구독 (VoiceConversationService → AUDIO_LEVEL)
|
// audio-level 구독 (VoiceConversationService → AUDIO_LEVEL)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = window.electronAPI.voiceConversation.onAudioLevel((e) => {
|
const unsubscribe = window.electronAPI.voiceConversation.onAudioLevel((e) => {
|
||||||
audioLevelRef.current = e.level
|
setAudioLevel(e.level)
|
||||||
})
|
})
|
||||||
return unsubscribe
|
return unsubscribe
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 9바 애니메이션 루프 (100ms interval)
|
// 경과 시간 타이머
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
startedAtRef.current = Date.now()
|
startedAtRef.current = Date.now()
|
||||||
heightsRef.current = new Array(BAR_COUNT).fill(MIN_HEIGHT)
|
setElapsedMs(0)
|
||||||
audioLevelRef.current = 0
|
|
||||||
|
|
||||||
const animInterval = window.setInterval(() => {
|
|
||||||
const level = audioLevelRef.current
|
|
||||||
// 최소 진동: audioLevel이 0이어도 바가 미세하게 움직여 "살아있음" 표현
|
|
||||||
const effectiveLevel = Math.max(0.08, level)
|
|
||||||
const next = new Array(BAR_COUNT).fill(MIN_HEIGHT) as number[]
|
|
||||||
for (let i = 0; i < BAR_COUNT; i++) {
|
|
||||||
const baseTarget = effectiveLevel * MAX_HEIGHT * weights[i]
|
|
||||||
const randomized = baseTarget * (1 + (Math.random() - 0.5) * 2 * RANDOM_FACTOR)
|
|
||||||
const target = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, randomized))
|
|
||||||
// 스무딩 보간
|
|
||||||
const prev = heightsRef.current[i]
|
|
||||||
const smoothed = prev + (target - prev) * SMOOTHING
|
|
||||||
heightsRef.current[i] = smoothed
|
|
||||||
next[i] = Math.round(smoothed)
|
|
||||||
}
|
|
||||||
setHeights(next)
|
|
||||||
}, UPDATE_INTERVAL)
|
|
||||||
|
|
||||||
const durationInterval = window.setInterval(() => {
|
const durationInterval = window.setInterval(() => {
|
||||||
setElapsedMs(Date.now() - startedAtRef.current)
|
setElapsedMs(Date.now() - startedAtRef.current)
|
||||||
}, 500)
|
}, 500)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.clearInterval(animInterval)
|
|
||||||
window.clearInterval(durationInterval)
|
window.clearInterval(durationInterval)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
@ -111,7 +72,7 @@ export function VoiceRecordingPanel(): React.ReactElement {
|
||||||
{/* REC LED + 타이머 */}
|
{/* REC LED + 타이머 */}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||||
<Led color="red" pulse size={10} />
|
<Led color="red" pulse size={10} />
|
||||||
<PhosphorText variant="label" sx={{ color: d3roPalette.accent.amber }}>
|
<PhosphorText variant="label" sx={{ color: d3roPalette.accent.main }}>
|
||||||
{t('conversation.state.recording')}
|
{t('conversation.state.recording')}
|
||||||
</PhosphorText>
|
</PhosphorText>
|
||||||
<Box
|
<Box
|
||||||
|
|
@ -119,7 +80,7 @@ export function VoiceRecordingPanel(): React.ReactElement {
|
||||||
ml: 1,
|
ml: 1,
|
||||||
fontFamily: d3roFontMono,
|
fontFamily: d3roFontMono,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: d3roPalette.accent.amber,
|
color: d3roPalette.accent.light,
|
||||||
fontVariantNumeric: 'tabular-nums',
|
fontVariantNumeric: 'tabular-nums',
|
||||||
minWidth: 40,
|
minWidth: 40,
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
|
|
@ -129,30 +90,27 @@ export function VoiceRecordingPanel(): React.ReactElement {
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 9바 waveform */}
|
{/* 그라디언트 스펙트럼 웨이브 */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
width: '100%',
|
||||||
alignItems: 'center',
|
maxWidth: 320,
|
||||||
justifyContent: 'center',
|
px: 2,
|
||||||
gap: '4px',
|
py: 1,
|
||||||
height: `${MAX_HEIGHT + 4}px`,
|
borderRadius: 2,
|
||||||
|
border: `1px solid ${d3roPalette.glass.hairline}`,
|
||||||
|
bgcolor: d3roPalette.glass.surface,
|
||||||
}}
|
}}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
>
|
>
|
||||||
{heights.map((h, i) => (
|
<GradientWave
|
||||||
<Box
|
audioLevel={audioLevel}
|
||||||
key={i}
|
idleAmplitude={0.2}
|
||||||
sx={{
|
barWidth={3}
|
||||||
width: '4px',
|
barGap={3}
|
||||||
height: `${h}px`,
|
glow={8}
|
||||||
bgcolor: d3roPalette.accent.amber,
|
height={WAVE_HEIGHT}
|
||||||
borderRadius: '2px',
|
/>
|
||||||
transition: 'height 100ms ease-out',
|
|
||||||
boxShadow: `0 0 6px ${d3roPalette.accent.amberGlow}`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 힌트 */}
|
{/* 힌트 */}
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,7 @@
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { Box, Button, IconButton, Dialog, DialogTitle, DialogContent, DialogActions, TextField, Chip, Select, MenuItem, FormControl, InputLabel } from '@mui/material'
|
import { Box, Button, IconButton, Dialog, DialogTitle, DialogContent, DialogActions, TextField, Chip, Select, MenuItem, FormControl, InputLabel } from '@mui/material'
|
||||||
import AddIcon from '@mui/icons-material/Add'
|
import { Plus, Trash2, Pencil, CircleCheck, Play, Link } from 'lucide-react'
|
||||||
import DeleteIcon from '@mui/icons-material/Delete'
|
|
||||||
import EditIcon from '@mui/icons-material/Edit'
|
|
||||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
|
||||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow'
|
|
||||||
import LinkIcon from '@mui/icons-material/Link'
|
|
||||||
import { MetalCard, PhosphorText, Led, ScreenPanel } from '@d3ro/ui/components/ds'
|
import { MetalCard, PhosphorText, Led, ScreenPanel } from '@d3ro/ui/components/ds'
|
||||||
import { PageHeader, EmptyStateCard } from '../components/shared'
|
import { PageHeader, EmptyStateCard } from '../components/shared'
|
||||||
import { isImeComposingEvent } from '../utils/keyboard'
|
import { isImeComposingEvent } from '../utils/keyboard'
|
||||||
|
|
@ -131,7 +126,7 @@ export function CommandsPage(): React.ReactElement {
|
||||||
title={t('commands.title').toUpperCase()}
|
title={t('commands.title').toUpperCase()}
|
||||||
count={t('commands.count', { count: instructions.length }).toUpperCase()}
|
count={t('commands.count', { count: instructions.length }).toUpperCase()}
|
||||||
action={
|
action={
|
||||||
<Button variant="contained" startIcon={<AddIcon />} onClick={openAdd} size="small">
|
<Button variant="contained" startIcon={<Plus size={16} />} onClick={openAdd} size="small">
|
||||||
{t('commands.add').toUpperCase()}
|
{t('commands.add').toUpperCase()}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|
@ -183,7 +178,7 @@ export function CommandsPage(): React.ReactElement {
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flex: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flex: 1 }}>
|
||||||
{isActive ? (
|
{isActive ? (
|
||||||
<CheckCircleIcon sx={{ fontSize: 16, color: d3roPalette.accent.amber }} />
|
<CircleCheck size={16} style={{ color: d3roPalette.accent.amber }} />
|
||||||
) : (
|
) : (
|
||||||
<Led color={inst.isBuiltin ? 'amber' : 'green'} size={6} />
|
<Led color={inst.isBuiltin ? 'amber' : 'green'} size={6} />
|
||||||
)}
|
)}
|
||||||
|
|
@ -210,7 +205,7 @@ export function CommandsPage(): React.ReactElement {
|
||||||
onClick={() => openEdit(inst)}
|
onClick={() => openEdit(inst)}
|
||||||
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}
|
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}
|
||||||
>
|
>
|
||||||
<EditIcon sx={{ fontSize: 16 }} />
|
<Pencil size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{!inst.isBuiltin && (
|
{!inst.isBuiltin && (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|
@ -218,7 +213,7 @@ export function CommandsPage(): React.ReactElement {
|
||||||
onClick={() => handleDelete(inst.id)}
|
onClick={() => handleDelete(inst.id)}
|
||||||
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}
|
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}
|
||||||
>
|
>
|
||||||
<DeleteIcon sx={{ fontSize: 16 }} />
|
<Trash2 size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -360,7 +355,7 @@ function VoiceKeywordsSection({ instructions }: { instructions: CustomInstructio
|
||||||
onClick={() => { setEditingRule(rule.instructionId); setKeywordInput('') }}
|
onClick={() => { setEditingRule(rule.instructionId); setKeywordInput('') }}
|
||||||
sx={{ p: 0.25, color: d3roPalette.text.muted, '&:hover': { color: d3roPalette.accent.amber } }}
|
sx={{ p: 0.25, color: d3roPalette.text.muted, '&:hover': { color: d3roPalette.accent.amber } }}
|
||||||
>
|
>
|
||||||
<AddIcon sx={{ fontSize: 14 }} />
|
<Plus size={14} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -456,7 +451,7 @@ function ChainSection({ instructions }: { instructions: CustomInstruction[] }):
|
||||||
{t('chain.title').toUpperCase()}
|
{t('chain.title').toUpperCase()}
|
||||||
</PhosphorText>
|
</PhosphorText>
|
||||||
<Box sx={{ flex: 1, height: '1px', bgcolor: d3roPalette.border.subtle }} />
|
<Box sx={{ flex: 1, height: '1px', bgcolor: d3roPalette.border.subtle }} />
|
||||||
<Button variant="outlined" startIcon={<LinkIcon />} onClick={openAdd} size="small" sx={{ fontSize: d3roTypo.micro.size }}>
|
<Button variant="outlined" startIcon={<Link size={16} />} onClick={openAdd} size="small" sx={{ fontSize: d3roTypo.micro.size }}>
|
||||||
{t('chain.add').toUpperCase()}
|
{t('chain.add').toUpperCase()}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -497,15 +492,15 @@ function ChainSection({ instructions }: { instructions: CustomInstruction[] }):
|
||||||
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
||||||
<IconButton size="small" onClick={() => handleExecute(chain.id)}
|
<IconButton size="small" onClick={() => handleExecute(chain.id)}
|
||||||
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.green } }}>
|
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.green } }}>
|
||||||
<PlayArrowIcon sx={{ fontSize: 16 }} />
|
<Play size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton size="small" onClick={() => openEdit(chain)}
|
<IconButton size="small" onClick={() => openEdit(chain)}
|
||||||
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}>
|
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}>
|
||||||
<EditIcon sx={{ fontSize: 16 }} />
|
<Pencil size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton size="small" onClick={() => handleDelete(chain.id)}
|
<IconButton size="small" onClick={() => handleDelete(chain.id)}
|
||||||
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}>
|
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}>
|
||||||
<DeleteIcon sx={{ fontSize: 16 }} />
|
<Trash2 size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -563,12 +558,12 @@ function ChainSection({ instructions }: { instructions: CustomInstruction[] }):
|
||||||
)}
|
)}
|
||||||
<IconButton size="small" onClick={() => removeStep(idx)} disabled={formSteps.length <= 1}
|
<IconButton size="small" onClick={() => removeStep(idx)} disabled={formSteps.length <= 1}
|
||||||
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}>
|
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}>
|
||||||
<DeleteIcon sx={{ fontSize: 16 }} />
|
<Trash2 size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<Button onClick={addStep} startIcon={<AddIcon />} size="small" sx={{ mt: 1 }}>
|
<Button onClick={addStep} startIcon={<Plus size={16} />} size="small" sx={{ mt: 1 }}>
|
||||||
{t('chain.addStep')}
|
{t('chain.addStep')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,7 @@
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { Box, TextField, Button, IconButton, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material'
|
import { Box, TextField, Button, IconButton, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material'
|
||||||
import AddIcon from '@mui/icons-material/Add'
|
import { Plus, Trash2, Pencil } from 'lucide-react'
|
||||||
import DeleteIcon from '@mui/icons-material/Delete'
|
|
||||||
import EditIcon from '@mui/icons-material/Edit'
|
|
||||||
import { MetalCard, PhosphorText } from '@d3ro/ui/components/ds'
|
import { MetalCard, PhosphorText } from '@d3ro/ui/components/ds'
|
||||||
import { PageHeader, SearchInput, EmptyStateCard } from '../components/shared'
|
import { PageHeader, SearchInput, EmptyStateCard } from '../components/shared'
|
||||||
import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme'
|
||||||
|
|
@ -73,7 +71,7 @@ export function DictionaryPage(): React.ReactElement {
|
||||||
title={t('dictionary.title').toUpperCase()}
|
title={t('dictionary.title').toUpperCase()}
|
||||||
count={t('dictionary.words', { count: data?.total ?? 0 }).toUpperCase()}
|
count={t('dictionary.words', { count: data?.total ?? 0 }).toUpperCase()}
|
||||||
action={
|
action={
|
||||||
<Button variant="contained" startIcon={<AddIcon />} onClick={openAdd} size="small">
|
<Button variant="contained" startIcon={<Plus size={16} />} onClick={openAdd} size="small">
|
||||||
{t('dictionary.add').toUpperCase()}
|
{t('dictionary.add').toUpperCase()}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|
@ -114,11 +112,11 @@ export function DictionaryPage(): React.ReactElement {
|
||||||
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
||||||
<IconButton size="small" onClick={() => openEdit(entry)}
|
<IconButton size="small" onClick={() => openEdit(entry)}
|
||||||
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}>
|
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}>
|
||||||
<EditIcon sx={{ fontSize: 16 }} />
|
<Pencil size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton size="small" onClick={() => { window.electronAPI.dictionary.delete({ id: entry.id }); loadData() }}
|
<IconButton size="small" onClick={() => { window.electronAPI.dictionary.delete({ id: entry.id }); loadData() }}
|
||||||
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}>
|
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}>
|
||||||
<DeleteIcon sx={{ fontSize: 16 }} />
|
<Trash2 size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||||
import { Box, Chip, IconButton, Tooltip } from '@mui/material'
|
import { Box, Chip, IconButton, Tooltip } from '@mui/material'
|
||||||
import FileDownloadIcon from '@mui/icons-material/FileDownload'
|
import { Download } from 'lucide-react'
|
||||||
import { PhosphorText } from '@d3ro/ui/components/ds'
|
import { PhosphorText } from '@d3ro/ui/components/ds'
|
||||||
import { d3roPalette, d3roTypo, d3roFontMono, d3roRadius } from '@d3ro/ui/theme'
|
import { d3roPalette, d3roTypo, d3roFontMono, d3roRadius } from '@d3ro/ui/theme'
|
||||||
import { useI18n } from '@d3ro/i18n'
|
import { useI18n } from '@d3ro/i18n'
|
||||||
|
|
@ -96,7 +96,7 @@ export function HistoryPage(): React.ReactElement {
|
||||||
onClick={handleExport}
|
onClick={handleExport}
|
||||||
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}
|
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}
|
||||||
>
|
>
|
||||||
<FileDownloadIcon sx={{ fontSize: 18 }} />
|
<Download size={18} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<PhosphorText variant="meta" sx={{ color: d3roPalette.text.dimLabel }}>
|
<PhosphorText variant="meta" sx={{ color: d3roPalette.text.dimLabel }}>
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,7 @@
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { Box, TextField, IconButton, Tooltip, LinearProgress } from '@mui/material'
|
import { Box, TextField, IconButton, Tooltip, LinearProgress } from '@mui/material'
|
||||||
import AddIcon from '@mui/icons-material/Add'
|
import { Plus, Trash2, RefreshCw, Search, Send, FileText } from 'lucide-react'
|
||||||
import DeleteIcon from '@mui/icons-material/Delete'
|
|
||||||
import RefreshIcon from '@mui/icons-material/Refresh'
|
|
||||||
import SearchIcon from '@mui/icons-material/Search'
|
|
||||||
import SendIcon from '@mui/icons-material/Send'
|
|
||||||
import DescriptionIcon from '@mui/icons-material/Description'
|
|
||||||
import { MetalCard, PhosphorText, Led, PhysicalButton, ScreenPanel } from '@d3ro/ui/components/ds'
|
import { MetalCard, PhosphorText, Led, PhysicalButton, ScreenPanel } from '@d3ro/ui/components/ds'
|
||||||
import { PageHeader, EmptyStateCard } from '../components/shared'
|
import { PageHeader, EmptyStateCard } from '../components/shared'
|
||||||
import { isImeComposingEvent } from '../utils/keyboard'
|
import { isImeComposingEvent } from '../utils/keyboard'
|
||||||
|
|
@ -96,7 +91,7 @@ export function KnowledgeBasePage(): React.ReactElement {
|
||||||
title={t('rag.title').toUpperCase()}
|
title={t('rag.title').toUpperCase()}
|
||||||
action={
|
action={
|
||||||
<PhysicalButton size="small" onClick={handleAddDocument} disabled={adding}>
|
<PhysicalButton size="small" onClick={handleAddDocument} disabled={adding}>
|
||||||
<AddIcon sx={{ fontSize: 14, mr: 0.5 }} /> {adding ? t('rag.adding') : t('rag.addDocument')}
|
<Plus size={14} style={{ marginRight: 4 }} /> {adding ? t('rag.adding') : t('rag.addDocument')}
|
||||||
</PhysicalButton>
|
</PhysicalButton>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
@ -160,7 +155,7 @@ export function KnowledgeBasePage(): React.ReactElement {
|
||||||
{documents.map((doc) => (
|
{documents.map((doc) => (
|
||||||
<MetalCard key={doc.id}>
|
<MetalCard key={doc.id}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
<DescriptionIcon sx={{ fontSize: 20, color: d3roPalette.text.inactive }} />
|
<FileText size={20} style={{ color: d3roPalette.text.inactive }} />
|
||||||
<Box sx={{ flex: 1 }}>
|
<Box sx={{ flex: 1 }}>
|
||||||
<PhosphorText variant="body">{doc.fileName}</PhosphorText>
|
<PhosphorText variant="body">{doc.fileName}</PhosphorText>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.25 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.25 }}>
|
||||||
|
|
@ -176,13 +171,13 @@ export function KnowledgeBasePage(): React.ReactElement {
|
||||||
<Tooltip title={t('rag.reindex')}>
|
<Tooltip title={t('rag.reindex')}>
|
||||||
<IconButton size="small" onClick={() => handleReindex(doc.id)}
|
<IconButton size="small" onClick={() => handleReindex(doc.id)}
|
||||||
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}>
|
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}>
|
||||||
<RefreshIcon sx={{ fontSize: 16 }} />
|
<RefreshCw size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t('common.delete')}>
|
<Tooltip title={t('common.delete')}>
|
||||||
<IconButton size="small" onClick={() => handleRemoveDocument(doc.id)}
|
<IconButton size="small" onClick={() => handleRemoveDocument(doc.id)}
|
||||||
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}>
|
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}>
|
||||||
<DeleteIcon sx={{ fontSize: 16 }} />
|
<Trash2 size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -198,7 +193,7 @@ export function KnowledgeBasePage(): React.ReactElement {
|
||||||
|
|
||||||
<MetalCard>
|
<MetalCard>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
<SearchIcon sx={{ fontSize: 20, color: d3roPalette.text.inactive }} />
|
<Search size={20} style={{ color: d3roPalette.text.inactive }} />
|
||||||
<TextField
|
<TextField
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
|
@ -219,7 +214,7 @@ export function KnowledgeBasePage(): React.ReactElement {
|
||||||
/>
|
/>
|
||||||
<IconButton onClick={handleQuery} disabled={!query.trim() || querying}
|
<IconButton onClick={handleQuery} disabled={!query.trim() || querying}
|
||||||
sx={{ color: query.trim() ? d3roPalette.accent.amber : d3roPalette.text.inactive }}>
|
sx={{ color: query.trim() ? d3roPalette.accent.amber : d3roPalette.text.inactive }}>
|
||||||
<SendIcon sx={{ fontSize: 20 }} />
|
<Send size={20} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
</MetalCard>
|
</MetalCard>
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,7 @@ import {
|
||||||
Snackbar,
|
Snackbar,
|
||||||
Alert,
|
Alert,
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import AddIcon from '@mui/icons-material/Add'
|
import { Plus, Square, Circle } from 'lucide-react'
|
||||||
import StopIcon from '@mui/icons-material/Stop'
|
|
||||||
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'
|
|
||||||
import { MetalCard, PhosphorText, Led, PhysicalButton, ScreenPanel } from '@d3ro/ui/components/ds'
|
import { MetalCard, PhosphorText, Led, PhysicalButton, ScreenPanel } from '@d3ro/ui/components/ds'
|
||||||
import { MeetingDetailTabs } from '../components/meeting/MeetingDetailTabs'
|
import { MeetingDetailTabs } from '../components/meeting/MeetingDetailTabs'
|
||||||
import { EditableSegment } from '../components/meeting/EditableSegment'
|
import { EditableSegment } from '../components/meeting/EditableSegment'
|
||||||
|
|
@ -229,7 +227,7 @@ export function MeetingModePage(): React.ReactElement {
|
||||||
count={totalSessions > 0 ? t('meeting.sessions', { count: totalSessions }) : undefined}
|
count={totalSessions > 0 ? t('meeting.sessions', { count: totalSessions }) : undefined}
|
||||||
action={
|
action={
|
||||||
<PhysicalButton size="small" onClick={handleStartRecording}>
|
<PhysicalButton size="small" onClick={handleStartRecording}>
|
||||||
<AddIcon sx={{ fontSize: 16, mr: 0.5 }} />
|
<Plus size={16} style={{ marginRight: 4 }} />
|
||||||
{t('meeting.newMeeting')}
|
{t('meeting.newMeeting')}
|
||||||
</PhysicalButton>
|
</PhysicalButton>
|
||||||
}
|
}
|
||||||
|
|
@ -344,7 +342,7 @@ export function MeetingModePage(): React.ReactElement {
|
||||||
{/* 상단 바 */}
|
{/* 상단 바 */}
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
<FiberManualRecordIcon sx={{ color: d3roPalette.tag.red, fontSize: 16, animation: 'pulse 1s infinite' }} />
|
<Circle size={16} fill="currentColor" style={{ color: d3roPalette.tag.red, animation: 'pulse 1s infinite' }} />
|
||||||
<PhosphorText variant="label" sx={{ fontFamily: d3roFontMono }}>
|
<PhosphorText variant="label" sx={{ fontFamily: d3roFontMono }}>
|
||||||
{formatTime(elapsedMs)}
|
{formatTime(elapsedMs)}
|
||||||
</PhosphorText>
|
</PhosphorText>
|
||||||
|
|
@ -354,7 +352,7 @@ export function MeetingModePage(): React.ReactElement {
|
||||||
</Box>
|
</Box>
|
||||||
{!isProcessing && (
|
{!isProcessing && (
|
||||||
<PhysicalButton size="small" color="error" onClick={handleStopRecording}>
|
<PhysicalButton size="small" color="error" onClick={handleStopRecording}>
|
||||||
<StopIcon sx={{ fontSize: 16, mr: 0.5 }} />
|
<Square size={16} style={{ marginRight: 4 }} />
|
||||||
{t('meeting.stopRecording')}
|
{t('meeting.stopRecording')}
|
||||||
</PhysicalButton>
|
</PhysicalButton>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,7 @@
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
import { Alert, Box, IconButton, Snackbar, TextField, Tooltip } from '@mui/material'
|
import { Alert, Box, IconButton, Snackbar, TextField, Tooltip } from '@mui/material'
|
||||||
import MicIcon from '@mui/icons-material/Mic'
|
import { Mic, Square, Send, Trash2, XCircle } from 'lucide-react'
|
||||||
import StopIcon from '@mui/icons-material/Stop'
|
|
||||||
import SendIcon from '@mui/icons-material/Send'
|
|
||||||
import DeleteSweepIcon from '@mui/icons-material/DeleteSweep'
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel'
|
|
||||||
import { MetalCard, PhosphorText, Led, PhysicalButton, ScreenPanel, InstrumentPanel } from '@d3ro/ui/components/ds'
|
import { MetalCard, PhosphorText, Led, PhysicalButton, ScreenPanel, InstrumentPanel } from '@d3ro/ui/components/ds'
|
||||||
import { PageHeader } from '../components/shared'
|
import { PageHeader } from '../components/shared'
|
||||||
import { VoiceRecordingPanel } from '../components/voice-conversation/VoiceRecordingPanel'
|
import { VoiceRecordingPanel } from '../components/voice-conversation/VoiceRecordingPanel'
|
||||||
|
|
@ -248,7 +244,7 @@ export function VoiceConversationPage(): React.ReactElement {
|
||||||
{messages.length > 0 && (
|
{messages.length > 0 && (
|
||||||
<Tooltip title={t('conversation.clearHistory')}>
|
<Tooltip title={t('conversation.clearHistory')}>
|
||||||
<IconButton size="small" onClick={handleClearHistory} sx={{ color: d3roPalette.text.inactive }}>
|
<IconButton size="small" onClick={handleClearHistory} sx={{ color: d3roPalette.text.inactive }}>
|
||||||
<DeleteSweepIcon sx={{ fontSize: 18 }} />
|
<Trash2 size={18} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
@ -378,7 +374,7 @@ export function VoiceConversationPage(): React.ReactElement {
|
||||||
{/* 녹음 버튼 */}
|
{/* 녹음 버튼 */}
|
||||||
{!isActive ? (
|
{!isActive ? (
|
||||||
<PhysicalButton onClick={handleStartSession} sx={{ minWidth: 48, px: 2 }}>
|
<PhysicalButton onClick={handleStartSession} sx={{ minWidth: 48, px: 2 }}>
|
||||||
<MicIcon sx={{ fontSize: 20 }} />
|
<Mic size={20} />
|
||||||
</PhysicalButton>
|
</PhysicalButton>
|
||||||
) : state === 'listening' ? (
|
) : state === 'listening' ? (
|
||||||
<PhysicalButton
|
<PhysicalButton
|
||||||
|
|
@ -386,15 +382,15 @@ export function VoiceConversationPage(): React.ReactElement {
|
||||||
onClick={isRealtime ? handleStopSession : handleFinishListening}
|
onClick={isRealtime ? handleStopSession : handleFinishListening}
|
||||||
sx={{ minWidth: 48, px: 2 }}
|
sx={{ minWidth: 48, px: 2 }}
|
||||||
>
|
>
|
||||||
<StopIcon sx={{ fontSize: 20 }} />
|
<Square size={20} />
|
||||||
</PhysicalButton>
|
</PhysicalButton>
|
||||||
) : state === 'thinking' || state === 'speaking' ? (
|
) : state === 'thinking' || state === 'speaking' ? (
|
||||||
<PhysicalButton onClick={handleCancelResponse} sx={{ minWidth: 48, px: 2 }}>
|
<PhysicalButton onClick={handleCancelResponse} sx={{ minWidth: 48, px: 2 }}>
|
||||||
<CancelIcon sx={{ fontSize: 20 }} />
|
<XCircle size={20} />
|
||||||
</PhysicalButton>
|
</PhysicalButton>
|
||||||
) : (
|
) : (
|
||||||
<PhysicalButton onClick={handleFinishListening} sx={{ minWidth: 48, px: 2 }}>
|
<PhysicalButton onClick={handleFinishListening} sx={{ minWidth: 48, px: 2 }}>
|
||||||
<MicIcon sx={{ fontSize: 20 }} />
|
<Mic size={20} />
|
||||||
</PhysicalButton>
|
</PhysicalButton>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -439,7 +435,7 @@ export function VoiceConversationPage(): React.ReactElement {
|
||||||
: d3roPalette.text.inactive,
|
: d3roPalette.text.inactive,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SendIcon sx={{ fontSize: 20 }} />
|
<Send size={20} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@
|
||||||
* WindowManager.insertCSS()가 :root에 테마 변수를 주입하면 자동 전환.
|
* WindowManager.insertCSS()가 :root에 테마 변수를 주입하면 자동 전환.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* ── 다크 테마 fallback 기본값 (:root) ─────────────────────────── */
|
/* ── Midnight(다크) fallback 기본값 (:root) ────────────────────── */
|
||||||
:root {
|
:root {
|
||||||
--d3-accent-main: #f25b29;
|
--d3-accent-main: #3b82f6;
|
||||||
--d3-accent-glow: rgba(242, 91, 41, 0.6);
|
--d3-accent-glow: rgba(59, 130, 246, 0.6);
|
||||||
--d3-accent-glow-dim: rgba(242, 91, 41, 0.3);
|
--d3-accent-glow-dim: rgba(59, 130, 246, 0.3);
|
||||||
--d3-accent-light: #ff8a65;
|
--d3-accent-light: #60a5fa;
|
||||||
--d3-accent-light-glow: rgba(255, 138, 101, 0.7);
|
--d3-accent-light-glow: rgba(96, 165, 250, 0.7);
|
||||||
--d3-accent-light-glow-dim: rgba(255, 138, 101, 0.4);
|
--d3-accent-light-glow-dim: rgba(96, 165, 250, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|
@ -59,8 +59,10 @@ html, body {
|
||||||
0 0 8px var(--d3-accent-glow),
|
0 0 8px var(--d3-accent-glow),
|
||||||
0 0 16px var(--d3-accent-glow-dim),
|
0 0 16px var(--d3-accent-glow-dim),
|
||||||
0 1px 3px rgba(0, 0, 0, 0.8);
|
0 1px 3px rgba(0, 0, 0, 0.8);
|
||||||
background: rgba(0, 0, 0, 0.92);
|
background: rgba(13, 20, 38, 0.92);
|
||||||
border-radius: 6px;
|
/* 헤어라인은 box-shadow inset — border는 라인 높이에 영향을 주므로 금지 */
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(148, 180, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: opacity 0.5s ease-out;
|
transition: opacity 0.5s ease-out;
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,19 @@
|
||||||
* WindowManager.insertCSS()가 :root에 테마 변수를 주입하면 자동 전환.
|
* WindowManager.insertCSS()가 :root에 테마 변수를 주입하면 자동 전환.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* ── 다크 테마 fallback 기본값 (:root) ─────────────────────────── */
|
/* ── Midnight(다크) fallback 기본값 (:root) ────────────────────── */
|
||||||
:root {
|
:root {
|
||||||
--d3-bg-card: #242427;
|
--d3-bg-card: rgba(17, 26, 48, 0.85);
|
||||||
--d3-border-default: rgba(255, 255, 255, 0.08);
|
--d3-border-default: rgba(148, 180, 255, 0.12);
|
||||||
--d3-border-subtle: rgba(255, 255, 255, 0.06);
|
--d3-border-subtle: rgba(148, 180, 255, 0.07);
|
||||||
--d3-text-primary: rgba(255, 255, 255, 0.87);
|
--d3-text-primary: #eef2fb;
|
||||||
--d3-text-inactive: rgba(255, 255, 255, 0.3);
|
--d3-text-inactive: #7385ab;
|
||||||
--d3-text-muted: rgba(255, 255, 255, 0.25);
|
--d3-text-muted: rgba(147, 164, 200, 0.45);
|
||||||
--d3-text-dimLabel: #5c2615;
|
--d3-text-dimLabel: #4e5f85;
|
||||||
--d3-text-secondary: rgba(255, 255, 255, 0.35);
|
--d3-text-secondary: #93a4c8;
|
||||||
--d3-accent-main: #f25b29;
|
--d3-accent-main: #3b82f6;
|
||||||
--d3-accent-dim: rgba(242, 91, 41, 0.08);
|
--d3-accent-dim: rgba(59, 130, 246, 0.14);
|
||||||
--d3-shadow-popup: 0 8px 32px rgba(0, 0, 0, 0.5);
|
--d3-shadow-popup: 0 12px 40px rgba(3, 7, 18, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|
@ -39,8 +39,9 @@ body {
|
||||||
.command-popup {
|
.command-popup {
|
||||||
background: var(--d3-bg-card);
|
background: var(--d3-bg-card);
|
||||||
border: 1px solid var(--d3-border-default);
|
border: 1px solid var(--d3-border-default);
|
||||||
border-radius: 12px;
|
border-radius: 14px;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
box-shadow: var(--d3-shadow-popup);
|
box-shadow: var(--d3-shadow-popup);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(4px) scale(0.98);
|
transform: translateY(4px) scale(0.98);
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,16 @@
|
||||||
* WindowManager.insertCSS()가 :root에 테마 변수를 주입하면 자동 전환.
|
* WindowManager.insertCSS()가 :root에 테마 변수를 주입하면 자동 전환.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* ── 다크 테마 fallback 기본값 (:root) ─────────────────────────── */
|
/* ── Midnight(다크) fallback 기본값 (:root) ────────────────────── */
|
||||||
:root {
|
:root {
|
||||||
--d3-bg-card: #242427;
|
--d3-bg-card: rgba(17, 26, 48, 0.85);
|
||||||
--d3-border-default: rgba(255, 255, 255, 0.08);
|
--d3-border-default: rgba(148, 180, 255, 0.12);
|
||||||
--d3-border-subtle: rgba(255, 255, 255, 0.06);
|
--d3-border-subtle: rgba(148, 180, 255, 0.07);
|
||||||
--d3-text-primary: rgba(255, 255, 255, 0.87);
|
--d3-text-primary: #eef2fb;
|
||||||
--d3-text-inactive: rgba(255, 255, 255, 0.3);
|
--d3-text-inactive: #7385ab;
|
||||||
--d3-text-muted: rgba(255, 255, 255, 0.25);
|
--d3-text-muted: rgba(147, 164, 200, 0.45);
|
||||||
--d3-accent-main: #f25b29;
|
--d3-accent-main: #3b82f6;
|
||||||
--d3-shadow-popup: 0 8px 32px rgba(0, 0, 0, 0.5);
|
--d3-shadow-popup: 0 12px 40px rgba(3, 7, 18, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|
@ -36,8 +36,9 @@ body {
|
||||||
.history-popup {
|
.history-popup {
|
||||||
background: var(--d3-bg-card);
|
background: var(--d3-bg-card);
|
||||||
border: 1px solid var(--d3-border-default);
|
border: 1px solid var(--d3-border-default);
|
||||||
border-radius: 12px;
|
border-radius: 14px;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
box-shadow: var(--d3-shadow-popup);
|
box-shadow: var(--d3-shadow-popup);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(4px) scale(0.98);
|
transform: translateY(4px) scale(0.98);
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,15 @@
|
||||||
* WindowManager.insertCSS()가 :root에 테마 변수를 주입하면 자동 전환.
|
* WindowManager.insertCSS()가 :root에 테마 변수를 주입하면 자동 전환.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* ── 다크 테마 fallback 기본값 (:root) ─────────────────────────── */
|
/* ── Midnight(다크) fallback 기본값 (:root) ────────────────────── */
|
||||||
:root {
|
:root {
|
||||||
--d3-bg-tip: rgba(0, 0, 0, 0.85);
|
--d3-bg-tip: rgba(17, 26, 48, 0.85);
|
||||||
--d3-text-primary: rgba(255, 255, 255, 0.87);
|
--d3-text-primary: #eef2fb;
|
||||||
--d3-text-secondary: rgba(255, 255, 255, 0.6);
|
--d3-text-secondary: #93a4c8;
|
||||||
--d3-accent-main: #f25b29;
|
--d3-accent-main: #3b82f6;
|
||||||
--d3-border-strong: rgba(255, 255, 255, 0.15);
|
--d3-border-default: rgba(148, 180, 255, 0.12);
|
||||||
|
--d3-border-strong: rgba(148, 180, 255, 0.2);
|
||||||
|
--d3-shadow-popup: 0 12px 40px rgba(3, 7, 18, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|
@ -35,12 +37,14 @@ body {
|
||||||
|
|
||||||
.recording-tip {
|
.recording-tip {
|
||||||
background: var(--d3-bg-tip);
|
background: var(--d3-bg-tip);
|
||||||
border-radius: 8px;
|
border: 1px solid var(--d3-border-default);
|
||||||
|
border-radius: 14px;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(24px);
|
||||||
|
box-shadow: var(--d3-shadow-popup);
|
||||||
transition: opacity 150ms ease-in-out;
|
transition: opacity 150ms ease-in-out;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
@ -60,6 +64,17 @@ body {
|
||||||
min-height: 2px;
|
min-height: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 9바 시안→블루→퍼플 스펙트럼 (v2 Midnight — RAW.dark gradient.wave1~3 보간) */
|
||||||
|
.wave-bar:nth-child(1) { background: #22d3ee; box-shadow: 0 0 6px rgba(34, 211, 238, 0.35); }
|
||||||
|
.wave-bar:nth-child(2) { background: #28bef0; box-shadow: 0 0 6px rgba(40, 190, 240, 0.35); }
|
||||||
|
.wave-bar:nth-child(3) { background: #2fa9f2; box-shadow: 0 0 6px rgba(47, 169, 242, 0.35); }
|
||||||
|
.wave-bar:nth-child(4) { background: #3596f4; box-shadow: 0 0 6px rgba(53, 150, 244, 0.35); }
|
||||||
|
.wave-bar:nth-child(5) { background: #3b82f6; box-shadow: 0 0 6px rgba(59, 130, 246, 0.35); }
|
||||||
|
.wave-bar:nth-child(6) { background: #4f78f6; box-shadow: 0 0 6px rgba(79, 120, 246, 0.35); }
|
||||||
|
.wave-bar:nth-child(7) { background: #636ff6; box-shadow: 0 0 6px rgba(99, 111, 246, 0.35); }
|
||||||
|
.wave-bar:nth-child(8) { background: #7765f6; box-shadow: 0 0 6px rgba(119, 101, 246, 0.35); }
|
||||||
|
.wave-bar:nth-child(9) { background: #8b5cf6; box-shadow: 0 0 6px rgba(139, 92, 246, 0.35); }
|
||||||
|
|
||||||
.duration {
|
.duration {
|
||||||
color: var(--d3-text-primary);
|
color: var(--d3-text-primary);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,15 @@
|
||||||
* WindowManager.insertCSS()가 :root에 테마 변수를 주입하면 자동 전환.
|
* WindowManager.insertCSS()가 :root에 테마 변수를 주입하면 자동 전환.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* ── 다크 테마 fallback 기본값 (:root) ─────────────────────────── */
|
/* ── Midnight(다크) fallback 기본값 (:root) ────────────────────── */
|
||||||
:root {
|
:root {
|
||||||
--d3-bg-result: #1e1e1e;
|
--d3-bg-result: rgba(17, 26, 48, 0.85);
|
||||||
--d3-border-result: rgba(255, 255, 255, 0.08);
|
--d3-border-result: rgba(148, 180, 255, 0.12);
|
||||||
--d3-text-result: rgba(255, 255, 255, 0.87);
|
--d3-text-result: #eef2fb;
|
||||||
--d3-action-btn: rgba(255, 255, 255, 0.4);
|
--d3-action-btn: rgba(147, 164, 200, 0.7);
|
||||||
--d3-action-btn-hover-bg: rgba(255, 255, 255, 0.08);
|
--d3-action-btn-hover-bg: rgba(148, 180, 255, 0.08);
|
||||||
--d3-action-btn-hover: rgba(255, 255, 255, 0.7);
|
--d3-action-btn-hover: #c9d5f2;
|
||||||
--d3-shadow-popup: 0 8px 32px rgba(0, 0, 0, 0.5);
|
--d3-shadow-popup: 0 12px 40px rgba(3, 7, 18, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|
@ -37,8 +37,9 @@ body {
|
||||||
.result-popup {
|
.result-popup {
|
||||||
background: var(--d3-bg-result);
|
background: var(--d3-bg-result);
|
||||||
border: 1px solid var(--d3-border-result);
|
border: 1px solid var(--d3-border-result);
|
||||||
border-radius: 12px;
|
border-radius: 14px;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
box-shadow: var(--d3-shadow-popup);
|
box-shadow: var(--d3-shadow-popup);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(4px);
|
transform: translateY(4px);
|
||||||
|
|
@ -89,7 +90,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn.copied {
|
.action-btn.copied {
|
||||||
color: #4caf50;
|
color: #34d399;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,12 @@
|
||||||
- **StatusBar**: 브랜드+버전+모델 / 로컬 시간 시계
|
- **StatusBar**: 브랜드+버전+모델 / 로컬 시간 시계
|
||||||
- 검증: tsc 0, vitest 41/41, 실행 스크린샷으로 대시보드/히스토리/대화/회의/지식/설정 전 페이지 확인. 스크롤 시 폭 불변 확인. 상단 1px 잡티는 detached DevTools 겹침(앱 버그 아님)
|
- 검증: tsc 0, vitest 41/41, 실행 스크린샷으로 대시보드/히스토리/대화/회의/지식/설정 전 페이지 확인. 스크롤 시 폭 불변 확인. 상단 1px 잡티는 detached DevTools 겹침(앱 버그 아님)
|
||||||
|
|
||||||
### 남은 마감(진행 중)
|
### 마감 완료 (워크플로 8 에이전트 병렬)
|
||||||
1. 내부 페이지들의 @mui/icons-material → lucide-react 통일
|
1. ✅ @mui/icons-material → lucide-react 전환 (23파일, 잔여 0 — 의존성 제거는 추후)
|
||||||
2. Vanilla 팝업 5종(recording-tip 등) 스타일 v2 정합
|
2. ✅ Vanilla 팝업 5종 CSS v2 정합 (글래스 네이비 + 블루 액센트, var(--) 주입 구조 유지)
|
||||||
3. VoiceRecordingPanel(대화 몰입 패널) → GradientWave 교체
|
3. ✅ VoiceRecordingPanel → GradientWave 교체
|
||||||
4. 라이트 테마 시각 검수, v0.2.0-alpha 릴리스
|
4. 검증: tsc 0 / vitest 41/41 / 실화면 캡처 (Midnight·Nord 두 테마)
|
||||||
|
5. 잔여(후속): 라이트 테마 정밀 검수, @mui/icons-material 의존성 package.json 제거, MetalDial(미사용) 정리, v0.2.0-alpha 릴리스
|
||||||
|
|
||||||
## 모델 현대화 트랙 (2026-07-21) — B 완료, A 완료(코드), C 완료(코드)
|
## 모델 현대화 트랙 (2026-07-21) — B 완료, A 완료(코드), C 완료(코드)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue