Listening Convo Edit and a bunch of ts errors

This commit is contained in:
Carlos-Mesquita
2024-11-06 19:43:06 +00:00
parent b5ac908d09
commit 5165b6ae6d
13 changed files with 555 additions and 218 deletions

View File

@@ -0,0 +1,86 @@
import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea";
import { useState } from "react";
import { FaEdit, FaFemale, FaMale } from "react-icons/fa";
import { FaTrash } from "react-icons/fa6";
import { ScriptLine } from ".";
interface MessageProps {
message: ScriptLine & { position: 'left' | 'right' };
color: string;
editing: boolean;
onEdit?: (text: string) => void;
onDelete?: () => void;
}
const Message: React.FC<MessageProps> = ({ message, color, editing, onEdit, onDelete }) => {
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(message.text);
return (
<div className={`flex items-start gap-2 ${message.position === 'left' ? 'justify-start' : 'justify-end'}`}>
<div className="flex flex-col w-[50%]">
<div className={`flex items-center gap-2 ${message.position === 'right' && 'self-end'}`}>
{message.gender === 'male' ? (
<FaMale className="w-5 h-5 text-blue-500" />
) : (
<FaFemale className="w-5 h-5 text-pink-500" />
)}
<span className="text-sm font-medium">{message.name}</span>
</div>
<div className={`rounded-lg p-3 bg-${color}-100 relative group mt-1`}>
{isEditing ? (
<div className="flex flex-col gap-2">
<AutoExpandingTextArea
value={editText}
onChange={setEditText}
placeholder="Edit message..."
className="w-full min-h-[96px] px-4 py-2 border border-gray-200 rounded-lg focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-base resize-none"
/>
<div className="flex justify-between">
<button
className="px-3 py-1 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors text-sm font-medium"
onClick={() => {
onEdit?.(editText);
setIsEditing(false);
}}
>
Save
</button>
<button
className="px-3 py-1 bg-red-500 rounded-md hover:bg-gray-100 transition-colors text-sm font-medium text-white"
onClick={() => setIsEditing(false)}
>
Cancel
</button>
</div>
</div>
) : (
<div className="flex items-start justify-between gap-2">
<p className="text-gray-700 whitespace-pre-wrap flex-grow">{message.text}</p>
{editing && (
<div className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
<div className="flex gap-1">
<button
onClick={() => setIsEditing(true)}
className="p-1 rounded hover:bg-gray-200 transition-colors"
>
<FaEdit className="w-3.5 h-3.5 text-gray-600" />
</button>
<button
onClick={onDelete}
className="p-1 rounded hover:bg-gray-200 transition-colors"
>
<FaTrash className="w-3.5 h-3.5 text-red-500" />
</button>
</div>
</div>
)}
</div>
)}
</div>
</div>
</div>
);
};
export default Message;

View File

@@ -0,0 +1,330 @@
import React, { useState, useMemo } from 'react';
import { Script } from "@/interfaces/exam";
import Message from './Message';
import AutoExpandingTextArea from '@/components/Low/AutoExpandingTextarea';
import { Card, CardContent } from '@/components/ui/card';
import Input from '@/components/Low/Input';
import { FaFemale, FaMale, FaPlus } from 'react-icons/fa';
import clsx from 'clsx';
export interface Speaker {
id: number;
name: string;
gender: 'male' | 'female';
color: string;
position: 'left' | 'right';
}
type Gender = 'male' | 'female';
export interface ScriptLine {
name: string;
gender: Gender;
text: string;
voice?: string;
}
interface MessageWithPosition extends ScriptLine {
position: 'left' | 'right';
}
interface Props {
section: number;
editing?: boolean;
local: Script;
setLocal: (script: Script) => void;
}
const colorOptions = [
'red', 'blue', 'green', 'purple', 'pink', 'indigo', 'teal', 'orange',
'cyan', 'emerald', 'sky', 'violet', 'fuchsia', 'rose', 'lime', 'slate'
];
const ScriptEditor: React.FC<Props> = ({ section, editing = false, local, setLocal }) => {
const isConversation = [1, 3].includes(section);
const [selectedSpeaker, setSelectedSpeaker] = useState<string>('');
const [newMessage, setNewMessage] = useState('');
const speakerCount = section === 1 ? 2 : 4;
const [speakers, setSpeakers] = useState<Speaker[]>(() => {
const existingScript = local as ScriptLine[];
const existingSpeakers = new Set<string>();
const speakerGenders = new Map<string, 'male' | 'female'>();
if (Array.isArray(existingScript)) {
existingScript.forEach(line => {
existingSpeakers.add(line.name);
speakerGenders.set(line.name, line.gender.toLowerCase() === 'female' ? 'female' : 'male' as 'male' | 'female');
});
}
const speakerArray = Array.from(existingSpeakers);
const totalNeeded = Math.max(speakerCount, speakerArray.length);
return Array.from({ length: totalNeeded }, (_, index) => {
if (index < speakerArray.length) {
return {
id: index,
name: speakerArray[index],
gender: speakerGenders.get(speakerArray[index]) || 'male',
color: colorOptions[index],
position: index % 2 === 0 ? 'left' : 'right'
};
}
return {
id: index,
name: '',
gender: 'male',
color: colorOptions[index],
position: index % 2 === 0 ? 'left' : 'right'
};
});
});
const speakerProperties = useMemo(() => {
return speakers.reduce((acc, speaker) => {
if (speaker.name) {
acc[speaker.name] = {
color: speaker.color,
gender: speaker.gender
};
}
return acc;
}, {} as Record<string, { color: string; gender: 'male' | 'female' }>);
}, [speakers]);
const allSpeakersConfigured = useMemo(() => {
return speakers.every(speaker => speaker.name.trim() !== '');
}, [speakers]);
const updateSpeaker = (index: number, updates: Partial<Speaker>) => {
const updatedSpeakers = speakers.map((speaker, i) => {
if (i === index) {
return { ...speaker, ...updates };
}
return speaker;
});
setSpeakers(updatedSpeakers);
if (Array.isArray(local)) {
if ('name' in updates && speakers[index].name) {
const oldName = speakers[index].name;
const newName = updates.name || '';
const updatedScript = local.map(line => {
if (line.name === oldName) {
return { ...line, name: newName };
}
return line;
});
setLocal(updatedScript);
}
if ('gender' in updates && speakers[index].name && updates.gender) {
const name = speakers[index].name;
const newGender = updates.gender;
const updatedScript = local.map(line => {
if (line.name === name) {
return { ...line, gender: newGender };
}
return line;
});
setLocal(updatedScript);
}
}
if ('name' in updates && speakers[index].name === selectedSpeaker) {
setSelectedSpeaker(updates.name || '');
}
};
const addMessage = () => {
if (!isConversation || !selectedSpeaker || !newMessage.trim()) return;
if (!Array.isArray(local)) return;
const speaker = speakers.find(s => s.name === selectedSpeaker);
if (!speaker) return;
const newLine: ScriptLine = {
name: selectedSpeaker,
gender: speaker.gender,
text: newMessage.trim()
};
const updatedScript = [...local, newLine];
setLocal(updatedScript);
setNewMessage('');
};
const updateMessage = (index: number, newText: string) => {
if (!Array.isArray(local)) return;
const updatedScript = [...local];
updatedScript[index] = { ...updatedScript[index], text: newText };
setLocal(updatedScript);
};
const deleteMessage = (index: number) => {
if (!Array.isArray(local)) return;
const updatedScript = local.filter((_, i) => i !== index);
setLocal(updatedScript);
};
const updateMonologue = (text: string) => {
setLocal(text);
};
const messages = useMemo(() => {
if (typeof local === 'string' || !Array.isArray(local)) return [];
return local.reduce<MessageWithPosition[]>((acc, line, index) => {
const normalizedLine = {
...line,
gender: line.gender.toLowerCase() === 'female' ? 'female' : 'male'
} as ScriptLine;
if (index === 0) {
acc.push({ ...normalizedLine, position: 'left' });
} else {
const prevMsg = acc[index - 1];
const position = line.name === prevMsg.name
? prevMsg.position
: (prevMsg.position === 'left' ? 'right' : 'left');
acc.push({ ...normalizedLine, position });
}
return acc;
}, []);
}, [local]);
if (!isConversation) {
return (
<Card>
<CardContent className="py-10">
<div className="w-full">
{editing ? (
<AutoExpandingTextArea
value={local as string}
onChange={updateMonologue}
placeholder='Write the monologue here...'
/>
) : (
<div className="p-8 shadow-inner border border-gray-200 bg-gray-50 rounded-xl">
<span>{(local as string) || "Edit, generate or import your own audio."}</span>
</div>
)}
</div>
</CardContent>
</Card>
);
}
return (
<Card>
<CardContent className="py-10">
<div className="space-y-6">
{editing && (
<div className="bg-white rounded-2xl p-6 shadow-inner border">
<h3 className="text-lg font-medium text-gray-700 mb-6">Edit Conversation</h3>
<div className="space-y-4 mb-6">
{speakers.map((speaker, index) => (
<div key={index} className="flex items-center gap-4">
<div className="flex-1">
<Input
type="text"
name=""
value={speaker.name}
onChange={(text) => updateSpeaker(index, { name: text })}
placeholder="Speaker name"
/>
</div>
<div className="w-[140px] relative">
<select
value={speaker.gender}
onChange={(e) => updateSpeaker(index, { gender: e.target.value as 'male' | 'female' })}
className="w-full appearance-none px-4 py-2 border border-gray-200 rounded-full text-base bg-white focus:ring-1 focus:ring-blue-500 focus:outline-none"
>
<option value="female">Female</option>
<option value="male">Male</option>
</select>
<div className="absolute right-3 top-2.5 pointer-events-none">
{speaker.gender === 'male' ? (
<FaMale className="w-5 h-5 text-blue-500" />
) : (
<FaFemale className="w-5 h-5 text-pink-500" />
)}
</div>
</div>
</div>
))}
</div>
<div className="flex gap-4">
<div className="w-[240px] flex flex-col gap-2">
<select
value={selectedSpeaker}
onChange={(e) => setSelectedSpeaker(e.target.value)}
disabled={!allSpeakersConfigured}
className="w-full h-[42px] px-4 appearance-none border border-gray-200 rounded-full focus:ring-1 focus:ring-blue-500 focus:outline-none bg-white text-gray-700 text-base disabled:bg-gray-100"
>
<option value="">Select Speaker ...</option>
{speakers.filter(s => s.name).map((speaker) => (
<option key={speaker.id} value={speaker.name}>
{speaker.name}
</option>
))}
</select>
<button
onClick={addMessage}
disabled={!selectedSpeaker || !newMessage.trim() || !allSpeakersConfigured}
className={clsx(
"w-full h-[42px] rounded-lg flex items-center justify-center gap-2 transition-colors font-medium",
!selectedSpeaker || !newMessage.trim() || !allSpeakersConfigured
? 'bg-gray-100 text-gray-500 cursor-not-allowed'
: 'bg-blue-500 text-white hover:bg-blue-600'
)}
>
<FaPlus className="w-4 h-4" />
Add
</button>
</div>
<div className="flex-1">
<AutoExpandingTextArea
value={newMessage}
onChange={setNewMessage}
placeholder={allSpeakersConfigured ? "Type your message..." : "Configure all speakers first"}
disabled={!allSpeakersConfigured}
className="w-full min-h-[96px] px-4 py-2 border border-gray-200 rounded-lg focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-base resize-none disabled:bg-gray-100"
/>
</div>
</div>
</div>
)}
<div className="space-y-4">
{messages.map((message, index) => {
const properties = speakerProperties[message.name];
if (!properties) return null;
return (
<Message
key={index}
message={message}
color={properties.color}
editing={editing}
onEdit={(text: string) => updateMessage(index, text)}
onDelete={() => deleteMessage(index)}
/>
);
})}
</div>
</div>
</CardContent>
</Card>
);
};
export default ScriptEditor;

View File

@@ -1,134 +0,0 @@
import React, { useState, useMemo } from 'react';
import { Script } from '@/interfaces/exam';
import { FaFemale, FaMale } from "react-icons/fa";
import AutoExpandingTextArea from '@/components/Low/AutoExpandingTextarea';
import clsx from 'clsx';
const colorOptions = [
'red', 'blue', 'green', 'purple', 'pink', 'indigo', 'teal', 'orange',
'cyan', 'emerald', 'sky', 'violet', 'fuchsia', 'rose', 'lime', 'slate'
];
interface Speaker {
id: number;
name: string;
gender: 'male' | 'female';
color: string;
position: 'left' | 'right';
}
interface Props {
script?: Script;
setScript: React.Dispatch<React.SetStateAction<Script | undefined>>;
editing?: boolean;
}
const ScriptRender: React.FC<Props> = ({ script, setScript, editing = false }) => {
const [speakers, setSpeakers] = useState<Speaker[]>(() => {
if (!script || typeof script === 'string') return [];
const uniqueSpeakers = new Map();
const usedColors = new Set();
let isLeft = true;
script.forEach((line, index) => {
if (!uniqueSpeakers.has(line.name)) {
const availableColors = colorOptions.filter(color => !usedColors.has(color));
if (availableColors.length === 0) {
usedColors.clear();
}
const randomColor = availableColors[Math.floor(Math.random() * availableColors.length)];
usedColors.add(randomColor);
uniqueSpeakers.set(line.name, {
id: index,
name: line.name,
gender: line.gender,
color: randomColor,
position: isLeft ? 'left' : 'right'
});
isLeft = !isLeft;
}
});
return Array.from(uniqueSpeakers.values());
});
const speakerProperties = useMemo(() => {
return speakers.reduce((acc, speaker) => {
acc[speaker.name] = {
color: speaker.color,
position: speaker.position
};
return acc;
}, {} as Record<string, { color: string; position: 'left' | 'right' }>);
}, [speakers]);
if (script === undefined) return null;
if (typeof script === 'string') {
return (
<div className="w-full px-4">
{editing ? (
<AutoExpandingTextArea
className="w-full p-3 border rounded bg-white"
value={script}
onChange={(text) => setScript(text)}
/>
) : (
<p className="text-gray-700 p-3 bg-gray-100 rounded-lg" dangerouslySetInnerHTML={{ __html: script.split("\n").join("<br>") }} />
)}
</div>
);
}
const updateMessage = (index: number, newText: string) => {
setScript([
...script.slice(0, index),
{ ...script[index], text: newText },
...script.slice(index + 1)
]);
};
return (
<div className="w-full px-4">
<div className="space-y-2">
{script.map((line, index) => {
const { color, position } = speakerProperties[line.name];
return (
<div
key={index}
className={`flex items-start gap-2 ${position === 'left' ? 'justify-start' : 'justify-end'}`}
>
<div className="flex flex-col w-[50%]">
<div className={clsx('flex', position !== 'left' && 'self-end')}>
{line.gender === 'male' ? (
<FaMale className="w-5 h-5 text-blue-500 mb-1" />
) : (
<FaFemale className="w-5 h-5 text-pink-500 mb-1" />
)}
<span className="text-sm mb-1">
{line.name}
</span>
</div>
<div className={`rounded-lg p-3 bg-${color}-100`}>
{editing ? (
<AutoExpandingTextArea
className="w-full p-2 border rounded bg-white"
value={line.text}
onChange={(text) => updateMessage(index, text)}
/>
) : (
<p className="text-gray-700">{line.text}</p>
)}
</div>
</div>
</div>
);
})}
</div>
</div>
);
};
export default ScriptRender;