stable
This commit is contained in:
parent
b7614eb3f8
commit
db4ce8b14f
22 changed files with 3557 additions and 823 deletions
|
|
@ -1,38 +1,199 @@
|
|||
import { useEffect, useRef } from "preact/hooks";
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
import type { LogLine } from "../types";
|
||||
|
||||
interface Props {
|
||||
lines: LogLine[];
|
||||
disabled: boolean;
|
||||
onSendMessage(text: string): Promise<void>;
|
||||
onExpandChange?(expanded: boolean): void;
|
||||
}
|
||||
|
||||
export function LogPanel({ lines }: Props) {
|
||||
const innerRef = useRef<HTMLDivElement>(null);
|
||||
interface LogViewProps {
|
||||
lines: LogLine[];
|
||||
scrollRef: { current: HTMLElement | null };
|
||||
}
|
||||
|
||||
// Scroll to top (newest line — column-reverse layout) after each update
|
||||
useEffect(() => {
|
||||
const el = innerRef.current?.parentElement;
|
||||
if (el) el.scrollTop = 0;
|
||||
}, [lines]);
|
||||
function SendIcon() {
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
||||
<path d="M2 9L16 2L9.5 16L8 10.5L2 9Z" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function CloseIcon() {
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
||||
<path d="M14 4L4 14M4 4L14 14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function formatLine(line: LogLine): string {
|
||||
const time = line.timestamp ? new Date(line.timestamp).toLocaleTimeString() : "";
|
||||
const role = line.role.trim().toLowerCase();
|
||||
if (role === "nanobot") {
|
||||
return `[${time}] ${line.text.replace(/^(?:nanobot|napbot)\b\s*[:>-]?\s*/i, "")}`;
|
||||
}
|
||||
if (role === "tool") {
|
||||
return `[${time}] tool: ${line.text}`;
|
||||
}
|
||||
return `[${time}] ${line.role}: ${line.text}`;
|
||||
}
|
||||
|
||||
function LogCompose({
|
||||
disabled,
|
||||
sending,
|
||||
text,
|
||||
setText,
|
||||
onClose,
|
||||
onSend,
|
||||
}: {
|
||||
disabled: boolean;
|
||||
sending: boolean;
|
||||
text: string;
|
||||
setText(value: string): void;
|
||||
onClose(): void;
|
||||
onSend(): void;
|
||||
}) {
|
||||
const onKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
onSend();
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[onClose, onSend],
|
||||
);
|
||||
|
||||
return (
|
||||
<div id="log">
|
||||
<div id="log-inner" ref={innerRef}>
|
||||
{lines.map((line) => {
|
||||
const time = line.timestamp ? new Date(line.timestamp).toLocaleTimeString() : "";
|
||||
const role = line.role.trim().toLowerCase();
|
||||
let text: string;
|
||||
if (role === "nanobot") {
|
||||
text = `[${time}] ${line.text.replace(/^(?:nanobot|napbot)\b\s*[:>-]?\s*/i, "")}`;
|
||||
} else {
|
||||
text = `[${time}] ${line.role}: ${line.text}`;
|
||||
}
|
||||
return (
|
||||
<div key={line.id} class={`line ${line.role}`}>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div id="log-compose">
|
||||
<textarea
|
||||
id="log-compose-input"
|
||||
placeholder="Type a message to nanobot..."
|
||||
disabled={disabled || sending}
|
||||
value={text}
|
||||
onInput={(e) => setText((e.target as HTMLTextAreaElement).value)}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
<div id="log-compose-actions">
|
||||
<button id="log-close-btn" type="button" aria-label="Close" onClick={onClose}>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
<button
|
||||
id="log-send-btn"
|
||||
type="button"
|
||||
aria-label="Send message"
|
||||
disabled={disabled || sending || text.trim().length === 0}
|
||||
onClick={onSend}
|
||||
>
|
||||
<SendIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ExpandedLogView({ lines, scrollRef }: LogViewProps) {
|
||||
return (
|
||||
<div
|
||||
id="log-scroll"
|
||||
ref={(node) => {
|
||||
scrollRef.current = node;
|
||||
}}
|
||||
>
|
||||
<div id="log-inner">
|
||||
{lines.map((line) => (
|
||||
<div key={line.id} class={`line ${line.role}`}>
|
||||
{formatLine(line)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CollapsedLogView({ lines, scrollRef, onExpand }: LogViewProps & { onExpand(): void }) {
|
||||
return (
|
||||
<button
|
||||
id="log-collapsed"
|
||||
ref={(node) => {
|
||||
scrollRef.current = node;
|
||||
}}
|
||||
type="button"
|
||||
aria-label="Open message composer"
|
||||
onClick={onExpand}
|
||||
>
|
||||
<div id="log-inner">
|
||||
{lines.map((line) => (
|
||||
<span key={line.id} class={`line ${line.role}`}>
|
||||
{formatLine(line)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function LogPanel({ lines, disabled, onSendMessage, onExpandChange }: Props) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [text, setText] = useState("");
|
||||
const [sending, setSending] = useState(false);
|
||||
const scrollRef = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => onExpandChange?.(expanded), [expanded, onExpandChange]);
|
||||
useEffect(() => () => onExpandChange?.(false), [onExpandChange]);
|
||||
useEffect(() => {
|
||||
const el = scrollRef.current;
|
||||
if (el) el.scrollTop = el.scrollHeight;
|
||||
}, [lines, expanded]);
|
||||
|
||||
const collapse = useCallback(() => {
|
||||
setExpanded(false);
|
||||
setText("");
|
||||
}, []);
|
||||
|
||||
const expand = useCallback(() => {
|
||||
if (!expanded) setExpanded(true);
|
||||
}, [expanded]);
|
||||
|
||||
const send = useCallback(async () => {
|
||||
const message = text.trim();
|
||||
if (!message || sending || disabled) return;
|
||||
setSending(true);
|
||||
try {
|
||||
await onSendMessage(message);
|
||||
setText("");
|
||||
} catch (err) {
|
||||
window.alert(`Could not send message: ${String(err)}`);
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
}, [disabled, onSendMessage, sending, text]);
|
||||
|
||||
return (
|
||||
<div id="log" class={expanded ? "expanded" : ""} data-no-swipe="1">
|
||||
{expanded ? (
|
||||
<ExpandedLogView lines={lines} scrollRef={scrollRef} />
|
||||
) : (
|
||||
<CollapsedLogView lines={lines} scrollRef={scrollRef} onExpand={expand} />
|
||||
)}
|
||||
{expanded && (
|
||||
<LogCompose
|
||||
disabled={disabled}
|
||||
sending={sending}
|
||||
text={text}
|
||||
setText={setText}
|
||||
onClose={collapse}
|
||||
onSend={() => {
|
||||
void send();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue