Showing posts with label Javascript. Show all posts
Showing posts with label Javascript. Show all posts

Wednesday, 24 January 2024

SGF Grammar Parser with Peggy

Ideally, I would like to parse SGF's complete grammar. However, at this point, I'm stuck at trying to handle the recursive part only. Here's my feeble attempt at it so far:

import { generate } from "peggy"

const grammar = /* peggy */ `
  string = .*

  parensList = '(' string parensList ')'
             / string
`

const sgf1 =
  "(;GM[1]FF[4]CA[UTF-8]AP[Sabaki:0.52.2]KM[6.5]SZ[19]DT[2023-12-25];B[pd];W[dd];B[pq];W[dp])"

const parser = generate(grammar)

const parse = parser.parse(sgf1)

console.log(parse)
// [
//   '(', ';', 'G', 'M', '[', '1', ']', 'F', 'F', '[', '4',
//   ']', 'C', 'A', '[', 'U', 'T', 'F', '-', '8', ']', 'A',
//   'P', '[', 'S', 'a', 'b', 'a', 'k', 'i', ':', '0', '.',
//   '5', '2', '.', '2', ']', 'K', 'M', '[', '6', '.', '5',
//   ']', 'S', 'Z', '[', '1', '9', ']', 'D', 'T', '[', '2',
//   '0', '2', '3', '-', '1', '2', '-', '2', '5', ']', ';',
//   'B', '[', 'p', 'd', ']', ';', 'W', '[', 'd', 'd', ']',
//   ';', 'B', '[', 'p', 'q', ']', ';', 'W', '[', 'd', 'p',
//   ']', ')'
// ]

Peggy is the successor of Peg.js.

I think I'm failing to identify how to make this recursive properly. How do I make it identify a ( and get into another level with parensList? (I think I need to define string without ( and ) as well...)

What I'm expecting as a result is some sort of tree or JSON like this:

<Branch>{
  moves: [
    <Move>{
      [property]: <Array<String>>
    },
    ...
  ],
  children: <Array<Branch>>
}

But this would be fine as well:

<NodeObject>{
  data: {
    [property]: <Array<String>>
  },
  children: <Array<NodeObject>>
}

SGF is basically a text-based tree format for saving Go (board game) records. Here's an example — SGF doesn't support comments, and it's usually a one-liner, the code below is just to make it easier to read and understand —:

(
  ;GM[1]FF[4]CA[UTF-8]AP[Sabaki:0.52.2]KM[6.5]SZ[19]DT[2023-12-25] // Game Metadata
  ;B[pd] // Black's Move (`pd` = coordinates on the board)
  ;W[dd] // White's Move
    ( // Parentheses denote a branch in the tree
      ;B[pq]
      ;W[dp]
    )
    (
      ;B[dp]
      ;W[pp]
    )
)

You could also have more than one tree at the top, which would yield something like (tree)(tree)...:

(;GM[1]FF[4]CA[UTF-8]AP[Sabaki:0.52.2]KM[6.5]SZ[19]DT[2023-12-25];B[pd];W[dd];B[pq];W[dp])(;GM[1]FF[4]CA[UTF-8]AP[Sabaki:0.52.2]KM[6.5]SZ[19]DT[2023-12-25];B[pd];W[dd](;B[pq];W[dp])(;B[dp];W[pp]))

The whole grammar is this:

Collection     = { GameTree }
GameTree       = "(" RootNode NodeSequence { Tail } ")"
Tail           = "(" NodeSequence { Tail } ")"
NodeSequence   = { Node }
RootNode       = Node
Node           = ";" { Property }
Property       = PropIdent PropValue { PropValue }
PropIdent      = UcLetter { UcLetter }
PropValue      = "[" Value "]"
UcLetter       = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" |
                 "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" |
                 "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z"

You can use the editor Sabaki to create SGF files.



from SGF Grammar Parser with Peggy

Tuesday, 23 January 2024

Is there an event or property on the window that shows when network calls are being made or have completed?

Is there a property on the window object or document that indicates if a network call is being made?

I have this code all over my pages that displays an icon when a network call is made:

  showNetworkIcon();
  var response = await fetch(url);
  var data = await response.json();
  showNetworkIcon(false);

But if there are two calls at once then one of them will hide the network call indicator while there are still network calls happening.

Is there a property like this:

var networkCall = window.requestsOpen;

Then I can not hide the network icon if that value is true.

Or if there is an event I can listen for:

window.addEventListener("networkCallOpen", ()=>{ showNetworkIcon() });
window.addEventListener("networkCallClosed", ()=>{ hideNetworkIcon() });

The problem with the above is that if two calls are still one will close before the other so there still needs to be a property to check. Unless there was a an all calls closed event.

window.addEventListener("allNetworkCallsClosed", ()=>{ hideNetworkIcon() });


from Is there an event or property on the window that shows when network calls are being made or have completed?

Monday, 22 January 2024

WEBVTT subtitle issue on ROKU and LG TV using Dash stream on HTML5 player

WebVTTV Subtitles not working on dash stream using webvtt subtitles. My app uses native HTML5 player and I can see with the help of mpeg-dash chrome plugin that subtitle do load. However, video plays without subtitles on different smart tvs I tested like Hisense, ROKU, TCL and it simply crashes LG TV .

I can see in network logs, it doesn't make request for text streams at all. There is styling and positioning information within the WebVTTV file, wondering if that is the issue. However, I don't see request for WebVTTV file also as I can see if I test same in chrome Dash plugin.

Test stream : http://vod-pbsamerica.simplestreamcdn.com/pbs/encoded/394438.ism/manifest.mpd?filter=(FourCC%20!%3D%20%22JPEG%22%20%26%26%20systemBitrate%20%3C%203500000)

Example of video tag :

<video id="mediaPlayerVideo" preload="auto" style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%;"><source src="http://vod-pbsamerica.simplestreamcdn.com/pbs/encoded/394438.ism/manifest.mpd?filter=(FourCC%20!%3D%20%22JPEG%22%20%26%26%20systemBitrate%20%3C%203500000)" type="application/dash+xml"></video>

Can't figure out the issue as I don't see anything in network log of the TV browsers on debugging.

I wonder if it's because it can't parse the url of the webvttv file "textstream_eng=1000.webvtt ".



from WEBVTT subtitle issue on ROKU and LG TV using Dash stream on HTML5 player

How to implement Default Language without URL Prefix in Next.js 14 for Static Export?

I am currently working on a website using Next.js 14, with the aim of exporting it as a static site for distribution via a CDN (Cloudflare Pages). My site requires internationalization (i18n) support for multiple languages. I have set up a folder structure for language support, which looks like this:

- [language]
  -- layout.tsx  // generateStaticParams with possible languages
  -- page.tsx
  -- ...

This setup allows me to access pages with language prefixes in the URL, such as /en/example and /de/example.

However, I want to implement a default language (e.g., English) that is accessible at the root path without a language prefix (/example). Importantly, I do not wish to redirect users to a URL with the language prefix for SEO purposes. Nor can I use the rewrite function because I'm using static export.

Here are my specific requirements:

  1. Access the default language pages directly via the root path (e.g., /example for English).
  2. Avoid redirects to language-prefixed URLs (e.g., not redirecting /example to /en/example).
  3. Maintain the ability to access other languages with their respective prefixes (e.g., /de/example for German).

I am looking for guidance on:

How to realise this with Next.js 14 to serve the default language pages at the root path without a language prefix. Ensuring that this setup is compatible with the static export feature of Next.js.

Any insights, code snippets, or references to relevant documentation would be greatly appreciated.



from How to implement Default Language without URL Prefix in Next.js 14 for Static Export?

Saturday, 20 January 2024

Multiple Schema types for react-hook-form useForm (schema type varies according to a form value)

I'm attempting to make a type vary according to a variable selection, I have a schema that is built like the following:

const schemaBasedOnColor = useMemo(() => {
    if (vendida === VendidaEnum[0]) {
      switch (receitaColor) {
        case CoresDeReceitaEnum.Branca:
          return yupResolver(receitaBrancaSchema) as Resolver<
            SchemaProps,
            ReceitaBrancaSchemaProps
          >
        case CoresDeReceitaEnum.Amarela:
          return yupResolver(receitaAmarelaSchema) as Resolver<
            SchemaProps,
            ReceitaAmarelaSchemaProps
          >
        default:
          return yupResolver(receitaAzulSchema) as Resolver<
            SchemaProps,
            ReceitaAzulSchemaProps
          >
      }
    }
    return schema
  }, [receitaColor, vendida])

 const getInitialValues = () => ({
    date: params?.dataReceita?.seconds
      ? moment(params?.dataReceita?.seconds * 1000).format('DD/MM/YYYY')
      : '',
    color: params?.tipo || 'Branca',
    dataVenda: params?.dataVenda?.seconds
      ? moment(params?.dataVenda?.seconds * 1000).format('DD/MM/YYYY')
      : '',
    imagens: createImageNoAllowedRemove(params?.imagens),
    crm: params?.crm || '',
    tipoDocumento: params?.tipoDocumento || '',
    nomeComprador: params?.comprador || '',
    documentoComprador: params?.documento || '',
    produto: params?.produto || '',
    laboratorio: params?.laboratorio || '',
    quantidade: params?.quantidade?.toString() || '',
    lotes: formatReceitaLoteQtdToString(params?.lotes as any),
  })

  const {
    control,
    formState: { errors, isDirty },
    handleSubmit,
    clearErrors,
    setError,
    setValue,
    getValues,
    reset,
    resetField,
    watch,
  } = useForm<SchemaProps>({
    resolver: schemaBasedOnColor,
    defaultValues: getInitialValues(),
  })

I attempted changing the schema type with that Resolver<> which did not work. This is the type error the resolver gives me:

Type 'ObjectSchema<{ date: string; crm: string | undefined; color: string; dataVenda: string; imagens: ({ url: string | undefined; extension: string | undefined; } | undefined)[]; }, AnyObject, { ...; }, ""> | Resolver<...>' is not assignable to type 'Resolver<SchemaProps, any> | undefined'.
  Type 'ObjectSchema<{ date: string; crm: string | undefined; color: string; dataVenda: string; imagens: ({ url: string | undefined; extension: string | undefined; } | undefined)[]; }, AnyObject, { ...; }, "">' is not assignable to type 'Resolver<SchemaProps, any>'.
    Type 'ObjectSchema<{ date: string; crm: string | undefined; color: string; dataVenda: string; imagens: ({ url: string | undefined; extension: string | undefined; } | undefined)[]; }, AnyObject, { ...; }, "">' provides no match for the signature '(values: SchemaProps, context: any, options: ResolverOptions<SchemaProps>): ResolverResult<SchemaProps> | Promise<...>'.ts(2322)
(property) resolver?: Resolver<SchemaProps, any> | undefined

This are the types:

import { ReceitasProps } from '@/components/FileCard/types'

export interface ReceitaBrancaSchemaProps {
  date: string
  dataVenda: string
  color: ReceitasProps['tipo']
  imagens: {
    url: string
    extension: string
    allowRemove?: boolean
  }[]
  crm: string | undefined
  tipoDocumento: string
  nomeComprador: string
  documentoComprador: string
  produto: string
  laboratorio: string
  quantidade: string
  lotes:
    | {
        lote: string | undefined
        loteQtd: string | undefined
      }[]
}

export interface ReceitaAzulSchemaProps {
  date: string
  dataVenda: string
  color: ReceitasProps['tipo']
  imagens: {
    url: string
    extension: string
    allowRemove?: boolean
  }[]
  telefone: string
  cep: string
  rua: string
  bairro: string
  cidade: string
  crm: string | undefined
  tipoDocumento: string
  nomeComprador: string
  documentoComprador: string
  produto: string
  laboratorio: string
  quantidade: string
  lotes:
    | {
        lote: string | undefined
        loteQtd: string | undefined
      }[]
}

export interface ReceitaAmarelaSchemaProps {
  date: string
  dataVenda: string
  color: ReceitasProps['tipo']
  imagens: {
    url: string
    extension: string
    allowRemove?: boolean
  }[]
  crm: string | undefined
  tipoDocumento: string
  nomeComprador: string
  documentoComprador: string
  produto: string
  laboratorio: string
  quantidade: string
  lotes:
    | {
        lote: string | undefined
        loteQtd: string | undefined
      }[]
}

export type SchemaProps =
  | ReceitaBrancaSchemaProps
  | ReceitaAzulSchemaProps
  | ReceitaAmarelaSchemaProps

For now ReceitaBrancaSchema and ReceitaAmarelaSchemaProps are equal, but they will be diferent



from Multiple Schema types for react-hook-form useForm (schema type varies according to a form value)

Friday, 19 January 2024

Remove focus from listbox options on mouse click only

I am implementing the following combobox: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/

As you can notice in the example code from the link above, when I click with my mouse on the combobox, there is a blue focus around the selected option in the listbox.

How do I remove this focus when interacting with the combobox using mouse? I would like to keep the focus when interacting with the combobox using keyboard only.

/*
 *   This content is licensed according to the W3C Software License at
 *   https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
 */

'use strict';

// Save a list of named combobox actions, for future readability
const SelectActions = {
  Close: 0,
  CloseSelect: 1,
  First: 2,
  Last: 3,
  Next: 4,
  Open: 5,
  PageDown: 6,
  PageUp: 7,
  Previous: 8,
  Select: 9,
  Type: 10,
};

/*
 * Helper functions
 */

// filter an array of options against an input string
// returns an array of options that begin with the filter string, case-independent
function filterOptions(options = [], filter, exclude = []) {
  return options.filter((option) => {
    const matches = option.toLowerCase().indexOf(filter.toLowerCase()) === 0;
    return matches && exclude.indexOf(option) < 0;
  });
}

// map a key press to an action
function getActionFromKey(event, menuOpen) {
  const {
    key,
    altKey,
    ctrlKey,
    metaKey
  } = event;
  const openKeys = ['ArrowDown', 'ArrowUp', 'Enter', ' ']; // all keys that will do the default open action
  // handle opening when closed
  if (!menuOpen && openKeys.includes(key)) {
    return SelectActions.Open;
  }

  // home and end move the selected option when open or closed
  if (key === 'Home') {
    return SelectActions.First;
  }
  if (key === 'End') {
    return SelectActions.Last;
  }

  // handle typing characters when open or closed
  if (
    key === 'Backspace' ||
    key === 'Clear' ||
    (key.length === 1 && key !== ' ' && !altKey && !ctrlKey && !metaKey)
  ) {
    return SelectActions.Type;
  }

  // handle keys when open
  if (menuOpen) {
    if (key === 'ArrowUp' && altKey) {
      return SelectActions.CloseSelect;
    } else if (key === 'ArrowDown' && !altKey) {
      return SelectActions.Next;
    } else if (key === 'ArrowUp') {
      return SelectActions.Previous;
    } else if (key === 'PageUp') {
      return SelectActions.PageUp;
    } else if (key === 'PageDown') {
      return SelectActions.PageDown;
    } else if (key === 'Escape') {
      return SelectActions.Close;
    } else if (key === 'Enter' || key === ' ') {
      return SelectActions.CloseSelect;
    }
  }
}

// return the index of an option from an array of options, based on a search string
// if the filter is multiple iterations of the same letter (e.g "aaa"), then cycle through first-letter matches
function getIndexByLetter(options, filter, startIndex = 0) {
  const orderedOptions = [
    ...options.slice(startIndex),
    ...options.slice(0, startIndex),
  ];
  const firstMatch = filterOptions(orderedOptions, filter)[0];
  const allSameLetter = (array) => array.every((letter) => letter === array[0]);

  // first check if there is an exact match for the typed string
  if (firstMatch) {
    return options.indexOf(firstMatch);
  }

  // if the same letter is being repeated, cycle through first-letter matches
  else if (allSameLetter(filter.split(''))) {
    const matches = filterOptions(orderedOptions, filter[0]);
    return options.indexOf(matches[0]);
  }

  // if no matches, return -1
  else {
    return -1;
  }
}

// get an updated option index after performing an action
function getUpdatedIndex(currentIndex, maxIndex, action) {
  const pageSize = 10; // used for pageup/pagedown

  switch (action) {
    case SelectActions.First:
      return 0;
    case SelectActions.Last:
      return maxIndex;
    case SelectActions.Previous:
      return Math.max(0, currentIndex - 1);
    case SelectActions.Next:
      return Math.min(maxIndex, currentIndex + 1);
    case SelectActions.PageUp:
      return Math.max(0, currentIndex - pageSize);
    case SelectActions.PageDown:
      return Math.min(maxIndex, currentIndex + pageSize);
    default:
      return currentIndex;
  }
}

// check if element is visible in browser view port
function isElementInView(element) {
  var bounding = element.getBoundingClientRect();

  return (
    bounding.top >= 0 &&
    bounding.left >= 0 &&
    bounding.bottom <=
    (window.innerHeight || document.documentElement.clientHeight) &&
    bounding.right <=
    (window.innerWidth || document.documentElement.clientWidth)
  );
}

// check if an element is currently scrollable
function isScrollable(element) {
  return element && element.clientHeight < element.scrollHeight;
}

// ensure a given child element is within the parent's visible scroll area
// if the child is not visible, scroll the parent
function maintainScrollVisibility(activeElement, scrollParent) {
  const {
    offsetHeight,
    offsetTop
  } = activeElement;
  const {
    offsetHeight: parentOffsetHeight,
    scrollTop
  } = scrollParent;

  const isAbove = offsetTop < scrollTop;
  const isBelow = offsetTop + offsetHeight > scrollTop + parentOffsetHeight;

  if (isAbove) {
    scrollParent.scrollTo(0, offsetTop);
  } else if (isBelow) {
    scrollParent.scrollTo(0, offsetTop - parentOffsetHeight + offsetHeight);
  }
}

/*
 * Select Component
 * Accepts a combobox element and an array of string options
 */
const Select = function(el, options = []) {
  // element refs
  this.el = el;
  this.comboEl = el.querySelector('[role=combobox]');
  this.listboxEl = el.querySelector('[role=listbox]');

  // data
  this.idBase = this.comboEl.id || 'combo';
  this.options = options;

  // state
  this.activeIndex = 0;
  this.open = false;
  this.searchString = '';
  this.searchTimeout = null;

  // init
  if (el && this.comboEl && this.listboxEl) {
    this.init();
  }
};

Select.prototype.init = function() {
  // select first option by default
  this.comboEl.innerHTML = this.options[0];

  // add event listeners
  this.comboEl.addEventListener('blur', this.onComboBlur.bind(this));
  this.listboxEl.addEventListener('focusout', this.onComboBlur.bind(this));
  this.comboEl.addEventListener('click', this.onComboClick.bind(this));
  this.comboEl.addEventListener('keydown', this.onComboKeyDown.bind(this));

  // create options
  this.options.map((option, index) => {
    const optionEl = this.createOption(option, index);
    this.listboxEl.appendChild(optionEl);
  });
};

Select.prototype.createOption = function(optionText, index) {
  const optionEl = document.createElement('div');
  optionEl.setAttribute('role', 'option');
  optionEl.id = `${this.idBase}-${index}`;
  optionEl.className =
    index === 0 ? 'combo-option option-current' : 'combo-option';
  optionEl.setAttribute('aria-selected', `${index === 0}`);
  optionEl.innerText = optionText;

  optionEl.addEventListener('click', (event) => {
    event.stopPropagation();
    this.onOptionClick(index);
  });
  optionEl.addEventListener('mousedown', this.onOptionMouseDown.bind(this));

  return optionEl;
};

Select.prototype.getSearchString = function(char) {
  // reset typing timeout and start new timeout
  // this allows us to make multiple-letter matches, like a native select
  if (typeof this.searchTimeout === 'number') {
    window.clearTimeout(this.searchTimeout);
  }

  this.searchTimeout = window.setTimeout(() => {
    this.searchString = '';
  }, 500);

  // add most recent letter to saved search string
  this.searchString += char;
  return this.searchString;
};

Select.prototype.onComboBlur = function(event) {
  // do nothing if relatedTarget is contained within listboxEl
  if (this.listboxEl.contains(event.relatedTarget)) {
    return;
  }

  // select current option and close
  if (this.open) {
    this.selectOption(this.activeIndex);
    this.updateMenuState(false, false);
  }
};

Select.prototype.onComboClick = function() {
  this.updateMenuState(!this.open, false);
};

Select.prototype.onComboKeyDown = function(event) {
  const {
    key
  } = event;
  const max = this.options.length - 1;

  const action = getActionFromKey(event, this.open);

  switch (action) {
    case SelectActions.Last:
    case SelectActions.First:
      this.updateMenuState(true);
      // intentional fallthrough
    case SelectActions.Next:
    case SelectActions.Previous:
    case SelectActions.PageUp:
    case SelectActions.PageDown:
      event.preventDefault();
      return this.onOptionChange(
        getUpdatedIndex(this.activeIndex, max, action)
      );
    case SelectActions.CloseSelect:
      event.preventDefault();
      this.selectOption(this.activeIndex);
      // intentional fallthrough
    case SelectActions.Close:
      event.preventDefault();
      return this.updateMenuState(false);
    case SelectActions.Type:
      return this.onComboType(key);
    case SelectActions.Open:
      event.preventDefault();
      return this.updateMenuState(true);
  }
};

Select.prototype.onComboType = function(letter) {
  // open the listbox if it is closed
  this.updateMenuState(true);

  // find the index of the first matching option
  const searchString = this.getSearchString(letter);
  const searchIndex = getIndexByLetter(
    this.options,
    searchString,
    this.activeIndex + 1
  );

  // if a match was found, go to it
  if (searchIndex >= 0) {
    this.onOptionChange(searchIndex);
  }
  // if no matches, clear the timeout and search string
  else {
    window.clearTimeout(this.searchTimeout);
    this.searchString = '';
  }
};

Select.prototype.onOptionChange = function(index) {
  // update state
  this.activeIndex = index;

  // update aria-activedescendant
  this.comboEl.setAttribute('aria-activedescendant', `${this.idBase}-${index}`);

  // update active option styles
  const options = this.el.querySelectorAll('[role=option]');
  [...options].forEach((optionEl) => {
    optionEl.classList.remove('option-current');
  });
  options[index].classList.add('option-current');

  // ensure the new option is in view
  if (isScrollable(this.listboxEl)) {
    maintainScrollVisibility(options[index], this.listboxEl);
  }

  // ensure the new option is visible on screen
  // ensure the new option is in view
  if (!isElementInView(options[index])) {
    options[index].scrollIntoView({
      behavior: 'smooth',
      block: 'nearest'
    });
  }
};

Select.prototype.onOptionClick = function(index) {
  this.onOptionChange(index);
  this.selectOption(index);
  this.updateMenuState(false);
};

Select.prototype.onOptionMouseDown = function() {
  // Clicking an option will cause a blur event,
  // but we don't want to perform the default keyboard blur action
  this.ignoreBlur = true;
};

Select.prototype.selectOption = function(index) {
  // update state
  this.activeIndex = index;

  // update displayed value
  const selected = this.options[index];
  this.comboEl.innerHTML = selected;

  // update aria-selected
  const options = this.el.querySelectorAll('[role=option]');
  [...options].forEach((optionEl) => {
    optionEl.setAttribute('aria-selected', 'false');
  });
  options[index].setAttribute('aria-selected', 'true');
};

Select.prototype.updateMenuState = function(open, callFocus = true) {
  if (this.open === open) {
    return;
  }

  // update state
  this.open = open;

  // update aria-expanded and styles
  this.comboEl.setAttribute('aria-expanded', `${open}`);
  open ? this.el.classList.add('open') : this.el.classList.remove('open');

  // update activedescendant
  const activeID = open ? `${this.idBase}-${this.activeIndex}` : '';
  this.comboEl.setAttribute('aria-activedescendant', activeID);

  if (activeID === '' && !isElementInView(this.comboEl)) {
    this.comboEl.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest'
    });
  }

  // move focus back to the combobox, if needed
  callFocus && this.comboEl.focus();
};

// init select
window.addEventListener('load', function() {
  const options = [
    'Choose a Fruit',
    'Apple',
    'Banana',
    'Blueberry',
    'Boysenberry',
    'Cherry',
    'Cranberry',
    'Durian',
    'Eggplant',
    'Fig',
    'Grape',
    'Guava',
    'Huckleberry',
  ];
  const selectEls = document.querySelectorAll('.js-select');

  selectEls.forEach((el) => {
    new Select(el, options);
  });
});
.combo *,
.combo *::before,
.combo *::after {
  box-sizing: border-box;
}

.combo {
  display: block;
  margin-bottom: 1.5em;
  max-width: 400px;
  position: relative;
}

.combo::after {
  border-bottom: 2px solid rgb(0 0 0 / 75%);
  border-right: 2px solid rgb(0 0 0 / 75%);
  content: "";
  display: block;
  height: 12px;
  pointer-events: none;
  position: absolute;
  right: 16px;
  top: 50%;
  transform: translate(0, -65%) rotate(45deg);
  width: 12px;
}

.combo-input {
  background-color: #f5f5f5;
  border: 2px solid rgb(0 0 0 / 75%);
  border-radius: 4px;
  display: block;
  font-size: 1em;
  min-height: calc(1.4em + 26px);
  padding: 12px 16px 14px;
  text-align: left;
  width: 100%;
}

.open .combo-input {
  border-radius: 4px 4px 0 0;
}

.combo-input:focus { 
  border-color: #0067b8;
  box-shadow: 0 0 4px 2px #0067b8;
  outline: 4px solid transparent;
}

.combo-label {
  display: block;
  font-size: 20px;
  font-weight: 100;
  margin-bottom: 0.25em;
}

.combo-menu {
  background-color: #f5f5f5;
  border: 1px solid rgb(0 0 0 / 75%);
  border-radius: 0 0 4px 4px;
  display: none;
  max-height: 300px;
  overflow-y: scroll;
  left: 0;
  position: absolute;
  top: 100%;
  width: 100%;
  z-index: 100;
}

.open .combo-menu {
  display: block;
}

.combo-option {
  padding: 10px 12px 12px;
}

.combo-option:hover {
  background-color: rgb(0 0 0 / 10%);
}

.combo-option.option-current {
  outline: 3px solid #0067b8;
  outline-offset: -3px;
}

.combo-option[aria-selected="true"] {
  padding-right: 30px;
  position: relative;
}

.combo-option[aria-selected="true"]::after {
  border-bottom: 2px solid #000;
  border-right: 2px solid #000;
  content: "";
  height: 16px;
  position: absolute;
  right: 15px;
  top: 50%;
  transform: translate(0, -50%) rotate(45deg);
  width: 8px;
}
<label id="combo1-label" class="combo-label">Favorite Fruit</label>
<div class="combo js-select">
  <div aria-controls="listbox1" aria-expanded="false" aria-haspopup="listbox" aria-labelledby="combo1-label" id="combo1" class="combo-input" role="combobox" tabindex="0"></div>
  <div class="combo-menu" role="listbox" id="listbox1" aria-labelledby="combo1-label" tabindex="-1">


  </div>
</div>


from Remove focus from listbox options on mouse click only

Excel Online Javascript Api Add Allow Edit Range

I'm having trouble adding an allowed edit range to a worksheet protection object using Excel Javascript API. I keep getting an error Cannot read properties of undefined (reading 'add'). I believe I've added the property with statement

worksheet.load("protection/protected", "protection/allowEditRanges");

but maybe this is wrong?

I've referred to the API reference here https://learn.microsoft.com/en-us/javascript/api/excel/excel.alloweditrangecollection?view=excel-js-preview

async function protect(worksheetName) {
await Excel.run(async (context) => {
    worksheet = context.workbook.worksheets.getItem(worksheetName);
    worksheet.load("protection/protected", "protection/allowEditRanges");
    await context.sync();
    //can't add without pausing protection 
    worksheet.protection.unprotect("");
           
    var wholerange = worksheet.getRange();
    wholerange.format.protection.locked = true;                            

    worksheet.protection.allowEditRange.add({title: "Range1", rangeAddress: "A4:G500"});
    worksheet.protection.allowEditRange.add({title: "Range2", rangeAddress: "I4::L500"});        

    worksheet.protection.protect({
        allowFormatCells: true,
        allowAutoFilter: true,
        allowDeleteRows: true,
        allowEditObjects: true,
        //allowFormatColumns: true,
        allowFormatRows: true,
        allowInsertHyperlinks: true,
        allowInsertRows: true,
        allowPivotTables: true,
        allowSort: true
    }, "");

    await context.sync();

});

}



from Excel Online Javascript Api Add Allow Edit Range

Friday, 12 January 2024

place the tooltip on available large space inside a container in angular

I have an editor in which the user can create a banner the user can drag the element in any position he/she wants inside a banner, the element has a tooltip, on hover, it should show the tooltip positioned on the side where the space is larger than the rest (top, left, bottom, right) and the tooltip should never go outside the container no matter what.

HTML

<div id="banner-container" class="banner-container">
    <span
        (cdkDragReleased)="onCircleButtonDragEnd($event)"
        id="point-button"
        class="point-button"
        cdkDragBoundary=".banner-container"
        cdkDrag
        [style.left]="banner.bannerElements.x"
        [style.top]="banner.bannerElements.y"
        [attr.data-id]="banner.bannerElements.buttonId"
        [id]="'button-' + banner.bannerElements.buttonId"
    ></span>
    <span
        id="tooltip"
        [style.left]="banner.bannerElements.x"
        [style.top]="banner.bannerElements.y"
        [attr.data-id]="banner.bannerElements.tooltipId"
        [id]="'button-' + banner.bannerElements.tooltipId"
    >
        Szanujemy Twoją prywatność
    </span>
</div>

TS

  banner = {
        buttonId: 11,
        tooltipId: 2,
        x: 0,
        y: 0
    };

onCircleButtonDragEnd(event) {
        const container = event.currentTarget as HTMLElement;
        const containerWidth = container.clientWidth;
        const containerHeight = container.clientHeight;

        this.banner.y =
            ((event.clientX - container.getBoundingClientRect().left) /
                containerWidth) *
            100;
        this.banner.y =
            ((event.clientY - container.getBoundingClientRect().top) /
                containerHeight) *
            100;
    }``

CSS

.point-button {
  cursor: pointer;
  display: block;
  width: 24px;
  height: 24px;
  border: 2px solid rgb(179, 115, 188);
  background-color: rgb(255, 255, 255);
  background-image: none;
  border-radius: 100%;
  position: relative;
  z-index: 1;
  box-sizing: border-box;
}

.point-button:active {
  box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
    0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
}

.banner-container {
  width: 350px;
  height: 200px;
  max-width: 100%;
  border: dotted #ccc 2px;
}
.tooltip {
  width: fit-content;
  height: 50px;
  border: 2px #ccc solid;
  display: none;
}
.point-button:hover + .tooltip {
  display: block;
}`

**

LIVE DEMO

** : DEMO

enter image description here enter image description here enter image description here enter image description here



from place the tooltip on available large space inside a container in angular

Thursday, 11 January 2024

JSON Web Token (JWT) Error: Invalid Signature with RSA Key Pairs

I'm encountering an issue in my Node.js (20.5.1) application related to JSON Web Token (JWT) verification using RSA key pairs. The error message is as follows:

[16:39:56.959] FATAL (26460): invalid signature
err: {
  "type": "JsonWebTokenError",
  "message": "invalid signature",
  "stack":
      JsonWebTokenError: invalid signature
          at U:\Coding\MCShop-API\node_modules\jsonwebtoken\verify.js:171:19
          at getSecret (U:\Coding\MCShop-API\node_modules\jsonwebtoken\verify.js:97:14)
          at module.exports (U:\Coding\MCShop-API\node_modules\jsonwebtoken\verify.js:101:10)
          at verifyJWTToken (U:\Coding\MCShop-API\src\crypto.ts:28:37)
          at U:\Coding\MCShop-API\src\app.ts:39:45
          at Layer.handle [as handle_request] (U:\Coding\MCShop-API\node_modules\express\lib\router\layer.js:95:5)
          at trim_prefix (U:\Coding\MCShop-API\node_modules\express\lib\router\index.js:328:13)
          at U:\Coding\MCShop-API\node_modules\express\lib\router\index.js:286:9
          at Function.process_params (U:\Coding\MCShop-API\node_modules\express\lib\router\index.js:346:12)
          at next (U:\Coding\MCShop-API\node_modules\express\lib\router\index.js:280:10)
  "name": "JsonWebTokenError"
}

I have also attached the crypto.ts file that handles the JSON Web Tokens for my application.

import crypto from 'crypto';
import { readFileSync } from 'fs';
import { JwtPayload, sign, verify } from 'jsonwebtoken';
import { logger } from './app';

export function generateRSAKeyPair() {
    const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
        modulusLength: 512,
        publicKeyEncoding: { type: 'pkcs1', format: 'pem' },
        privateKeyEncoding: { type: 'pkcs1', format: 'pem' }
    });

    return { privateKey, publicKey };
}

export function generateJWTToken(admin: boolean, username: string) {
    const key = readFileSync('private.key', { encoding: 'utf-8', flag: 'r' });
    return sign({
        admin,
        username
    }, key, { algorithm: 'RS256' });
}

export function verifyJWTToken(token: string) {
    try {
        const key = readFileSync('public.key', { encoding: 'utf-8', flag: 'r' });
        const verifiedToken = verify(token, key, { algorithms: ['RS256'] }) as JwtPayload;
        if (!verifiedToken) return false;
        return verifiedToken

    } catch (error) {
        logger.fatal(error);
        return false;
    }
}

I have confirmed the following:

  • The key variable is not undefined and is fetching the contents of the file.
  • readFileSync does not use caching.
  • The values that get passed into the function are valid.
  • The attempted JWT is indeed valid confirmed by JWT.io jwt.io

I suspect there might be an error in how I'm handling the keys or in the JWT library version.

Can someone help me identify the root cause of the "invalid signature" error and suggest potential solutions? Any insights or advice would be greatly appreciated.



from JSON Web Token (JWT) Error: Invalid Signature with RSA Key Pairs

Wednesday, 27 December 2023

How to have the dialog for choosing download location appeared in the frontend, before the file gets downloaded, using FastAPI?

I have a GET endpoint that should return a huge file (500Mb). I am using FileResponse to do that(code is simplified for clarity reasons):

 async def get_file()
       headers = {"Content-Disposition": f"attachment; filename={filename}"}
       return FileResponse(file_path, headers=headers)

The problem is that I have to wait on frontend till that file is completely downloaded until I am shown this dialog: enter image description here

And then this file is saved instantly.

So for example I have a file with size 500 MB, when I click download on UI I have to wait a minute or something till the "Save dialog" is displayed. Then when I click "Save" the file is saved instantly. Obviously the frontend was waiting for the file to be downloaded, what I need is that frontend shows "save dialog" instantly and then downloading starts in the background.

What I need is this: Dialog is shown instantly and then the user waits for the download to finish after he clicks 'Save'.

So how can I achieve that?



from How to have the dialog for choosing download location appeared in the frontend, before the file gets downloaded, using FastAPI?

Tuesday, 26 December 2023

Not Found for an API route

In my Next.js project I have main/app/api/worker-callback/route.ts file:

import { NextApiResponse } from "next";
import { NextResponse } from "next/server";

type ResponseData = {
    error?: string
};

export async function POST(req: Request, res: NextApiResponse<ResponseData>) {
    if (req.headers.get('Authorization') !== process.env.BACKEND_SECRET!) {
        res.status(403).json({ error: "Allowed only by backend" });
        // return Response.json({ error: "Allowed only by backend" }, { status: 403 });
    }
    return NextResponse.json({});
}

But when I query it, I get error 404. Why?

curl -d '' -v -o/dev/null -H "accept: application/json" http://localhost:3000/api/worker-callback
...
< HTTP/1.1 404 Not Found
...

Note that HTML pages in main/app work just fine. I build it by the cd main && next build command.



from Not Found for an API route

Tuesday, 19 December 2023

Shared Runtime gives Error in Office Word JS Add-in

when I this Shared Runtime Requirements in my manifest file my add-in gives Error and not working. I am using this code for

    <Requirements>
      <Sets DefaultMinVersion="1.1">
        <Set Name="SharedRuntime" MinVersion="1.1"/>
      </Sets>
   </Requirements>

and

    <Runtimes>
       <Runtime resid="contoso.taskpane.url" lifetime="long"></Runtime>
    </Runtimes>

Shared Runtime code image

facing error like this

Shared Runtime Error

When I remove Requirements from my manifest then my add-in is working. When I use the same code in another system there its work why it is not working in my system?



from Shared Runtime gives Error in Office Word JS Add-in

Friday, 15 December 2023

cannot appear as a child of and cannot appear as a child of

Please take a look at the schematic structure of my table. I removed all unnecessary stuff for ease of reading. As you can see, there is a header and there is a body. Only the body is wrapped in ScrollArea (https://www.radix-ui.com/primitives/docs/components/scroll-area), since I want the user to be able to scroll only the body and not the entire table

<table>
 <thead>
  <tr>
    <th>Name</th>
    <th>Surname</th>
    <th>City</th>
  </tr>
 </thead>
 <ScrollArea.Root>
  <ScrollArea.Viewport>
   {data.map((person) => (
     <tbody>
      <tr>
       <td>{person.name}</td>
       <td>{person.surname}</td>
       <td>{person.city}</td>
      </tr>
     </tbody>
  ))}
  </ScrollArea.Viewport>
  <ScrollArea.Scrollbar/>
 </ScrollArea.Root>
</table>

And so, when I go to the page with this table, I receive two warnings in the console:

Warning: validateDOMNesting(...): div cant appear as a child of table.

Warning: validateDOMNesting(...): tbody cannot appear as a child of div.

If I remove ScrollArea from the table, then the warnings disappear. But ScrollArea is very important to me for moving through long tables.

Tell me how can I get rid of these warnings?



from cannot appear as a child of
and
cannot appear as a child of

React Native Touch Through Flatlist

For "react-native": "^0.70.5"

Requirement:

  • Flatlist as an overlay above Clickable elements
  • Flatlist header has a transparent area, with pointerEvents="none" to make the elements below clickable and yet allow the Flatlist to scroll. enter image description here

Issues with some possible approaches

  1. pointerEvents="none" doesn't work with Flatlist, as internally how Flatlist is built it will block the events at all values of pointerEvents. It's the same with Scrollview as well.
  2. react-native-touch-through-view (the exact library I need) doesn't work with RN 0.70.2, library is outdated. After fixing the build issues, touch events are not propagating to the clickable elements.
  3. Created a custom component ScrollableView, as pointerEvents with View work well. With this adding pointerEvents to none on parts of the children, lets the touch event to propagate to elements below.
  • This is working well on Android, but failing on iOS.
  • Also the scrolling of the view is not smooth.
  • Requires further handling for performance optimisation for long lists
import React, { useState, useRef } from 'react';
import { View, PanResponder, Animated } from 'react-native';

const ScrollableView = ({children, style, onScroll}) => {
    const scrollY = useRef(new Animated.Value(0)).current;
    const lastScrollY = useRef(0);
    const scrollYClamped = Animated.diffClamp(scrollY, 0, 1000);

    const panResponder = useRef(
        PanResponder.create({
            onStartShouldSetPanResponder: () => true,
            onPanResponderMove: (_, gestureState) => {
                scrollY.setValue(lastScrollY.current + gestureState.dy);
            },
            onPanResponderRelease: (_, { vy, dy }) => {
                lastScrollY.current += dy;
                Animated.spring(scrollY, {
                    toValue: lastScrollY.current,
                    velocity: vy,
                    tension: 2,
                    friction: 8,
                    useNativeDriver: false,
                }).start();
            },

        })
    ).current;

    const combinedStyle = [
        {
            transform: [{ translateY: scrollYClamped }],
        },
        style
    ];

    return (
        <Animated.View
            {...panResponder.panHandlers}
            pointerEvents="box-none"
            style={combinedStyle}
        >
            {children}
        </Animated.View>
    );
};

export default ScrollableView;

Any solution to any of the above three approaches is appreciated.



from React Native Touch Through Flatlist

Thursday, 14 December 2023

PWA won't go fullscreen after deinstall/install

My PWA worked happily in manifest -> display: fullscreen for years. Yesterday I came across a recent Chrome/Android bug where my PWA would no longer auto rotate from lansdscape/portrait. Having seen in SO that the fix required a de-install and re-install of my PWA I went ahead.

Good news, is my PWA is once again responsive to orientation change.

Bad news, my PWA with not go back to fullscreen mode and maxes out at standalone :-(

I have upped the version on my cache to hopefully reload all files but still no joy.

Please help!

Yes, you have to grant geolocation access but all the code is available on github.

To reproduce: -

Please navigate to this URL and

  • If you don't have a Google Maps API key just click "No Maps!"
  • Wait a bit and you will be prompted to install the PWA.
  • Click install
  • Exit Android Chrome
  • Go to your phone "Apps" scroll across and you should see Brotkrumen with a ginger-bread house icon.
  • The app launches in standalone mode and not fullscreen.
  • Watch the wicked witch capture Hansel and Gretel

EDIT 1 Further info. If you open Brotkrumen in Chrome and then press the kebab menu, you'll see the option to "Open Brotkrumen". If you click that it opens in full screen but with black space at the top. (Just like the image below)

ScreenShot example of PWA trip replay



from PWA won't go fullscreen after deinstall/install

Wednesday, 13 December 2023

"Uncaught TypeError: Illegal invocation" while overriding EventSource

Playground link: https://www.w3schools.com/html/tryit.asp?filename=tryhtml5_sse

Problem: I am trying to override EventSource in such a way that there will be a console.log whenever onmessage triggers.

Code:

<!DOCTYPE html>
<html>
<body>

<h1>Getting server updates</h1>
<div id="result"></div>

<script>
if (typeof EventSource !== "undefined") {

  const original = EventSource;

  // part of chrome extension
  window.EventSource = function EventSource(url) {
    const ori = new original(url);

    Object.getPrototypeOf(ori).onmessage = function (event) {
      console.log(event); // log on every message
      ori.onmessage(event);
    };

    return ori;
  };
  
  
  
  // actual source code
  var source = new EventSource("demo_sse.php");
  source.onmessage = function (event) {
    document.getElementById("result").innerHTML += event.data + "<br>";
  };
} else {
  document.getElementById("result").innerHTML = "Sorry, your browser does not support server-sent events...";
}
</script>

</body>
</html>

But I am getting this error:

Error:

VM461:10 Uncaught TypeError: Illegal invocation
    at new EventSource (<anonymous>:10:42)
    at <anonymous>:21:16
    at submitTryit (tryit.asp?filename=tryhtml5_sse:853:17)
    at HTMLButtonElement.onclick (tryit.asp?filename=tryhtml5_sse:755:133)
EventSource @ VM461:10
(anonymous) @ VM461:21
submitTryit @ tryit.asp?filename=tryhtml5_sse:853
onclick @ tryit.asp?filename=tryhtml5_sse:755
uic.js?v=1.0.5:1 Uncaught ReferenceError: adngin is not defined
    at uic_r_p (uic.js?v=1.0.5:1:54492)
    at HTMLButtonElement.onclick (tryit.asp?filename=tryhtml5_sse:755:148)
uic_r_p @ uic.js?v=1.0.5:1
onclick @ tryit.asp?filename=tryhtml5_sse:755


from "Uncaught TypeError: Illegal invocation" while overriding EventSource

Wednesday, 6 December 2023

Why does my Firefox browser take over a hundred times longer to upload large files compared to Google Chrome?

I have a server-side program on localhost, and I use a form to upload a 300MB file. However, I've noticed a discrepancy between Google Chrome and Firefox – Google Chrome returns a successful result within a few seconds, while Firefox takes around 3 minutes. The code and network environment are consistent. What could be causing this discrepancy?

Here is my code:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title><!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Upload</title>
</head>
<body>

<h2>Upload</h2>

<form action="http://127.0.0.1:9527/api/upgrade/upload" method="post" enctype="multipart/form-data">
  <label for="fileInput">chooseFile:</label>
  <input type="file" id="fileInput" name="files">

  <button type="submit">Upload File</button>
</form>

</body>
</html>

I have tried changing the server-side programming languages (Java, Go) and upgrading versions, but they are already the latest.



from Why does my Firefox browser take over a hundred times longer to upload large files compared to Google Chrome?

Tensorflow JS learning data too big to fit in memory at once, how to learn?

I have the problem that my dataset became too large to fit in memory at once in tensorflow js. What are good solutions to learn from all data entries? My data comes from a mongodb instance and needs to be loaded asynchronously.

I tried to play with generator functions, but couldnt get async generators to work yet. I was also thinking that maybe fitting the model in batches to the data would be possible?

It would be great if someone could provide me with a minimal example on how to fit on data that is loaded asynchronously through either batches or a database cursor.

For example when trying to return promises from the generator, I get a typescript error.

    const generate = function* () {
        yield new Promise(() => {});
    };

    tf.data.generator(generate);

Argument of type '() => Generator<Promise<unknown>, void, unknown>' is not assignable to parameter of type '() => Iterator<TensorContainer, any, undefined> | Promise<Iterator<TensorContainer, any, undefined>>'.


Also using async generators doesnt work:

Async generators result in a type error

tf.data.generator(async function* () {})

throws Argument of type '() => AsyncGenerator<any, void, unknown>' is not assignable to parameter of type '() => Iterator<TensorContainer, any, undefined> | Promise<Iterator<TensorContainer, any, undefined>>'.



from Tensorflow JS learning data too big to fit in memory at once, how to learn?

Monday, 4 December 2023

svelte: Issue with API call staying pending when accessing application via IP address or hostname, server response as stream data, chat-UI

Title: Issue with API call staying pending when accessing application via IP address

Description:

I have forked the chat-ui project and made several changes, including Azure AD integration, OpenAI API compatible serving layer support, and making it more container-friendly. The application works fine on localhost, but when I try to access it via an IP address, I encounter an issue.

Problem:

The backend code provides data as a stream to the frontend using the POST method. On localhost, everything works as expected, but when accessing the application via an IP address, the API call from the network stays pending until it reaches component.close() in the frontend. The issue seems to be related to the stream not being processed properly.

Backend Code (excerpt):

export async function POST({ request, locals, params, getClientAddress }) {
    const id = z.string().parse(params.id);
    const convId = new ObjectId(id);
    const promptedAt = new Date();

    const userId = locals.user?._id ?? locals.sessionId;

    // check user
    if (!userId) {
        throw error(401, "Unauthorized");
    }
    console.log("post", {userId, params, ip: getClientAddress()})

    // check if the user has access to the conversation
    const conv = await collections.conversations.findOne({
        _id: convId,
        ...authCondition(locals),
    });

    if (!conv) {
        throw error(404, "Conversation not found");
    }

    // register the event for ratelimiting
    await collections.messageEvents.insertOne({
        userId: userId,
        createdAt: new Date(),
        ip: getClientAddress(),
    });

    // guest mode check
    if (
        !locals.user?._id &&
        requiresUser &&
        (MESSAGES_BEFORE_LOGIN ? parseInt(MESSAGES_BEFORE_LOGIN) : 0) > 0
    ) {
        const totalMessages =
            (
                await collections.conversations
                    .aggregate([
                        { $match: authCondition(locals) },
                        { $project: { messages: 1 } },
                        { $unwind: "$messages" },
                        { $match: { "messages.from": "assistant" } },
                        { $count: "messages" },
                    ])
                    .toArray()
            )[0]?.messages ?? 0;

        if (totalMessages > parseInt(MESSAGES_BEFORE_LOGIN)) {
            throw error(429, "Exceeded number of messages before login");
        }
    }

    // check if the user is rate limited
    const nEvents = Math.max(
        await collections.messageEvents.countDocuments({ userId }),
        await collections.messageEvents.countDocuments({ ip: getClientAddress() })
    );

    if (RATE_LIMIT != "" && nEvents > parseInt(RATE_LIMIT)) {
        throw error(429, ERROR_MESSAGES.rateLimited);
    }

    // fetch the model
    const model = models.find((m) => m.id === conv.model);

    if (!model) {
        throw error(410, "Model not available anymore");
    }

    // finally parse the content of the request
    const json = await request.json();

    const {
        inputs: newPrompt,
        response_id: responseId,
        id: messageId,
        is_retry,
        web_search: webSearch,
    } = z
        .object({
            inputs: z.string().trim().min(1),
            id: z.optional(z.string().uuid()),
            response_id: z.optional(z.string().uuid()),
            is_retry: z.optional(z.boolean()),
            web_search: z.optional(z.boolean()),
        })
        .parse(json);

    // get the list of messages
    // while checking for retries
    let messages = (() => {
        if (is_retry && messageId) {
            // if the message is a retry, replace the message and remove the messages after it
            let retryMessageIdx = conv.messages.findIndex((message) => message.id === messageId);
            if (retryMessageIdx === -1) {
                retryMessageIdx = conv.messages.length;
            }
            return [
                ...conv.messages.slice(0, retryMessageIdx),
                { content: newPrompt, from: "user", id: messageId as Message["id"], updatedAt: new Date() },
            ];
        } // else append the message at the bottom

        return [
            ...conv.messages,
            {
                content: newPrompt,
                from: "user",
                id: (messageId as Message["id"]) || crypto.randomUUID(),
                createdAt: new Date(),
                updatedAt: new Date(),
            },
        ];
    })() satisfies Message[];

    await collections.conversations.updateOne(
        {
            _id: convId,
        },
        {
            $set: {
                messages,
                title: conv.title,
                updatedAt: new Date(),
            },
        }
    );

    // we now build the stream
    const stream = new ReadableStream({
        async start(controller) {
            const updates: MessageUpdate[] = [];

            function update(newUpdate: MessageUpdate) {
                if (newUpdate.type !== "stream") {
                    updates.push(newUpdate);
                }
                controller.enqueue(JSON.stringify(newUpdate) + "\n");
            }

            update({ type: "status", status: "started" });

            if (conv.title === "New Chat" && messages.length === 1) {
                try {
                    conv.title = (await summarize(newPrompt)) ?? conv.title;
                    update({ type: "status", status: "title", message: conv.title });
                } catch (e) {
                    console.error(e);
                }
            }

            await collections.conversations.updateOne(
                {
                    _id: convId,
                },
                {
                    $set: {
                        messages,
                        title: conv.title,
                        updatedAt: new Date(),
                    },
                }
            );

            let webSearchResults: WebSearch | undefined;

            if (webSearch) {
                webSearchResults = await runWebSearch(conv, newPrompt, update);
            }

            messages[messages.length - 1].webSearch = webSearchResults;

            conv.messages = messages;

            const endpoint = await model.getEndpoint();

            for await (const output of await endpoint({ conversation: conv })) {
                // if not generated_text is here it means the generation is not done
                if (!output.generated_text) {
                    // else we get the next token
                    if (!output.token.special) {
                        update({
                            type: "stream",
                            token: output.token.text,
                        });

                        // if the last message is not from assistant, it means this is the first token
                        const lastMessage = messages[messages.length - 1];

                        if (lastMessage?.from !== "assistant") {
                            // so we create a new message
                            messages = [
                                ...messages,
                                // id doesn't match the backend id but it's not important for assistant messages
                                // First token has a space at the beginning, trim it
                                {
                                    from: "assistant",
                                    content: output.token.text.trimStart(),
                                    webSearch: webSearchResults,
                                    updates: updates,
                                    id: (responseId as Message["id"]) || crypto.randomUUID(),
                                    createdAt: new Date(),
                                    updatedAt: new Date(),
                                },
                            ];
                        } else {
                            // abort check
                            const date = abortedGenerations.get(convId.toString());
                            if (date && date > promptedAt) {
                                break;
                            }

                            if (!output) {
                                break;
                            }

                            // otherwise we just concatenate tokens
                            lastMessage.content += output.token.text;
                        }
                    }
                } else {
                    // add output.generated text to the last message
                    messages = [
                        ...messages.slice(0, -1),
                        {
                            ...messages[messages.length - 1],
                            content: output.generated_text,
                            updates: updates,
                            updatedAt: new Date(),
                        },
                    ];
                }
            }

            await collections.conversations.updateOne(
                {
                    _id: convId,
                },
                {
                    $set: {
                        messages,
                        title: conv?.title,
                        updatedAt: new Date(),
                    },
                }
            );

            update({
                type: "finalAnswer",
                text: messages[messages.length - 1].content,
            });
            controller.close();
        },
        async cancel() {
            await collections.conversations.updateOne(
                {
                    _id: convId,
                },
                {
                    $set: {
                        messages,
                        title: conv.title,
                        updatedAt: new Date(),
                    },
                }
            );
        },
    });

    // Todo: maybe we should wait for the message to be saved before ending the response - in case of errors
    return new Response(stream, {
        headers: {
            "Content-Type": "application/x-ndjson",
        },
    });
}

Frontend Code (excerpt):

async function writeMessage(message: string, messageId = randomUUID()) {
        if (!message.trim()) return;

        try {
            isAborted = false;
            loading = true;
            pending = true;

            // first we check if the messageId already exists, indicating a retry

            let retryMessageIndex = messages.findIndex((msg) => msg.id === messageId);
            const isRetry = retryMessageIndex !== -1;
            // if it's not a retry we just use the whole array
            if (!isRetry) {
                retryMessageIndex = messages.length;
            }

            // slice up to the point of the retry
            messages = [
                ...messages.slice(0, retryMessageIndex),
                { from: "user", content: message, id: messageId },
            ];

            const responseId = randomUUID();

            const response = await fetch(`${base}/conversation/${$page.params.id}`, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({
                    inputs: message,
                    id: messageId,
                    response_id: responseId,
                    is_retry: isRetry,
                    web_search: $webSearchParameters.useSearch,
                }),
            });

            if (!response.body) {
                throw new Error("Body not defined");
            }

            if (!response.ok) {
                error.set((await response.json())?.message);
                return;
            }
            // eslint-disable-next-line no-undef
            const encoder = new TextDecoderStream();
            const reader = response?.body?.pipeThrough(encoder).getReader();
            let finalAnswer = "";

            // this is a bit ugly
            // we read the stream until we get the final answer
            while (finalAnswer === "") {
                await new Promise((r) => setTimeout(r, 25));

                // check for abort
                if (isAborted) {
                    reader?.cancel();
                    break;
                }

                // if there is something to read
                await reader?.read().then(async ({ done, value }) => {
                    // we read, if it's done we cancel
                    if (done) {
                        reader.cancel();
                        return;
                    }

                    if (!value) {
                        return;
                    }

                    // if it's not done we parse the value, which contains all messages
                    const inputs = value.split("\n");
                    inputs.forEach(async (el: string) => {
                        try {
                            const update = JSON.parse(el) as MessageUpdate;
                            if (update.type === "finalAnswer") {
                                finalAnswer = update.text;
                                reader.cancel();
                                invalidate(UrlDependency.Conversation);
                            } else if (update.type === "stream") {
                                pending = false;

                                let lastMessage = messages[messages.length - 1];

                                if (lastMessage.from !== "assistant") {
                                    messages = [
                                        ...messages,
                                        { from: "assistant", id: randomUUID(), content: update.token },
                                    ];
                                } else {
                                    lastMessage.content += update.token;
                                    messages = [...messages];
                                }
                            } else if (update.type === "webSearch") {
                                webSearchMessages = [...webSearchMessages, update];
                            } else if (update.type === "status") {
                                if (update.status === "title" && update.message) {
                                    const conv = data.conversations.find(({ id }) => id === $page.params.id);
                                    if (conv) {
                                        conv.title = update.message;

                                        $titleUpdate = {
                                            title: update.message,
                                            convId: $page. params.id,
                                        };
                                    }
                                }
                            }
                        } catch (parseError) {
                            // in case of parsing error we wait for the next message
                            return;
                        }
                    });
                });
            }

            // reset the websearchmessages
            webSearchMessages = [];

            await invalidate(UrlDependency.ConversationList);
        } catch (err) {
            if (err instanceof Error && err.message.includes("overloaded")) {
                $error = "Too much traffic, please try again.";
            } else if (err instanceof Error && err.message.includes("429")) {
                $error = ERROR_MESSAGES.rateLimited;
            } else if (err instanceof Error) {
                $error = err.message;
            } else {
                $error = ERROR_MESSAGES.default;
            }
            console.error(err);
        } finally {
            loading = false;
            pending = false;
        }
    }

Steps to Reproduce:

  1. Fork the chat-ui project.
  2. Make the specified changes related to Azure AD integration, OpenAI API, and container support.
  3. Run the application on localhost and access it via an IP address.
  4. Observe the behavior where the API call stays pending until component.close() is reached.

Expected Behavior:

The application should behave consistently whether accessed via localhost or an IP address or hostname. The API call should not stay pending, and the stream should be processed correctly.

Additional Information:

  • before Adding component.close() in the stream code the windows machine was not at all returning the response and at the end it was saying promise uncaught

  • Network requests and responses from the browser's developer tools. Local host starts immediately and waterfall of api start getting response and stream is wring at FE correctly : enter image description here With IP it stays in pending state and when final message arrives at that time it writes everything at once
    enter image description here server LOGs: enter image description here you can see the difference when ip ::1 it starts writing at FE and processing stream data, when IP '::ffff:10.10.100.106' it stays in pending until the stream generation is completed i assume

  • Any specific configurations or dependencies related to hosting the application via an IP address.

Environment:

  • Operating System: it is working fine on mac but facing issues in windows
  • Browser: chrome
  • Node.js version: >18
  • Any other relevant environment details.

Note: Please let me know if additional code snippets or information are needed. Thanks for your help!


Feel free to customize the template based on your specific situation and provide any additional details that might be relevant to the issue.



from svelte: Issue with API call staying pending when accessing application via IP address or hostname, server response as stream data, chat-UI

Saturday, 2 December 2023

Fit Text to Circle (With Scaling) in HTML Canvas, while Typing, with React

I'm trying to have text fit a circle while typing, something like this:

Example Image 1

I've tried following Mike Bostock's tutorial, but failed so far, here's my pitiful attempt:

import React, { useEffect, useRef, useState } from "react";

export const TwoPI = 2 * Math.PI;

export function setupGridWidthHeightAndScale(
  width: number,
  height: number,
  canvas: HTMLCanvasElement
) {
  canvas.style.width = width + "px";
  canvas.style.height = height + "px";

  // Otherwise we get blurry lines
  // Referenece: [Stack Overflow - Canvas drawings, like lines, are blurry](https://stackoverflow.com/a/59143499/4756173)
  const scale = window.devicePixelRatio;

  canvas.width = width * scale;
  canvas.height = height * scale;

  const canvasCtx = canvas.getContext("2d")!;

  canvasCtx.scale(scale, scale);
}


type CanvasProps = {
  width: number;
  height: number;
};

export function TextInCircle({
  width,
  height,
}: CanvasProps) {
  const [text, setText] = useState("");

  const canvasRef = useRef<HTMLCanvasElement>(null);

  function getContext() {
    const canvas = canvasRef.current!;
    return canvas.getContext("2d")!;
  }

  useEffect(() => {
    const canvas = canvasRef.current!;
    setupGridWidthHeightAndScale(width, height, canvas);

    const ctx = getContext();

    // Background
    ctx.fillStyle = "black";
    ctx.fillRect(0, 0, width, height);

    // Circle
    ctx.beginPath();
    ctx.arc(width / 2, height / 2, 100, 0, TwoPI);
    ctx.closePath();

    // Fill the Circle
    ctx.fillStyle = "white";
    ctx.fill();
  }, [width, height]);

  function handleChange(
    e: React.ChangeEvent<HTMLInputElement>
  ) {
    const newText = e.target.value;
    setText(newText);

    // Split Words
    const words = text.split(/\s+/g); // To hyphenate: /\s+|(?<=-)/
    if (!words[words.length - 1]) words.pop();
    if (!words[0]) words.shift();

    // Get Width
    const lineHeight = 12;
    const targetWidth = Math.sqrt(
      measureWidth(text.trim()) * lineHeight
    );

    // Split Lines accordingly
    const lines = splitLines(targetWidth, words);

    // Get radius so we can scale
    const radius = getRadius(lines, lineHeight);

    // Draw Text
    const ctx = getContext();

    ctx.textAlign = "center";
    ctx.fillStyle = "black";
    for (const [i, l] of lines.entries()) {
      // I'm totally lost as to how to proceed here...
      ctx.fillText(
        l.text,
        width / 2 - l.width / 2,
        height / 2 + i * lineHeight
      );
    }
  }

  function measureWidth(s: string) {
    const ctx = getContext();
    return ctx.measureText(s).width;
  }

  function splitLines(
    targetWidth: number,
    words: string[]
  ) {
    let line;
    let lineWidth0 = Infinity;
    const lines = [];

    for (let i = 0, n = words.length; i < n; ++i) {
      let lineText1 =
        (line ? line.text + " " : "") + words[i];

      let lineWidth1 = measureWidth(lineText1);

      if ((lineWidth0 + lineWidth1) / 2 < targetWidth) {
        line!.width = lineWidth0 = lineWidth1;
        line!.text = lineText1;
      } else {
        lineWidth0 = measureWidth(words[i]);
        line = { width: lineWidth0, text: words[i] };
        lines.push(line);
      }
    }
    return lines;
  }

  function getRadius(
    lines: { width: number; text: string }[],
    lineHeight: number
  ) {
    let radius = 0;

    for (let i = 0, n = lines.length; i < n; ++i) {
      const dy =
        (Math.abs(i - n / 2 + 0.5) + 0.5) * lineHeight;

      const dx = lines[i].width / 2;

      radius = Math.max(
        radius,
        Math.sqrt(dx ** 2 + dy ** 2)
      );
    }

    return radius;
  }

  return (
    <>
      <input type="text" onChange={handleChange} />

      <canvas ref={canvasRef}></canvas>
    </>
  );
}

I've also tried to follow @markE's answer from 2013. But the text doesn't seem to be made to scale with the circle's radius, it's the other way around in that example, with the radius being scaled to fit the text, as far as I was able to understand. And, for some reason, changing the example text yields a text is undefined error, I have no idea why.

import React, { useEffect, useRef, useState } from "react";

export const TwoPI = 2 * Math.PI;

export function setupGridWidthHeightAndScale(
  width: number,
  height: number,
  canvas: HTMLCanvasElement
) {
  canvas.style.width = width + "px";
  canvas.style.height = height + "px";

  // Otherwise we get blurry lines
  // Referenece: [Stack Overflow - Canvas drawings, like lines, are blurry](https://stackoverflow.com/a/59143499/4756173)
  const scale = window.devicePixelRatio;

  canvas.width = width * scale;
  canvas.height = height * scale;

  const canvasCtx = canvas.getContext("2d")!;

  canvasCtx.scale(scale, scale);
}

type CanvasProps = {
  width: number;
  height: number;
};

export function TextInCircle({
  width,
  height,
}: CanvasProps) {
  const [typedText, setTypedText] = useState("");

  const canvasRef = useRef<HTMLCanvasElement>(null);

  function getContext() {
    const canvas = canvasRef.current!;
    return canvas.getContext("2d")!;
  }

  useEffect(() => {
    const canvas = canvasRef.current!;
    setupGridWidthHeightAndScale(width, height, canvas);
  }, [width, height]);

  const textHeight = 15;
  const lineHeight = textHeight + 5;
  const cx = 150;
  const cy = 150;
  const r = 100;

  function handleChange(
    e: React.ChangeEvent<HTMLInputElement>
  ) {
    const ctx = getContext();

    const text = e.target.value; // This gives out an error
    // "'Twas the night before Christmas, when all through the house,  Not a creature was stirring, not even a mouse.  And so begins the story of the day of";

    const lines = initLines();
    wrapText(text, lines);

    ctx.beginPath();
    ctx.arc(cx, cy, r, 0, Math.PI * 2, false);
    ctx.closePath();
    ctx.strokeStyle = "skyblue";
    ctx.lineWidth = 2;
    ctx.stroke();
  }

  // pre-calculate width of each horizontal chord of the circle
  // This is the max width allowed for text

  function initLines() {
    const lines: any[] = [];

    for (let y = r * 0.9; y > -r; y -= lineHeight) {
      let h = Math.abs(r - y);

      if (y - lineHeight < 0) {
        h += 20;
      }

      let length = 2 * Math.sqrt(h * (2 * r - h));

      if (length && length > 10) {
        lines.push({
          y: y,
          maxLength: length,
        });
      }
    }

    return lines;
  }

  // draw text on each line of the circle

  function wrapText(text: string, lines: any[]) {
    const ctx = getContext();

    let i = 0;
    let words = text.split(" ");

    while (i < lines.length && words.length > 0) {
      let line = lines[i++];

      let lineData = calcAllowableWords(
        line.maxLength,
        words
      );

      ctx.fillText(
        lineData!.text,
        cx - lineData!.width / 2,
        cy - line.y + textHeight
      );

      words.splice(0, lineData!.count);
    }
  }

  // calculate how many words will fit on a line

  function calcAllowableWords(
    maxWidth: number,
    words: any[]
  ) {
    const ctx = getContext();

    let wordCount = 0;
    let testLine = "";
    let spacer = "";
    let fittedWidth = 0;
    let fittedText = "";

    const font = "12pt verdana";
    ctx.font = font;

    for (let i = 0; i < words.length; i++) {
      testLine += spacer + words[i];
      spacer = " ";

      let width = ctx.measureText(testLine).width;

      if (width > maxWidth) {
        return {
          count: i,
          width: fittedWidth,
          text: fittedText,
        };
      }

      fittedWidth = width;
      fittedText = testLine;
    }
  }

  return (
    <>
      <input type="text" onChange={handleChange} />

      <canvas ref={canvasRef}></canvas>
    </>
  );
}


from Fit Text to Circle (With Scaling) in HTML Canvas, while Typing, with React