Merge pull request #537 from wonderwhy-er/Voice-input-with-fixes
Browse files- app/components/chat/BaseChat.tsx +94 -43
- app/components/chat/ModelSelector.tsx +63 -0
- app/components/chat/SpeechRecognition.tsx +28 -0
- app/types/global.d.ts +2 -0
- app/utils/logger.ts +1 -1
- package.json +1 -0
- pnpm-lock.yaml +9 -1
- tsconfig.json +1 -1
app/components/chat/BaseChat.tsx
CHANGED
@@ -23,45 +23,8 @@ import { ImportButtons } from '~/components/chat/chatExportAndImport/ImportButto
|
|
23 |
import { ExamplePrompts } from '~/components/chat/ExamplePrompts';
|
24 |
|
25 |
import FilePreview from './FilePreview';
|
26 |
-
|
27 |
-
|
28 |
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
29 |
-
const ModelSelector = ({ model, setModel, provider, setProvider, modelList, providerList, apiKeys }) => {
|
30 |
-
return (
|
31 |
-
<div className="mb-2 flex gap-2 flex-col sm:flex-row">
|
32 |
-
<select
|
33 |
-
value={provider?.name}
|
34 |
-
onChange={(e) => {
|
35 |
-
setProvider(providerList.find((p: ProviderInfo) => p.name === e.target.value));
|
36 |
-
|
37 |
-
const firstModel = [...modelList].find((m) => m.provider == e.target.value);
|
38 |
-
setModel(firstModel ? firstModel.name : '');
|
39 |
-
}}
|
40 |
-
className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
|
41 |
-
>
|
42 |
-
{providerList.map((provider: ProviderInfo) => (
|
43 |
-
<option key={provider.name} value={provider.name}>
|
44 |
-
{provider.name}
|
45 |
-
</option>
|
46 |
-
))}
|
47 |
-
</select>
|
48 |
-
<select
|
49 |
-
key={provider?.name}
|
50 |
-
value={model}
|
51 |
-
onChange={(e) => setModel(e.target.value)}
|
52 |
-
className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all lg:max-w-[70%]"
|
53 |
-
>
|
54 |
-
{[...modelList]
|
55 |
-
.filter((e) => e.provider == provider?.name && e.name)
|
56 |
-
.map((modelOption) => (
|
57 |
-
<option key={modelOption.name} value={modelOption.name}>
|
58 |
-
{modelOption.label}
|
59 |
-
</option>
|
60 |
-
))}
|
61 |
-
</select>
|
62 |
-
</div>
|
63 |
-
);
|
64 |
-
};
|
65 |
|
66 |
const TEXTAREA_MIN_HEIGHT = 76;
|
67 |
|
@@ -92,6 +55,7 @@ interface BaseChatProps {
|
|
92 |
imageDataList?: string[];
|
93 |
setImageDataList?: (dataList: string[]) => void;
|
94 |
}
|
|
|
95 |
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
96 |
(
|
97 |
{
|
@@ -126,7 +90,11 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
126 |
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
|
127 |
const [modelList, setModelList] = useState(MODEL_LIST);
|
128 |
const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false);
|
|
|
|
|
|
|
129 |
|
|
|
130 |
useEffect(() => {
|
131 |
// Load API keys from cookies on component mount
|
132 |
try {
|
@@ -149,8 +117,72 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
149 |
initializeModelList().then((modelList) => {
|
150 |
setModelList(modelList);
|
151 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
152 |
}, []);
|
153 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
154 |
const updateApiKey = (provider: string, key: string) => {
|
155 |
try {
|
156 |
const updatedApiKeys = { ...apiKeys, [provider]: key };
|
@@ -316,7 +348,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
316 |
</button>
|
317 |
</div>
|
318 |
|
319 |
-
|
320 |
<div className={isModelSettingsCollapsed ? 'hidden' : ''}>
|
321 |
<ModelSelector
|
322 |
key={provider?.name + ':' + modelList.length}
|
@@ -395,7 +426,12 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
395 |
|
396 |
event.preventDefault();
|
397 |
|
398 |
-
|
|
|
|
|
|
|
|
|
|
|
399 |
}
|
400 |
}}
|
401 |
value={input}
|
@@ -422,7 +458,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
422 |
}
|
423 |
|
424 |
if (input.length > 0 || uploadedFiles.length > 0) {
|
425 |
-
|
426 |
}
|
427 |
}}
|
428 |
/>
|
@@ -457,6 +493,13 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
457 |
</>
|
458 |
)}
|
459 |
</IconButton>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
460 |
{chatStarted && <ClientOnly>{() => <ExportChatButton exportChat={exportChat} />}</ClientOnly>}
|
461 |
</div>
|
462 |
{input.length > 3 ? (
|
@@ -471,7 +514,15 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
471 |
</div>
|
472 |
</div>
|
473 |
{!chatStarted && ImportButtons(importChat)}
|
474 |
-
{!chatStarted &&
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
475 |
</div>
|
476 |
<ClientOnly>{() => <Workbench chatStarted={chatStarted} isStreaming={isStreaming} />}</ClientOnly>
|
477 |
</div>
|
|
|
23 |
import { ExamplePrompts } from '~/components/chat/ExamplePrompts';
|
24 |
|
25 |
import FilePreview from './FilePreview';
|
26 |
+
import { ModelSelector } from '~/components/chat/ModelSelector';
|
27 |
+
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
|
29 |
const TEXTAREA_MIN_HEIGHT = 76;
|
30 |
|
|
|
55 |
imageDataList?: string[];
|
56 |
setImageDataList?: (dataList: string[]) => void;
|
57 |
}
|
58 |
+
|
59 |
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
60 |
(
|
61 |
{
|
|
|
90 |
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
|
91 |
const [modelList, setModelList] = useState(MODEL_LIST);
|
92 |
const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false);
|
93 |
+
const [isListening, setIsListening] = useState(false);
|
94 |
+
const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
|
95 |
+
const [transcript, setTranscript] = useState('');
|
96 |
|
97 |
+
console.log(transcript);
|
98 |
useEffect(() => {
|
99 |
// Load API keys from cookies on component mount
|
100 |
try {
|
|
|
117 |
initializeModelList().then((modelList) => {
|
118 |
setModelList(modelList);
|
119 |
});
|
120 |
+
|
121 |
+
if (typeof window !== 'undefined' && ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window)) {
|
122 |
+
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
123 |
+
const recognition = new SpeechRecognition();
|
124 |
+
recognition.continuous = true;
|
125 |
+
recognition.interimResults = true;
|
126 |
+
|
127 |
+
recognition.onresult = (event) => {
|
128 |
+
const transcript = Array.from(event.results)
|
129 |
+
.map((result) => result[0])
|
130 |
+
.map((result) => result.transcript)
|
131 |
+
.join('');
|
132 |
+
|
133 |
+
setTranscript(transcript);
|
134 |
+
|
135 |
+
if (handleInputChange) {
|
136 |
+
const syntheticEvent = {
|
137 |
+
target: { value: transcript },
|
138 |
+
} as React.ChangeEvent<HTMLTextAreaElement>;
|
139 |
+
handleInputChange(syntheticEvent);
|
140 |
+
}
|
141 |
+
};
|
142 |
+
|
143 |
+
recognition.onerror = (event) => {
|
144 |
+
console.error('Speech recognition error:', event.error);
|
145 |
+
setIsListening(false);
|
146 |
+
};
|
147 |
+
|
148 |
+
setRecognition(recognition);
|
149 |
+
}
|
150 |
}, []);
|
151 |
|
152 |
+
const startListening = () => {
|
153 |
+
if (recognition) {
|
154 |
+
recognition.start();
|
155 |
+
setIsListening(true);
|
156 |
+
}
|
157 |
+
};
|
158 |
+
|
159 |
+
const stopListening = () => {
|
160 |
+
if (recognition) {
|
161 |
+
recognition.stop();
|
162 |
+
setIsListening(false);
|
163 |
+
}
|
164 |
+
};
|
165 |
+
|
166 |
+
const handleSendMessage = (event: React.UIEvent, messageInput?: string) => {
|
167 |
+
if (sendMessage) {
|
168 |
+
sendMessage(event, messageInput);
|
169 |
+
|
170 |
+
if (recognition) {
|
171 |
+
recognition.abort(); // Stop current recognition
|
172 |
+
setTranscript(''); // Clear transcript
|
173 |
+
setIsListening(false);
|
174 |
+
|
175 |
+
// Clear the input by triggering handleInputChange with empty value
|
176 |
+
if (handleInputChange) {
|
177 |
+
const syntheticEvent = {
|
178 |
+
target: { value: '' },
|
179 |
+
} as React.ChangeEvent<HTMLTextAreaElement>;
|
180 |
+
handleInputChange(syntheticEvent);
|
181 |
+
}
|
182 |
+
}
|
183 |
+
}
|
184 |
+
};
|
185 |
+
|
186 |
const updateApiKey = (provider: string, key: string) => {
|
187 |
try {
|
188 |
const updatedApiKeys = { ...apiKeys, [provider]: key };
|
|
|
348 |
</button>
|
349 |
</div>
|
350 |
|
|
|
351 |
<div className={isModelSettingsCollapsed ? 'hidden' : ''}>
|
352 |
<ModelSelector
|
353 |
key={provider?.name + ':' + modelList.length}
|
|
|
426 |
|
427 |
event.preventDefault();
|
428 |
|
429 |
+
if (isStreaming) {
|
430 |
+
handleStop?.();
|
431 |
+
return;
|
432 |
+
}
|
433 |
+
|
434 |
+
handleSendMessage?.(event);
|
435 |
}
|
436 |
}}
|
437 |
value={input}
|
|
|
458 |
}
|
459 |
|
460 |
if (input.length > 0 || uploadedFiles.length > 0) {
|
461 |
+
handleSendMessage?.(event);
|
462 |
}
|
463 |
}}
|
464 |
/>
|
|
|
493 |
</>
|
494 |
)}
|
495 |
</IconButton>
|
496 |
+
|
497 |
+
<SpeechRecognitionButton
|
498 |
+
isListening={isListening}
|
499 |
+
onStart={startListening}
|
500 |
+
onStop={stopListening}
|
501 |
+
disabled={isStreaming}
|
502 |
+
/>
|
503 |
{chatStarted && <ClientOnly>{() => <ExportChatButton exportChat={exportChat} />}</ClientOnly>}
|
504 |
</div>
|
505 |
{input.length > 3 ? (
|
|
|
514 |
</div>
|
515 |
</div>
|
516 |
{!chatStarted && ImportButtons(importChat)}
|
517 |
+
{!chatStarted &&
|
518 |
+
ExamplePrompts((event, messageInput) => {
|
519 |
+
if (isStreaming) {
|
520 |
+
handleStop?.();
|
521 |
+
return;
|
522 |
+
}
|
523 |
+
|
524 |
+
handleSendMessage?.(event, messageInput);
|
525 |
+
})}
|
526 |
</div>
|
527 |
<ClientOnly>{() => <Workbench chatStarted={chatStarted} isStreaming={isStreaming} />}</ClientOnly>
|
528 |
</div>
|
app/components/chat/ModelSelector.tsx
ADDED
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { ProviderInfo } from '~/types/model';
|
2 |
+
import type { ModelInfo } from '~/utils/types';
|
3 |
+
|
4 |
+
interface ModelSelectorProps {
|
5 |
+
model?: string;
|
6 |
+
setModel?: (model: string) => void;
|
7 |
+
provider?: ProviderInfo;
|
8 |
+
setProvider?: (provider: ProviderInfo) => void;
|
9 |
+
modelList: ModelInfo[];
|
10 |
+
providerList: ProviderInfo[];
|
11 |
+
apiKeys: Record<string, string>;
|
12 |
+
}
|
13 |
+
|
14 |
+
export const ModelSelector = ({
|
15 |
+
model,
|
16 |
+
setModel,
|
17 |
+
provider,
|
18 |
+
setProvider,
|
19 |
+
modelList,
|
20 |
+
providerList,
|
21 |
+
}: ModelSelectorProps) => {
|
22 |
+
return (
|
23 |
+
<div className="mb-2 flex gap-2 flex-col sm:flex-row">
|
24 |
+
<select
|
25 |
+
value={provider?.name ?? ''}
|
26 |
+
onChange={(e) => {
|
27 |
+
const newProvider = providerList.find((p: ProviderInfo) => p.name === e.target.value);
|
28 |
+
|
29 |
+
if (newProvider && setProvider) {
|
30 |
+
setProvider(newProvider);
|
31 |
+
}
|
32 |
+
|
33 |
+
const firstModel = [...modelList].find((m) => m.provider === e.target.value);
|
34 |
+
|
35 |
+
if (firstModel && setModel) {
|
36 |
+
setModel(firstModel.name);
|
37 |
+
}
|
38 |
+
}}
|
39 |
+
className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
|
40 |
+
>
|
41 |
+
{providerList.map((provider: ProviderInfo) => (
|
42 |
+
<option key={provider.name} value={provider.name}>
|
43 |
+
{provider.name}
|
44 |
+
</option>
|
45 |
+
))}
|
46 |
+
</select>
|
47 |
+
<select
|
48 |
+
key={provider?.name}
|
49 |
+
value={model}
|
50 |
+
onChange={(e) => setModel?.(e.target.value)}
|
51 |
+
className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all lg:max-w-[70%]"
|
52 |
+
>
|
53 |
+
{[...modelList]
|
54 |
+
.filter((e) => e.provider == provider?.name && e.name)
|
55 |
+
.map((modelOption) => (
|
56 |
+
<option key={modelOption.name} value={modelOption.name}>
|
57 |
+
{modelOption.label}
|
58 |
+
</option>
|
59 |
+
))}
|
60 |
+
</select>
|
61 |
+
</div>
|
62 |
+
);
|
63 |
+
};
|
app/components/chat/SpeechRecognition.tsx
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { IconButton } from '~/components/ui/IconButton';
|
2 |
+
import { classNames } from '~/utils/classNames';
|
3 |
+
import React from 'react';
|
4 |
+
|
5 |
+
export const SpeechRecognitionButton = ({
|
6 |
+
isListening,
|
7 |
+
onStart,
|
8 |
+
onStop,
|
9 |
+
disabled,
|
10 |
+
}: {
|
11 |
+
isListening: boolean;
|
12 |
+
onStart: () => void;
|
13 |
+
onStop: () => void;
|
14 |
+
disabled: boolean;
|
15 |
+
}) => {
|
16 |
+
return (
|
17 |
+
<IconButton
|
18 |
+
title={isListening ? 'Stop listening' : 'Start speech recognition'}
|
19 |
+
disabled={disabled}
|
20 |
+
className={classNames('transition-all', {
|
21 |
+
'text-bolt-elements-item-contentAccent': isListening,
|
22 |
+
})}
|
23 |
+
onClick={isListening ? onStop : onStart}
|
24 |
+
>
|
25 |
+
{isListening ? <div className="i-ph:microphone-slash text-xl" /> : <div className="i-ph:microphone text-xl" />}
|
26 |
+
</IconButton>
|
27 |
+
);
|
28 |
+
};
|
app/types/global.d.ts
CHANGED
@@ -1,3 +1,5 @@
|
|
1 |
interface Window {
|
2 |
showDirectoryPicker(): Promise<FileSystemDirectoryHandle>;
|
|
|
|
|
3 |
}
|
|
|
1 |
interface Window {
|
2 |
showDirectoryPicker(): Promise<FileSystemDirectoryHandle>;
|
3 |
+
webkitSpeechRecognition: typeof SpeechRecognition;
|
4 |
+
SpeechRecognition: typeof SpeechRecognition;
|
5 |
}
|
app/utils/logger.ts
CHANGED
@@ -11,7 +11,7 @@ interface Logger {
|
|
11 |
setLevel: (level: DebugLevel) => void;
|
12 |
}
|
13 |
|
14 |
-
let currentLevel: DebugLevel = import.meta.env.VITE_LOG_LEVEL ?? import.meta.env.DEV ? 'debug' : 'info';
|
15 |
|
16 |
const isWorker = 'HTMLRewriter' in globalThis;
|
17 |
const supportsColor = !isWorker;
|
|
|
11 |
setLevel: (level: DebugLevel) => void;
|
12 |
}
|
13 |
|
14 |
+
let currentLevel: DebugLevel = (import.meta.env.VITE_LOG_LEVEL ?? import.meta.env.DEV) ? 'debug' : 'info';
|
15 |
|
16 |
const isWorker = 'HTMLRewriter' in globalThis;
|
17 |
const supportsColor = !isWorker;
|
package.json
CHANGED
@@ -101,6 +101,7 @@
|
|
101 |
"@cloudflare/workers-types": "^4.20241127.0",
|
102 |
"@remix-run/dev": "^2.15.0",
|
103 |
"@types/diff": "^5.2.3",
|
|
|
104 |
"@types/file-saver": "^2.0.7",
|
105 |
"@types/js-cookie": "^3.0.6",
|
106 |
"@types/react": "^18.3.12",
|
|
|
101 |
"@cloudflare/workers-types": "^4.20241127.0",
|
102 |
"@remix-run/dev": "^2.15.0",
|
103 |
"@types/diff": "^5.2.3",
|
104 |
+
"@types/dom-speech-recognition": "^0.0.4",
|
105 |
"@types/file-saver": "^2.0.7",
|
106 |
"@types/js-cookie": "^3.0.6",
|
107 |
"@types/react": "^18.3.12",
|
pnpm-lock.yaml
CHANGED
@@ -222,6 +222,9 @@ importers:
|
|
222 |
'@types/diff':
|
223 |
specifier: ^5.2.3
|
224 |
version: 5.2.3
|
|
|
|
|
|
|
225 |
'@types/file-saver':
|
226 |
specifier: ^2.0.7
|
227 |
version: 2.0.7
|
@@ -2039,6 +2042,9 @@ packages:
|
|
2039 |
'@types/[email protected]':
|
2040 |
resolution: {integrity: sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ==}
|
2041 |
|
|
|
|
|
|
|
2042 |
'@types/[email protected]':
|
2043 |
resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==}
|
2044 |
|
@@ -7464,6 +7470,8 @@ snapshots:
|
|
7464 |
|
7465 |
'@types/[email protected]': {}
|
7466 |
|
|
|
|
|
7467 |
'@types/[email protected]':
|
7468 |
dependencies:
|
7469 |
'@types/estree': 1.0.6
|
@@ -7812,7 +7820,7 @@ snapshots:
|
|
7812 |
'@babel/plugin-syntax-typescript': 7.25.9(@babel/[email protected])
|
7813 |
'@vanilla-extract/babel-plugin-debug-ids': 1.1.0
|
7814 |
'@vanilla-extract/css': 1.16.1
|
7815 |
-
esbuild: 0.17.
|
7816 |
eval: 0.1.8
|
7817 |
find-up: 5.0.0
|
7818 |
javascript-stringify: 2.1.0
|
|
|
222 |
'@types/diff':
|
223 |
specifier: ^5.2.3
|
224 |
version: 5.2.3
|
225 |
+
'@types/dom-speech-recognition':
|
226 |
+
specifier: ^0.0.4
|
227 |
+
version: 0.0.4
|
228 |
'@types/file-saver':
|
229 |
specifier: ^2.0.7
|
230 |
version: 2.0.7
|
|
|
2042 |
'@types/[email protected]':
|
2043 |
resolution: {integrity: sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ==}
|
2044 |
|
2045 |
+
'@types/[email protected]':
|
2046 |
+
resolution: {integrity: sha512-zf2GwV/G6TdaLwpLDcGTIkHnXf8JEf/viMux+khqKQKDa8/8BAUtXXZS563GnvJ4Fg0PBLGAaFf2GekEVSZ6GQ==}
|
2047 |
+
|
2048 |
'@types/[email protected]':
|
2049 |
resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==}
|
2050 |
|
|
|
7470 |
|
7471 |
'@types/[email protected]': {}
|
7472 |
|
7473 |
+
'@types/[email protected]': {}
|
7474 |
+
|
7475 |
'@types/[email protected]':
|
7476 |
dependencies:
|
7477 |
'@types/estree': 1.0.6
|
|
|
7820 |
'@babel/plugin-syntax-typescript': 7.25.9(@babel/[email protected])
|
7821 |
'@vanilla-extract/babel-plugin-debug-ids': 1.1.0
|
7822 |
'@vanilla-extract/css': 1.16.1
|
7823 |
+
esbuild: 0.17.19
|
7824 |
eval: 0.1.8
|
7825 |
find-up: 5.0.0
|
7826 |
javascript-stringify: 2.1.0
|
tsconfig.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
{
|
2 |
"compilerOptions": {
|
3 |
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
4 |
-
"types": ["@remix-run/cloudflare", "vite/client", "@cloudflare/workers-types/2023-07-01"],
|
5 |
"isolatedModules": true,
|
6 |
"esModuleInterop": true,
|
7 |
"jsx": "react-jsx",
|
|
|
1 |
{
|
2 |
"compilerOptions": {
|
3 |
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
4 |
+
"types": ["@remix-run/cloudflare", "vite/client", "@cloudflare/workers-types/2023-07-01", "@types/dom-speech-recognition"],
|
5 |
"isolatedModules": true,
|
6 |
"esModuleInterop": true,
|
7 |
"jsx": "react-jsx",
|