nandi/oripublic Fork 0
2434cc7fb724f02b34c0189dfd5fb30c1a8a68e3
Commits
Clone
git clone https://git.rickub.com/nandi/ori.git
git clone ssh://git@rickub.com/nandi/ori.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

forked from bots-garden/ori

ChatThread.tsx · 75 lines · 2.2 KBTypeScript Blame HistoryRaw
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
 * ChatThread renders the conversation: user prompts, streamed agent answers
 * (markdown) and inline errors, auto-scrolled to the latest entry.
 *
 * @example
 * <ChatThread thread={[{ kind: "user", text: "hi" }]} turnActive={false} />
 */

import { useEffect, useRef } from "react";
import type { ThreadItem } from "../reducer";
import { Markdown } from "./Markdown";
import { ToolCallCard } from "./ToolCallCard";

interface ChatThreadProps {
	thread: ThreadItem[];
	turnActive: boolean;
}

export function ChatThread({ thread, turnActive }: ChatThreadProps) {
	const endRef = useRef<HTMLDivElement>(null);

	// biome-ignore lint/correctness/useExhaustiveDependencies: thread and turnActive intentionally retrigger the scroll-to-bottom on every conversation change.
	useEffect(() => {
		// Optional call: test DOMs (jsdom) do not implement scrollIntoView.
		endRef.current?.scrollIntoView?.({ behavior: "smooth" });
	}, [thread, turnActive]);

	return (
		<section className="thread" aria-label="conversation">
			{thread.length === 0 && (
				<p className="thread-empty">
					Send a prompt to start working with the agent.
				</p>
			)}
			{thread.map((item, index) => (
				// biome-ignore lint/suspicious/noArrayIndexKey: the thread is append-only (never reordered or spliced), so the index is a stable identity.
				<ThreadEntry key={index} item={item} />
			))}
			{turnActive && (
				<output className="thread-working">the agent is working</output>
			)}
			<div ref={endRef} />
		</section>
	);
}

function ThreadEntry({ item }: { item: ThreadItem }) {
	switch (item.kind) {
		case "user":
			return <div className="message message-user">{item.text}</div>;
		case "agent":
			return (
				<div className="message message-agent">
					<Markdown text={item.text} />
				</div>
			);
		case "thought":
			return (
				<details className="thought">
					<summary className="thought-summary">💭 Thinking</summary>
					<div className="thought-body">
						<Markdown text={item.text} />
					</div>
				</details>
			);
		case "tool":
			return <ToolCallCard call={item.call} />;
		case "error":
			return (
				<div className="message message-error" role="alert">
					{item.text}
				</div>
			);
	}
}