chat panel reaches zero convertible (203 -> 181)
The remaining interpolated strings and the shared Cancel/Save/Delete labels. What is left in Chat is three thrown Errors, which the never-translate category already excludes. useAudioRecording used `t` as its media-track loop variable in four places, which the translation function then shadowed. Fourth panel with that collision.
This commit is contained in:
@@ -16,7 +16,7 @@ type AgentIdentityBarProps = { panel: UseAgentPanelType };
|
|||||||
* the two apart is to notice that the conversation did not reset.
|
* the two apart is to notice that the conversation did not reset.
|
||||||
*/
|
*/
|
||||||
export const AgentIdentityBar = ({ panel }: AgentIdentityBarProps) => {
|
export const AgentIdentityBar = ({ panel }: AgentIdentityBarProps) => {
|
||||||
const { t } = useTranslation();
|
const { t, tUi } = useTranslation();
|
||||||
const [naming, setNaming] = useState(false);
|
const [naming, setNaming] = useState(false);
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -58,7 +58,7 @@ export const AgentIdentityBar = ({ panel }: AgentIdentityBarProps) => {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-6 w-6 p-0"
|
className="h-6 w-6 p-0"
|
||||||
onClick={() => setNaming(false)}
|
onClick={() => setNaming(false)}
|
||||||
aria-label="Cancel"
|
aria-label={tUi('button.cancel')}
|
||||||
>
|
>
|
||||||
<X className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -103,7 +103,7 @@ export const AgentIdentityBar = ({ panel }: AgentIdentityBarProps) => {
|
|||||||
<span className="font-medium">{panel.agent.name}</span>
|
<span className="font-medium">{panel.agent.name}</span>
|
||||||
<span
|
<span
|
||||||
className="text-muted-foreground flex-1 truncate"
|
className="text-muted-foreground flex-1 truncate"
|
||||||
title={peers.length ? `Peers: ${peers.map((p) => p.name).join(', ')}` : undefined}
|
title={peers.length ? t('chat.agent.peers', { names: peers.map((p) => p.name).join(', ') }) : undefined}
|
||||||
>
|
>
|
||||||
{peers.length === 0
|
{peers.length === 0
|
||||||
? 'no peers yet'
|
? 'no peers yet'
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export type ChatSessionSelection = {
|
|||||||
const title = (s: { title: string }) => s.title || '(untitled)';
|
const title = (s: { title: string }) => s.title || '(untitled)';
|
||||||
|
|
||||||
export const ChatHeader = ({ panelId }: { panelId?: string }) => {
|
export const ChatHeader = ({ panelId }: { panelId?: string }) => {
|
||||||
const { t } = useTranslation();
|
const { t, tUi } = useTranslation();
|
||||||
const { workspace, cwd } = useWorkspace();
|
const { workspace, cwd } = useWorkspace();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,10 +109,15 @@ export const ChatHeader = ({ panelId }: { panelId?: string }) => {
|
|||||||
}}
|
}}
|
||||||
className="min-w-0 flex-1 rounded border border-duck-dark/20 bg-transparent px-1.5 py-0.5 text-xs"
|
className="min-w-0 flex-1 rounded border border-duck-dark/20 bg-transparent px-1.5 py-0.5 text-xs"
|
||||||
/>
|
/>
|
||||||
<button type="submit" className="p-0.5 cursor-pointer" title="Save">
|
<button type="submit" className="p-0.5 cursor-pointer" title={t('common.save')}>
|
||||||
<Check className="h-3.5 w-3.5" />
|
<Check className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={() => setRenaming(null)} className="p-0.5 cursor-pointer" title="Cancel">
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setRenaming(null)}
|
||||||
|
className="p-0.5 cursor-pointer"
|
||||||
|
title={tUi('button.cancel')}
|
||||||
|
>
|
||||||
<X className="h-3.5 w-3.5" />
|
<X className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -138,11 +143,11 @@ export const ChatHeader = ({ panelId }: { panelId?: string }) => {
|
|||||||
<Pencil className="h-3 w-3" />
|
<Pencil className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
title="Delete"
|
title={t('common.delete')}
|
||||||
onClick={(ev) => {
|
onClick={(ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
if (!confirm(`Delete "${title(s)}"?`)) return;
|
if (!confirm(t('chat.deleteQ', { title: title(s) }))) return;
|
||||||
// Deleting the one on screen leaves this panel pointing at nothing, so it starts a
|
// Deleting the one on screen leaves this panel pointing at nothing, so it starts a
|
||||||
// new conversation rather than showing a transcript that no longer exists.
|
// new conversation rather than showing a transcript that no longer exists.
|
||||||
if (s.id === sessionId) setSessionId(null);
|
if (s.id === sessionId) setSessionId(null);
|
||||||
|
|||||||
@@ -68,7 +68,12 @@ export function AttachButton({
|
|||||||
const fence = text.includes('```') ? '````' : '```';
|
const fence = text.includes('```') ? '````' : '```';
|
||||||
onAttachText(`${fence} ${file.name}\n${text.replace(/\s+$/, '')}\n${fence}`);
|
onAttachText(`${fence} ${file.name}\n${text.replace(/\s+$/, '')}\n${fence}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(`Could not read ${file.name}: ${err instanceof Error ? err.message : 'unknown error'}`);
|
toast.error(
|
||||||
|
t('chat.attach.readFailed', {
|
||||||
|
name: file.name,
|
||||||
|
reason: err instanceof Error ? err.message : t('chat.attach.unknownError'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export const BackgroundJobDialog = ({
|
|||||||
isCommander = false,
|
isCommander = false,
|
||||||
recordPaths,
|
recordPaths,
|
||||||
}: BackgroundJobDialogProps) => {
|
}: BackgroundJobDialogProps) => {
|
||||||
const { t } = useTranslation();
|
const { t, tUi } = useTranslation();
|
||||||
const [mode, setMode] = useState<'command' | 'prompt'>('command');
|
const [mode, setMode] = useState<'command' | 'prompt'>('command');
|
||||||
const [body, setBody] = useState('');
|
const [body, setBody] = useState('');
|
||||||
const [watching, setWatching] = useState(false);
|
const [watching, setWatching] = useState(false);
|
||||||
@@ -220,7 +220,7 @@ export const BackgroundJobDialog = ({
|
|||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
|
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
|
||||||
Cancel
|
{tUi('button.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={!canSubmit}>
|
<Button type="submit" disabled={!canSubmit}>
|
||||||
{t('chat.job.start')}
|
{t('chat.job.start')}
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ const TaskChip = ({ task, active, pinned, onClick, onTogglePin, onDismiss, onSto
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onStop}
|
onClick={onStop}
|
||||||
title={t('chat.tasks.stopJob')}
|
title={t('chat.tasks.stopJob')}
|
||||||
aria-label={`Stop ${label}`}
|
aria-label={t('chat.tasks.stopNamed', { label })}
|
||||||
className="cursor-pointer py-1 pl-0.5 pr-2 text-muted-foreground/50 transition-colors hover:text-destructive"
|
className="cursor-pointer py-1 pl-0.5 pr-2 text-muted-foreground/50 transition-colors hover:text-destructive"
|
||||||
>
|
>
|
||||||
<Square className="h-3 w-3" />
|
<Square className="h-3 w-3" />
|
||||||
@@ -327,7 +327,7 @@ const TaskChip = ({ task, active, pinned, onClick, onTogglePin, onDismiss, onSto
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onDismiss}
|
onClick={onDismiss}
|
||||||
title={t('chat.tasks.dismiss')}
|
title={t('chat.tasks.dismiss')}
|
||||||
aria-label={`Dismiss ${label}`}
|
aria-label={t('chat.tasks.dismissNamed', { label })}
|
||||||
className="cursor-pointer rounded-full py-1 pl-0.5 pr-2 text-muted-foreground/50 transition-colors hover:text-foreground"
|
className="cursor-pointer rounded-full py-1 pl-0.5 pr-2 text-muted-foreground/50 transition-colors hover:text-foreground"
|
||||||
>
|
>
|
||||||
<X className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ type Started = { id: string; url: string; expiresInMs: number };
|
|||||||
type Props = { open: boolean; onOpenChange: (open: boolean) => void };
|
type Props = { open: boolean; onOpenChange: (open: boolean) => void };
|
||||||
|
|
||||||
export const ClaudeLoginDialog = ({ open, onOpenChange }: Props) => {
|
export const ClaudeLoginDialog = ({ open, onOpenChange }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t, tUi } = useTranslation();
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const [started, setStarted] = useState<Started | null>(null);
|
const [started, setStarted] = useState<Started | null>(null);
|
||||||
const [code, setCode] = useState('');
|
const [code, setCode] = useState('');
|
||||||
@@ -162,7 +162,7 @@ export const ClaudeLoginDialog = ({ open, onOpenChange }: Props) => {
|
|||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
className="rounded-md px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
|
className="rounded-md px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
Cancel
|
{tUi('button.cancel')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ export const CodeSurface = ({ code, lang = '', startLine = 1, maxLines, numbered
|
|||||||
onClick={() => setExpanded(!expanded)}
|
onClick={() => setExpanded(!expanded)}
|
||||||
className="mt-1 cursor-pointer text-xs text-duck-teal hover:underline"
|
className="mt-1 cursor-pointer text-xs text-duck-teal hover:underline"
|
||||||
>
|
>
|
||||||
{expanded ? 'Show less' : `Show more (${hidden} more lines)`}
|
{expanded ? 'Show less' : t('chat.message.showMore', { count: hidden })}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ const CollapsibleBlock = ({ label, content }: { label: string; content: string }
|
|||||||
type TtsState = 'idle' | 'loading' | 'playing';
|
type TtsState = 'idle' | 'loading' | 'playing';
|
||||||
|
|
||||||
const ReadAloudButton = ({ id, text }: { id: string; text: string }) => {
|
const ReadAloudButton = ({ id, text }: { id: string; text: string }) => {
|
||||||
const { t } = useTranslation();
|
const { t, tUi } = useTranslation();
|
||||||
const [state, setState] = useState<TtsState>('idle');
|
const [state, setState] = useState<TtsState>('idle');
|
||||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||||
const api = useFilesAPI('user-data');
|
const api = useFilesAPI('user-data');
|
||||||
@@ -122,7 +122,7 @@ const ReadAloudButton = ({ id, text }: { id: string; text: string }) => {
|
|||||||
// the icon comes back, the button looks broken rather than failed. Most often this is the TTS
|
// the icon comes back, the button looks broken rather than failed. Most often this is the TTS
|
||||||
// service being down, which is worth saying out loud.
|
// service being down, which is worth saying out loud.
|
||||||
setState('idle');
|
setState('idle');
|
||||||
toast.error(`Could not read this message aloud: ${errorText(err)}`);
|
toast.error(t('chat.message.readAloudFailed', { reason: errorText(err) }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -343,6 +343,7 @@ export const MessageBubble = ({ message, onAnswer, onRetry, onDecidePlan, defaul
|
|||||||
* both sides discarded the taskId that relates them.
|
* both sides discarded the taskId that relates them.
|
||||||
*/
|
*/
|
||||||
const TaskActivity = ({ message }: { message: Extract<ChatMessage, { role: 'task' }> }) => {
|
const TaskActivity = ({ message }: { message: Extract<ChatMessage, { role: 'task' }> }) => {
|
||||||
|
const { t, tUi } = useTranslation();
|
||||||
const { status, summary, description, taskType } = message;
|
const { status, summary, description, taskType } = message;
|
||||||
const Icon = status === 'completed' ? Check : status === 'failed' ? X : status === 'stopped' ? CircleSlash : Clock;
|
const Icon = status === 'completed' ? Check : status === 'failed' ? X : status === 'stopped' ? CircleSlash : Clock;
|
||||||
const tone: Tone =
|
const tone: Tone =
|
||||||
@@ -357,7 +358,7 @@ const TaskActivity = ({ message }: { message: Extract<ChatMessage, { role: 'task
|
|||||||
{taskType && <span className="ml-2 text-xs text-muted-foreground">{taskType}</span>}
|
{taskType && <span className="ml-2 text-xs text-muted-foreground">{taskType}</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
{status ? summary || `Task ${status}.` : 'Running in the background…'}
|
{status ? summary || t('chat.tasks.status', { status }) : 'Running in the background…'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -384,6 +385,7 @@ const formatTokens = (n: number): string => (n >= 1000 ? `${Math.round(n / 1000)
|
|||||||
* the sidecar measured — which is the one a replay in another tab gets.
|
* the sidecar measured — which is the one a replay in another tab gets.
|
||||||
*/
|
*/
|
||||||
const CompactActivity = ({ message }: { message: Extract<ChatMessage, { role: 'compact' }> }) => {
|
const CompactActivity = ({ message }: { message: Extract<ChatMessage, { role: 'compact' }> }) => {
|
||||||
|
const { t, tUi } = useTranslation();
|
||||||
const { done, trigger, preTokens, durationMs } = message;
|
const { done, trigger, preTokens, durationMs } = message;
|
||||||
const startedAt = useRef(Date.now());
|
const startedAt = useRef(Date.now());
|
||||||
const [elapsed, setElapsed] = useState(0);
|
const [elapsed, setElapsed] = useState(0);
|
||||||
@@ -405,7 +407,11 @@ const CompactActivity = ({ message }: { message: Extract<ChatMessage, { role: 'c
|
|||||||
<div className="h-px flex-1 bg-border" />
|
<div className="h-px flex-1 bg-border" />
|
||||||
<span className="flex items-center gap-1.5 text-[11px] font-medium tracking-wide uppercase">
|
<span className="flex items-center gap-1.5 text-[11px] font-medium tracking-wide uppercase">
|
||||||
<FoldVertical className={`h-3 w-3 ${done ? '' : 'animate-pulse'}`} />
|
<FoldVertical className={`h-3 w-3 ${done ? '' : 'animate-pulse'}`} />
|
||||||
{done ? `Context compacted${trigger === 'manual' ? '' : ' automatically'}` : 'Compacting the conversation…'}
|
{done
|
||||||
|
? trigger === 'manual'
|
||||||
|
? t('chat.message.compacted')
|
||||||
|
: t('chat.message.compactedAuto')
|
||||||
|
: 'Compacting the conversation…'}
|
||||||
{detail && <span className="normal-case opacity-70 tabular-nums">· {detail}</span>}
|
{detail && <span className="normal-case opacity-70 tabular-nums">· {detail}</span>}
|
||||||
</span>
|
</span>
|
||||||
<div className="h-px flex-1 bg-border" />
|
<div className="h-px flex-1 bg-border" />
|
||||||
@@ -502,7 +508,7 @@ const PlanCard = ({
|
|||||||
message: Extract<ChatMessage, { role: 'plan' }>;
|
message: Extract<ChatMessage, { role: 'plan' }>;
|
||||||
onDecide?: (requestId: string, approve: boolean, feedback?: string) => void;
|
onDecide?: (requestId: string, approve: boolean, feedback?: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t, tUi } = useTranslation();
|
||||||
const [feedback, setFeedback] = useState('');
|
const [feedback, setFeedback] = useState('');
|
||||||
const [rejecting, setRejecting] = useState(false);
|
const [rejecting, setRejecting] = useState(false);
|
||||||
const decided = message.approved !== undefined;
|
const decided = message.approved !== undefined;
|
||||||
@@ -554,7 +560,7 @@ const PlanCard = ({
|
|||||||
onClick={() => setRejecting(false)}
|
onClick={() => setRejecting(false)}
|
||||||
className="cursor-pointer rounded px-2 py-1 text-xs text-muted-foreground hover:text-foreground"
|
className="cursor-pointer rounded px-2 py-1 text-xs text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
Cancel
|
{tUi('button.cancel')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -308,6 +308,7 @@ export const ToolActivity = ({ message, defaultOpen }: ToolActivityProps) => {
|
|||||||
const DIFF_MAX_LINES = 40;
|
const DIFF_MAX_LINES = 40;
|
||||||
|
|
||||||
const DiffView = ({ lines }: { lines: DiffLine[] }) => {
|
const DiffView = ({ lines }: { lines: DiffLine[] }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const hidden = lines.length - DIFF_MAX_LINES;
|
const hidden = lines.length - DIFF_MAX_LINES;
|
||||||
const shown = expanded ? lines : lines.slice(0, DIFF_MAX_LINES);
|
const shown = expanded ? lines : lines.slice(0, DIFF_MAX_LINES);
|
||||||
@@ -353,7 +354,7 @@ const DiffView = ({ lines }: { lines: DiffLine[] }) => {
|
|||||||
onClick={() => setExpanded(!expanded)}
|
onClick={() => setExpanded(!expanded)}
|
||||||
className="mt-1 cursor-pointer text-xs text-duck-teal hover:underline"
|
className="mt-1 cursor-pointer text-xs text-duck-teal hover:underline"
|
||||||
>
|
>
|
||||||
{expanded ? 'Show less' : `Show more (${hidden} more lines)`}
|
{expanded ? 'Show less' : t('chat.message.showMore', { count: hidden })}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -118,7 +118,12 @@ const Models = ({ account }: { account: UsageAccount }) => {
|
|||||||
<div
|
<div
|
||||||
key={model.name}
|
key={model.name}
|
||||||
className="relative flex items-center justify-between gap-2 overflow-hidden rounded-md px-2 py-1.5"
|
className="relative flex items-center justify-between gap-2 overflow-hidden rounded-md px-2 py-1.5"
|
||||||
title={`In ${formatTokens(model.input)} · out ${formatTokens(model.output)} · cache read ${formatTokens(model.cacheRead)} · cache write ${formatTokens(model.cacheWrite)}`}
|
title={t('chat.usage.modelBreakdown', {
|
||||||
|
input: formatTokens(model.input),
|
||||||
|
output: formatTokens(model.output),
|
||||||
|
cacheRead: formatTokens(model.cacheRead),
|
||||||
|
cacheWrite: formatTokens(model.cacheWrite),
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
{/* The bar sits BEHIND the row rather than beside it — the model names are long and a separate
|
{/* The bar sits BEHIND the row rather than beside it — the model names are long and a separate
|
||||||
track would leave no room for either. */}
|
track would leave no room for either. */}
|
||||||
@@ -287,8 +292,10 @@ export function UsagePopover({ label, harness }: UsagePopoverProps) {
|
|||||||
title: a.current
|
title: a.current
|
||||||
? `${a.emailAccount} — signed in`
|
? `${a.emailAccount} — signed in`
|
||||||
: a.usable
|
: a.usable
|
||||||
? `Switch to ${a.emailAccount}${a.orgName ? ` (${a.orgName})` : ''}`
|
? a.orgName
|
||||||
: `${a.emailAccount} — sign-in expired, run /login`,
|
? t('chat.usage.switchToOrg', { account: a.emailAccount, org: a.orgName })
|
||||||
|
: t('chat.usage.switchTo', { account: a.emailAccount })
|
||||||
|
: t('chat.usage.signInExpired', { account: a.emailAccount }),
|
||||||
})),
|
})),
|
||||||
...archived.map((a) => ({
|
...archived.map((a) => ({
|
||||||
key: `archived:${a.email}`,
|
key: `archived:${a.email}`,
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import { useState } from 'react';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useClient } from 'hooks/useClient';
|
import { useClient } from 'hooks/useClient';
|
||||||
import type { Attachment } from './types';
|
import type { Attachment } from './types';
|
||||||
|
import { useTranslation } from '../../locales';
|
||||||
|
|
||||||
type UseAttachmentsParams = {
|
type UseAttachmentsParams = {
|
||||||
sessionId?: string | null;
|
sessionId?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useAttachments(params?: UseAttachmentsParams) {
|
export function useAttachments(params?: UseAttachmentsParams) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
||||||
|
|
||||||
@@ -33,7 +35,7 @@ export function useAttachments(params?: UseAttachmentsParams) {
|
|||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
setAttachments((prev) => prev.filter((_, i) => i !== idx));
|
setAttachments((prev) => prev.filter((_, i) => i !== idx));
|
||||||
toast.error('Failed to scrape webpage');
|
toast.error(t('chat.attach.scrapeFailed'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -58,7 +60,7 @@ export function useAttachments(params?: UseAttachmentsParams) {
|
|||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
setAttachments((prev) => prev.filter((_, i) => i !== idx));
|
setAttachments((prev) => prev.filter((_, i) => i !== idx));
|
||||||
toast.error('Failed to upload image');
|
toast.error(t('chat.attach.uploadFailed'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { useTranslation } from '../../locales';
|
||||||
|
|
||||||
export function useAudioRecording(onTranscription: (text: string) => void) {
|
export function useAudioRecording(onTranscription: (text: string) => void) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [recording, setRecording] = useState(false);
|
const [recording, setRecording] = useState(false);
|
||||||
const [transcribing, setTranscribing] = useState(false);
|
const [transcribing, setTranscribing] = useState(false);
|
||||||
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
||||||
@@ -16,7 +18,7 @@ export function useAudioRecording(onTranscription: (text: string) => void) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (recorder.state === 'inactive') {
|
if (recorder.state === 'inactive') {
|
||||||
recorder.stream.getTracks().forEach((t) => t.stop());
|
recorder.stream.getTracks().forEach((track) => track.stop());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,10 +32,10 @@ export function useAudioRecording(onTranscription: (text: string) => void) {
|
|||||||
recorder.stop();
|
recorder.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
recorder.stream.getTracks().forEach((t) => t.stop());
|
recorder.stream.getTracks().forEach((track) => track.stop());
|
||||||
|
|
||||||
if (blob.size === 0) {
|
if (blob.size === 0) {
|
||||||
toast.error('No audio was captured');
|
toast.error(t('chat.composer.noAudio'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +63,7 @@ export function useAudioRecording(onTranscription: (text: string) => void) {
|
|||||||
setTranscribing(false);
|
setTranscribing(false);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
recorder.stream?.getTracks().forEach((t) => t.stop());
|
recorder.stream?.getTracks().forEach((track) => track.stop());
|
||||||
chunksRef.current = [];
|
chunksRef.current = [];
|
||||||
toast.error(err instanceof Error ? err.message : 'Recording failed');
|
toast.error(err instanceof Error ? err.message : 'Recording failed');
|
||||||
}
|
}
|
||||||
@@ -94,7 +96,7 @@ export function useAudioRecording(onTranscription: (text: string) => void) {
|
|||||||
const recorder = mediaRecorderRef.current;
|
const recorder = mediaRecorderRef.current;
|
||||||
if (!recorder) return;
|
if (!recorder) return;
|
||||||
if (recorder.state !== 'inactive') recorder.stop();
|
if (recorder.state !== 'inactive') recorder.stop();
|
||||||
recorder.stream?.getTracks().forEach((t) => t.stop());
|
recorder.stream?.getTracks().forEach((track) => track.stop());
|
||||||
mediaRecorderRef.current = null;
|
mediaRecorderRef.current = null;
|
||||||
chunksRef.current = [];
|
chunksRef.current = [];
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -361,7 +361,9 @@
|
|||||||
"byModel": "Tokens by model",
|
"byModel": "Tokens by model",
|
||||||
"title": "Usage and limits",
|
"title": "Usage and limits",
|
||||||
"modelBreakdown": "In {{input}} · out {{output}} · cache read {{cacheRead}} · cache write {{cacheWrite}}",
|
"modelBreakdown": "In {{input}} · out {{output}} · cache read {{cacheRead}} · cache write {{cacheWrite}}",
|
||||||
"switchTo": "Switch to {{account}}"
|
"switchTo": "Switch to {{account}}",
|
||||||
|
"switchToOrg": "Switch to {{account}} ({{org}})",
|
||||||
|
"signInExpired": "{{account}} — sign-in expired, run /login"
|
||||||
},
|
},
|
||||||
"attach": {
|
"attach": {
|
||||||
"image": "Image",
|
"image": "Image",
|
||||||
|
|||||||
@@ -361,7 +361,9 @@
|
|||||||
"byModel": "Tokens por modelo",
|
"byModel": "Tokens por modelo",
|
||||||
"title": "Consumo e limites",
|
"title": "Consumo e limites",
|
||||||
"modelBreakdown": "Entrada {{input}} · saída {{output}} · leitura de cache {{cacheRead}} · escrita de cache {{cacheWrite}}",
|
"modelBreakdown": "Entrada {{input}} · saída {{output}} · leitura de cache {{cacheRead}} · escrita de cache {{cacheWrite}}",
|
||||||
"switchTo": "Mudar para {{account}}"
|
"switchTo": "Mudar para {{account}}",
|
||||||
|
"switchToOrg": "Mudar para {{account}} ({{org}})",
|
||||||
|
"signInExpired": "{{account}} — sessão expirada, execute /login"
|
||||||
},
|
},
|
||||||
"attach": {
|
"attach": {
|
||||||
"image": "Imagem",
|
"image": "Imagem",
|
||||||
|
|||||||
Reference in New Issue
Block a user