From 5bc4a18ab9620f9697038df06196dd3fdbbdfa8d Mon Sep 17 00:00:00 2001 From: Offirmo Date: Tue, 1 Oct 2024 20:58:33 +1000 Subject: [PATCH] +++ --- .../senior-dev/tech-bites--frontend-web.md | 1 + .../src/__fixtures/primitives--console.ts | 30 +++++++++--- .../active/view--chat/src/__fixtures/tour.ts | 8 +-- .../active/view--chat/src/loop/index.ts | 39 +++++++++++++-- .../{implementation => primitives}/types.ts | 26 ++++++++-- .../active/view--chat/src/steps/bases.ts | 1 + .../active/view--chat/src/steps/types.ts | 49 ++++++++++++------- 7 files changed, 120 insertions(+), 34 deletions(-) rename stack--current/5-incubator/active/view--chat/src/{implementation => primitives}/types.ts (59%) diff --git a/stack--current/5-incubator/active--no-pkg/bite-sized/lists/senior-dev/tech-bites--frontend-web.md b/stack--current/5-incubator/active--no-pkg/bite-sized/lists/senior-dev/tech-bites--frontend-web.md index e02ea327..bb24a121 100644 --- a/stack--current/5-incubator/active--no-pkg/bite-sized/lists/senior-dev/tech-bites--frontend-web.md +++ b/stack--current/5-incubator/active--no-pkg/bite-sized/lists/senior-dev/tech-bites--frontend-web.md @@ -12,6 +12,7 @@ [ ] security https://portswigger.net/web-security/all-topics [ ] security https://xsleaks.dev/ [ ] TODO shearing layers https://en.wikipedia.org/wiki/Shearing_layers +abort controller https://kettanaito.com/blog/dont-sleep-on-abort-controller AJAX (Asynchronous JavaScript And XML) = LEGACY interactive web apps now fetch + json AMP (Accelerated Mobile Pages) animations -- API https://devdocs.io/dom/web_animations_api diff --git a/stack--current/5-incubator/active/view--chat/src/__fixtures/primitives--console.ts b/stack--current/5-incubator/active/view--chat/src/__fixtures/primitives--console.ts index 5d068cb9..4348b5b7 100644 --- a/stack--current/5-incubator/active/view--chat/src/__fixtures/primitives--console.ts +++ b/stack--current/5-incubator/active/view--chat/src/__fixtures/primitives--console.ts @@ -1,14 +1,18 @@ /** trivial console-based chat primitives for testing * no need to be fancy. */ -import { type ChatPrimitives } from '../implementation/types.js' +import * as readline from 'node:readline/promises' +import { stdin as input, stdout as output } from 'node:process' +const rl = readline.createInterface({ input, output }) + +import { type ChatPrimitives, type InputParameters } from '../primitives/types.js' +import type { InputStep } from '../steps' +import { Parameters } from '@offirmo-private/storypad/src/types/csf' const DEBUG = false const CHAT_CONSOLE: ChatPrimitives = { - setup: async () => { - DEBUG && console.log('[ChatPrimitives.setup()]') - }, + display_message: async ({msg, choices}) => { DEBUG && console.log('[ChatPrimitives.display_message()]') @@ -39,8 +43,6 @@ const CHAT_CONSOLE: ChatPrimitives = { console.log('↳ ' + msg_after) }, - //read_answer: async () => { throw new Error('NO UI read_answer') }, - display_task: async ({ msg_before, promise, @@ -63,6 +65,19 @@ const CHAT_CONSOLE: ChatPrimitives = { console.log('↳ ' + msg_after(success, result || error)) }, + input: async ({ + prompt, + + // we ignore the rest in this primitive implementation + input_type, + default_value, + placeholder, + normalizer, + validators, + }: InputParameters): Promise => { + return rl.question(prompt + ' ') + }, + spin_until_resolution: async ({promise}) => { DEBUG && console.log('[ChatPrimitives.spin_until_resolution(...)]') @@ -72,6 +87,9 @@ const CHAT_CONSOLE: ChatPrimitives = { return promise }, + setup: async () => { + DEBUG && console.log('[ChatPrimitives.setup()]') + }, teardown: async () => { DEBUG && console.log('[ChatPrimitives.teardown()]') }, diff --git a/stack--current/5-incubator/active/view--chat/src/__fixtures/tour.ts b/stack--current/5-incubator/active/view--chat/src/__fixtures/tour.ts index 2491e184..22e31adb 100755 --- a/stack--current/5-incubator/active/view--chat/src/__fixtures/tour.ts +++ b/stack--current/5-incubator/active/view--chat/src/__fixtures/tour.ts @@ -15,7 +15,7 @@ export default function* get_next_step(skip_to_index: number = 0) { } const warmup_promise = new Deferred() - setTimeout(() => warmup_promise.reject(new Error('Failed!')), 3000) + setTimeout(() => warmup_promise.reject(new Error('Failed!')), 1000) const STEPS: Array> = [ @@ -23,16 +23,16 @@ export default function* get_next_step(skip_to_index: number = 0) { type: StepType.perceived_labor, msg_before: 'Waking up...', - duration_ms: 1000, + duration_ms: 500, msg_after: 'Awoken!', }, { type: StepType.progress, - msg_before: 'Warming up...', + msg_before: 'Dialing home...', promise: warmup_promise, - msg_after: success => success ? '✔ Ready!' : '❌ Warm up unsuccessful.', + msg_after: success => success ? '✔ Ready!' : '❌ Dial up unsuccessful.', callback: success => console.log(`[callback called: ${success}]`), }, diff --git a/stack--current/5-incubator/active/view--chat/src/loop/index.ts b/stack--current/5-incubator/active/view--chat/src/loop/index.ts index b22a50e9..9193192f 100644 --- a/stack--current/5-incubator/active/view--chat/src/loop/index.ts +++ b/stack--current/5-incubator/active/view--chat/src/loop/index.ts @@ -2,7 +2,7 @@ import is_promise from 'is-promise' import { type Immutable } from '@offirmo-private/ts-types' import {} from '@offirmo-private/normalize-string' -import { type ChatPrimitives } from '../implementation/types.js' +import { type ChatPrimitives } from '../primitives/types.js' import { type Step, StepType } from '../steps/index.js' import { create_dummy_progress_promise } from '../utils/index.js' import { StepsGenerator } from './types.js' @@ -67,6 +67,7 @@ function create({ // TODO process the separation with the previous step const elapsed_time_ms = (+new Date()) - step_start_timestamp_ms /* + await primitives.pretend_to_think(inter_msg_delay_ms) if (is_step_input(last_step)) { // pretend to have processed the user answer await primitives.pretend_to_think(Math.max(0, after_input_delay_ms - elapsed_time_ms)) @@ -92,11 +93,12 @@ function create({ async function execute_step(step: Step) { if (DEBUG) console.log('↘ ${LIB}.execute_step(', DEBUG_to_prettified_str(step), ')') + //const step = normalize_step(raw_step) switch (step.type) { + case StepType.simple_message: - await primitives.pretend_to_think({duration_ms: inter_msg_delay_ms}) await primitives.display_message({ msg: step.msg }) break @@ -138,12 +140,43 @@ function create({ break } + case StepType.input: { + let answer: any = '' + let is_valid: boolean = false // so far + + do { + // not printing the prompt as the underlying is better suited to do it + const raw_answer = await primitives.input(step) + if (DEBUG) console.log(`↖ input(…) result =`, DEBUG_to_prettified_str(raw_answer)) + answer = step.normalizer ? step.normalizer(raw_answer) : raw_answer + const validations = step.validators.map(validator => validator(answer)) + is_valid = validations.every(([is_valid]) => is_valid) + if (!is_valid) { + const failed_validations = validations.filter(([is_valid]) => !is_valid) + await Promise.all( + failed_validations + .map(([_, msg]) => primitives.display_message({msg})) + ) + } + } while (!is_valid) + + let ೱcallback = Promise.resolve(step.callback?.(answer)) + let ೱfeedback = primitives.display_message({ + msg: step.msg_as_user?.(answer) || `My answer is: "${answer}".`, + }) + .then(() => primitives.pretend_to_think({duration_ms: after_input_delay_ms})) + .then(() => primitives.display_message({ + msg: step.msg_acknowledge?.(answer) || 'Got it.', + })) + await Promise.all([ೱcallback, ೱfeedback]) + + break + } /* case 'ask_for_confirmation': case 'ask_for_string': case 'ask_for_choice': { - await primitives.pretend_to_think(inter_msg_delay_ms) const answer = await ask_user(step) let reported = false diff --git a/stack--current/5-incubator/active/view--chat/src/implementation/types.ts b/stack--current/5-incubator/active/view--chat/src/primitives/types.ts similarity index 59% rename from stack--current/5-incubator/active/view--chat/src/implementation/types.ts rename to stack--current/5-incubator/active/view--chat/src/primitives/types.ts index 19857975..279041af 100644 --- a/stack--current/5-incubator/active/view--chat/src/implementation/types.ts +++ b/stack--current/5-incubator/active/view--chat/src/primitives/types.ts @@ -2,15 +2,27 @@ import { Enum } from 'typescript-string-enums' import { PProgress as PromiseWithProgress } from 'p-progress' import { type Immutable } from '@offirmo-private/ts-types' -import { type TaskProgressStep } from '../steps/types.js' +import { type TaskProgressStep, type InputStep } from '../steps/types.js' ///////////////////////////////////////////////// +// helper type +interface InputParameters { + // everything needed for an + // primitive is free to ignore some params if not needed/supported + prompt: ContentType | string, // required to be displayed if present + input_type?: InputStep['input_type'], + default_value?: T, + placeholder?: ContentType | string, + normalizer?: (raw: any) => T // raw is most likely string, + validators: Array<(value: T) => [ boolean, ContentType | string ]>, +} + // primitives should always accept string = common lowest denominator // up to it to convert to rich text if needed interface ChatPrimitives { - setup(): Promise + ///////////////////////////////////////////////// // core primitives display_message(p: { msg: ContentType | string, @@ -27,25 +39,31 @@ interface ChatPrimitives { msg_after: ContentType | string, }): Promise - //read_answer(step) TODO clarify - display_task(p: { msg_before: ContentType | string, promise: TaskProgressStep['promise'], msg_after: NonNullable['msg_after']>, }): Promise + // return type: some input method can't give sth else than a string (ex. terminal) + // caller must be ready to process the result + input(p: InputParameters): Promise + // while we wait for the next step. // wraps the promise, should return it // TODO clarify spin_until_resolution(p: { promise: Promise }): Promise + ///////////////////////////////////////////////// + // technical // if cleanup is needed + setup(): Promise teardown(): Promise } ///////////////////////////////////////////////// export { + type InputParameters, type ChatPrimitives, } diff --git a/stack--current/5-incubator/active/view--chat/src/steps/bases.ts b/stack--current/5-incubator/active/view--chat/src/steps/bases.ts index a4cba750..fc3bce7d 100644 --- a/stack--current/5-incubator/active/view--chat/src/steps/bases.ts +++ b/stack--current/5-incubator/active/view--chat/src/steps/bases.ts @@ -11,6 +11,7 @@ function getꓽInputStepⵧnonEmptyString( ): InputStep { const step: InputStep = { type: StepType.input, + input_type: 'text', normalizer: (raw: any): string => { let val = ensure_string(raw) diff --git a/stack--current/5-incubator/active/view--chat/src/steps/types.ts b/stack--current/5-incubator/active/view--chat/src/steps/types.ts index 8870a2a5..af8524db 100644 --- a/stack--current/5-incubator/active/view--chat/src/steps/types.ts +++ b/stack--current/5-incubator/active/view--chat/src/steps/types.ts @@ -5,11 +5,14 @@ import { type Immutable } from '@offirmo-private/ts-types' ///////////////////////////////////////////////// export const StepType = Enum( + // output 'simple_message', 'perceived_labor', - 'ask_for_confirmation', 'progress', + + // input 'input', + 'select', ) export type StepType = Enum // eslint-disable-line no-redeclare @@ -47,26 +50,16 @@ interface TaskProgressStep extends BaseStep { callback?: (success: boolean, result: T | Error) => void } -// TODO merge with input? -interface AskForConfirmationStep extends BaseStep { - type: typeof StepType.ask_for_confirmation - - prompt?: string - msg_after?: (confirmation: boolean) => ContentType | string - - callback?: (confirmation: boolean) => void -} - // inspired by https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input -// TODO refine -// TODO select between choices -// TODO types interface InputStep extends BaseStep { type: typeof StepType.input prompt: ContentType | string + input_type?: // hint to use for HTML input, primitive is free to use or ignore https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types + | 'text' + | 'checkbox' // = confirmation placeholder?: ContentType | string // may be useful in input, but primitive is free to ignore it https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#placeholder - default?: T + default_value?: T normalizer?: (raw: any) => T // raw is most likely string validators: Array<(value: T) => [ boolean, ContentType | string ]> msg_as_user: (value: T) => ContentType | string @@ -75,12 +68,34 @@ interface InputStep extends BaseStep { callback?: (value: T) => void } +// inspired by