@pcoi/components 0.1.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/index.d.ts +829 -609
- package/dist/index.js +3517 -2
- package/dist/styles.css +2 -0
- package/package.json +35 -31
- package/dist/components.css +0 -1
- package/dist/index.mjs +0 -994
- package/src/Badge/Badge.css +0 -40
- package/src/Badge/Badge.tsx +0 -36
- package/src/Badge/index.ts +0 -2
- package/src/Button/Button.css +0 -93
- package/src/Button/Button.figma.tsx +0 -27
- package/src/Button/Button.test.tsx +0 -32
- package/src/Button/Button.tsx +0 -47
- package/src/Button/index.ts +0 -1
- package/src/Callout/Callout.css +0 -48
- package/src/Callout/Callout.figma.tsx +0 -25
- package/src/Callout/Callout.tsx +0 -43
- package/src/Callout/index.ts +0 -1
- package/src/Card/Card.css +0 -88
- package/src/Card/Card.figma.tsx +0 -28
- package/src/Card/Card.tsx +0 -60
- package/src/Card/index.ts +0 -1
- package/src/ChatInterface/ChatInterface.css +0 -50
- package/src/ChatInterface/ChatInterface.integration.test.tsx +0 -123
- package/src/ChatInterface/ChatInterface.tsx +0 -125
- package/src/ChatInterface/index.ts +0 -6
- package/src/ChatMessage/ChatMessage.css +0 -55
- package/src/ChatMessage/ChatMessage.tsx +0 -71
- package/src/ChatMessage/index.ts +0 -2
- package/src/ChatMessageList/ChatMessageList.css +0 -24
- package/src/ChatMessageList/ChatMessageList.test.tsx +0 -70
- package/src/ChatMessageList/ChatMessageList.tsx +0 -56
- package/src/ChatMessageList/index.ts +0 -2
- package/src/Checkbox/Checkbox.css +0 -97
- package/src/Checkbox/Checkbox.tsx +0 -70
- package/src/Checkbox/index.ts +0 -2
- package/src/CitationMark/CitationMark.css +0 -40
- package/src/CitationMark/CitationMark.tsx +0 -38
- package/src/CitationMark/index.ts +0 -2
- package/src/CitedExcerpt/CitedExcerpt.css +0 -75
- package/src/CitedExcerpt/CitedExcerpt.tsx +0 -53
- package/src/CitedExcerpt/index.ts +0 -2
- package/src/ComparisonTable/ComparisonTable.css +0 -66
- package/src/ComparisonTable/ComparisonTable.tsx +0 -54
- package/src/ComparisonTable/index.ts +0 -1
- package/src/ContactForm/ContactForm.css +0 -38
- package/src/ContactForm/ContactForm.tsx +0 -58
- package/src/ContactForm/index.ts +0 -1
- package/src/DataTable/DataTable.css +0 -56
- package/src/DataTable/DataTable.tsx +0 -104
- package/src/DataTable/index.ts +0 -2
- package/src/DocumentOverlay/DocumentOverlay.css +0 -57
- package/src/DocumentOverlay/DocumentOverlay.test.tsx +0 -95
- package/src/DocumentOverlay/DocumentOverlay.tsx +0 -87
- package/src/DocumentOverlay/index.ts +0 -2
- package/src/Footer/Footer.css +0 -72
- package/src/Footer/Footer.tsx +0 -59
- package/src/Footer/index.ts +0 -1
- package/src/FormField/FormField.css +0 -78
- package/src/FormField/FormField.figma.tsx +0 -28
- package/src/FormField/FormField.tsx +0 -103
- package/src/FormField/index.ts +0 -2
- package/src/HowStep/HowStep.css +0 -48
- package/src/HowStep/HowStep.figma.tsx +0 -23
- package/src/HowStep/HowStep.tsx +0 -38
- package/src/HowStep/index.ts +0 -1
- package/src/LogoMark/LogoMark.css +0 -16
- package/src/LogoMark/LogoMark.tsx +0 -24
- package/src/LogoMark/index.ts +0 -2
- package/src/Modal/Modal.css +0 -101
- package/src/Modal/Modal.figma.tsx +0 -28
- package/src/Modal/Modal.test.tsx +0 -46
- package/src/Modal/Modal.tsx +0 -144
- package/src/Modal/index.ts +0 -2
- package/src/Nav/Nav.css +0 -161
- package/src/Nav/Nav.tsx +0 -105
- package/src/Nav/index.ts +0 -1
- package/src/Panel/Panel.css +0 -35
- package/src/Panel/Panel.tsx +0 -61
- package/src/Panel/index.ts +0 -2
- package/src/PromptBar/PromptBar.css +0 -68
- package/src/PromptBar/PromptBar.figma.tsx +0 -25
- package/src/PromptBar/PromptBar.test.tsx +0 -83
- package/src/PromptBar/PromptBar.tsx +0 -93
- package/src/PromptBar/index.ts +0 -2
- package/src/RadioGroup/RadioGroup.css +0 -117
- package/src/RadioGroup/RadioGroup.tsx +0 -112
- package/src/RadioGroup/index.ts +0 -2
- package/src/SectionHeader/SectionHeader.css +0 -38
- package/src/SectionHeader/SectionHeader.figma.tsx +0 -23
- package/src/SectionHeader/SectionHeader.tsx +0 -55
- package/src/SectionHeader/index.ts +0 -1
- package/src/Select/Select.css +0 -90
- package/src/Select/Select.figma.tsx +0 -33
- package/src/Select/Select.tsx +0 -100
- package/src/Select/index.ts +0 -2
- package/src/SignalsPanel/SignalsPanel.css +0 -51
- package/src/SignalsPanel/SignalsPanel.tsx +0 -35
- package/src/SignalsPanel/index.ts +0 -1
- package/src/SuggestionCard/SuggestionCard.css +0 -51
- package/src/SuggestionCard/SuggestionCard.tsx +0 -34
- package/src/SuggestionCard/index.ts +0 -2
- package/src/SuggestionCards/SuggestionCards.css +0 -15
- package/src/SuggestionCards/SuggestionCards.test.tsx +0 -27
- package/src/SuggestionCards/SuggestionCards.tsx +0 -40
- package/src/SuggestionCards/index.ts +0 -2
- package/src/Toast/Toast.css +0 -85
- package/src/Toast/Toast.tsx +0 -82
- package/src/Toast/index.ts +0 -2
- package/src/Toggle/Toggle.css +0 -110
- package/src/Toggle/Toggle.figma.tsx +0 -24
- package/src/Toggle/Toggle.tsx +0 -73
- package/src/Toggle/index.ts +0 -2
- package/src/TypingIndicator/TypingIndicator.css +0 -70
- package/src/TypingIndicator/TypingIndicator.tsx +0 -37
- package/src/TypingIndicator/index.ts +0 -2
- package/src/index.ts +0 -39
- package/src/styles/utilities.css +0 -14
- package/src/styles.css +0 -33
- package/src/types.ts +0 -66
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
2
|
-
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
3
|
-
import { describe, expect, it, vi } from "vitest";
|
|
4
|
-
import type { Citation } from "../types";
|
|
5
|
-
import { DocumentOverlay } from "../DocumentOverlay/DocumentOverlay";
|
|
6
|
-
import { ChatInterface, type ChatInterfaceMessage } from "./ChatInterface";
|
|
7
|
-
|
|
8
|
-
const citation: Citation = {
|
|
9
|
-
index: 1,
|
|
10
|
-
excerpt: "Structured capture improves onboarding outcomes.",
|
|
11
|
-
sourceTitle: "Knowledge Report",
|
|
12
|
-
sourceId: "doc-1",
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const messages: ChatInterfaceMessage[] = [
|
|
16
|
-
{
|
|
17
|
-
id: "m-1",
|
|
18
|
-
role: "assistant",
|
|
19
|
-
content: "Structured capture improves onboarding outcomes.",
|
|
20
|
-
citations: [citation],
|
|
21
|
-
timestamp: "2:35 PM",
|
|
22
|
-
},
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
function ChatCitationHarness() {
|
|
26
|
-
const [overlayOpen, setOverlayOpen] = useState(false);
|
|
27
|
-
const [activeCitation, setActiveCitation] = useState<Citation | null>(null);
|
|
28
|
-
|
|
29
|
-
const handleCitationClick = (clickedCitation: Citation) => {
|
|
30
|
-
setActiveCitation(clickedCitation);
|
|
31
|
-
setOverlayOpen(true);
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const doc = activeCitation
|
|
35
|
-
? {
|
|
36
|
-
title: "Knowledge Retention Report",
|
|
37
|
-
sourceLabel: "Internal Research",
|
|
38
|
-
highlightId: "cite-1",
|
|
39
|
-
content: (
|
|
40
|
-
<>
|
|
41
|
-
<p id="cite-1">Structured capture improves onboarding outcomes.</p>
|
|
42
|
-
<p>Additional context.</p>
|
|
43
|
-
</>
|
|
44
|
-
),
|
|
45
|
-
}
|
|
46
|
-
: null;
|
|
47
|
-
|
|
48
|
-
return (
|
|
49
|
-
<>
|
|
50
|
-
<ChatInterface
|
|
51
|
-
messages={messages}
|
|
52
|
-
promptValue=""
|
|
53
|
-
onPromptChange={() => {}}
|
|
54
|
-
onPromptSubmit={() => {}}
|
|
55
|
-
onCitationClick={handleCitationClick}
|
|
56
|
-
/>
|
|
57
|
-
{doc && (
|
|
58
|
-
<DocumentOverlay
|
|
59
|
-
open={overlayOpen}
|
|
60
|
-
onClose={() => {
|
|
61
|
-
setOverlayOpen(false);
|
|
62
|
-
setActiveCitation(null);
|
|
63
|
-
}}
|
|
64
|
-
title={doc.title}
|
|
65
|
-
sourceLabel={doc.sourceLabel}
|
|
66
|
-
highlightId={doc.highlightId}
|
|
67
|
-
highlightIndex={activeCitation?.index}
|
|
68
|
-
>
|
|
69
|
-
{doc.content}
|
|
70
|
-
</DocumentOverlay>
|
|
71
|
-
)}
|
|
72
|
-
</>
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
describe("ChatInterface integration", () => {
|
|
77
|
-
it("opens document overlay with highlighted citation content", async () => {
|
|
78
|
-
if (!(globalThis.CSS && typeof globalThis.CSS.escape === "function")) {
|
|
79
|
-
const existing = globalThis.CSS ?? ({} as CSS);
|
|
80
|
-
globalThis.CSS = {
|
|
81
|
-
...existing,
|
|
82
|
-
escape: (value: string) => value,
|
|
83
|
-
} as CSS;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const raf = vi
|
|
87
|
-
.spyOn(window, "requestAnimationFrame")
|
|
88
|
-
.mockImplementation((cb: FrameRequestCallback) => {
|
|
89
|
-
cb(0);
|
|
90
|
-
return 1;
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
const scrollIntoView = vi.fn();
|
|
94
|
-
Object.defineProperty(window.HTMLElement.prototype, "scrollIntoView", {
|
|
95
|
-
configurable: true,
|
|
96
|
-
value: scrollIntoView,
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
render(<ChatCitationHarness />);
|
|
100
|
-
|
|
101
|
-
fireEvent.click(screen.getByRole("button", { name: "View source: Knowledge Report" }));
|
|
102
|
-
|
|
103
|
-
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
104
|
-
expect(screen.getByText("Knowledge Retention Report")).toBeInTheDocument();
|
|
105
|
-
expect(screen.getByText("Internal Research")).toBeInTheDocument();
|
|
106
|
-
|
|
107
|
-
const highlighted = document.querySelector("#cite-1");
|
|
108
|
-
if (!highlighted) {
|
|
109
|
-
throw new Error("Expected highlighted citation element to exist");
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
await waitFor(() => {
|
|
113
|
-
expect(highlighted).toHaveClass("pcoi-doc-overlay__highlight");
|
|
114
|
-
expect(highlighted).toHaveAttribute("data-highlight-index", "[1]");
|
|
115
|
-
expect(scrollIntoView).toHaveBeenCalled();
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
fireEvent.click(screen.getByRole("button", { name: "Close modal" }));
|
|
119
|
-
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
120
|
-
|
|
121
|
-
raf.mockRestore();
|
|
122
|
-
});
|
|
123
|
-
});
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { ChatMessageList } from "../ChatMessageList/ChatMessageList";
|
|
3
|
-
import { ChatMessage } from "../ChatMessage/ChatMessage";
|
|
4
|
-
import { SuggestionCards } from "../SuggestionCards/SuggestionCards";
|
|
5
|
-
import { PromptBar } from "../PromptBar/PromptBar";
|
|
6
|
-
import { TypingIndicator } from "../TypingIndicator/TypingIndicator";
|
|
7
|
-
import type { ChatMessageRole, Citation, Suggestion } from "../types";
|
|
8
|
-
|
|
9
|
-
export interface ChatInterfaceMessage {
|
|
10
|
-
/** Unique message identifier */
|
|
11
|
-
id: string;
|
|
12
|
-
/** Sender role: "user" or "assistant" */
|
|
13
|
-
role: ChatMessageRole;
|
|
14
|
-
/** Message body content */
|
|
15
|
-
content: React.ReactNode;
|
|
16
|
-
/** Citations referenced in the message */
|
|
17
|
-
citations?: Citation[];
|
|
18
|
-
/** ISO timestamp displayed below the message */
|
|
19
|
-
timestamp?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ChatInterfaceProps
|
|
23
|
-
extends React.HTMLAttributes<HTMLDivElement> {
|
|
24
|
-
/** Conversation messages */
|
|
25
|
-
messages: ChatInterfaceMessage[];
|
|
26
|
-
/** Suggestion prompts for the empty state */
|
|
27
|
-
suggestions?: Suggestion[];
|
|
28
|
-
/** Current prompt textarea value */
|
|
29
|
-
promptValue: string;
|
|
30
|
-
/** Called when the prompt textarea changes */
|
|
31
|
-
onPromptChange: (value: string) => void;
|
|
32
|
-
/** Called when the user submits a prompt */
|
|
33
|
-
onPromptSubmit: (value: string) => void;
|
|
34
|
-
/** Called when a suggestion card is clicked */
|
|
35
|
-
onSuggestionSelect?: (suggestion: Suggestion) => void;
|
|
36
|
-
/** Called when a citation is clicked */
|
|
37
|
-
onCitationClick?: (citation: Citation) => void;
|
|
38
|
-
/** Textarea placeholder */
|
|
39
|
-
placeholder?: string;
|
|
40
|
-
/** Disable input while loading */
|
|
41
|
-
loading?: boolean;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* PCOI ChatInterface — Top-level chat composition
|
|
46
|
-
* Empty state: suggestions + prompt. Active state: messages + prompt.
|
|
47
|
-
* Tokens: bg/default, container/narrow, spacing-16/24
|
|
48
|
-
*/
|
|
49
|
-
export const ChatInterface = React.forwardRef<
|
|
50
|
-
HTMLDivElement,
|
|
51
|
-
ChatInterfaceProps
|
|
52
|
-
>(
|
|
53
|
-
(
|
|
54
|
-
{
|
|
55
|
-
messages,
|
|
56
|
-
suggestions,
|
|
57
|
-
promptValue,
|
|
58
|
-
onPromptChange,
|
|
59
|
-
onPromptSubmit,
|
|
60
|
-
onSuggestionSelect,
|
|
61
|
-
onCitationClick,
|
|
62
|
-
placeholder,
|
|
63
|
-
loading = false,
|
|
64
|
-
className = "",
|
|
65
|
-
...rest
|
|
66
|
-
},
|
|
67
|
-
ref
|
|
68
|
-
) => {
|
|
69
|
-
const isEmpty = messages.length === 0;
|
|
70
|
-
|
|
71
|
-
const classes = [
|
|
72
|
-
"pcoi-chat",
|
|
73
|
-
isEmpty ? "pcoi-chat--empty" : "",
|
|
74
|
-
className,
|
|
75
|
-
]
|
|
76
|
-
.filter(Boolean)
|
|
77
|
-
.join(" ");
|
|
78
|
-
|
|
79
|
-
return (
|
|
80
|
-
<div ref={ref} className={classes} {...rest}>
|
|
81
|
-
{isEmpty ? (
|
|
82
|
-
<div className="pcoi-chat__empty-state">
|
|
83
|
-
{suggestions && suggestions.length > 0 && onSuggestionSelect && (
|
|
84
|
-
<div className="pcoi-chat__suggestions">
|
|
85
|
-
<SuggestionCards
|
|
86
|
-
suggestions={suggestions}
|
|
87
|
-
onSelect={onSuggestionSelect}
|
|
88
|
-
/>
|
|
89
|
-
</div>
|
|
90
|
-
)}
|
|
91
|
-
</div>
|
|
92
|
-
) : (
|
|
93
|
-
<ChatMessageList className="pcoi-chat__messages" aria-live="polite" aria-relevant="additions">
|
|
94
|
-
{messages.map((msg) => (
|
|
95
|
-
<ChatMessage
|
|
96
|
-
key={msg.id}
|
|
97
|
-
role={msg.role}
|
|
98
|
-
citations={msg.citations}
|
|
99
|
-
timestamp={msg.timestamp}
|
|
100
|
-
onCitationClick={onCitationClick}
|
|
101
|
-
>
|
|
102
|
-
{msg.content}
|
|
103
|
-
</ChatMessage>
|
|
104
|
-
))}
|
|
105
|
-
{loading && <TypingIndicator />}
|
|
106
|
-
</ChatMessageList>
|
|
107
|
-
)}
|
|
108
|
-
|
|
109
|
-
<div className="pcoi-chat__prompt">
|
|
110
|
-
<PromptBar
|
|
111
|
-
value={promptValue}
|
|
112
|
-
onChange={onPromptChange}
|
|
113
|
-
onSubmit={onPromptSubmit}
|
|
114
|
-
placeholder={placeholder}
|
|
115
|
-
loading={loading}
|
|
116
|
-
disabled={loading}
|
|
117
|
-
/>
|
|
118
|
-
</div>
|
|
119
|
-
</div>
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
ChatInterface.displayName = "ChatInterface";
|
|
125
|
-
export default ChatInterface;
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/* ChatMessage — @pcoi/components */
|
|
2
|
-
|
|
3
|
-
.pcoi-chat-message {
|
|
4
|
-
display: flex;
|
|
5
|
-
flex-direction: column;
|
|
6
|
-
gap: var(--pcoi-semantic-spacing-inline-sm);
|
|
7
|
-
padding: var(--pcoi-semantic-spacing-stack-md);
|
|
8
|
-
border-radius: var(--pcoi-semantic-radius-card);
|
|
9
|
-
border: 1px solid var(--pcoi-semantic-border-card);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/* ── Role variants ── */
|
|
13
|
-
.pcoi-chat-message--user {
|
|
14
|
-
background: var(--pcoi-semantic-bg-surface);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
.pcoi-chat-message--assistant {
|
|
18
|
-
background: var(--pcoi-semantic-surface-accent-dim);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
.pcoi-chat-message__header {
|
|
22
|
-
display: flex;
|
|
23
|
-
align-items: center;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
.pcoi-chat-message__body {
|
|
27
|
-
font-family: var(--pcoi-semantic-type-body-font);
|
|
28
|
-
font-size: var(--pcoi-semantic-type-body-size);
|
|
29
|
-
line-height: var(--pcoi-semantic-type-body-line-height);
|
|
30
|
-
color: var(--pcoi-semantic-text-primary);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
.pcoi-chat-message__citations {
|
|
34
|
-
display: flex;
|
|
35
|
-
flex-direction: column;
|
|
36
|
-
gap: var(--pcoi-semantic-spacing-inline-sm);
|
|
37
|
-
margin-top: var(--pcoi-semantic-spacing-inline-2xs);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
.pcoi-chat-message__timestamp {
|
|
41
|
-
font-family: var(--pcoi-semantic-type-body-font);
|
|
42
|
-
font-size: var(--pcoi-semantic-type-body-sm-size);
|
|
43
|
-
color: var(--pcoi-semantic-text-muted);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/* ── Responsive: offset margins on desktop ── */
|
|
47
|
-
@media (min-width: 1025px) {
|
|
48
|
-
.pcoi-chat-message--user {
|
|
49
|
-
margin-left: var(--pcoi-semantic-spacing-inline-xl);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
.pcoi-chat-message--assistant {
|
|
53
|
-
margin-right: var(--pcoi-semantic-spacing-inline-xl);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Badge } from "../Badge/Badge";
|
|
3
|
-
import { CitedExcerpt } from "../CitedExcerpt/CitedExcerpt";
|
|
4
|
-
import type { ChatMessageRole, Citation } from "../types";
|
|
5
|
-
|
|
6
|
-
export interface ChatMessageProps
|
|
7
|
-
extends React.HTMLAttributes<HTMLDivElement> {
|
|
8
|
-
/** Message sender role */
|
|
9
|
-
role: ChatMessageRole;
|
|
10
|
-
/** Message body content */
|
|
11
|
-
children: React.ReactNode;
|
|
12
|
-
/** Citations referenced in the message */
|
|
13
|
-
citations?: Citation[];
|
|
14
|
-
/** Displayed below the message body */
|
|
15
|
-
timestamp?: string;
|
|
16
|
-
/** Called when a citation marker or source link is clicked */
|
|
17
|
-
onCitationClick?: (citation: Citation) => void;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* PCOI ChatMessage — User or assistant message bubble
|
|
22
|
-
* Tokens: surface/elevated (user), bg/card (assistant), border/card,
|
|
23
|
-
* radius-md, text/primary, text/muted, type/body-size
|
|
24
|
-
*/
|
|
25
|
-
export const ChatMessage = React.forwardRef<HTMLDivElement, ChatMessageProps>(
|
|
26
|
-
(
|
|
27
|
-
{ role, children, citations, timestamp, onCitationClick, className = "", ...rest },
|
|
28
|
-
ref
|
|
29
|
-
) => {
|
|
30
|
-
const classes = [
|
|
31
|
-
"pcoi-chat-message",
|
|
32
|
-
`pcoi-chat-message--${role}`,
|
|
33
|
-
className,
|
|
34
|
-
]
|
|
35
|
-
.filter(Boolean)
|
|
36
|
-
.join(" ");
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<div ref={ref} className={classes} {...rest}>
|
|
40
|
-
<div className="pcoi-chat-message__header">
|
|
41
|
-
<Badge variant={role === "assistant" ? "default" : "info"}>
|
|
42
|
-
{role === "assistant" ? "PCOI" : "You"}
|
|
43
|
-
</Badge>
|
|
44
|
-
</div>
|
|
45
|
-
|
|
46
|
-
<div className="pcoi-chat-message__body">{children}</div>
|
|
47
|
-
|
|
48
|
-
{citations && citations.length > 0 && (
|
|
49
|
-
<div className="pcoi-chat-message__citations">
|
|
50
|
-
{citations.map((c) => (
|
|
51
|
-
<CitedExcerpt
|
|
52
|
-
key={c.index}
|
|
53
|
-
index={c.index}
|
|
54
|
-
excerpt={c.excerpt}
|
|
55
|
-
sourceTitle={c.sourceTitle}
|
|
56
|
-
onSourceClick={() => onCitationClick?.(c)}
|
|
57
|
-
/>
|
|
58
|
-
))}
|
|
59
|
-
</div>
|
|
60
|
-
)}
|
|
61
|
-
|
|
62
|
-
{timestamp && (
|
|
63
|
-
<time className="pcoi-chat-message__timestamp">{timestamp}</time>
|
|
64
|
-
)}
|
|
65
|
-
</div>
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
ChatMessage.displayName = "ChatMessage";
|
|
71
|
-
export default ChatMessage;
|
package/src/ChatMessage/index.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/* ChatMessageList — @pcoi/components */
|
|
2
|
-
|
|
3
|
-
.pcoi-chat-message-list {
|
|
4
|
-
flex: 1;
|
|
5
|
-
min-height: 0;
|
|
6
|
-
overflow: hidden;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
.pcoi-chat-message-list__inner {
|
|
10
|
-
height: 100%;
|
|
11
|
-
overflow-y: auto;
|
|
12
|
-
display: flex;
|
|
13
|
-
flex-direction: column;
|
|
14
|
-
gap: var(--pcoi-semantic-spacing-stack-md);
|
|
15
|
-
padding: var(--pcoi-semantic-spacing-panel-padding-sm);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/* ── Responsive: wider spacing on tablet+ ── */
|
|
19
|
-
@media (min-width: 768px) {
|
|
20
|
-
.pcoi-chat-message-list__inner {
|
|
21
|
-
gap: var(--pcoi-semantic-spacing-card-gap);
|
|
22
|
-
padding: var(--pcoi-semantic-spacing-panel-padding);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
-
import { describe, expect, it, vi } from "vitest";
|
|
3
|
-
import { ChatMessage } from "../ChatMessage";
|
|
4
|
-
import type { Citation } from "../types";
|
|
5
|
-
import { ChatMessageList } from "./ChatMessageList";
|
|
6
|
-
|
|
7
|
-
describe("ChatMessageList", () => {
|
|
8
|
-
it("renders children and relays citation clicks", () => {
|
|
9
|
-
const citation: Citation = {
|
|
10
|
-
index: 1,
|
|
11
|
-
excerpt: "Referenced excerpt.",
|
|
12
|
-
sourceTitle: "Knowledge Report",
|
|
13
|
-
sourceId: "doc-1",
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const onCitationClick = vi.fn();
|
|
17
|
-
|
|
18
|
-
render(
|
|
19
|
-
<ChatMessageList>
|
|
20
|
-
<ChatMessage
|
|
21
|
-
role="assistant"
|
|
22
|
-
timestamp="2:35 PM"
|
|
23
|
-
citations={[citation]}
|
|
24
|
-
onCitationClick={onCitationClick}
|
|
25
|
-
>
|
|
26
|
-
Assistant response
|
|
27
|
-
</ChatMessage>
|
|
28
|
-
</ChatMessageList>
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
expect(screen.getByText("Assistant response")).toBeInTheDocument();
|
|
32
|
-
|
|
33
|
-
fireEvent.click(screen.getByRole("button", { name: "View source: Knowledge Report" }));
|
|
34
|
-
fireEvent.click(screen.getByRole("button", { name: "Citation 1, view source: Knowledge Report" }));
|
|
35
|
-
|
|
36
|
-
expect(onCitationClick).toHaveBeenCalledTimes(2);
|
|
37
|
-
expect(onCitationClick).toHaveBeenCalledWith(citation);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("auto-scrolls last message into view when new messages are added", async () => {
|
|
41
|
-
const scrollIntoViewMock = vi.fn();
|
|
42
|
-
Element.prototype.scrollIntoView = scrollIntoViewMock;
|
|
43
|
-
|
|
44
|
-
vi.spyOn(window, "requestAnimationFrame").mockImplementation(
|
|
45
|
-
(cb: FrameRequestCallback) => {
|
|
46
|
-
cb(0);
|
|
47
|
-
return 1;
|
|
48
|
-
}
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
const { rerender } = render(
|
|
52
|
-
<ChatMessageList>
|
|
53
|
-
<div>Message 1</div>
|
|
54
|
-
</ChatMessageList>
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
scrollIntoViewMock.mockClear();
|
|
58
|
-
|
|
59
|
-
rerender(
|
|
60
|
-
<ChatMessageList>
|
|
61
|
-
<div>Message 1</div>
|
|
62
|
-
<div>Message 2</div>
|
|
63
|
-
</ChatMessageList>
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
await waitFor(() => {
|
|
67
|
-
expect(scrollIntoViewMock).toHaveBeenCalledWith({ block: "nearest" });
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
});
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import React, { useRef, useEffect } from "react";
|
|
2
|
-
|
|
3
|
-
export interface ChatMessageListProps
|
|
4
|
-
extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
|
-
/** ChatMessage elements */
|
|
6
|
-
children: React.ReactNode;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* PCOI ChatMessageList — Scrollable container that auto-scrolls to newest message
|
|
11
|
-
* Tokens: spacing-16/20 (gap), spacing-16/24 (padding)
|
|
12
|
-
*/
|
|
13
|
-
export const ChatMessageList = React.forwardRef<
|
|
14
|
-
HTMLDivElement,
|
|
15
|
-
ChatMessageListProps
|
|
16
|
-
>(({ children, className = "", ...rest }, ref) => {
|
|
17
|
-
const innerRef = useRef<HTMLDivElement>(null);
|
|
18
|
-
|
|
19
|
-
/* Scroll to bottom whenever the DOM inside the list changes */
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
const el = innerRef.current;
|
|
22
|
-
if (!el) return;
|
|
23
|
-
|
|
24
|
-
const scrollToEnd = () => {
|
|
25
|
-
requestAnimationFrame(() => {
|
|
26
|
-
const lastChild = el.lastElementChild;
|
|
27
|
-
if (lastChild) {
|
|
28
|
-
lastChild.scrollIntoView({ block: "nearest" });
|
|
29
|
-
} else {
|
|
30
|
-
el.scrollTop = el.scrollHeight;
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const observer = new MutationObserver(scrollToEnd);
|
|
36
|
-
observer.observe(el, { childList: true, subtree: true });
|
|
37
|
-
|
|
38
|
-
scrollToEnd();
|
|
39
|
-
return () => observer.disconnect();
|
|
40
|
-
}, []);
|
|
41
|
-
|
|
42
|
-
const classes = ["pcoi-chat-message-list", className]
|
|
43
|
-
.filter(Boolean)
|
|
44
|
-
.join(" ");
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<div ref={ref} className={classes} role="log" aria-label="Conversation" {...rest}>
|
|
48
|
-
<div ref={innerRef} className="pcoi-chat-message-list__inner">
|
|
49
|
-
{children}
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
ChatMessageList.displayName = "ChatMessageList";
|
|
56
|
-
export default ChatMessageList;
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/* Checkbox — @pcoi/components */
|
|
2
|
-
|
|
3
|
-
.pcoi-checkbox {
|
|
4
|
-
display: flex;
|
|
5
|
-
flex-direction: column;
|
|
6
|
-
gap: var(--pcoi-semantic-spacing-form-gap-compact);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
.pcoi-checkbox__control {
|
|
10
|
-
display: inline-flex;
|
|
11
|
-
align-items: center;
|
|
12
|
-
gap: var(--pcoi-semantic-spacing-inline-sm);
|
|
13
|
-
cursor: pointer;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
.pcoi-checkbox__input {
|
|
17
|
-
position: absolute;
|
|
18
|
-
width: 1px;
|
|
19
|
-
height: 1px;
|
|
20
|
-
padding: 0;
|
|
21
|
-
margin: -1px;
|
|
22
|
-
overflow: hidden;
|
|
23
|
-
clip: rect(0, 0, 0, 0);
|
|
24
|
-
white-space: nowrap;
|
|
25
|
-
border-width: 0;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
.pcoi-checkbox__box {
|
|
29
|
-
width: var(--pcoi-semantic-sizing-control-box-size);
|
|
30
|
-
height: var(--pcoi-semantic-sizing-control-box-size);
|
|
31
|
-
flex-shrink: 0;
|
|
32
|
-
border: 1px solid var(--pcoi-semantic-border-default);
|
|
33
|
-
border-radius: var(--pcoi-semantic-radius-input);
|
|
34
|
-
background: var(--pcoi-semantic-bg-default);
|
|
35
|
-
transition: background var(--pcoi-effect-transition-fast, 0.2s ease),
|
|
36
|
-
border-color var(--pcoi-effect-transition-fast, 0.2s ease);
|
|
37
|
-
position: relative;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
.pcoi-checkbox__box::after {
|
|
41
|
-
content: "";
|
|
42
|
-
position: absolute;
|
|
43
|
-
top: 2px;
|
|
44
|
-
left: 5px;
|
|
45
|
-
width: 5px;
|
|
46
|
-
height: 9px;
|
|
47
|
-
border: solid var(--pcoi-semantic-action-primary-text);
|
|
48
|
-
border-width: 0 2px 2px 0;
|
|
49
|
-
transform: rotate(45deg);
|
|
50
|
-
opacity: 0;
|
|
51
|
-
transition: opacity var(--pcoi-effect-transition-fast, 0.2s ease);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
.pcoi-checkbox__input:checked + .pcoi-checkbox__box {
|
|
55
|
-
background: var(--pcoi-semantic-action-primary-bg);
|
|
56
|
-
border-color: var(--pcoi-semantic-action-primary-bg);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
.pcoi-checkbox__input:checked + .pcoi-checkbox__box::after {
|
|
60
|
-
opacity: 1;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
.pcoi-checkbox__control:hover .pcoi-checkbox__box {
|
|
64
|
-
border-color: var(--pcoi-semantic-border-input-hover);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
.pcoi-checkbox__control:active .pcoi-checkbox__box {
|
|
68
|
-
transform: var(--pcoi-effect-transform-press-scale-control);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
.pcoi-checkbox__input:focus-visible + .pcoi-checkbox__box {
|
|
72
|
-
border-color: var(--pcoi-semantic-focus-border);
|
|
73
|
-
box-shadow: var(--pcoi-effect-shadow-focus-ring);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
.pcoi-checkbox__label {
|
|
77
|
-
font-size: var(--pcoi-semantic-type-body-size);
|
|
78
|
-
color: var(--pcoi-semantic-text-primary);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/* ── Error state ── */
|
|
82
|
-
.pcoi-checkbox--error .pcoi-checkbox__box {
|
|
83
|
-
border-color: var(--pcoi-semantic-border-error);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.pcoi-checkbox__error {
|
|
87
|
-
font-size: var(--pcoi-semantic-type-label-size);
|
|
88
|
-
color: var(--pcoi-semantic-text-error);
|
|
89
|
-
margin: 0;
|
|
90
|
-
padding-left: calc(var(--pcoi-semantic-sizing-control-box-size) + var(--pcoi-semantic-spacing-inline-sm));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/* ── Disabled state ── */
|
|
94
|
-
.pcoi-checkbox--disabled {
|
|
95
|
-
opacity: var(--pcoi-effect-opacity-disabled);
|
|
96
|
-
pointer-events: none;
|
|
97
|
-
}
|