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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
419 changes: 212 additions & 207 deletions ui/dist/assets/index-B5hkWMAS.js → ui/dist/assets/index-BeOXVYw4.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions ui/dist/assets/index-D0CQWAie.css

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion ui/dist/assets/index-D8H-2O6f.css

This file was deleted.

4 changes: 2 additions & 2 deletions ui/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@
html { background: #ffffff; }
html[data-theme="dark"] { background: #0e0c0c; }
</style>
<script type="module" crossorigin src="/assets/index-B5hkWMAS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D8H-2O6f.css">
<script type="module" crossorigin src="/assets/index-BeOXVYw4.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D0CQWAie.css">
</head>
<body>
<div
Expand Down
10 changes: 9 additions & 1 deletion ui/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1143,5 +1143,13 @@
"local_models_custom_endpoint": "Custom Endpoint",
"local_models_compatible_api": "(OpenAI compatible)",
"chat_session_limit_reached": "Session limit reached",
"chat_model_unavailable": "Model unavailable"
"chat_model_unavailable": "Model unavailable",
"chat_panel_dictate": "Dictate",
"chat_panel_stop_dictating": "Stop dictating",
"chat_dictation_unsupported": "Voice input needs Chrome, Edge or Safari",
"chat_dictation_error_not_allowed": "Microphone access was blocked. Allow it for this page, then try again.",
"chat_dictation_error_audio_capture": "No microphone was found.",
"chat_dictation_error_network": "Speech recognition could not reach the network.",
"chat_dictation_error_language": "Your browser cannot transcribe this language.",
"chat_dictation_error_generic": "Voice input stopped unexpectedly. Try again."
}
10 changes: 9 additions & 1 deletion ui/messages/fa.json
Original file line number Diff line number Diff line change
Expand Up @@ -1143,5 +1143,13 @@
"skills_remove_imported_label": "حذف {name} از OpenResearch",
"skills_remove_imported_confirm": "«{name}» از OpenResearch حذف شود؟ فایل‌های اصلی در عامل برنامه‌نویسی شما باقی می‌مانند.",
"chat_session_limit_reached": "به سقف مصرف نشست رسیده‌اید",
"chat_model_unavailable": "مدل در دسترس نیست"
"chat_model_unavailable": "مدل در دسترس نیست",
"chat_panel_dictate": "ورودی صوتی",
"chat_panel_stop_dictating": "توقف ورودی صوتی",
"chat_dictation_unsupported": "ورودی صوتی به کروم، اج یا سافاری نیاز دارد",
"chat_dictation_error_not_allowed": "دسترسی به میکروفون مسدود شد. آن را برای این صفحه اجازه دهید و دوباره تلاش کنید.",
"chat_dictation_error_audio_capture": "میکروفونی یافت نشد.",
"chat_dictation_error_network": "تشخیص گفتار نتوانست به شبکه متصل شود.",
"chat_dictation_error_language": "مرورگر شما نمی‌تواند این زبان را رونویسی کند.",
"chat_dictation_error_generic": "ورودی صوتی به‌طور غیرمنتظره متوقف شد. دوباره تلاش کنید."
}
10 changes: 9 additions & 1 deletion ui/messages/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1143,5 +1143,13 @@
"skills_remove_imported_label": "从 OpenResearch 移除 {name}",
"skills_remove_imported_confirm": "从 OpenResearch 移除“{name}”?源文件将保留在你的编程智能体中。",
"chat_session_limit_reached": "已达到会话使用限额",
"chat_model_unavailable": "模型不可用"
"chat_model_unavailable": "模型不可用",
"chat_panel_dictate": "语音输入",
"chat_panel_stop_dictating": "停止语音输入",
"chat_dictation_unsupported": "语音输入需要 Chrome、Edge 或 Safari",
"chat_dictation_error_not_allowed": "麦克风权限被拒绝。请为此页面开启后重试。",
"chat_dictation_error_audio_capture": "未检测到麦克风。",
"chat_dictation_error_network": "语音识别无法连接网络。",
"chat_dictation_error_language": "当前浏览器无法识别这种语言。",
"chat_dictation_error_generic": "语音输入意外中断,请重试。"
}
71 changes: 71 additions & 0 deletions ui/src/components/ChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { getProjectStarterPromptsQuery } from "../queries/projects";
import { m } from "../paraglide/messages.js";
import { autoDir, ltr } from "../i18n";
import { useLocale } from "../locale";
import { speechLocale, useDictation } from "../useDictation";
import { getThemePreference } from "../theme";
import {
ArrowDown,
Expand All @@ -43,6 +44,7 @@ import {
HelpCircle,
Lightbulb,
MessageSquareQuote,
Mic,
MoreHorizontal,
PanelLeft,
Paperclip,
Expand Down Expand Up @@ -4205,6 +4207,24 @@ function RemoteHostDialog({
);
}

/** Web Speech error codes the user can act on, as sentences they can act on.
* `no-restart` is ours: the recognizer kept ending the moment it started. */
function dictationErrorText(code: string): string {
switch (code) {
case "not-allowed":
case "service-not-allowed":
return m.chat_dictation_error_not_allowed();
case "audio-capture":
return m.chat_dictation_error_audio_capture();
case "network":
return m.chat_dictation_error_network();
case "language-not-supported":
return m.chat_dictation_error_language();
default:
return m.chat_dictation_error_generic();
}
}

export function ChatPanel({
projectId,
projectName,
Expand Down Expand Up @@ -4405,6 +4425,21 @@ export function ChatPanel({
// IME guard: mid-composition text can transiently look like a full command.
const composingRef = useRef(false);

// Dictation writes into the composer through the same state the keyboard
// does, so skill chips, the slash menu and send() need no special case. It
// listens in the UI language, which is the one the user is typing in.
const dictationLang = speechLocale(useLocale());
const dictation = useDictation({
lang: dictationLang,
target: composerRef,
onChange: (value, caret) => {
setDraft(value);
setComposerCursor(caret);
setSkillMenuDismissed(false);
},
describeError: dictationErrorText,
});

// Only reachable while the menu is open, which needs a live slash context.
function pickSkill(skill: SkillInfo) {
if (!slashContext) return;
Expand Down Expand Up @@ -5142,6 +5177,9 @@ export function ChatPanel({

/** `queue` (the ⌘/Ctrl+Enter chord) parks the message even on a harness that steers. */
async function send({ queue = false }: { queue?: boolean } = {}) {
// The dictated text is on its way out; a live recognizer would otherwise
// keep writing into whatever replaces it.
dictation.stop();
captureUiEvent({
name: "first_action",
surface: telemetrySurface,
Expand Down Expand Up @@ -5478,6 +5516,7 @@ export function ChatPanel({
async function runShell() {
const command = shellCommand;
if (!command) return;
dictation.stop();
setSettingsError(null);
if (busy) {
setSettingsError(m.chat_bash_busy());
Expand Down Expand Up @@ -6278,6 +6317,11 @@ export function ChatPanel({
{settingsError}
</div>
)}
{dictation.error && (
<div className="composer-dictation-error pt-1.5 px-3 pb-0 text-sm text-accent-red" role="alert">
{dictation.error}
</div>
)}
<div className={`composer-input relative flex overflow-hidden [&_textarea]:flex-1 ${bashActive ? "[&_textarea]:font-mono [&_textarea]:text-sm" : ""}`}>
<textarea
dir="auto"
Expand Down Expand Up @@ -6428,6 +6472,33 @@ export function ChatPanel({
>
<Paperclip size={16} />
</IconButton>
<IconButton
type="button"
className="composer-dictate"
active={dictation.listening}
// Kept visible but inert where the browser has no speech API,
// so its absence reads as a browser limit rather than a bug.
disabled={!dictation.supported}
title={
!dictation.supported
? m.chat_dictation_unsupported()
: dictation.listening
? m.chat_panel_stop_dictating()
: m.chat_panel_dictate()
}
aria-label={dictation.listening ? m.chat_panel_stop_dictating() : m.chat_panel_dictate()}
aria-pressed={dictation.listening}
// Declining focus keeps the composer's caret where the user
// put it, so dictation lands mid-message when that is where
// they were typing.
onMouseDown={(e) => e.preventDefault()}
onClick={dictation.toggle}
>
<Mic
size={16}
className={dictation.listening ? "text-accent-red motion-safe:animate-pulse" : undefined}
/>
</IconButton>
{planActive && (
<Button
type="button"
Expand Down
Loading