161 lines
4.9 KiB
TypeScript
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 få 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;
|