Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/src/components/AztecDocsWidget/Icons.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,34 @@ export const Icons = {
<path d="M3 21l7-7" />
</svg>
),
thumbUp: (p) => (
<svg
viewBox="0 0 24 24"
width={p.size || 13}
height={p.size || 13}
fill={p.filled ? "currentColor" : "none"}
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M7 11v9H4a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1h3z" />
<path d="M7 11l5-8a2 2 0 0 1 2 2v4h5a2 2 0 0 1 2 2l-2 7a2 2 0 0 1-2 1H7" />
</svg>
),
thumbDown: (p) => (
<svg
viewBox="0 0 24 24"
width={p.size || 13}
height={p.size || 13}
fill={p.filled ? "currentColor" : "none"}
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M17 13V4h3a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1h-3z" />
<path d="M17 13l-5 8a2 2 0 0 1-2-2v-4H5a2 2 0 0 1-2-2l2-7a2 2 0 0 1 2-1h10" />
</svg>
),
};
88 changes: 88 additions & 0 deletions docs/src/components/AztecDocsWidget/Message.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,46 @@ export function AssistantBody({
text,
sources,
thinking,
error,
tokens,
mdComponents,
feedback,
feedbackError,
onFeedback,
showFeedback,
}) {
const { isInk, accentColor, panelFg, panelFg2 } = tokens;
const feedbackBtn = (kind) => {
const Icon = kind === "like" ? Icons.thumbUp : Icons.thumbDown;
const active = feedback === kind;
const dimmed = feedback && !active;
const label = kind === "like" ? "Helpful" : "Not helpful";
return (
<button
type="button"
onClick={() => onFeedback?.(kind)}
disabled={!onFeedback || !!feedback}
title={label}
aria-label={label}
aria-pressed={active}
style={{
width: 26,
height: 26,
padding: 0,
border: `1px solid ${isInk ? "rgba(242,238,225,0.2)" : "var(--azw-ink-tint-1)"}`,
background: active ? accentColor : "transparent",
color: active ? "var(--azw-ink)" : dimmed ? panelFg2 : panelFg,
cursor: onFeedback && !feedback ? "pointer" : "default",
display: "flex",
alignItems: "center",
justifyContent: "center",
opacity: dimmed ? 0.5 : 1,
}}
>
<Icon size={12} filled={active} />
</button>
);
};
return (
<div
style={{
Expand Down Expand Up @@ -107,6 +143,26 @@ export function AssistantBody({
</ReactMarkdown>
) : null}
</div>
{error && (
<div
role="alert"
style={{
marginTop: text ? 10 : 0,
padding: "8px 10px",
border: `1px solid var(--azw-vermillion, ${accentColor})`,
background: isInk
? "rgba(217, 74, 58, 0.12)"
: "rgba(217, 74, 58, 0.08)",
color: "var(--azw-vermillion, #d94a3a)",
fontFamily: "var(--azw-font-sans)",
fontSize: 12.5,
lineHeight: 1.45,
letterSpacing: "-0.01em",
}}
>
{error}
</div>
)}
{sources?.length > 0 && (
<div style={{ marginTop: 10 }}>
<div
Expand Down Expand Up @@ -153,6 +209,38 @@ export function AssistantBody({
</div>
</div>
)}
{showFeedback && (
<div
style={{
marginTop: 10,
display: "flex",
alignItems: "center",
gap: 8,
fontFamily: "var(--azw-font-mono)",
fontSize: 10,
letterSpacing: "0.1em",
textTransform: "uppercase",
color: panelFg2,
}}
>
<span>Was this helpful?</span>
<div style={{ display: "flex", gap: 6 }}>
{feedbackBtn("like")}
{feedbackBtn("dislike")}
</div>
{feedback && !feedbackError && (
<span style={{ color: panelFg2 }}>Thanks for the feedback.</span>
)}
{feedbackError && (
<span
style={{ color: "var(--azw-vermillion, #d94a3a)" }}
role="alert"
>
Couldn't save feedback.
</span>
)}
</div>
)}
</div>
</div>
);
Expand Down
18 changes: 17 additions & 1 deletion docs/src/components/AztecDocsWidget/Panel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export default function Panel({
expanded,
onToggleExpanded,
scrollRef,
feedbackByIndex,
feedbackErrorsByIndex,
onFeedback,
conversationId,
}) {
const {
isInk,
Expand Down Expand Up @@ -224,16 +228,28 @@ export default function Panel({
const text = isLast && streaming ? streamText : m.response;
const sources = isLast && streaming ? streamSources : m.sources;
const isStreamingLast = isLast && streaming;
const error = isStreamingLast ? null : m.error;
const showFeedback =
!isStreamingLast && !!m.response && !!conversationId;
return (
<React.Fragment key={i}>
<UserBubble text={m.prompt} tokens={tokens} />
{(text || isStreamingLast) && (
{(text || isStreamingLast || error) && (
<AssistantBody
text={text}
sources={sources}
thinking={isStreamingLast}
error={error}
tokens={tokens}
mdComponents={mdComponents}
showFeedback={showFeedback}
feedback={feedbackByIndex?.[i]}
feedbackError={feedbackErrorsByIndex?.[i]}
onFeedback={
showFeedback
? (kind) => onFeedback?.(i, kind)
: undefined
}
/>
)}
</React.Fragment>
Expand Down
53 changes: 50 additions & 3 deletions docs/src/components/AztecDocsWidget/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import "./styles.css";
import { DEFAULT_SUGGESTED, getTheme } from "./theme";
import { makeMarkdownComponents } from "./markdown";
import { streamAnswer } from "./streamAnswer";
import { sendFeedback } from "./sendFeedback";
import LauncherButton from "./LauncherButton";
import Panel from "./Panel";

Expand All @@ -28,6 +29,8 @@ export default function AztecDocsWidget({
const [streamText, setStreamText] = useState("");
const [streamSources, setStreamSources] = useState([]);
const [conversationId, setConversationId] = useState(null);
const [feedbackByIndex, setFeedbackByIndex] = useState({});
const [feedbackErrorsByIndex, setFeedbackErrorsByIndex] = useState({});
const scrollRef = useRef(null);
const abortRef = useRef(null);

Expand Down Expand Up @@ -70,6 +73,7 @@ export default function AztecDocsWidget({

let acc = "";
let sources = [];
let errorMessage = null;
try {
await streamAnswer({
apiHost,
Expand All @@ -87,18 +91,27 @@ export default function AztecDocsWidget({
setStreamSources(sources);
},
onConversationId: (id) => setConversationId(id),
onError: (message) => {
errorMessage = message;
},
onDone: () => {},
});
} catch (err) {
if (err.name !== "AbortError") {
acc =
acc || "Something went wrong fetching an answer. Please try again.";
errorMessage =
errorMessage ||
"Something went wrong fetching an answer. Please try again.";
}
}

setMessages((prev) => {
const copy = [...prev];
copy[copy.length - 1] = { prompt: question, response: acc, sources };
copy[copy.length - 1] = {
prompt: question,
response: acc,
sources,
error: errorMessage,
};
return copy;
});
setStreaming(false);
Expand All @@ -113,6 +126,36 @@ export default function AztecDocsWidget({
setStreamText("");
setStreamSources([]);
setConversationId(null);
setFeedbackByIndex({});
setFeedbackErrorsByIndex({});
}

async function handleFeedback(messageIndex, kind) {
if (!conversationId) return;
if (feedbackByIndex[messageIndex]) return;
setFeedbackByIndex((prev) => ({ ...prev, [messageIndex]: kind }));
setFeedbackErrorsByIndex((prev) => {
if (!prev[messageIndex]) return prev;
const copy = { ...prev };
delete copy[messageIndex];
return copy;
});
try {
await sendFeedback({
apiHost,
apiKey,
conversationId,
questionIndex: messageIndex,
feedback: kind,
});
} catch (err) {
setFeedbackByIndex((prev) => {
const copy = { ...prev };
delete copy[messageIndex];
return copy;
});
setFeedbackErrorsByIndex((prev) => ({ ...prev, [messageIndex]: true }));
}
}

return (
Expand Down Expand Up @@ -148,6 +191,10 @@ export default function AztecDocsWidget({
expanded={expanded}
onToggleExpanded={() => setExpanded((v) => !v)}
scrollRef={scrollRef}
conversationId={conversationId}
feedbackByIndex={feedbackByIndex}
feedbackErrorsByIndex={feedbackErrorsByIndex}
onFeedback={handleFeedback}
/>
)}
</div>
Expand Down
27 changes: 27 additions & 0 deletions docs/src/components/AztecDocsWidget/sendFeedback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export async function sendFeedback({
apiHost,
apiKey,
conversationId,
questionIndex,
feedback,
signal,
}) {
if (!conversationId || questionIndex == null) {
throw new Error("Feedback requires conversationId and questionIndex");
}
const res = await fetch(`${apiHost.replace(/\/$/, "")}/api/feedback`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
conversation_id: conversationId,
question_index: questionIndex,
feedback,
api_key: apiKey,
}),
signal,
});
if (!res.ok) {
throw new Error(`DocsGPT feedback failed: ${res.status}`);
}
return res.json().catch(() => ({}));
}
9 changes: 9 additions & 0 deletions docs/src/components/AztecDocsWidget/streamAnswer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export async function streamAnswer({
onToken,
onSource,
onConversationId,
onError,
onDone,
signal,
}) {
Expand Down Expand Up @@ -56,6 +57,14 @@ export async function streamAnswer({
onSource(sources);
} else if (parsed.type === "id" && parsed.id) {
onConversationId(parsed.id);
} else if (parsed.type === "error") {
const message =
typeof parsed.error === "string" && parsed.error.trim()
? parsed.error
: "Something went wrong generating an answer.";
onError?.(message);
onDone();
return;
} else if (parsed.type === "end") {
onDone();
return;
Expand Down
Loading