OSVauco/opax-web/frontend/src/components/ChatWorkspace.tsx

161 lines
4.9 KiB
TypeScript

import React, { useEffect, useRef } from 'react';
export interface Message {
id: string;
role: 'user' | 'assistant' | 'status';
content: string;
isError?: boolean;
retryPayload?: Message;
}
interface ChatWorkspaceProps {
messages: Message[];
isLoading: boolean;
onSendMessage: (message: string) => void;
onRetry: (originalMessage: Message, errorStatusId: string) => void;
composerValue: string;
setComposerValue: (value: string) => void;
composerRef: React.RefObject<HTMLTextAreaElement>;
}
const StarterPrompts = ({ onSelect }: { onSelect: (prompt: string) => void }) => {
const prompts = [
'Gi meg en kort status på det viktigste jeg bør følge opp.',
'Hjelp meg å strukturere neste arbeidsøkt.',
'Oppsummer risikoer og neste beslutninger.',
'Lag et utkast til en konkret oppfølgingsplan.',
];
return (
<div className="empty-state">
<div className="empty-state-mark"></div>
<h1>Hva vil du oversikt over i dag?</h1>
<p>Emma hjelper deg å strukturere arbeid, analysere situasjoner og finne tydelige neste steg.</p>
<div className="starter-prompts">
{prompts.map((p) => (
<button key={p} className="starter-prompt" onClick={() => onSelect(p)}>
{p}
</button>
))}
</div>
</div>
);
};
const PromptComposer = ({ onSend, isLoading, value, setValue, composerRef }: {
onSend: (val: string) => void;
isLoading: boolean;
value: string;
setValue: (val: string) => void;
composerRef: React.RefObject<HTMLTextAreaElement>;
}) => {
useEffect(() => {
if (composerRef.current) {
composerRef.current.style.height = 'auto';
composerRef.current.style.height = `${composerRef.current.scrollHeight}px`;
}
}, [value, composerRef]);
const handleSend = () => {
if (value.trim()) onSend(value.trim());
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
return (
<div className="composer-container">
<div className="composer-inner">
<div className="composer">
<textarea
ref={composerRef}
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Spør Emma om status, analyse eller neste steg …"
disabled={isLoading}
rows={1}
aria-label="Chat message input"
/>
<button onClick={handleSend} disabled={isLoading || !value.trim()} aria-label="Send message">
</button>
</div>
</div>
</div>
);
};
const ChatWorkspace: React.FC<ChatWorkspaceProps> = ({ messages, isLoading, onSendMessage, onRetry, composerValue, setComposerValue, composerRef }) => {
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [messages, isLoading]);
const handleStarterSelect = (prompt: string) => {
setComposerValue(prompt);
setTimeout(() => {
if (composerRef.current) {
composerRef.current.focus();
composerRef.current.setSelectionRange(prompt.length, prompt.length);
}
}, 0);
};
return (
<div className="chat-workspace">
<div className="transcript" ref={scrollRef}>
<div className="transcript-inner">
{messages.length === 0 && !isLoading ? (
<StarterPrompts onSelect={handleStarterSelect} />
) : (
messages.map((msg) => {
if (msg.role === 'status' && msg.isError) {
return (
<div key={msg.id} className="message assistant">
<div className="error-message" role="alert">
<p>{msg.content}</p>
{msg.retryPayload && <button onClick={() => onRetry(msg.retryPayload!, msg.id)}>Prøv igjen</button>}
</div>
</div>
);
}
return (
<div key={msg.id} className={`message ${msg.role}`}>
<div className="message-content">
<div className="message-role" aria-hidden="true">{msg.role}</div>
<p>{msg.content}</p>
</div>
</div>
);
})
)}
{isLoading && (
<div className="message assistant">
<div className="message-content">
<div className="loading-indicator" aria-live="polite">Emma tenker </div>
</div>
</div>
)}
</div>
</div>
<PromptComposer
onSend={onSendMessage}
isLoading={isLoading}
value={composerValue}
setValue={setComposerValue}
composerRef={composerRef}
/>
</div>
);
};
export default ChatWorkspace;