= ({ onSearch, onChange }) => {
+ return (
+
+ {
+ if (event.key === 'Enter') {
+ onSearch(event.currentTarget.value);
+ }
+ }}
+ onChange={(event) => {
+ onChange(event.target.value);
+ }}
+ style={{
+ width: '200px',
+ padding: '0.5rem',
+ marginTop: '0.5rem',
+ border: '1px solid #ccc',
+ borderRadius: '4px',
+ fontSize: '0.9rem',
+ textAlign: 'left',
+ }}
+ />
+
+ );
+};
\ No newline at end of file
diff --git a/app/components/header/Feedback.tsx b/app/components/header/Feedback.tsx
index f0c39f3f..29e0c674 100644
--- a/app/components/header/Feedback.tsx
+++ b/app/components/header/Feedback.tsx
@@ -2,7 +2,7 @@ import { toast } from 'react-toastify';
import ReactModal from 'react-modal';
import { useState } from 'react';
import { supabaseSubmitFeedback } from '~/lib/supabase/feedback';
-import { getLastChatMessages } from '~/components/chat/Chat.client';
+import { getLastChatMessages } from '~/utils/chat/messageUtils';
ReactModal.setAppElement('#root');
diff --git a/app/hooks/useSpeechRecognition.ts b/app/hooks/useSpeechRecognition.ts
new file mode 100644
index 00000000..01a2a092
--- /dev/null
+++ b/app/hooks/useSpeechRecognition.ts
@@ -0,0 +1,67 @@
+import { useState, useEffect } from 'react';
+
+interface UseSpeechRecognitionProps {
+ onTranscriptChange: (transcript: string) => void;
+}
+
+export const useSpeechRecognition = ({ onTranscriptChange }: UseSpeechRecognitionProps) => {
+ const [isListening, setIsListening] = useState(false);
+ const [recognition, setRecognition] = useState(null);
+ const [transcript, setTranscript] = useState('');
+
+ useEffect(() => {
+ if (typeof window !== 'undefined' && ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window)) {
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
+ const recognition = new SpeechRecognition();
+ recognition.continuous = true;
+ recognition.interimResults = true;
+
+ recognition.onresult = (event) => {
+ const transcript = Array.from(event.results)
+ .map((result) => result[0])
+ .map((result) => result.transcript)
+ .join('');
+
+ setTranscript(transcript);
+ onTranscriptChange(transcript);
+ };
+
+ recognition.onerror = (event) => {
+ console.error('Speech recognition error:', event.error);
+ setIsListening(false);
+ };
+
+ setRecognition(recognition);
+ }
+ }, [onTranscriptChange]);
+
+ const startListening = () => {
+ if (recognition) {
+ recognition.start();
+ setIsListening(true);
+ }
+ };
+
+ const stopListening = () => {
+ if (recognition) {
+ recognition.stop();
+ setIsListening(false);
+ }
+ };
+
+ const abortListening = () => {
+ if (recognition) {
+ recognition.abort();
+ setTranscript('');
+ setIsListening(false);
+ }
+ };
+
+ return {
+ isListening,
+ transcript,
+ startListening,
+ stopListening,
+ abortListening,
+ };
+};
\ No newline at end of file
diff --git a/app/lib/persistence/message.ts b/app/lib/persistence/message.ts
index dc641705..383ffb55 100644
--- a/app/lib/persistence/message.ts
+++ b/app/lib/persistence/message.ts
@@ -16,12 +16,12 @@ interface MessageBase {
approved?: boolean;
}
-interface MessageText extends MessageBase {
+export interface MessageText extends MessageBase {
type: 'text';
content: string;
}
-interface MessageImage extends MessageBase {
+export interface MessageImage extends MessageBase {
type: 'image';
dataURL: string;
}
diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx
index 7a50de3d..3bcfeb1c 100644
--- a/app/routes/_index.tsx
+++ b/app/routes/_index.tsx
@@ -2,7 +2,7 @@ import { json, type MetaFunction } from '~/lib/remix-types';
import { Suspense } from 'react';
import { ClientOnly } from 'remix-utils/client-only';
import { BaseChat } from '~/components/chat/BaseChat/BaseChat';
-import { Chat } from '~/components/chat/Chat/Chat.client';
+import { Chat } from '~/components/chat/ChatComponent/Chat.client';
import { PageContainer } from '~/layout/PageContainer';
export const meta: MetaFunction = () => {
return [{ title: 'Nut' }];
diff --git a/app/types/chat.ts b/app/types/chat.ts
new file mode 100644
index 00000000..67c6bcb9
--- /dev/null
+++ b/app/types/chat.ts
@@ -0,0 +1,27 @@
+import type { Message, MessageImage, MessageText } from '~/lib/persistence/message';
+import type { ResumeChatInfo } from '~/lib/persistence/useChatHistory';
+import type { RejectChangeData } from '../components/chat/ApproveChange';
+
+export interface ChatProps {
+ initialMessages: Message[];
+ resumeChat: ResumeChatInfo | undefined;
+ storeMessageHistory: (messages: Message[]) => void;
+}
+
+export interface ChatImplProps extends ChatProps {
+ onApproveChange?: (messageId: string) => Promise;
+ onRejectChange?: (messageId: string, data: RejectChangeData) => Promise;
+}
+
+// Re-export types we need
+export type { Message, MessageImage, MessageText, ResumeChatInfo };
+
+export interface UserMessage extends MessageText {
+ role: 'user';
+ type: 'text';
+}
+
+export interface UserImageMessage extends MessageImage {
+ role: 'user';
+ type: 'image';
+}
\ No newline at end of file
diff --git a/app/utils/chat/messageUtils.ts b/app/utils/chat/messageUtils.ts
new file mode 100644
index 00000000..6ed260be
--- /dev/null
+++ b/app/utils/chat/messageUtils.ts
@@ -0,0 +1,44 @@
+import { assert } from '~/lib/replay/ReplayProtocolClient';
+import type { Message } from '~/lib/persistence/message';
+
+let gLastChatMessages: Message[] | undefined;
+
+export function getLastChatMessages() {
+ return gLastChatMessages;
+}
+
+export function setLastChatMessages(messages: Message[] | undefined) {
+ gLastChatMessages = messages;
+}
+
+export function mergeResponseMessage(msg: Message, messages: Message[]): Message[] {
+ const lastMessage = messages[messages.length - 1];
+ if (lastMessage.id == msg.id) {
+ messages.pop();
+ assert(lastMessage.type == 'text', 'Last message must be a text message');
+ assert(msg.type == 'text', 'Message must be a text message');
+ messages.push({
+ ...msg,
+ content: lastMessage.content + msg.content,
+ });
+ } else {
+ messages.push(msg);
+ }
+ return messages;
+}
+
+// Get the index of the last message that will be present after rewinding.
+// This is the last message which is either a user message or has a repository change.
+export function getRewindMessageIndexAfterReject(messages: Message[], messageId: string): number {
+ for (let i = messages.length - 1; i >= 0; i--) {
+ const { id, role, repositoryId } = messages[i];
+ if (role == 'user') {
+ return i;
+ }
+ if (repositoryId && id != messageId) {
+ return i;
+ }
+ }
+ console.error('No rewind message found', messages, messageId);
+ return -1;
+}
\ No newline at end of file
diff --git a/app/utils/chat/simulationUtils.ts b/app/utils/chat/simulationUtils.ts
new file mode 100644
index 00000000..95566ef3
--- /dev/null
+++ b/app/utils/chat/simulationUtils.ts
@@ -0,0 +1,26 @@
+import { getIFrameSimulationData } from '~/lib/replay/Recording';
+import { getCurrentIFrame } from '~/components/workbench/Preview';
+import { simulationAddData } from '~/lib/replay/ChatManager';
+
+export async function flushSimulationData() {
+ const iframe = getCurrentIFrame();
+
+ if (!iframe) {
+ return;
+ }
+
+ const simulationData = await getIFrameSimulationData(iframe);
+
+ if (!simulationData.length) {
+ return;
+ }
+
+ simulationAddData(simulationData);
+}
+
+// Set up the interval in a separate function that can be called once
+export function setupSimulationInterval() {
+ setInterval(async () => {
+ flushSimulationData();
+ }, 1000);
+}
\ No newline at end of file