mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
add agent warning and update style (#6629)
This commit is contained in:
@@ -2695,6 +2695,7 @@ clearChat = "Clear chat"
|
||||
[chat.input]
|
||||
placeholder = "What do you want to do?"
|
||||
send = "Send message"
|
||||
disclaimer = "AI can make mistakes. Be sure to verify the output before sharing."
|
||||
|
||||
[chat.progress]
|
||||
analyzing = "Analysing your request..."
|
||||
|
||||
@@ -43,7 +43,10 @@ test.describe("Policy editing gate — non-admin (login on)", () => {
|
||||
});
|
||||
|
||||
test.describe("Policy editing gate — admin (login on)", () => {
|
||||
test.use({ stubOptions: { enableLogin: true, isAdmin: true }, seedJwt: true });
|
||||
test.use({
|
||||
stubOptions: { enableLogin: true, isAdmin: true },
|
||||
seedJwt: true,
|
||||
});
|
||||
|
||||
test("admin can reach the setup wizard", async ({ page }) => {
|
||||
await openSecurityPolicy(page);
|
||||
|
||||
@@ -165,6 +165,16 @@
|
||||
.chat-panel-messages {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
/* Establish a size container so the message list's bottom padding can be
|
||||
expressed relative to the scroll viewport height (cqh) below. */
|
||||
container-type: size;
|
||||
}
|
||||
|
||||
/* Extra room beneath the last message so that, scrolled to the bottom, the most
|
||||
recent turn rests near the vertical centre of the conversation rather than
|
||||
jammed against the composer. cqh is a fraction of the scroll viewport height. */
|
||||
.chat-panel-messages__content {
|
||||
padding-bottom: 15cqh;
|
||||
}
|
||||
|
||||
/* Quick action cards above the input. */
|
||||
@@ -282,12 +292,14 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Input area: textarea on top row, action icons on bottom row. */
|
||||
/* Input area: send button on the left, textarea fills the rest — one compact
|
||||
row so the composer stays short when the welcome screen is fully populated. */
|
||||
.chat-panel-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.85rem 1rem 0.75rem;
|
||||
padding: 0.7rem 0.8rem;
|
||||
margin: 0.75rem 0.75rem 1rem;
|
||||
border: none;
|
||||
border-radius: 1.1rem;
|
||||
@@ -309,7 +321,7 @@
|
||||
.chat-panel-input__field,
|
||||
.chat-panel-input__field:focus,
|
||||
.chat-panel-input__field:focus-visible {
|
||||
font-size: 0.95rem;
|
||||
font-size: 1.05rem;
|
||||
padding: 0.15rem 0 !important;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
@@ -317,10 +329,49 @@
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.chat-panel-input__actions {
|
||||
/* Textarea grows to fill the row; the send button keeps its size on the left. */
|
||||
.chat-panel-input__textarea {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chat-panel-input__send {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* AI disclaimer. Two presentations share one base:
|
||||
- --banner: a soft grey card below the header on the welcome screen.
|
||||
- --inline: a compact line pinned above the composer once a chat is underway. */
|
||||
.chat-panel-disclaimer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.45rem;
|
||||
flex-shrink: 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.chat-panel-disclaimer__icon {
|
||||
flex-shrink: 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Welcome-screen banner: a little grey box just under the header. */
|
||||
.chat-panel-disclaimer--banner {
|
||||
margin: 0.35rem 0.75rem 0;
|
||||
padding: 0.6rem 0.75rem;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 0.65rem;
|
||||
background: var(--mantine-color-gray-light);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Compact line above the composer once the conversation has started. */
|
||||
.chat-panel-disclaimer--inline {
|
||||
justify-content: center;
|
||||
padding: 0.1rem 1.25rem 0;
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* Message layout */
|
||||
@@ -425,109 +476,88 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* ─── Progress step log ─────────────────────────────────────────────────── */
|
||||
/* ─── Live progress indicator ──────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* Shown while the AI is working. Each step slides in from below as events
|
||||
* arrive; the active (last) step is full-opacity with a pulsing icon, and
|
||||
* earlier completed steps are dimmed. Only the most-recent N steps are
|
||||
* rendered — there is no scrollable list.
|
||||
* Shown while the AI works — one step at a time, no growing list. Our animated
|
||||
* logo sits on the left, the current step's label shimmers in the middle, and
|
||||
* the phase-specific icon (what it's doing right now) sits on the right.
|
||||
*/
|
||||
|
||||
.chat-progress-log {
|
||||
.chat-progress-live {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.15rem 0.5rem 0.35rem;
|
||||
/* No gap — the connector line div provides the inter-step spacing. */
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
width: 100%;
|
||||
padding: 0.2rem 0.5rem 0.35rem;
|
||||
}
|
||||
|
||||
.chat-progress-step {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.65rem;
|
||||
/*
|
||||
* Use color rather than opacity for past-step dimming so the connector
|
||||
* line (a sibling element inside __left, not a text node) is NOT affected
|
||||
* and stays clearly visible. currentColor flows into SVG icon fills;
|
||||
* color inherits into the label span automatically.
|
||||
*/
|
||||
color: var(--text-muted);
|
||||
transition: color 250ms ease-out;
|
||||
animation: chat-step-enter 300ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
/*
|
||||
* Active step: white text on dark mode so currentColor feeds directly into
|
||||
* the StirlingLogoAnimated SVG paths and the label at the same time.
|
||||
* Light-mode falls back to the default text colour so it's not invisible.
|
||||
*/
|
||||
/* Active step: icon and label are coloured independently so the logo can be
|
||||
blue while the text uses the normal text colour. */
|
||||
.chat-progress-step--active .chat-progress-step__icon {
|
||||
.chat-progress-live__logo {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: var(--mantine-color-blue-filled);
|
||||
}
|
||||
|
||||
.chat-progress-step--active .chat-progress-step__label {
|
||||
color: var(--mantine-color-text);
|
||||
/* Shimmer: a soft highlight sweeps left-to-right across the muted label. */
|
||||
.chat-progress-live__label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.35;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--text-muted) 0%,
|
||||
var(--text-muted) 40%,
|
||||
var(--mantine-color-text) 50%,
|
||||
var(--text-muted) 60%,
|
||||
var(--text-muted) 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
color: transparent;
|
||||
animation: chat-shimmer 1.8s linear infinite;
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme="dark"]
|
||||
.chat-progress-step--active
|
||||
.chat-progress-step__label {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.chat-progress-step__left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
/* Icon cell — fixed height so the connector line aligns cleanly. */
|
||||
.chat-progress-step__icon {
|
||||
width: 20px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
.chat-progress-live__phase-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
/* Inherits color from .chat-progress-step so currentColor flows into icons. */
|
||||
color: var(--text-muted);
|
||||
animation: chat-phase-pulse 1.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/*
|
||||
* Connector line between steps. Not a child of the label so it is unaffected
|
||||
* by the step's color; it sits on its own and reads clearly against the
|
||||
* toolbar background.
|
||||
*/
|
||||
.chat-progress-step__line {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background: var(--border-subtle);
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
@keyframes chat-shimmer {
|
||||
from {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
to {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Active step label — slightly larger, inherits the active color. */
|
||||
.chat-progress-step__label {
|
||||
font-size: 0.875rem;
|
||||
padding-top: 3px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
/* Past step labels — a touch smaller so the active row reads as the primary. */
|
||||
.chat-progress-step:not(.chat-progress-step--active)
|
||||
.chat-progress-step__label {
|
||||
font-size: 0.8rem;
|
||||
padding-top: 4px;
|
||||
@keyframes chat-phase-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Wrapper that scales a registry tool icon (LocalIcon, typically 1.5rem/24px)
|
||||
* down to fit the 20px icon column. Transform does not affect layout, so the
|
||||
* parent overflow:hidden clips the un-scaled footprint cleanly.
|
||||
* down to fit the compact icon slots used by the live indicator and the
|
||||
* completed tool list. Transform does not affect layout, so the parent's
|
||||
* overflow:hidden clips the un-scaled footprint cleanly.
|
||||
*/
|
||||
.chat-step-icon-scaled {
|
||||
display: inline-flex;
|
||||
@@ -538,22 +568,12 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Entry: steps slide up from a few pixels below their final position. */
|
||||
@keyframes chat-step-enter {
|
||||
from {
|
||||
transform: translateY(7px);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Completed progress log dropdown ──────────────────────────────────── */
|
||||
/* ─── Completed tool breakdown ─────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* Shown above each completed assistant turn. The toggle displays a small
|
||||
* "Ran for X seconds" label; expanding reveals the full ordered step list
|
||||
* for that turn in a compact, read-only format.
|
||||
* "Ran for X seconds" label; expanding reveals a numbered list of just the
|
||||
* tools that ran. Turns with no tools show the duration as a plain label.
|
||||
*/
|
||||
|
||||
.chat-completed-log {
|
||||
@@ -573,31 +593,56 @@
|
||||
background: var(--mantine-color-default-hover);
|
||||
}
|
||||
|
||||
.chat-completed-log__steps {
|
||||
.chat-completed-log__static {
|
||||
display: inline-block;
|
||||
padding: 0.1rem;
|
||||
}
|
||||
|
||||
/* Numbered list of the tools that ran — icon + name, no connector lines. */
|
||||
.chat-completed-log__tools {
|
||||
list-style: none;
|
||||
margin: 0.25rem 0 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.35rem 0.4rem 0.2rem 0.25rem;
|
||||
margin-top: 0.2rem;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
|
||||
/* In the completed log every step is a past step — no active highlighting,
|
||||
no entry animation (the content is static once opened). */
|
||||
.chat-completed-log__steps .chat-progress-step {
|
||||
animation: none;
|
||||
.chat-completed-log__tool {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.2rem 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--mantine-color-text);
|
||||
}
|
||||
|
||||
/* Tighten the icon and connector dimensions for the denser historical view. */
|
||||
.chat-completed-log__steps .chat-progress-step__icon {
|
||||
height: 20px;
|
||||
.chat-completed-log__tool-num {
|
||||
flex-shrink: 0;
|
||||
min-width: 1.1rem;
|
||||
text-align: right;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.chat-completed-log__steps .chat-progress-step__line {
|
||||
height: 14px;
|
||||
.chat-completed-log__tool-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.chat-completed-log__steps .chat-progress-step__label {
|
||||
font-size: 0.775rem;
|
||||
padding-top: 2px;
|
||||
.chat-completed-log__tool-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Legacy thinking indicator (kept in case any other code references it). */
|
||||
@@ -610,9 +655,15 @@
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.chat-progress-step {
|
||||
.chat-progress-live__label {
|
||||
animation: none;
|
||||
transition: none;
|
||||
background: none;
|
||||
-webkit-text-fill-color: var(--text-muted);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.chat-progress-live__phase-icon {
|
||||
animation: none;
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,12 +31,12 @@ import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
||||
import DeleteSweepIcon from "@mui/icons-material/DeleteSweep";
|
||||
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import {
|
||||
useChat,
|
||||
AiWorkflowPhase,
|
||||
ChatRole,
|
||||
PROGRESS_LOG_MAX,
|
||||
isKnownEngineProgressDetail,
|
||||
type AiWorkflowProgress,
|
||||
type AnyEngineProgressDetail,
|
||||
@@ -151,20 +151,15 @@ function formatEngineProgress(
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose an icon for a progress step.
|
||||
*
|
||||
* The active (current) step always shows the animated Stirling logo so it reads
|
||||
* as the "live" indicator. Past steps get a phase-specific icon so the trail
|
||||
* is scannable at a glance.
|
||||
* Phase-specific icon for a progress step: the tool's registry icon while a
|
||||
* tool runs, or a generic glyph for the read/extract/think phases. Used for the
|
||||
* right-hand "what it's doing" icon in the live indicator and for each row of
|
||||
* the completed tool breakdown.
|
||||
*/
|
||||
function progressStepIcon(
|
||||
progress: AiWorkflowProgress,
|
||||
resolveToolIcon: ToolIconResolver,
|
||||
isActive: boolean,
|
||||
): ReactNode {
|
||||
if (isActive) {
|
||||
return <StirlingLogoAnimated size={18} />;
|
||||
}
|
||||
if (progress.phase === AiWorkflowPhase.EXECUTING_TOOL) {
|
||||
const registryIcon = progress.tool ? resolveToolIcon(progress.tool) : null;
|
||||
if (registryIcon) {
|
||||
@@ -182,9 +177,10 @@ function progressStepIcon(
|
||||
}
|
||||
|
||||
/**
|
||||
* Animated step-by-step progress log shown while the AI is working.
|
||||
* Displays the last {@link PROGRESS_LOG_VISIBLE} steps from the live event stream,
|
||||
* with the active (most recent) step highlighted and older steps dimmed.
|
||||
* Live progress indicator shown while the AI is working. One step at a time:
|
||||
* our animated logo on the left, the current step's label shimmering in the
|
||||
* middle, and the phase-specific icon (what it's doing right now) on the right.
|
||||
* The latest event replaces the previous one in place — no growing list.
|
||||
*/
|
||||
function ProgressLogDisplay({
|
||||
progressLog,
|
||||
@@ -197,55 +193,23 @@ function ProgressLogDisplay({
|
||||
resolveToolName: ToolNameResolver;
|
||||
resolveToolIcon: ToolIconResolver;
|
||||
}) {
|
||||
// Placeholder shown before the first SSE event arrives.
|
||||
if (progressLog.length === 0) {
|
||||
const current =
|
||||
progressLog.length > 0 ? progressLog[progressLog.length - 1] : null;
|
||||
const label = current
|
||||
? formatProgress(current, t, resolveToolName)
|
||||
: t("chat.progress.thinking");
|
||||
|
||||
return (
|
||||
<div className="chat-progress-log">
|
||||
<div className="chat-progress-step chat-progress-step--active">
|
||||
<div className="chat-progress-step__left">
|
||||
<div className="chat-progress-step__icon">
|
||||
<div className="chat-progress-live">
|
||||
<span className="chat-progress-live__logo">
|
||||
<StirlingLogoAnimated size={18} />
|
||||
</div>
|
||||
</div>
|
||||
<span className="chat-progress-step__label">
|
||||
{t("chat.progress.thinking")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Chronological order: oldest at top, newest (active) at bottom.
|
||||
// The reducer already caps progressLog at PROGRESS_LOG_MAX entries, so this
|
||||
// slice is effectively a no-op but kept for defensive correctness.
|
||||
const visibleSteps = progressLog.slice(-PROGRESS_LOG_MAX);
|
||||
const startIndex = progressLog.length - visibleSteps.length;
|
||||
|
||||
return (
|
||||
<div className="chat-progress-log">
|
||||
{visibleSteps.map((step, i) => {
|
||||
// Stable key based on absolute position in the full log — React reuses
|
||||
// existing DOM elements and only mounts (and animates) new ones.
|
||||
const globalIndex = startIndex + i;
|
||||
const isActive = i === visibleSteps.length - 1; // last = newest = bottom
|
||||
// Connector runs below every step except the active one at the bottom.
|
||||
const showConnector = i < visibleSteps.length - 1;
|
||||
const label = formatProgress(step, t, resolveToolName);
|
||||
return (
|
||||
<div
|
||||
key={globalIndex}
|
||||
className={`chat-progress-step${isActive ? " chat-progress-step--active" : ""}`}
|
||||
>
|
||||
<div className="chat-progress-step__left">
|
||||
<div className="chat-progress-step__icon">
|
||||
{progressStepIcon(step, resolveToolIcon, isActive)}
|
||||
</div>
|
||||
{showConnector && <div className="chat-progress-step__line" />}
|
||||
</div>
|
||||
<span className="chat-progress-step__label">{label}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<span className="chat-progress-live__label">{label}</span>
|
||||
{current && (
|
||||
<span className="chat-progress-live__phase-icon">
|
||||
{progressStepIcon(current, resolveToolIcon)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -265,8 +229,10 @@ function formatDuration(ms: number, t: TranslateFn): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapsed "Ran for X seconds" dropdown that appears above each completed
|
||||
* assistant turn. Expands to show the full ordered progress log for that turn.
|
||||
* Collapsed "Ran for X seconds" control above each completed assistant turn.
|
||||
* Expands to a numbered list of just the tools that actually ran — generic
|
||||
* progress phases (analysing, thinking, reading the document, …) are omitted.
|
||||
* When no tool ran, the duration shows as a plain label with nothing to expand.
|
||||
*/
|
||||
function CompletedProgressLogDropdown({
|
||||
progressLog,
|
||||
@@ -284,6 +250,22 @@ function CompletedProgressLogDropdown({
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const label = formatDuration(durationMs, t);
|
||||
|
||||
const toolSteps = progressLog.filter(
|
||||
(step) => step.phase === AiWorkflowPhase.EXECUTING_TOOL && step.tool,
|
||||
);
|
||||
|
||||
// A purely conversational turn (no tools): just show the duration, nothing
|
||||
// to expand.
|
||||
if (toolSteps.length === 0) {
|
||||
return (
|
||||
<div className="chat-completed-log">
|
||||
<Text size="xs" c="dimmed" className="chat-completed-log__static">
|
||||
{label}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chat-completed-log">
|
||||
<UnstyledButton
|
||||
@@ -303,28 +285,21 @@ function CompletedProgressLogDropdown({
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
<Collapse in={expanded}>
|
||||
<div className="chat-completed-log__steps">
|
||||
{progressLog.map((step, i) => {
|
||||
const showConnector = i < progressLog.length - 1;
|
||||
const stepLabel = formatProgress(step, t, resolveToolName);
|
||||
<ol className="chat-completed-log__tools">
|
||||
{toolSteps.map((step, i) => {
|
||||
const endpoint = step.tool ?? "";
|
||||
const name = resolveToolName(endpoint) ?? endpoint;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="chat-progress-step chat-progress-step--done"
|
||||
>
|
||||
<div className="chat-progress-step__left">
|
||||
<div className="chat-progress-step__icon">
|
||||
{progressStepIcon(step, resolveToolIcon, false)}
|
||||
</div>
|
||||
{showConnector && (
|
||||
<div className="chat-progress-step__line" />
|
||||
)}
|
||||
</div>
|
||||
<span className="chat-progress-step__label">{stepLabel}</span>
|
||||
</div>
|
||||
<li key={i} className="chat-completed-log__tool">
|
||||
<span className="chat-completed-log__tool-num">{i + 1}</span>
|
||||
<span className="chat-completed-log__tool-icon">
|
||||
{progressStepIcon(step, resolveToolIcon)}
|
||||
</span>
|
||||
<span className="chat-completed-log__tool-name">{name}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ol>
|
||||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
@@ -488,6 +463,10 @@ export function ChatPanel({ onBack, backLabel }: ChatPanelProps) {
|
||||
};
|
||||
|
||||
const showQuickActions = messages.length === 0 && !isLoading;
|
||||
const disclaimerText = t(
|
||||
"chat.input.disclaimer",
|
||||
"AI can make mistakes. Be sure to verify the output before sharing.",
|
||||
);
|
||||
|
||||
return (
|
||||
<Box className="chat-panel chat-panel--embedded">
|
||||
@@ -533,8 +512,23 @@ export function ChatPanel({ onBack, backLabel }: ChatPanelProps) {
|
||||
</ActionIcon>
|
||||
</div>
|
||||
|
||||
{showQuickActions && (
|
||||
<div className="chat-panel-disclaimer chat-panel-disclaimer--banner">
|
||||
<InfoOutlinedIcon
|
||||
className="chat-panel-disclaimer__icon"
|
||||
sx={{ fontSize: 18 }}
|
||||
/>
|
||||
<span>{disclaimerText}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ScrollArea className="chat-panel-messages" viewportRef={scrollRef}>
|
||||
<Stack gap="sm" px="md" py="sm">
|
||||
<Stack
|
||||
gap="sm"
|
||||
px="md"
|
||||
pt="sm"
|
||||
className="chat-panel-messages__content"
|
||||
>
|
||||
{messages.map((msg) => (
|
||||
<ChatMessageBubble
|
||||
key={msg.id}
|
||||
@@ -568,7 +562,29 @@ export function ChatPanel({ onBack, backLabel }: ChatPanelProps) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{!showQuickActions && (
|
||||
<div className="chat-panel-disclaimer chat-panel-disclaimer--inline">
|
||||
<InfoOutlinedIcon
|
||||
className="chat-panel-disclaimer__icon"
|
||||
sx={{ fontSize: 13 }}
|
||||
/>
|
||||
<span>{disclaimerText}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="chat-panel-input">
|
||||
<ActionIcon
|
||||
className="chat-panel-input__send"
|
||||
variant="filled"
|
||||
color="blue"
|
||||
radius="xl"
|
||||
size="sm"
|
||||
onClick={() => handleSend()}
|
||||
disabled={!input.trim() || isLoading}
|
||||
aria-label={t("chat.input.send", "Send message")}
|
||||
>
|
||||
<ArrowUpwardIcon sx={{ fontSize: 16 }} />
|
||||
</ActionIcon>
|
||||
<Textarea
|
||||
ref={inputRef}
|
||||
placeholder={t("chat.input.placeholder", "What do you want to do?")}
|
||||
@@ -580,21 +596,11 @@ export function ChatPanel({ onBack, backLabel }: ChatPanelProps) {
|
||||
minRows={1}
|
||||
maxRows={4}
|
||||
variant="unstyled"
|
||||
classNames={{ input: "chat-panel-input__field" }}
|
||||
classNames={{
|
||||
root: "chat-panel-input__textarea",
|
||||
input: "chat-panel-input__field",
|
||||
}}
|
||||
/>
|
||||
<div className="chat-panel-input__actions">
|
||||
<ActionIcon
|
||||
variant="filled"
|
||||
color="blue"
|
||||
radius="xl"
|
||||
size="md"
|
||||
onClick={() => handleSend()}
|
||||
disabled={!input.trim() || isLoading}
|
||||
aria-label={t("chat.input.send", "Send message")}
|
||||
>
|
||||
<ArrowUpwardIcon sx={{ fontSize: 16 }} />
|
||||
</ActionIcon>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -160,7 +160,10 @@ describe("handlePaygError — usage-limit modals", () => {
|
||||
|
||||
it("FEATURE_DEGRADED + unsubscribed → opens the free-limit modal (no spend-cap)", () => {
|
||||
handlePaygError("FEATURE_DEGRADED", {
|
||||
response: { status: 402, data: { error: "FEATURE_DEGRADED", subscribed: false } },
|
||||
response: {
|
||||
status: 402,
|
||||
data: { error: "FEATURE_DEGRADED", subscribed: false },
|
||||
},
|
||||
});
|
||||
expect(freeOpened).toBe(1);
|
||||
expect(spendOpened).toBe(0);
|
||||
@@ -168,7 +171,10 @@ describe("handlePaygError — usage-limit modals", () => {
|
||||
|
||||
it("FEATURE_DEGRADED + subscribed → opens the spend-cap modal", () => {
|
||||
handlePaygError("FEATURE_DEGRADED", {
|
||||
response: { status: 402, data: { error: "FEATURE_DEGRADED", subscribed: true } },
|
||||
response: {
|
||||
status: 402,
|
||||
data: { error: "FEATURE_DEGRADED", subscribed: true },
|
||||
},
|
||||
});
|
||||
expect(spendOpened).toBe(1);
|
||||
expect(freeOpened).toBe(0);
|
||||
@@ -176,7 +182,10 @@ describe("handlePaygError — usage-limit modals", () => {
|
||||
|
||||
it("PAYG_LIMIT_REACHED + subscribed → opens the spend-cap modal", () => {
|
||||
handlePaygError("PAYG_LIMIT_REACHED", {
|
||||
response: { status: 402, data: { error: "PAYG_LIMIT_REACHED", subscribed: true } },
|
||||
response: {
|
||||
status: 402,
|
||||
data: { error: "PAYG_LIMIT_REACHED", subscribed: true },
|
||||
},
|
||||
});
|
||||
expect(spendOpened).toBe(1);
|
||||
expect(freeOpened).toBe(0);
|
||||
@@ -184,7 +193,10 @@ describe("handlePaygError — usage-limit modals", () => {
|
||||
|
||||
it("PAYG_LIMIT_REACHED + unsubscribed → opens the free-limit modal", () => {
|
||||
handlePaygError("PAYG_LIMIT_REACHED", {
|
||||
response: { status: 402, data: { error: "PAYG_LIMIT_REACHED", subscribed: false } },
|
||||
response: {
|
||||
status: 402,
|
||||
data: { error: "PAYG_LIMIT_REACHED", subscribed: false },
|
||||
},
|
||||
});
|
||||
expect(freeOpened).toBe(1);
|
||||
expect(spendOpened).toBe(0);
|
||||
|
||||
Reference in New Issue
Block a user