LCOV - code coverage report
Current view: top level - dashql-app/src/demos/agent_face - agent_face_page.tsx Coverage Total Hit
Test: DashQL 915828b Lines: 96.2 % 160 154
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as React from 'react';
       2              : 
       3              : import { AgentFace, AgentFaceMode, DEFAULT_AGENT_FACE_TUNING, useFlavorAgentFaceBrand, type AgentFaceTuning } from '../../agent/agent_face.js';
       4              : import { DASHQL_BRAND, SIDB_BRAND } from '../../agent/agent_face_morph.js';
       5              : import { AgentGreeting } from '../../agent/agent_greeting.js';
       6              : import { Button, ButtonSize, ButtonVariant } from '../../ui/foundations/button.js';
       7              : import { SegmentedControl, SegmentedControlSize } from '../../ui/foundations/segmented_control.js';
       8              : import * as styles from './agent_face_page.module.css';
       9              : 
      10              : /// A slider of a tuning knob
      11              : interface Knob {
      12              :     key: keyof AgentFaceTuning;
      13              :     label: string;
      14              :     min: number;
      15              :     max: number;
      16              :     step: number;
      17              : }
      18              : 
      19              : /// The knobs, grouped by what they shape
      20            1 : const KNOB_GROUPS: { title: string; knobs: Knob[] }[] = [
      21            1 :     {
      22            1 :         title: 'Morph',
      23            1 :         knobs: [
      24            1 :             { key: 'stiffness', label: 'Spring stiffness', min: 10, max: 300, step: 1 },
      25            1 :             { key: 'damping', label: 'Spring damping', min: 1, max: 40, step: 0.5 },
      26            1 :             { key: 'lag', label: 'Point lag', min: 0, max: 0.9, step: 0.01 },
      27            1 :             { key: 'ripple', label: 'Ripple', min: 0, max: 12, step: 0.1 },
      28            1 :             { key: 'gooBlur', label: 'Goo blur', min: 0, max: 16, step: 0.1 },
      29            1 :         ],
      30            1 :     },
      31            1 :     {
      32            1 :         title: 'Liquid face',
      33            1 :         knobs: [
      34            1 :             { key: 'wobble', label: 'Wobble', min: 0, max: 0.08, step: 0.001 },
      35            1 :             { key: 'wobbleSpeed', label: 'Wobble speed', min: 0, max: 4, step: 0.05 },
      36            1 :         ],
      37            1 :     },
      38            1 :     {
      39            1 :         title: 'Eyes',
      40            1 :         knobs: [
      41            1 :             { key: 'glanceIntervalMs', label: 'Glance interval (ms)', min: 400, max: 8000, step: 100 },
      42            1 :             { key: 'glanceAmplitude', label: 'Glance amplitude', min: 0, max: 20, step: 0.5 },
      43            1 :             { key: 'blinkIntervalMs', label: 'Blink interval (ms)', min: 1000, max: 12000, step: 100 },
      44            1 :         ],
      45            1 :     },
      46            1 :     {
      47            1 :         title: 'Cycle',
      48            1 :         knobs: [
      49            1 :             { key: 'faceHoldMs', label: 'Face hold (ms)', min: 1000, max: 20000, step: 100 },
      50            1 :             { key: 'logoHoldMs', label: 'Logo hold (ms)', min: 500, max: 10000, step: 100 },
      51            1 :         ],
      52            1 :     },
      53            1 : ];
      54              : 
      55            1 : const BRANDS = [
      56            1 :     { label: 'DashQL', brand: DASHQL_BRAND },
      57            1 :     { label: 'SIDB', brand: SIDB_BRAND },
      58            1 : ];
      59              : /// The demo cycles faster than the app, to watch the morph without waiting
      60            1 : const DEMO_TUNING: AgentFaceTuning = { ...DEFAULT_AGENT_FACE_TUNING, faceHoldMs: 4000, logoHoldMs: 2000 };
      61              : 
      62              : /// The eyes follow the mouse pointer
      63            1 : const FOLLOW_POINTER = { pointer: true };
      64              : 
      65            1 : const MODES = [AgentFaceMode.Cycle, AgentFaceMode.Face, AgentFaceMode.Logo];
      66            1 : const MODE_LABELS = ['Cycle', 'Face', 'Logo'];
      67            1 : const SURFACES = ['light', 'dark'] as const;
      68            1 : const INKS = [
      69            1 :     { label: 'Neutral', color: 'var(--base_color_neutral_6)' },
      70            1 :     { label: 'Coral', color: 'var(--base_color_coral_5)' },
      71            1 :     { label: 'Blue', color: 'var(--base_color_blue_5)' },
      72            1 : ];
      73            1 : const SIZES = [24, 48, 96];
      74              : 
      75              : /// A playground of the agent face that morphs into the DashQL brand mark, to tune its motion
      76            1 : function AgentFacePage(): React.ReactElement {
      77            1 :     const [tuning, setTuning] = React.useState<AgentFaceTuning>(DEMO_TUNING);
      78              :     // The demo starts with the brand mark of the flavor, `demos.html?flavor=sidb#/agent_face`
      79            1 :     const flavorBrand = useFlavorAgentFaceBrand();
      80            1 :     const [brand, setBrand] = React.useState(() => Math.max(0, BRANDS.findIndex(entry => entry.brand === flavorBrand)));
      81            1 :     const [mode, setMode] = React.useState(0);
      82            1 :     const [followPointer, setFollowPointer] = React.useState(1);
      83              :     // The face gets excited about every key typed into the try field
      84            1 :     const lastInput = React.useRef<number | null>(null);
      85            1 :     const readLastInput = React.useCallback(() => lastInput.current, []);
      86            1 :     const [surface, setSurface] = React.useState(0);
      87            1 :     const [ink, setInk] = React.useState(0);
      88            1 :     const [copied, setCopied] = React.useState(false);
      89              : 
      90            1 :     const tuningText = React.useMemo(() => JSON.stringify(tuning, null, 4), [tuning]);
      91            1 :     const copyTuning = async () => {
      92            0 :         await navigator.clipboard.writeText(tuningText);
      93            0 :         setCopied(true);
      94            0 :         setTimeout(() => setCopied(false), 1200);
      95            0 :     };
      96              : 
      97            1 :     const faceProps = { brand: BRANDS[brand].brand, tuning, lastInput: readLastInput, gaze: followPointer === 1 ? FOLLOW_POINTER : undefined, mode: MODES[mode], style: { color: INKS[ink].color } };
      98            1 :     return (
      99            1 :         <div className={styles.root}>
     100            1 :             <div className={styles.header}>
     101            1 :                 <h1 className={styles.title}>Agent Face</h1>
     102            1 :                 <p className={styles.subtitle}>
     103              :                     The face of the agent on the empty session, it glances around and now and then melts into a brand mark.
     104            1 :                 </p>
     105            1 :             </div>
     106            1 :             <div className={styles.layout}>
     107            1 :                 <div className={styles.stage} data-surface={SURFACES[surface]}>
     108            1 :                     <AgentFace {...faceProps} className={styles.stage_face} label="Agent face" />
     109            1 :                     <div className={styles.greeting}>
     110            1 :                         <AgentGreeting face={{ ...faceProps, className: styles.greeting_face, label: 'Agent face next to its question' }}
     111            1 :                             questionClassName={styles.greeting_text} questionElement="p" />
     112            1 :                     </div>
     113            1 :                     <input id="agent-face-try" className={styles.try_input} placeholder="Type here, the face gets excited"
     114            1 :                         aria-label="Type to excite the face" onKeyDown={() => { lastInput.current = performance.now(); }} />
     115            1 :                     <div className={styles.sizes}>
     116            1 :                         {SIZES.map(size => (
     117            1 :                             <div key={size} className={styles.size}>
     118            1 :                                 <AgentFace {...faceProps} width={size} height={size} label={`Agent face at ${size} pixels`} />
     119            1 :                                 <span className={styles.size_label}>{size}px</span>
     120            1 :                             </div>
     121            1 :                         ))}
     122            1 :                     </div>
     123            1 :                 </div>
     124            1 :                 <div className={styles.controls}>
     125            1 :                     <div className={styles.control_row}>
     126            1 :                         <span className={styles.control_label}>Brand</span>
     127            1 :                         <SegmentedControl aria-label="Brand" size={SegmentedControlSize.Small} onChange={setBrand}>
     128            1 :                             {BRANDS.map((entry, i) => (
     129            1 :                                 <SegmentedControl.Button key={entry.label} selected={brand === i}>{entry.label}</SegmentedControl.Button>
     130            1 :                             ))}
     131            1 :                         </SegmentedControl>
     132            1 :                     </div>
     133            1 :                     <div className={styles.control_row}>
     134            1 :                         <span className={styles.control_label}>Gaze</span>
     135            1 :                         <SegmentedControl aria-label="What the eyes look at" size={SegmentedControlSize.Small} onChange={setFollowPointer}>
     136            1 :                             <SegmentedControl.Button selected={followPointer === 0}>Around</SegmentedControl.Button>
     137            1 :                             <SegmentedControl.Button selected={followPointer === 1}>Pointer</SegmentedControl.Button>
     138            1 :                         </SegmentedControl>
     139            1 :                     </div>
     140            1 :                     <div className={styles.control_row}>
     141            1 :                         <span className={styles.control_label}>Shows</span>
     142            1 :                         <SegmentedControl aria-label="What the face shows" size={SegmentedControlSize.Small} onChange={setMode}>
     143            1 :                             {MODE_LABELS.map((label, i) => (
     144            1 :                                 <SegmentedControl.Button key={label} selected={mode === i}>{label}</SegmentedControl.Button>
     145            1 :                             ))}
     146            1 :                         </SegmentedControl>
     147            1 :                     </div>
     148            1 :                     <div className={styles.control_row}>
     149            1 :                         <span className={styles.control_label}>Surface</span>
     150            1 :                         <SegmentedControl aria-label="Surface" size={SegmentedControlSize.Small} onChange={setSurface}>
     151            1 :                             <SegmentedControl.Button selected={surface === 0}>Light</SegmentedControl.Button>
     152            1 :                             <SegmentedControl.Button selected={surface === 1}>Dark</SegmentedControl.Button>
     153            1 :                         </SegmentedControl>
     154            1 :                     </div>
     155            1 :                     <div className={styles.control_row}>
     156            1 :                         <span className={styles.control_label}>Ink</span>
     157            1 :                         <SegmentedControl aria-label="Ink" size={SegmentedControlSize.Small} onChange={setInk}>
     158            1 :                             {INKS.map((entry, i) => (
     159            1 :                                 <SegmentedControl.Button key={entry.label} selected={ink === i}>{entry.label}</SegmentedControl.Button>
     160            1 :                             ))}
     161            1 :                         </SegmentedControl>
     162            1 :                     </div>
     163            1 :                     {KNOB_GROUPS.map(group => (
     164            1 :                         <fieldset key={group.title} className={styles.group}>
     165            1 :                             <legend className={styles.group_title}>{group.title}</legend>
     166            1 :                             {group.knobs.map(knob => (
     167            1 :                                 <label key={knob.key} className={styles.knob}>
     168            1 :                                     <span className={styles.knob_label}>{knob.label}</span>
     169            1 :                                     <input id={`agent-face-${knob.key}`} type="range" min={knob.min} max={knob.max} step={knob.step}
     170            1 :                                         value={tuning[knob.key]}
     171            1 :                                         onChange={e => {
     172            0 :                                             const value = Number(e.target.value);
     173            0 :                                             setTuning(t => ({ ...t, [knob.key]: value }));
     174            1 :                                         }} />
     175            1 :                                     <span className={styles.knob_value}>{tuning[knob.key]}</span>
     176            1 :                                 </label>
     177            1 :                             ))}
     178            1 :                         </fieldset>
     179            1 :                     ))}
     180            1 :                     <div className={styles.control_row}>
     181            1 :                         <Button size={ButtonSize.Small} variant={ButtonVariant.Default} onClick={() => setTuning(DEMO_TUNING)}>
     182              :                             Reset
     183            1 :                         </Button>
     184            1 :                         <Button size={ButtonSize.Small} variant={ButtonVariant.Primary} onClick={copyTuning}>
     185            1 :                             {copied ? 'Copied' : 'Copy tuning'}
     186            1 :                         </Button>
     187            1 :                     </div>
     188            1 :                     <pre className={styles.tuning}>{tuningText}</pre>
     189            1 :                 </div>
     190            1 :             </div>
     191            1 :         </div>
     192              :     );
     193            1 : }
     194              : 
     195              : export default AgentFacePage;
        

Generated by: LCOV version 2.0-1