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
40 changes: 40 additions & 0 deletions .changeset/confidence-uncertainty-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
'@loquix/core': minor
---

Add five new components for surfacing assistant confidence and user
disagreement / corrections in a conversation:

- `loquix-confidence-indicator` — score 0–1 rendered as bar / dots /
badge / numeric. Auto-derives `low` / `medium` / `high` from
`low-threshold` / `high-threshold`; invalid or inverted thresholds
reset to defaults so derivation still tracks the value. Exposes
`role="meter"` with `aria-valuetext` on every variant.
- `loquix-uncertainty-marker` — wraps an inline phrase as `unsure`,
`needs-verification`, or `speculative`. Variants: `underline` (wavy),
`highlight` (background), `icon` (trailing glyph). Tooltip opens on
hover/focus, closes on mouseleave/blur/Escape. Uses
`aria-describedby` so the slotted text remains the accessible name.
Enter and Space activate. Emits `loquix-uncertainty-click` with
`{ kind, reason? }`.
- `loquix-disagreement-marker` — inline pill or full banner attached
to a disputed message. Banner can show a "Mark resolved" button via
the `resolvable` attribute. Emits `loquix-disagreement-resolve`.
- `loquix-feedback-form` — higher-order flow that composes two
`loquix-action-feedback` buttons with a reasons + comment card.
Parent state drives the children's `active`; the inner
`loquix-feedback` is intercepted and never leaks. Emits
`loquix-feedback-submit` with `{ sentiment, reason?, comment? }` —
`reason` is a stable ID, never the localised chip label. Optional
`require-comment-on-down` enforces a non-empty comment for negative
feedback. Reasons use radio semantics (`role="radio"` +
`aria-checked`).
- `loquix-correction-input` — strikethrough original + correction
textarea + reason input + Submit / Cancel. Emits
`loquix-correction-submit` with `{ correction, reason?, original? }`
and `loquix-correction-cancel`. Submit gated by non-empty correction
(and non-empty reason when `reason-required`).

Adds confidence and uncertainty colour tokens, new
`HTMLElementEventMap` entries, new i18n keys for all five components,
and React wrappers in `@loquix/react`.
6 changes: 6 additions & 0 deletions docs/.storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ import '@loquix/core/define/define-filter-bar';
import '@loquix/core/define/define-drop-zone';
import '@loquix/core/define/define-scroll-anchor';
import '@loquix/core/define/define-message-attachments';
// Phase 4 PR #2 — Confidence & Uncertainty
import '@loquix/core/define/define-confidence-indicator';
import '@loquix/core/define/define-uncertainty-marker';
import '@loquix/core/define/define-disagreement-marker';
import '@loquix/core/define/define-feedback-form';
import '@loquix/core/define/define-correction-input';

const preview: Preview = {
globalTypes: {
Expand Down
87 changes: 87 additions & 0 deletions docs/src/stories/core/ConfidenceIndicator.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { Meta, StoryObj } from '@storybook/web-components';
import { html } from 'lit';

const meta: Meta = {
title: 'Core/ConfidenceIndicator',
component: 'loquix-confidence-indicator',
tags: ['autodocs'],
argTypes: {
value: { control: { type: 'range', min: 0, max: 1, step: 0.01 } },
variant: { control: 'select', options: ['bar', 'dots', 'badge', 'numeric'] },
level: { control: 'select', options: [undefined, 'low', 'medium', 'high'] },
label: { control: 'text' },
lowThreshold: { control: { type: 'number', step: 0.05 } },
highThreshold: { control: { type: 'number', step: 0.05 } },
showValue: { control: 'boolean' },
},
};
export default meta;

type Story = StoryObj;

export const Variants: Story = {
args: { value: 0.82 },
render: args =>
html`<div style="display:flex;flex-direction:column;gap:16px;align-items:flex-start">
<loquix-confidence-indicator value=${args.value} variant="bar"></loquix-confidence-indicator>
<loquix-confidence-indicator value=${args.value} variant="dots"></loquix-confidence-indicator>
<loquix-confidence-indicator
value=${args.value}
variant="badge"
></loquix-confidence-indicator>
<loquix-confidence-indicator
value=${args.value}
variant="numeric"
></loquix-confidence-indicator>
</div>`,
};

export const Levels: Story = {
render: () =>
html`<div style="display:flex;flex-direction:column;gap:12px;align-items:flex-start">
<loquix-confidence-indicator
value="0.22"
variant="bar"
label="Source quality"
></loquix-confidence-indicator>
<loquix-confidence-indicator
value="0.55"
variant="bar"
label="Source quality"
></loquix-confidence-indicator>
<loquix-confidence-indicator
value="0.88"
variant="bar"
label="Source quality"
></loquix-confidence-indicator>
<div style="display:flex;gap:12px;align-items:center;margin-top:8px">
<loquix-confidence-indicator value="0.22" variant="badge"></loquix-confidence-indicator>
<loquix-confidence-indicator value="0.55" variant="badge"></loquix-confidence-indicator>
<loquix-confidence-indicator value="0.88" variant="badge"></loquix-confidence-indicator>
</div>
<div style="display:flex;gap:16px;align-items:center;margin-top:8px">
<loquix-confidence-indicator value="0.22" variant="dots"></loquix-confidence-indicator>
<loquix-confidence-indicator value="0.55" variant="dots"></loquix-confidence-indicator>
<loquix-confidence-indicator value="0.88" variant="dots"></loquix-confidence-indicator>
</div>
</div>`,
};

export const WithLabel: Story = {
args: { value: 0.78, variant: 'bar', label: 'Answer confidence' },
render: args =>
html`<loquix-confidence-indicator
value=${args.value}
variant=${args.variant}
label=${args.label}
></loquix-confidence-indicator>`,
};

export const NumericLarge: Story = {
args: { value: 0.92 },
render: args =>
html`<loquix-confidence-indicator
value=${args.value}
variant="numeric"
></loquix-confidence-indicator>`,
};
49 changes: 49 additions & 0 deletions docs/src/stories/core/CorrectionInput.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { Meta, StoryObj } from '@storybook/web-components';
import { html } from 'lit';

const meta: Meta = {
title: 'Core/CorrectionInput',
component: 'loquix-correction-input',
tags: ['autodocs'],
argTypes: {
original: { control: 'text' },
value: { control: 'text' },
reason: { control: 'text' },
reasonRequired: { control: 'boolean' },
},
};
export default meta;

type Story = StoryObj;

export const Empty: Story = {
render: () => html`<loquix-correction-input></loquix-correction-input>`,
};

export const Prefilled: Story = {
args: {
original: "The Eiffel Tower is 320 meters tall and was built in 1889 for the World's Fair.",
value:
'The Eiffel Tower is 330 meters tall (including antennas) and was built in 1889 for the Exposition Universelle.',
},
render: args =>
html`<loquix-correction-input
original=${args.original}
value=${args.value}
@loquix-correction-submit=${(e: CustomEvent) =>
// eslint-disable-next-line no-console
console.log('correction submit', e.detail)}
></loquix-correction-input>`,
};

export const ReasonRequired: Story = {
args: {
original: 'Some claim that needs correcting.',
reasonRequired: true,
},
render: args =>
html`<loquix-correction-input
original=${args.original}
?reason-required=${args.reasonRequired}
></loquix-correction-input>`,
};
66 changes: 66 additions & 0 deletions docs/src/stories/core/DisagreementMarker.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { Meta, StoryObj } from '@storybook/web-components';
import { html } from 'lit';

const meta: Meta = {
title: 'Core/DisagreementMarker',
component: 'loquix-disagreement-marker',
tags: ['autodocs'],
argTypes: {
variant: { control: 'select', options: ['inline', 'banner'] },
reason: { control: 'text' },
resolvable: { control: 'boolean' },
},
};
export default meta;

type Story = StoryObj;

export const InlinePill: Story = {
args: { variant: 'inline' },
render: args =>
html`<p style="font-size:14px;max-width:460px">
Earlier response on Q3 numbers
<loquix-disagreement-marker variant=${args.variant}></loquix-disagreement-marker> — the user
pointed out the figure was outdated.
</p>`,
};

export const InlinePillWithReason: Story = {
args: { variant: 'inline', reason: 'figure was outdated' },
render: args =>
html`<loquix-disagreement-marker
variant=${args.variant}
reason=${args.reason}
></loquix-disagreement-marker>`,
};

export const Banner: Story = {
args: {
variant: 'banner',
reason: 'The reported revenue figure conflicts with the Q3 earnings call transcript.',
},
render: args =>
html`<div style="max-width:480px">
<loquix-disagreement-marker
variant=${args.variant}
reason=${args.reason}
></loquix-disagreement-marker>
</div>`,
};

export const BannerResolvable: Story = {
args: {
variant: 'banner',
reason: 'Including antennas, the tower is 330 m tall (verified via the official site).',
resolvable: true,
},
render: args =>
html`<div style="max-width:480px">
<loquix-disagreement-marker
variant=${args.variant}
reason=${args.reason}
?resolvable=${args.resolvable}
@loquix-disagreement-resolve=${() => alert('Marked resolved')}
></loquix-disagreement-marker>
</div>`,
};
56 changes: 56 additions & 0 deletions docs/src/stories/core/FeedbackForm.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { Meta, StoryObj } from '@storybook/web-components';
import { html } from 'lit';

const meta: Meta = {
title: 'Core/FeedbackForm',
component: 'loquix-feedback-form',
tags: ['autodocs'],
// Override the global 'centered' layout so the buttons row stays at the left
// edge in both idle and selected states. Otherwise the host (inline-flex,
// sized to content) centers and visibly "jumps" left when the form panel
// expands the host width.
parameters: { layout: 'padded' },
argTypes: {
value: { control: 'select', options: [null, 'positive', 'negative'] },
allowReason: { control: 'boolean' },
requireCommentOnDown: { control: 'boolean' },
},
};
export default meta;

type Story = StoryObj;

export const Idle: Story = {
render: () => html`<loquix-feedback-form></loquix-feedback-form>`,
};

export const PositiveSelected: Story = {
args: { value: 'positive' },
render: args =>
html`<loquix-feedback-form
value=${args.value}
@loquix-feedback-submit=${(e: CustomEvent) =>
// eslint-disable-next-line no-console
console.log('feedback submit', e.detail)}
></loquix-feedback-form>`,
};

export const NegativeSelected: Story = {
args: { value: 'negative' },
render: args =>
html`<loquix-feedback-form
value=${args.value}
@loquix-feedback-submit=${(e: CustomEvent) =>
// eslint-disable-next-line no-console
console.log('feedback submit', e.detail)}
></loquix-feedback-form>`,
};

export const RequireCommentOnDown: Story = {
args: { value: 'negative', requireCommentOnDown: true },
render: args =>
html`<loquix-feedback-form
value=${args.value}
?require-comment-on-down=${args.requireCommentOnDown}
></loquix-feedback-form>`,
};
Loading
Loading