Skip to content

Commit

Permalink
+++
Browse files Browse the repository at this point in the history
  • Loading branch information
Offirmo committed Oct 1, 2024
1 parent 99f6b46 commit 5bc4a18
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string> = {
setup: async () => {
DEBUG && console.log('[ChatPrimitives.setup()]')
},


display_message: async ({msg, choices}) => {
DEBUG && console.log('[ChatPrimitives.display_message()]')
Expand Down Expand Up @@ -39,8 +43,6 @@ const CHAT_CONSOLE: ChatPrimitives<string> = {
console.log('↳ ' + msg_after)
},

//read_answer: async () => { throw new Error('NO UI read_answer') },

display_task: async ({
msg_before,
promise,
Expand All @@ -63,6 +65,19 @@ const CHAT_CONSOLE: ChatPrimitives<string> = {
console.log('↳ ' + msg_after(success, result || error))
},

input: async <T>({
prompt,

// we ignore the rest in this primitive implementation
input_type,
default_value,
placeholder,
normalizer,
validators,
}: InputParameters<string, T>): Promise<string> => {
return rl.question(prompt + ' ')
},

spin_until_resolution: async ({promise}) => {
DEBUG && console.log('[ChatPrimitives.spin_until_resolution(...)]')

Expand All @@ -72,6 +87,9 @@ const CHAT_CONSOLE: ChatPrimitives<string> = {
return promise
},

setup: async () => {
DEBUG && console.log('[ChatPrimitives.setup()]')
},
teardown: async () => {
DEBUG && console.log('[ChatPrimitives.teardown()]')
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,24 @@ export default function* get_next_step(skip_to_index: number = 0) {
}

const warmup_promise = new Deferred<void>()
setTimeout(() => warmup_promise.reject(new Error('Failed!')), 3000)
setTimeout(() => warmup_promise.reject(new Error('Failed!')), 1000)

const STEPS: Array<Step<string>> = [

{
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}]`),
},
Expand Down
39 changes: 36 additions & 3 deletions stack--current/5-incubator/active/view--chat/src/loop/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -67,6 +67,7 @@ function create<ContentType>({
// 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))
Expand All @@ -92,11 +93,12 @@ function create<ContentType>({
async function execute_step(step: Step<ContentType>) {
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

Expand Down Expand Up @@ -138,12 +140,43 @@ function create<ContentType>({
break
}

case StepType.input: {
let answer: any = ''
let is_valid: boolean = false // so far

do {
// not printing the prompt as the underlying <input> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ContentType, T> {
// everything needed for an <input>
// primitive is free to ignore some params if not needed/supported
prompt: ContentType | string, // required to be displayed if present
input_type?: InputStep<ContentType, T>['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<ContentType> {
setup(): Promise<void>

/////////////////////////////////////////////////
// core primitives
display_message(p: {
msg: ContentType | string,
Expand All @@ -27,25 +39,31 @@ interface ChatPrimitives<ContentType> {
msg_after: ContentType | string,
}): Promise<void>

//read_answer(step) TODO clarify

display_task(p: {
msg_before: ContentType | string,
promise: TaskProgressStep<ContentType>['promise'],
msg_after: NonNullable<TaskProgressStep<ContentType>['msg_after']>,
}): Promise<void>

// return type: some input method can't give sth else than a string (ex. terminal)
// caller must be ready to process the result
input<T>(p: InputParameters<ContentType, T>): Promise<T | string>

// while we wait for the next step.
// wraps the promise, should return it
// TODO clarify
spin_until_resolution<T>(p: { promise: Promise<T> }): Promise<T>

/////////////////////////////////////////////////
// technical
// if cleanup is needed
setup(): Promise<void>
teardown(): Promise<void>
}

/////////////////////////////////////////////////

export {
type InputParameters,
type ChatPrimitives,
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ function getꓽInputStepⵧnonEmptyString<ContentType>(
): InputStep<ContentType, string> {
const step: InputStep<ContentType, string> = {
type: StepType.input,
input_type: 'text',

normalizer: (raw: any): string => {
let val = ensure_string(raw)
Expand Down
49 changes: 32 additions & 17 deletions stack--current/5-incubator/active/view--chat/src/steps/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof StepType> // eslint-disable-line no-redeclare

Expand Down Expand Up @@ -47,26 +50,16 @@ interface TaskProgressStep<ContentType, T = any> extends BaseStep {
callback?: (success: boolean, result: T | Error) => void
}

// TODO merge with input?
interface AskForConfirmationStep<ContentType> extends BaseStep {
type: typeof StepType.ask_for_confirmation

prompt?: string
msg_after?: (confirmation: boolean) => ContentType | string

callback?: (confirmation: boolean) => void
}

// inspired by <input> https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
// TODO refine
// TODO select between choices
// TODO types
interface InputStep<ContentType, T = string> 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
Expand All @@ -75,21 +68,43 @@ interface InputStep<ContentType, T = string> extends BaseStep {
callback?: (value: T) => void
}

// inspired by <select> https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select
interface SelectStep<ContentType, T = string> extends BaseStep {
type: typeof StepType.select

prompt?: ContentType | string
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_value?: T
options: {
[key: string]: { // key will be used as display if none provided
value: T,
display?: ContentType | string
callback?: (value: T) => void // optional dedicated callback
}
}

msg_as_user: (value: T) => ContentType | string
msg_acknowledge: (value: T) => ContentType | string

callback?: (value: T) => void
}


type Step<ContentType> =
| SimpleMessageStep<ContentType>
| PerceivedLaborStep<ContentType>
| TaskProgressStep<ContentType>
| AskForConfirmationStep<ContentType>
| InputStep<ContentType>
| SelectStep<ContentType>

/////////////////////////////////////////////////

export {
type SimpleMessageStep,
type PerceivedLaborStep,
type TaskProgressStep,
type AskForConfirmationStep,
type InputStep,
type SelectStep,
type Step,

// for convenience
Expand Down

0 comments on commit 5bc4a18

Please sign in to comment.